MOBILE-3817 dashboard: Create observable methods for getDashboardBlocks
parent
89ba05dd3e
commit
3e462979f7
|
@ -59,6 +59,7 @@ import {
|
|||
} from '@services/database/sites';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { finalize, map } from 'rxjs/operators';
|
||||
import { firstValueFrom } from '../utils/observables';
|
||||
|
||||
/**
|
||||
* QR Code type enumeration.
|
||||
|
@ -494,7 +495,7 @@ export class CoreSite {
|
|||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
read<T = unknown>(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise<T> {
|
||||
return this.readObservable<T>(method, data, preSets).toPromise();
|
||||
return firstValueFrom(this.readObservable<T>(method, data, preSets));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -525,7 +526,7 @@ export class CoreSite {
|
|||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
write<T = unknown>(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise<T> {
|
||||
return this.writeObservable<T>(method, data, preSets).toPromise();
|
||||
return firstValueFrom(this.writeObservable<T>(method, data, preSets));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -556,7 +557,7 @@ export class CoreSite {
|
|||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async request<T = unknown>(method: string, data: any, preSets: CoreSiteWSPreSets): Promise<T> {
|
||||
return this.requestObservable<T>(method, data, preSets).toPromise();
|
||||
return firstValueFrom(this.requestObservable<T>(method, data, preSets));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1640,7 +1641,7 @@ export class CoreSite {
|
|||
|
||||
// Check for an ongoing identical request if we're not ignoring cache.
|
||||
if (cachePreSets.getFromCache && this.ongoingRequests[cacheId] !== undefined) {
|
||||
return await this.ongoingRequests[cacheId].toPromise();
|
||||
return await firstValueFrom(this.ongoingRequests[cacheId]);
|
||||
}
|
||||
|
||||
const subject = new Subject<CoreSitePublicConfigResponse>();
|
||||
|
@ -1698,7 +1699,7 @@ export class CoreSite {
|
|||
subject.error(error);
|
||||
});
|
||||
|
||||
return observable.toPromise();
|
||||
return firstValueFrom(observable);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,12 +13,15 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
|
||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||
import { CoreCourseBlock } from '@features/course/services/course';
|
||||
import { CoreStatusWithWarningsWSResponse } from '@services/ws';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { asyncObservable, firstValueFrom } from '@/core/utils/observables';
|
||||
|
||||
const ROOT_CACHE_KEY = 'CoreCoursesDashboard:';
|
||||
|
||||
|
@ -51,40 +54,66 @@ export class CoreCoursesDashboardProvider {
|
|||
* @return Promise resolved with the list of blocks.
|
||||
* @since 3.6
|
||||
*/
|
||||
async getDashboardBlocksFromWS(
|
||||
getDashboardBlocksFromWS(
|
||||
myPage = CoreCoursesDashboardProvider.MY_PAGE_DEFAULT,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
): Promise<CoreCourseBlock[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
return firstValueFrom(this.getDashboardBlocksFromWSObservable({
|
||||
myPage,
|
||||
userId,
|
||||
siteId,
|
||||
}));
|
||||
}
|
||||
|
||||
const params: CoreBlockGetDashboardBlocksWSParams = {
|
||||
returncontents: true,
|
||||
};
|
||||
if (CoreSites.getRequiredCurrentSite().isVersionGreaterEqualThan('4.0')) {
|
||||
params.mypage = myPage;
|
||||
} else if (myPage != CoreCoursesDashboardProvider.MY_PAGE_DEFAULT) {
|
||||
throw new CoreError('mypage param is no accessible on core_block_get_dashboard_blocks');
|
||||
}
|
||||
/**
|
||||
* Get dashboard blocks from WS.
|
||||
*
|
||||
* @param options Options.
|
||||
* @return Observable that returns the list of blocks.
|
||||
* @since 3.6
|
||||
*/
|
||||
getDashboardBlocksFromWSObservable(options: GetDashboardBlocksOptions = {}): Observable<CoreCourseBlock[]> {
|
||||
return asyncObservable(async () => {
|
||||
const site = await CoreSites.getSite(options.siteId);
|
||||
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getDashboardBlocksCacheKey(myPage, userId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
};
|
||||
if (userId) {
|
||||
params.userid = userId;
|
||||
}
|
||||
const result = await site.read<CoreBlockGetDashboardBlocksWSResponse>('core_block_get_dashboard_blocks', params, preSets);
|
||||
const myPage = options.myPage ?? CoreCoursesDashboardProvider.MY_PAGE_DEFAULT;
|
||||
const params: CoreBlockGetDashboardBlocksWSParams = {
|
||||
returncontents: true,
|
||||
};
|
||||
if (CoreSites.getRequiredCurrentSite().isVersionGreaterEqualThan('4.0')) {
|
||||
params.mypage = myPage;
|
||||
} else if (myPage != CoreCoursesDashboardProvider.MY_PAGE_DEFAULT) {
|
||||
throw new CoreError('mypage param is no accessible on core_block_get_dashboard_blocks');
|
||||
}
|
||||
|
||||
if (site.isVersionGreaterEqualThan('4.0')) {
|
||||
// Temporary hack to have course overview on 3.9.5 but not on 4.0 onwards.
|
||||
// To be removed in a near future.
|
||||
// Remove myoverview when is forced. See MDL-72092.
|
||||
result.blocks = result.blocks.filter((block) =>
|
||||
block.instanceid != 0 || block.name != 'myoverview' || block.region != 'forced');
|
||||
}
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getDashboardBlocksCacheKey(myPage, options.userId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
|
||||
};
|
||||
if (options.userId) {
|
||||
params.userid = options.userId;
|
||||
}
|
||||
|
||||
return result.blocks || [];
|
||||
const observable = site.readObservable<CoreBlockGetDashboardBlocksWSResponse>(
|
||||
'core_block_get_dashboard_blocks',
|
||||
params,
|
||||
preSets,
|
||||
);
|
||||
|
||||
return observable.pipe(map(result => {
|
||||
if (site.isVersionGreaterEqualThan('4.0')) {
|
||||
// Temporary hack to have course overview on 3.9.5 but not on 4.0 onwards.
|
||||
// To be removed in a near future.
|
||||
// Remove myoverview when is forced. See MDL-72092.
|
||||
result.blocks = result.blocks.filter((block) =>
|
||||
block.instanceid != 0 || block.name != 'myoverview' || block.region != 'forced');
|
||||
}
|
||||
|
||||
return result.blocks || [];
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,39 +124,52 @@ export class CoreCoursesDashboardProvider {
|
|||
* @param myPage What my page to return blocks of. Default MY_PAGE_DEFAULT.
|
||||
* @return Promise resolved with the list of blocks.
|
||||
*/
|
||||
async getDashboardBlocks(
|
||||
getDashboardBlocks(
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
myPage = CoreCoursesDashboardProvider.MY_PAGE_DEFAULT,
|
||||
): Promise<CoreCoursesDashboardBlocks> {
|
||||
const blocks = await this.getDashboardBlocksFromWS(myPage, userId, siteId);
|
||||
return firstValueFrom(this.getDashboardBlocksObservable({
|
||||
myPage,
|
||||
userId,
|
||||
siteId,
|
||||
}));
|
||||
}
|
||||
|
||||
let mainBlocks: CoreCourseBlock[] = [];
|
||||
let sideBlocks: CoreCourseBlock[] = [];
|
||||
|
||||
blocks.forEach((block) => {
|
||||
if (block.region == 'content' || block.region == 'main') {
|
||||
mainBlocks.push(block);
|
||||
} else {
|
||||
sideBlocks.push(block);
|
||||
}
|
||||
});
|
||||
|
||||
if (mainBlocks.length == 0) {
|
||||
mainBlocks = [];
|
||||
sideBlocks = [];
|
||||
/**
|
||||
* Get dashboard blocks.
|
||||
*
|
||||
* @param options Options.
|
||||
* @return observable that returns the list of blocks.
|
||||
*/
|
||||
getDashboardBlocksObservable(options: GetDashboardBlocksOptions = {}): Observable<CoreCoursesDashboardBlocks> {
|
||||
return this.getDashboardBlocksFromWSObservable(options).pipe(map(blocks => {
|
||||
let mainBlocks: CoreCourseBlock[] = [];
|
||||
let sideBlocks: CoreCourseBlock[] = [];
|
||||
|
||||
blocks.forEach((block) => {
|
||||
if (block.region.match('side')) {
|
||||
sideBlocks.push(block);
|
||||
} else {
|
||||
if (block.region == 'content' || block.region == 'main') {
|
||||
mainBlocks.push(block);
|
||||
} else {
|
||||
sideBlocks.push(block);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { mainBlocks, sideBlocks };
|
||||
if (mainBlocks.length == 0) {
|
||||
mainBlocks = [];
|
||||
sideBlocks = [];
|
||||
|
||||
blocks.forEach((block) => {
|
||||
if (block.region.match('side')) {
|
||||
sideBlocks.push(block);
|
||||
} else {
|
||||
mainBlocks.push(block);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { mainBlocks, sideBlocks };
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -194,6 +236,14 @@ export type CoreCoursesDashboardBlocks = {
|
|||
sideBlocks: CoreCourseBlock[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for some get dashboard blocks calls.
|
||||
*/
|
||||
export type GetDashboardBlocksOptions = CoreSitesCommonWSOptions & {
|
||||
userId?: number; // User ID. If not defined, current user.
|
||||
myPage?: string; // Page to get. If not defined, CoreCoursesDashboardProvider.MY_PAGE_DEFAULT.
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of core_block_get_dashboard_blocks WS.
|
||||
*/
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { EventEmitter } from '@angular/core';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
|
||||
/**
|
||||
|
@ -31,37 +32,46 @@ export class CoreSubscriptions {
|
|||
* @param subscribable Subscribable to listen to.
|
||||
* @param onSuccess Callback to run when the subscription is updated.
|
||||
* @param onError Callback to run when the an error happens.
|
||||
* @param onComplete Callback to run when the observable completes.
|
||||
* @return A function to unsubscribe.
|
||||
*/
|
||||
static once<T>(
|
||||
subscribable: Subscribable<T>,
|
||||
onSuccess: (value: T) => unknown,
|
||||
onError?: (error: unknown) => unknown,
|
||||
onComplete?: () => void,
|
||||
): () => void {
|
||||
let unsubscribe = false;
|
||||
let callbackCalled = false;
|
||||
let subscription: Subscription | null = null;
|
||||
|
||||
const runCallback = (callback) => {
|
||||
if (!callbackCalled) {
|
||||
callbackCalled = true;
|
||||
callback();
|
||||
}
|
||||
};
|
||||
const unsubscribe = async () => {
|
||||
// Subscription variable might not be set because we can receive a value immediately. Wait for next tick.
|
||||
await CoreUtils.nextTick();
|
||||
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
|
||||
subscription = subscribable.subscribe(
|
||||
value => {
|
||||
// Subscription variable might not be set because we can receive a value immediately.
|
||||
unsubscribe = true;
|
||||
subscription?.unsubscribe();
|
||||
|
||||
onSuccess(value);
|
||||
unsubscribe();
|
||||
runCallback(() => onSuccess(value));
|
||||
},
|
||||
error => {
|
||||
// Subscription variable might not be set because we can receive a value immediately.
|
||||
unsubscribe = true;
|
||||
subscription?.unsubscribe();
|
||||
|
||||
onError && onError(error);
|
||||
unsubscribe();
|
||||
runCallback(() => onError?.(error));
|
||||
},
|
||||
() => {
|
||||
unsubscribe();
|
||||
runCallback(() => onComplete?.());
|
||||
},
|
||||
);
|
||||
|
||||
if (unsubscribe) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
|
||||
return () => subscription?.unsubscribe();
|
||||
}
|
||||
|
||||
|
|
|
@ -17,12 +17,20 @@ import { BehaviorSubject, Subject } from 'rxjs';
|
|||
|
||||
describe('CoreSubscriptions singleton', () => {
|
||||
|
||||
it('calls callbacks only once', async () => {
|
||||
// Test call success function.
|
||||
let subject = new Subject();
|
||||
let success = jest.fn();
|
||||
let error = jest.fn();
|
||||
CoreSubscriptions.once(subject, success, error);
|
||||
let subject: Subject<unknown>;
|
||||
let success: jest.Mock;
|
||||
let error: jest.Mock;
|
||||
let complete: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
subject = new Subject();
|
||||
success = jest.fn();
|
||||
error = jest.fn();
|
||||
complete = jest.fn();
|
||||
});
|
||||
|
||||
it('calls success callback only once', async () => {
|
||||
CoreSubscriptions.once(subject, success, error, complete);
|
||||
|
||||
subject.next('foo');
|
||||
expect(success).toHaveBeenCalledTimes(1);
|
||||
|
@ -32,11 +40,11 @@ describe('CoreSubscriptions singleton', () => {
|
|||
subject.error('foo');
|
||||
expect(success).toHaveBeenCalledTimes(1);
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
expect(complete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Test call error function.
|
||||
subject = new Subject(); // Create a new Subject because the previous one already has an error.
|
||||
success = jest.fn();
|
||||
CoreSubscriptions.once(subject, success, error);
|
||||
it('calls error callback only once', async () => {
|
||||
CoreSubscriptions.once(subject, success, error, complete);
|
||||
|
||||
subject.error('foo');
|
||||
expect(error).toHaveBeenCalledWith('foo');
|
||||
|
@ -45,11 +53,27 @@ describe('CoreSubscriptions singleton', () => {
|
|||
subject.error('bar');
|
||||
expect(error).toHaveBeenCalledTimes(1);
|
||||
expect(success).not.toHaveBeenCalled();
|
||||
expect(complete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls complete callback only once', async () => {
|
||||
CoreSubscriptions.once(subject, success, error, complete);
|
||||
|
||||
subject.complete();
|
||||
expect(complete).toHaveBeenCalled();
|
||||
|
||||
subject.next('foo');
|
||||
subject.error('bar');
|
||||
subject.complete();
|
||||
expect(complete).toHaveBeenCalledTimes(1);
|
||||
expect(success).not.toHaveBeenCalled();
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls success callback only once with behaviour subject', async () => {
|
||||
// Test with behaviour subject (success callback called immediately).
|
||||
const beaviourSubject = new BehaviorSubject('foo');
|
||||
error = jest.fn();
|
||||
CoreSubscriptions.once(beaviourSubject, success, error);
|
||||
CoreSubscriptions.once(beaviourSubject, success, error, complete);
|
||||
|
||||
expect(success).toHaveBeenCalledWith('foo');
|
||||
|
||||
|
@ -57,6 +81,7 @@ describe('CoreSubscriptions singleton', () => {
|
|||
beaviourSubject.error('foo');
|
||||
expect(success).toHaveBeenCalledTimes(1);
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
expect(complete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows unsubscribing from outside the once function', async () => {
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
// (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 { CoreError } from '@classes/errors/error';
|
||||
import { CoreSubscriptions } from '@singletons/subscriptions';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* Convert to an Observable a Promise that resolves to an Observable.
|
||||
*
|
||||
* @param createObservable A function returning a promise that resolves to an Observable.
|
||||
* @returns Observable.
|
||||
*/
|
||||
export function asyncObservable<T>(createObservable: () => Promise<Observable<T>>): Observable<T> {
|
||||
const promise = createObservable();
|
||||
|
||||
return new Observable(subscriber => {
|
||||
promise
|
||||
.then(observable => observable.subscribe(
|
||||
value => subscriber.next(value),
|
||||
error => subscriber.error(error),
|
||||
() => subscriber.complete(),
|
||||
))
|
||||
.catch(error => subscriber.error(error));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Promise that resolved with the first value returned from an observable.
|
||||
* This function can be removed when the app starts using rxjs v7.
|
||||
*
|
||||
* @param observable Observable.
|
||||
* @returns Promise resolved with the first value returned.
|
||||
*/
|
||||
export function firstValueFrom<T>(observable: Observable<T>): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
CoreSubscriptions.once(observable, resolve, reject, () => {
|
||||
// Subscription is completed, check if we can get its value.
|
||||
if (observable instanceof BehaviorSubject) {
|
||||
resolve(observable.getValue());
|
||||
}
|
||||
|
||||
reject(new CoreError('Couldn\'t get first value from observable because it\'s already completed'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore errors from an observable, returning a certain value instead.
|
||||
*
|
||||
* @param observable Observable to ignore errors.
|
||||
* @param fallback Value to return if the observer errors.
|
||||
* @return Observable with ignored errors, returning the fallback result if provided.
|
||||
*/
|
||||
export function ignoreErrors<Result>(observable: Observable<Result>): Observable<Result | undefined>;
|
||||
export function ignoreErrors<Result, Fallback>(observable: Observable<Result>, fallback: Fallback): Observable<Result | Fallback>;
|
||||
export function ignoreErrors<Result, Fallback>(
|
||||
observable: Observable<Result>,
|
||||
fallback?: Fallback,
|
||||
): Observable<Result | Fallback | undefined> {
|
||||
return observable.pipe(catchError(() => of(fallback)));
|
||||
}
|
Loading…
Reference in New Issue