forked from EVOgeek/Vmeda.Online
		
	MOBILE-3817 dashboard: Create observable methods for getDashboardBlocks
This commit is contained in:
		
							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 () => { | ||||
|  | ||||
							
								
								
									
										74
									
								
								src/core/utils/observables.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/core/utils/observables.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user