commit
7dd67a99b9
|
@ -30,8 +30,8 @@ npm-debug.log*
|
|||
/www
|
||||
/src/assets/lib
|
||||
|
||||
/moodle.*.config.json
|
||||
!/moodle.example.config.json
|
||||
/moodle.config.*.json
|
||||
!/moodle.config.example.json
|
||||
|
||||
/src/assets/lang/*
|
||||
/src/assets/env.json
|
||||
|
|
|
@ -37,6 +37,41 @@
|
|||
],
|
||||
"description": "[Moodle] Create a Page class"
|
||||
},
|
||||
"[Moodle] Module class": {
|
||||
"scope": "typescript",
|
||||
"prefix": "mamodule",
|
||||
"body": [
|
||||
"import { NgModule } from '@angular/core';",
|
||||
"",
|
||||
"@NgModule({",
|
||||
" $0",
|
||||
"})",
|
||||
"export class ${1}Module {}",
|
||||
""
|
||||
],
|
||||
"description": "[Moodle] Create a Module class"
|
||||
},
|
||||
"[Moodle] Lazy Module class": {
|
||||
"scope": "typescript",
|
||||
"prefix": "malazymodule",
|
||||
"body": [
|
||||
"import { NgModule } from '@angular/core';",
|
||||
"import { RouterModule, Routes } from '@angular/router';",
|
||||
"",
|
||||
"const routes: Routes = [",
|
||||
" $0",
|
||||
"];",
|
||||
"",
|
||||
"@NgModule({",
|
||||
" imports: [",
|
||||
" RouterModule.forChild(routes),",
|
||||
" ],",
|
||||
"})",
|
||||
"export class ${1}LazyModule {}",
|
||||
""
|
||||
],
|
||||
"description": "[Moodle] Create a Lazy Module class"
|
||||
},
|
||||
"[Moodle] Service Singleton": {
|
||||
"scope": "typescript",
|
||||
"prefix": "massingleton",
|
||||
|
@ -74,5 +109,5 @@
|
|||
""
|
||||
],
|
||||
"description": "[Moodle] Create a Pure Singleton"
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
"files.associations": {
|
||||
"moodle.config.json": "jsonc",
|
||||
"moodle.*.config.json": "jsonc",
|
||||
"moodle.config.*.json": "jsonc",
|
||||
},
|
||||
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ function getConfig(environment) {
|
|||
};
|
||||
const config = parseJsonc(readFileSync(resolve(__dirname, '../moodle.config.json')).toString());
|
||||
const envSuffixes = (envSuffixesMap[environment] || []);
|
||||
const envConfigPath = envSuffixes.map(suffix => resolve(__dirname, `../moodle.${suffix}.config.json`)).find(existsSync);
|
||||
const envConfigPath = envSuffixes.map(suffix => resolve(__dirname, `../moodle.config.${suffix}.json`)).find(existsSync);
|
||||
|
||||
if (envConfigPath) {
|
||||
const envConfig = parseJsonc(readFileSync(envConfigPath).toString());
|
||||
|
|
|
@ -48,5 +48,5 @@ gulp.task('default', gulp.parallel(['lang', 'env']));
|
|||
|
||||
gulp.task('watch', () => {
|
||||
gulp.watch(paths.lang, { interval: 500 }, gulp.parallel('lang'));
|
||||
gulp.watch(['./moodle.config.json', './moodle.*.config.json'], { interval: 500 }, gulp.parallel('env'));
|
||||
gulp.watch(['./moodle.config.json', './moodle.config.*.json'], { interval: 500 }, gulp.parallel('env'));
|
||||
});
|
||||
|
|
|
@ -380,7 +380,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
}
|
||||
|
||||
// Fill user data for Offline discussions (should be already cached).
|
||||
const promises = offlineDiscussions.map(async (discussion: any) => {
|
||||
const promises = offlineDiscussions.map(async (offlineDiscussion) => {
|
||||
const discussion = offlineDiscussion as unknown as AddonModForumDiscussion;
|
||||
|
||||
if (discussion.parent === 0 || forum.type === 'single') {
|
||||
// Do not show author for first post and type single.
|
||||
return;
|
||||
|
|
|
@ -71,8 +71,8 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
|||
@Input() discussion?: AddonModForumDiscussion; // Post's' discussion, only for starting posts.
|
||||
@Input() component!: string; // Component this post belong to.
|
||||
@Input() componentId!: number; // Component ID.
|
||||
@Input() replyData: any; // Object with the new post data. Usually shared between posts.
|
||||
@Input() originalData: any; // Object with the original post data. Usually shared between posts.
|
||||
@Input() replyData!: AddonModForumReply; // Object with the new post data. Usually shared between posts.
|
||||
@Input() originalData!: Omit<AddonModForumReply, 'id'>; // Object with the original post data. Usually shared between posts.
|
||||
@Input() trackPosts!: boolean; // True if post is being tracked.
|
||||
@Input() forum!: AddonModForumData; // The forum the post belongs to. Required for attachments and offline posts.
|
||||
@Input() accessInfo!: AddonModForumAccessInformation; // Forum access information.
|
||||
|
@ -103,7 +103,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
|||
get showForm(): boolean {
|
||||
return this.post.id > 0
|
||||
? !this.replyData.isEditing && this.replyData.replyingTo === this.post.id
|
||||
: this.replyData.isEditing && this.replyData.replyingTo === this.post.parentid;
|
||||
: !!this.replyData.isEditing && this.replyData.replyingTo === this.post.parentid;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -275,7 +275,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
|||
}
|
||||
|
||||
// Add some HTML to the message if needed.
|
||||
const message = CoreTextUtils.formatHtmlLines(data.message);
|
||||
const message = CoreTextUtils.formatHtmlLines(data.message!);
|
||||
const files = data.files;
|
||||
const options: AddonModForumUpdateDiscussionPostWSOptionsObject = {};
|
||||
|
||||
|
@ -295,14 +295,14 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
|||
}
|
||||
|
||||
// Try to send it to server.
|
||||
const sent = await AddonModForum.updatePost(this.post.id, data.subject, message, options);
|
||||
const sent = await AddonModForum.updatePost(this.post.id, data.subject!, message, options);
|
||||
|
||||
if (sent && this.forum.id) {
|
||||
// Data sent to server, delete stored files (if any).
|
||||
AddonModForumHelper.deleteReplyStoredFiles(this.forum.id, this.post.id);
|
||||
|
||||
this.onPostChange.emit();
|
||||
this.post.subject = data.subject;
|
||||
this.post.subject = data.subject!;
|
||||
this.post.message = message;
|
||||
this.post.attachments = data.files;
|
||||
}
|
||||
|
@ -419,7 +419,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges
|
|||
let saveOffline = false;
|
||||
let message = this.replyData.message;
|
||||
const subject = this.replyData.subject;
|
||||
const replyingTo = this.replyData.replyingTo;
|
||||
const replyingTo = this.replyData.replyingTo!;
|
||||
const files = this.replyData.files || [];
|
||||
const options: AddonModForumReplyOptions = {};
|
||||
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
||||
|
|
|
@ -38,6 +38,7 @@ import {
|
|||
AddonModForumDiscussion,
|
||||
AddonModForumPost,
|
||||
AddonModForumProvider,
|
||||
AddonModForumReply,
|
||||
} from '../../services/forum';
|
||||
import { AddonModForumHelper } from '../../services/forum-helper';
|
||||
import { AddonModForumOffline } from '../../services/forum-offline';
|
||||
|
@ -72,18 +73,18 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
|||
postHasOffline!: boolean;
|
||||
sort: SortType = 'nested';
|
||||
trackPosts!: boolean;
|
||||
replyData = {
|
||||
replyData: Omit<AddonModForumReply, 'id'> = {
|
||||
replyingTo: 0,
|
||||
isEditing: false,
|
||||
subject: '',
|
||||
message: null, // Null means empty or just white space.
|
||||
message: null,
|
||||
files: [],
|
||||
isprivatereply: false,
|
||||
};
|
||||
|
||||
originalData = {
|
||||
subject: null, // Null means original data is not set.
|
||||
message: null, // Null means empty or just white space.
|
||||
originalData: Omit<AddonModForumReply, 'id'> = {
|
||||
subject: null,
|
||||
message: null,
|
||||
files: [],
|
||||
isprivatereply: false,
|
||||
};
|
||||
|
|
|
@ -368,25 +368,25 @@ export class AddonModForumHelperProvider {
|
|||
/**
|
||||
* Check if the data of a post/discussion has changed.
|
||||
*
|
||||
* @param post Current data.
|
||||
* @param reply Current data.
|
||||
* @param original Original ata.
|
||||
* @return True if data has changed, false otherwise.
|
||||
*/
|
||||
hasPostDataChanged(post: any, original?: any): boolean {
|
||||
hasPostDataChanged(reply: AddonModForumPostData, original?: AddonModForumPostData): boolean {
|
||||
if (!original || original.subject == null) {
|
||||
// There is no original data, assume it hasn't changed.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (post.subject != original.subject || post.message != original.message) {
|
||||
if (reply.subject != original.subject || reply.message != original.message) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (post.isprivatereply != original.isprivatereply) {
|
||||
if (reply.isprivatereply != original.isprivatereply) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return CoreFileUploader.areFileListDifferent(post.files, original.files);
|
||||
return CoreFileUploader.areFileListDifferent(reply.files ?? [], original.files ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -541,3 +541,13 @@ export class AddonModForumHelperProvider {
|
|||
}
|
||||
|
||||
export const AddonModForumHelper = makeSingleton(AddonModForumHelperProvider);
|
||||
|
||||
/**
|
||||
* Forum post data used to check changes.
|
||||
*/
|
||||
type AddonModForumPostData = {
|
||||
subject?: string | null;
|
||||
message?: string | null;
|
||||
isprivatereply?: boolean;
|
||||
files?: CoreFileEntry[];
|
||||
};
|
||||
|
|
|
@ -1407,7 +1407,7 @@ export type AddonModForumDiscussion = {
|
|||
mailnow: number; // Mail now?.
|
||||
userfullname: string | boolean; // Post author full name.
|
||||
usermodifiedfullname: string; // Post modifier full name.
|
||||
userpictureurl: string; // Post author picture.
|
||||
userpictureurl?: string; // Post author picture.
|
||||
usermodifiedpictureurl: string; // Post modifier picture.
|
||||
numreplies: number; // The number of replies in the discussion.
|
||||
numunread: number; // The number of unread discussions.
|
||||
|
@ -1564,9 +1564,12 @@ export type AddonModForumAccessInformation = {
|
|||
*/
|
||||
export type AddonModForumReply = {
|
||||
id: number;
|
||||
subject: string;
|
||||
message: string;
|
||||
subject: string | null; // Null means original data is not set.
|
||||
message: string | null; // Null means empty or just white space.
|
||||
files: CoreFileEntry[];
|
||||
replyingTo?: number;
|
||||
isEditing?: boolean;
|
||||
isprivatereply?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -45,7 +45,7 @@ export class AddonModForumDiscussionLinkHandlerService extends CoreContentLinksH
|
|||
url: string,
|
||||
params: Params,
|
||||
courseId?: number,
|
||||
data?: any,
|
||||
data?: { instance?: string; cmid?: string; postid?: string },
|
||||
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||
data = data || {};
|
||||
|
||||
|
@ -56,13 +56,13 @@ export class AddonModForumDiscussionLinkHandlerService extends CoreContentLinksH
|
|||
action: (siteId): void => {
|
||||
const discussionId = parseInt(params.d, 10);
|
||||
const pageParams: Params = {
|
||||
forumId: data.instance && parseInt(data.instance, 10),
|
||||
cmId: data.cmid && parseInt(data.cmid, 10),
|
||||
forumId: data?.instance && parseInt(data.instance, 10),
|
||||
cmId: data?.cmid && parseInt(data.cmid, 10),
|
||||
courseId: courseId || parseInt(params.courseid, 10) || parseInt(params.cid, 10),
|
||||
};
|
||||
|
||||
if (data.postid || params.urlHash) {
|
||||
pageParams.postId = parseInt(data.postid || params.urlHash.replace('p', ''));
|
||||
if (data?.postid || params.urlHash) {
|
||||
pageParams.postId = parseInt(data?.postid || params.urlHash.replace('p', ''));
|
||||
}
|
||||
|
||||
if (params.parent) {
|
||||
|
|
|
@ -64,7 +64,7 @@ export class AddonModResourcePrefetchHandlerService extends CoreCourseResourcePr
|
|||
dirPath = await CoreFilepool.getPackageDirPathByUrl(CoreSites.getCurrentSiteId(), module.url!);
|
||||
}
|
||||
|
||||
const promises: Promise<any>[] = [];
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
promises.push(super.downloadOrPrefetch(module, courseId, prefetch, dirPath));
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import { makeSingleton } from '@singletons';
|
|||
import { CoreEvents } from '@singletons/events';
|
||||
|
||||
const SEPARATOR_35 = /\/\*\*? *3\.5(\.0)? *styles? *\*\//i; // A comment like "/* 3.5 styles */".
|
||||
const TMP_SITE_ID = 'tmpsite';
|
||||
export const TMP_SITE_ID = 'tmpsite';
|
||||
|
||||
/**
|
||||
* Service to handle remote themes. A remote theme is a CSS sheet stored in the site that allows customising the Mobile app.
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 972 B |
|
@ -560,7 +560,7 @@ export class CoreSite {
|
|||
return CoreUtils.clone(response);
|
||||
}
|
||||
|
||||
const promise = this.getFromCache<T>(method, data, preSets, false).catch(() => {
|
||||
const promise = this.getFromCache<T>(method, data, preSets, false).catch(async () => {
|
||||
if (preSets.forceOffline) {
|
||||
// Don't call the WS, just fail.
|
||||
throw new CoreError(
|
||||
|
@ -569,13 +569,15 @@ export class CoreSite {
|
|||
}
|
||||
|
||||
// Call the WS.
|
||||
return this.callOrEnqueueRequest<T>(method, data, preSets, wsPreSets).then((response) => {
|
||||
try {
|
||||
const response = await this.callOrEnqueueRequest<T>(method, data, preSets, wsPreSets);
|
||||
|
||||
if (preSets.saveToCache) {
|
||||
this.saveToCache(method, data, response, preSets);
|
||||
}
|
||||
|
||||
return response;
|
||||
}).catch((error) => {
|
||||
} catch (error) {
|
||||
if (error.errorcode == 'invalidtoken' ||
|
||||
(error.errorcode == 'accessexception' && error.message.indexOf('Invalid token - token expired') > -1)) {
|
||||
if (initialToken !== this.token && !retrying) {
|
||||
|
@ -585,7 +587,9 @@ export class CoreSite {
|
|||
return this.request<T>(method, data, preSets, true);
|
||||
} else if (CoreApp.isSSOAuthenticationOngoing()) {
|
||||
// There's an SSO authentication ongoing, wait for it to finish and try again.
|
||||
return CoreApp.waitForSSOAuthentication().then(() => this.request<T>(method, data, preSets, true));
|
||||
await CoreApp.waitForSSOAuthentication();
|
||||
|
||||
return this.request<T>(method, data, preSets, true);
|
||||
}
|
||||
|
||||
// Session expired, trigger event.
|
||||
|
@ -649,9 +653,7 @@ export class CoreSite {
|
|||
|
||||
if (preSets.deleteCacheIfWSError && CoreUtils.isWebServiceError(error)) {
|
||||
// Delete the cache entry and return the entry. Don't block the user with the delete.
|
||||
this.deleteFromCache(method, data, preSets).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
CoreUtils.ignoreErrors(this.deleteFromCache(method, data, preSets));
|
||||
|
||||
throw new CoreWSError(error);
|
||||
}
|
||||
|
@ -660,10 +662,12 @@ export class CoreSite {
|
|||
preSets.omitExpires = true;
|
||||
preSets.getFromCache = true;
|
||||
|
||||
return this.getFromCache<T>(method, data, preSets, true).catch(() => {
|
||||
try {
|
||||
return await this.getFromCache<T>(method, data, preSets, true);
|
||||
} catch (e) {
|
||||
throw new CoreWSError(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}).then((response: any) => {
|
||||
// Check if the response is an error, this happens if the error was stored in the cache.
|
||||
|
|
|
@ -38,10 +38,18 @@ export class CoreConstants {
|
|||
/* eslint-disable max-len */
|
||||
|
||||
static readonly SECONDS_YEAR = 31536000;
|
||||
static readonly SECONDS_MONTH = 2592000;
|
||||
static readonly SECONDS_WEEK = 604800;
|
||||
static readonly SECONDS_DAY = 86400;
|
||||
static readonly SECONDS_HOUR = 3600;
|
||||
static readonly SECONDS_MINUTE = 60;
|
||||
static readonly MILLISECONDS_YEAR = 31536000000;
|
||||
static readonly MILLISECONDS_MONTH = 2592000000;
|
||||
static readonly MILLISECONDS_WEEK = 604800000;
|
||||
static readonly MILLISECONDS_DAY = 86400000;
|
||||
static readonly MILLISECONDS_HOUR = 3600000;
|
||||
static readonly MILLISECONDS_MINUTE = 60000;
|
||||
static readonly MILLISECONDS_SECOND = 1000;
|
||||
static readonly WIFI_DOWNLOAD_THRESHOLD = 104857600; // 100MB.
|
||||
static readonly DOWNLOAD_THRESHOLD = 10485760; // 10MB.
|
||||
static readonly MINIMUM_FREE_SPACE = 10485760; // 10MB.
|
||||
|
@ -131,7 +139,7 @@ export class CoreConstants {
|
|||
|
||||
}
|
||||
|
||||
type EnvironmentConfig = {
|
||||
export interface EnvironmentConfig {
|
||||
app_id: string;
|
||||
appname: string;
|
||||
versioncode: number;
|
||||
|
@ -167,7 +175,7 @@ type EnvironmentConfig = {
|
|||
forceOpenLinksIn: 'app' | 'browser';
|
||||
};
|
||||
|
||||
type EnvironmentBuild = {
|
||||
export interface EnvironmentBuild {
|
||||
version: string;
|
||||
isProduction: boolean;
|
||||
isTesting: boolean;
|
||||
|
|
|
@ -31,7 +31,7 @@ import { Directive, ElementRef, OnInit, Input, Output, EventEmitter } from '@ang
|
|||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <a ion-button [core-suppress-events] (onClick)="toggle($event)">
|
||||
* <ion-button [core-suppress-events] (onClick)="toggle($event)">
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[core-suppress-events]',
|
||||
|
|
|
@ -34,10 +34,10 @@ export class CoreBlockComponent implements OnInit, OnDestroy, DoCheck {
|
|||
@Input() block!: CoreCourseBlock; // The block to render.
|
||||
@Input() contextLevel!: string; // The context where the block will be used.
|
||||
@Input() instanceId!: number; // The instance ID associated with the context level.
|
||||
@Input() extraData: any; // Any extra data to be passed to the block.
|
||||
@Input() extraData!: Record<string, unknown>; // Any extra data to be passed to the block.
|
||||
|
||||
componentClass?: Type<unknown>; // The class of the component to render.
|
||||
data: any = {}; // Data to pass to the component.
|
||||
data: Record<string, unknown> = {}; // Data to pass to the component.
|
||||
class?: string; // CSS class to apply to the block.
|
||||
loaded = false;
|
||||
|
||||
|
|
|
@ -140,17 +140,17 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy {
|
|||
*
|
||||
* @param e Click event.
|
||||
*/
|
||||
prefetchCourse(e: Event): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
async prefetchCourse(e?: Event): Promise<void> {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
|
||||
/* @ todo try {
|
||||
CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course);
|
||||
try {
|
||||
await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course);
|
||||
} catch (error) {
|
||||
if (!this.isDestroyed) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -112,7 +112,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
|
|||
|
||||
this.siteConfig = config;
|
||||
|
||||
await CoreSites.checkRequiredMinimumVersion(config);
|
||||
await CoreSites.checkApplication(config);
|
||||
|
||||
// Check logoURL if user avatar is not set.
|
||||
if (this.userAvatar.startsWith(this.siteUrl + '/theme/image.php')) {
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
<ion-spinner></ion-spinner>
|
||||
</div>
|
||||
<ng-container *ngFor="let site of sites">
|
||||
<ng-container *ngTemplateOutlet="site; context: {site: site}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="sitelisting; context: {site: site}"></ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ion-list>
|
||||
|
@ -118,7 +118,7 @@
|
|||
</ion-content>
|
||||
|
||||
<!-- Template site selector. -->
|
||||
<ng-template #site let-site="site">
|
||||
<ng-template #sitelisting let-site="site">
|
||||
<ion-item button (click)="connect($event, site.url, site)" [attr.aria-label]="site.name" detail="true">
|
||||
<ion-thumbnail *ngIf="siteFinderSettings.displayimage" slot="start">
|
||||
<img [src]="site.imageurl" *ngIf="site.imageurl" onError="this.src='assets/icon/icon.png'" alt="" role="presentation">
|
||||
|
|
|
@ -348,32 +348,36 @@ export class CoreLoginSitePage implements OnInit {
|
|||
* @return Promise resolved after logging in.
|
||||
*/
|
||||
protected async login(response: CoreSiteCheckResponse, foundSite?: CoreLoginSiteInfoExtended): Promise<void> {
|
||||
await CoreUtils.ignoreErrors(CoreSites.checkApplication(response));
|
||||
try {
|
||||
await CoreSites.checkApplication(response.config);
|
||||
|
||||
CoreForms.triggerFormSubmittedEvent(this.formElement, true);
|
||||
CoreForms.triggerFormSubmittedEvent(this.formElement, true);
|
||||
|
||||
if (response.warning) {
|
||||
CoreDomUtils.showErrorModal(response.warning, true, 4000);
|
||||
}
|
||||
|
||||
if (CoreLoginHelper.isSSOLoginNeeded(response.code)) {
|
||||
// SSO. User needs to authenticate in a browser.
|
||||
CoreLoginHelper.confirmAndOpenBrowserForSSOLogin(
|
||||
response.siteUrl,
|
||||
response.code,
|
||||
response.service,
|
||||
response.config?.launchurl,
|
||||
);
|
||||
} else {
|
||||
const pageParams = { siteUrl: response.siteUrl, siteConfig: response.config };
|
||||
if (foundSite && !this.fixedSites) {
|
||||
pageParams['siteName'] = foundSite.name;
|
||||
pageParams['logoUrl'] = foundSite.imageurl;
|
||||
if (response.warning) {
|
||||
CoreDomUtils.showErrorModal(response.warning, true, 4000);
|
||||
}
|
||||
|
||||
CoreNavigator.navigate('/login/credentials', {
|
||||
params: pageParams,
|
||||
});
|
||||
if (CoreLoginHelper.isSSOLoginNeeded(response.code)) {
|
||||
// SSO. User needs to authenticate in a browser.
|
||||
CoreLoginHelper.confirmAndOpenBrowserForSSOLogin(
|
||||
response.siteUrl,
|
||||
response.code,
|
||||
response.service,
|
||||
response.config?.launchurl,
|
||||
);
|
||||
} else {
|
||||
const pageParams = { siteUrl: response.siteUrl, siteConfig: response.config };
|
||||
if (foundSite && !this.fixedSites) {
|
||||
pageParams['siteName'] = foundSite.name;
|
||||
pageParams['logoUrl'] = foundSite.imageurl;
|
||||
}
|
||||
|
||||
CoreNavigator.navigate('/login/credentials', {
|
||||
params: pageParams,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -541,7 +545,7 @@ export class CoreLoginSitePage implements OnInit {
|
|||
// Check if site uses SSO.
|
||||
const response = await CoreSites.checkSite(siteUrl);
|
||||
|
||||
await CoreSites.checkApplication(response);
|
||||
await CoreSites.checkApplication(response.config);
|
||||
|
||||
if (!CoreLoginHelper.isSSOLoginNeeded(response.code)) {
|
||||
// No SSO, go to credentials page.
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreSitePublicConfigResponse } from '@classes/site';
|
||||
import { CoreCronHandler } from '@services/cron';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
@ -40,9 +39,9 @@ export class CoreLoginCronHandlerService implements CoreCronHandler {
|
|||
// Do not check twice in the same 10 minutes.
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const config = await CoreUtils.ignoreErrors(site.getPublicConfig(), <Partial<CoreSitePublicConfigResponse>> {});
|
||||
const config = await CoreUtils.ignoreErrors(site.getPublicConfig());
|
||||
|
||||
CoreUtils.ignoreErrors(CoreSites.checkApplication(<any> config));
|
||||
CoreUtils.ignoreErrors(CoreSites.checkApplication(config));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
} from './database/pushnotifications';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreWSExternalWarning } from '@services/ws';
|
||||
import { CoreSitesFactory } from '@services/sites-factory';
|
||||
|
||||
/**
|
||||
* Service to handle push notifications.
|
||||
|
@ -751,7 +752,7 @@ export class CorePushNotificationsProvider {
|
|||
|
||||
await Promise.all(results.map(async (result) => {
|
||||
// Create a temporary site to unregister.
|
||||
const tmpSite = new CoreSite(
|
||||
const tmpSite = CoreSitesFactory.makeSite(
|
||||
result.siteid,
|
||||
result.siteurl,
|
||||
result.token,
|
||||
|
|
|
@ -309,6 +309,7 @@ export class CoreNavigatorService {
|
|||
* @return Value of the parameter, undefined if not found.
|
||||
*/
|
||||
getRouteParam<T = unknown>(name: string, routeOptions: CoreNavigatorCurrentRouteOptions = {}): T | undefined {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let value: any;
|
||||
|
||||
if (!routeOptions.params) {
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
// (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 { CoreSite, CoreSiteConfig, CoreSiteInfo } from '@classes/site';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
||||
/*
|
||||
* Provider to create sites instances.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CoreSitesFactoryService {
|
||||
|
||||
/**
|
||||
* Make a site object.
|
||||
*
|
||||
* @param id Site ID.
|
||||
* @param siteUrl Site URL.
|
||||
* @param token Site's WS token.
|
||||
* @param info Site info.
|
||||
* @param privateToken Private token.
|
||||
* @param config Site public config.
|
||||
* @param loggedOut Whether user is logged out.
|
||||
* @return Site instance.
|
||||
*/
|
||||
makeSite(
|
||||
id: string | undefined,
|
||||
siteUrl: string,
|
||||
token?: string,
|
||||
info?: CoreSiteInfo,
|
||||
privateToken?: string,
|
||||
config?: CoreSiteConfig,
|
||||
loggedOut?: boolean,
|
||||
): CoreSite {
|
||||
return new CoreSite(id, siteUrl, token, info, privateToken, config, loggedOut);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreSitesFactory = makeSingleton(CoreSitesFactoryService);
|
|
@ -51,6 +51,7 @@ import {
|
|||
import { CoreArray } from '../singletons/array';
|
||||
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||
import { CoreNavigationOptions } from './navigator';
|
||||
import { CoreSitesFactory } from './sites-factory';
|
||||
|
||||
export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
|
||||
|
||||
|
@ -220,7 +221,7 @@ export class CoreSitesProvider {
|
|||
}
|
||||
|
||||
// Site exists. Create a temporary site to check if local_mobile is installed.
|
||||
const temporarySite = new CoreSite(undefined, siteUrl);
|
||||
const temporarySite = CoreSitesFactory.makeSite(undefined, siteUrl);
|
||||
let data: LocalMobileResponse;
|
||||
|
||||
try {
|
||||
|
@ -438,7 +439,7 @@ export class CoreSitesProvider {
|
|||
}
|
||||
|
||||
// Create a "candidate" site to fetch the site info.
|
||||
let candidateSite = new CoreSite(undefined, siteUrl, token, undefined, privateToken, undefined, undefined);
|
||||
let candidateSite = CoreSitesFactory.makeSite(undefined, siteUrl, token, undefined, privateToken, undefined, undefined);
|
||||
let isNewSite = true;
|
||||
|
||||
try {
|
||||
|
@ -706,20 +707,19 @@ export class CoreSitesProvider {
|
|||
/**
|
||||
* Check the app for a site and show a download dialogs if necessary.
|
||||
*
|
||||
* @param response Data obtained during site check.
|
||||
* @param config Config object of the site.
|
||||
*/
|
||||
async checkApplication(response: CoreSiteCheckResponse): Promise<void> {
|
||||
await this.checkRequiredMinimumVersion(response.config);
|
||||
async checkApplication(config?: CoreSitePublicConfigResponse): Promise<void> {
|
||||
await this.checkRequiredMinimumVersion(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the required minimum version of the app for a site and shows a download dialog.
|
||||
*
|
||||
* @param config Config object of the site.
|
||||
* @param siteId ID of the site to check. Current site id will be used otherwise.
|
||||
* @param config Config object of the site.
|
||||
* @return Resolved with if meets the requirements, rejected otherwise.
|
||||
*/
|
||||
async checkRequiredMinimumVersion(config?: CoreSitePublicConfigResponse, siteId?: string): Promise<void> {
|
||||
protected async checkRequiredMinimumVersion(config?: CoreSitePublicConfigResponse): Promise<void> {
|
||||
if (!config || !config.tool_mobile_minimumversion) {
|
||||
return;
|
||||
}
|
||||
|
@ -735,7 +735,7 @@ export class CoreSitesProvider {
|
|||
default: config.tool_mobile_setuplink,
|
||||
};
|
||||
|
||||
siteId = siteId || this.getCurrentSiteId();
|
||||
const siteId = this.getCurrentSiteId();
|
||||
|
||||
const downloadUrl = CoreApp.getAppStoreUrl(storesConfig);
|
||||
|
||||
|
@ -837,7 +837,7 @@ export class CoreSitesProvider {
|
|||
}
|
||||
|
||||
try {
|
||||
await this.checkRequiredMinimumVersion(config);
|
||||
await this.checkApplication(config);
|
||||
|
||||
this.login(siteId);
|
||||
// Update site info. We don't block the UI.
|
||||
|
@ -1004,7 +1004,15 @@ export class CoreSitesProvider {
|
|||
const info = entry.info ? <CoreSiteInfo> CoreTextUtils.parseJSON(entry.info) : undefined;
|
||||
const config = entry.config ? <CoreSiteConfig> CoreTextUtils.parseJSON(entry.config) : undefined;
|
||||
|
||||
const site = new CoreSite(entry.id, entry.siteUrl, entry.token, info, entry.privateToken, config, entry.loggedOut == 1);
|
||||
const site = CoreSitesFactory.makeSite(
|
||||
entry.id,
|
||||
entry.siteUrl,
|
||||
entry.token,
|
||||
info,
|
||||
entry.privateToken,
|
||||
config,
|
||||
entry.loggedOut == 1,
|
||||
);
|
||||
site.setOAuthId(entry.oauthId || undefined);
|
||||
|
||||
return this.migrateSiteSchemas(site).then(() => {
|
||||
|
@ -1165,28 +1173,27 @@ export class CoreSitesProvider {
|
|||
* @return Promise resolved when the user is logged out.
|
||||
*/
|
||||
async logout(): Promise<void> {
|
||||
let siteId: string | undefined;
|
||||
if (!this.currentSite) {
|
||||
return;
|
||||
}
|
||||
|
||||
const db = await this.appDB;
|
||||
|
||||
const promises: Promise<unknown>[] = [];
|
||||
const siteConfig = this.currentSite.getStoredConfig();
|
||||
const siteId = this.currentSite.getId();
|
||||
|
||||
if (this.currentSite) {
|
||||
const db = await this.appDB;
|
||||
const siteConfig = this.currentSite.getStoredConfig();
|
||||
siteId = this.currentSite.getId();
|
||||
this.currentSite = undefined;
|
||||
|
||||
this.currentSite = undefined;
|
||||
|
||||
if (siteConfig && siteConfig.tool_mobile_forcelogout == '1') {
|
||||
promises.push(this.setSiteLoggedOut(siteId, true));
|
||||
}
|
||||
|
||||
promises.push(db.deleteRecords(CURRENT_SITE_TABLE_NAME, { id: 1 }));
|
||||
if (siteConfig && siteConfig.tool_mobile_forcelogout == '1') {
|
||||
promises.push(this.setSiteLoggedOut(siteId, true));
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} finally {
|
||||
CoreEvents.trigger(CoreEvents.LOGOUT, {}, siteId);
|
||||
}
|
||||
promises.push(db.deleteRecords(CURRENT_SITE_TABLE_NAME, { id: 1 }));
|
||||
|
||||
await CoreUtils.ignoreErrors(Promise.all(promises));
|
||||
|
||||
CoreEvents.trigger(CoreEvents.LOGOUT, {}, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1293,7 +1300,7 @@ export class CoreSitesProvider {
|
|||
* @param siteid Site's ID.
|
||||
* @return A promise resolved when the site is updated.
|
||||
*/
|
||||
async updateSiteInfo(siteId: string): Promise<void> {
|
||||
async updateSiteInfo(siteId?: string): Promise<void> {
|
||||
const site = await this.getSite(siteId);
|
||||
|
||||
try {
|
||||
|
@ -1430,7 +1437,7 @@ export class CoreSitesProvider {
|
|||
* @return Promise resolved with the public config.
|
||||
*/
|
||||
getSitePublicConfig(siteUrl: string): Promise<CoreSitePublicConfigResponse> {
|
||||
const temporarySite = new CoreSite(undefined, siteUrl);
|
||||
const temporarySite = CoreSitesFactory.makeSite(undefined, siteUrl);
|
||||
|
||||
return temporarySite.getPublicConfig();
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ export class CoreCustomURLSchemesProvider {
|
|||
|
||||
data.siteUrl = result.siteUrl;
|
||||
|
||||
await CoreSites.checkApplication(result);
|
||||
await CoreSites.checkApplication(result.config);
|
||||
}
|
||||
|
||||
return CoreSites.newSite(
|
||||
|
|
|
@ -1491,7 +1491,7 @@ export class CoreUtilsProvider {
|
|||
debounce<T extends unknown[]>(fn: (...args: T) => unknown, delay: number): (...args: T) => void {
|
||||
let timeoutID: number;
|
||||
|
||||
const debounced = (...args: unknown[]): void => {
|
||||
const debounced = (...args: T): void => {
|
||||
clearTimeout(timeoutID);
|
||||
|
||||
timeoutID = window.setTimeout(() => fn.apply(null, args), delay);
|
||||
|
|
|
@ -16,6 +16,16 @@ import moment from 'moment';
|
|||
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
|
||||
import { CoreTime } from './time';
|
||||
|
||||
/**
|
||||
* Method to warn that logs are disabled, called only once.
|
||||
*/
|
||||
const warnLogsDisabled = CoreTime.once(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Log is disabled in production app');
|
||||
});
|
||||
|
||||
/**
|
||||
* Log function type.
|
||||
*/
|
||||
|
@ -59,8 +69,7 @@ export class CoreLogger {
|
|||
// Disable log on production and testing.
|
||||
if (CoreConstants.BUILD.isProduction || CoreConstants.BUILD.isTesting) {
|
||||
if (CoreConstants.BUILD.isProduction) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Log is disabled in production app');
|
||||
warnLogsDisabled();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
// (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.
|
||||
|
||||
/**
|
||||
* Singleton with helper functions for math operations.
|
||||
*/
|
||||
export class CoreMath {
|
||||
|
||||
/**
|
||||
* Clamp a value between a minimum and a maximum.
|
||||
*
|
||||
* @param value Original value.
|
||||
* @param min Minimum value.
|
||||
* @param max Maximum value.
|
||||
* @return Clamped value.
|
||||
*/
|
||||
static clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
}
|
|
@ -21,6 +21,17 @@ export type CoreObjectWithoutEmpty<T> = {
|
|||
*/
|
||||
export class CoreObject {
|
||||
|
||||
/**
|
||||
* Check if two objects have the same shape and the same leaf values.
|
||||
*
|
||||
* @param a First object.
|
||||
* @param b Second object.
|
||||
* @return Whether objects are equal.
|
||||
*/
|
||||
static deepEquals(a: unknown, b: unknown): boolean {
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given object is empty.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
// (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.
|
||||
|
||||
/**
|
||||
* Singleton with helper functions for time operations.
|
||||
*/
|
||||
export class CoreTime {
|
||||
|
||||
/**
|
||||
* Wrap a function so that it is called only once.
|
||||
*
|
||||
* @param fn Function.
|
||||
* @return Wrapper that will call the underlying function only once.
|
||||
*/
|
||||
static once<T extends unknown[]>(fn: (...args: T) => unknown): (...args: T) => void {
|
||||
let called = false;
|
||||
|
||||
return (...args: T) => {
|
||||
if (called) {
|
||||
return;
|
||||
}
|
||||
|
||||
called = true;
|
||||
fn.apply(null, args);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -161,3 +161,12 @@ $screen-breakpoints: (
|
|||
) !default;
|
||||
|
||||
$breakpoint-tablet: map-get($screen-breakpoints, tablet), !default;
|
||||
|
||||
/*
|
||||
* Z-indexes.
|
||||
*
|
||||
* https://github.com/ionic-team/ionic-framework/blob/master/core/src/themes/ionic.globals.scss
|
||||
*/
|
||||
|
||||
$z-index-overlay: 1001;
|
||||
$z-index-overlay-wrapper: 10;
|
||||
|
|
|
@ -37,6 +37,14 @@
|
|||
flex-direction: row;
|
||||
}
|
||||
|
||||
.margin-bottom-sm { margin-bottom: 8px; }
|
||||
.margin-bottom-md { margin-bottom: 12px; }
|
||||
|
||||
.font-bold { font-weight: bold; }
|
||||
.font-italic { font-style: italic; }
|
||||
.font-lg { font-size: 1.7rem; }
|
||||
.font-sm { font-size: 1.2rem; }
|
||||
|
||||
// Correctly inherit ion-text-wrap onto labels.
|
||||
ion-item.ion-text-wrap ion-label {
|
||||
white-space: normal !important;
|
||||
|
|
|
@ -122,7 +122,7 @@
|
|||
ion-slide {
|
||||
--background: var(--core-tab-background);
|
||||
--color: var(--core-tab-color);
|
||||
--border-color: var(--core-tab-border-colo);
|
||||
--border-color: var(--core-tab-border-color);
|
||||
--color-active: var(--core-tab-color-active);
|
||||
--border-color-active: var(--core-tab-border-color-active);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ declare module '@ionic/angular' {
|
|||
|
||||
export class NavController {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
navigateForward(url: string | UrlTree | any[], options?: NavigationOptions): Promise<boolean | null>;
|
||||
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ information provided here is intended especially for developers.
|
|||
=== 3.9.5 ===
|
||||
|
||||
- Several functions inside AddonNotificationsProvider have been modified to accept an "options" parameter instead of having several optional parameters.
|
||||
- Schemas are now registered using Angular providers with the CORE_SITE_SCHEMAS injection token instead of CoreSitesProvider.registerSiteSchema.
|
||||
|
||||
=== 3.9.3 ===
|
||||
|
||||
|
|
Loading…
Reference in New Issue