MOBILE-3817 courses: Apply update in background to My Courses
parent
ce9c086819
commit
01df501cad
|
@ -12,12 +12,17 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, Optional, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreCoursesProvider, CoreCourses, CoreCoursesMyCoursesUpdatedEventData } from '@features/courses/services/courses';
|
||||
import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
|
||||
import {
|
||||
CoreCoursesProvider,
|
||||
CoreCourses,
|
||||
CoreCoursesMyCoursesUpdatedEventData,
|
||||
CoreCourseSummaryData,
|
||||
} from '@features/courses/services/courses';
|
||||
import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper';
|
||||
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
|
||||
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
||||
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
|
||||
|
@ -28,6 +33,8 @@ import { CoreTextUtils } from '@services/utils/text';
|
|||
import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion';
|
||||
import { IonRefresher, IonSearchbar } from '@ionic/angular';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { PageLoadWatcher } from '@classes/page-load-watcher';
|
||||
import { PageLoadsManager } from '@classes/page-loads-manager';
|
||||
|
||||
const FILTER_PRIORITY: AddonBlockMyOverviewTimeFilters[] =
|
||||
['all', 'inprogress', 'future', 'past', 'favourite', 'allincludinghidden', 'hidden'];
|
||||
|
@ -40,9 +47,9 @@ const FILTER_PRIORITY: AddonBlockMyOverviewTimeFilters[] =
|
|||
templateUrl: 'addon-block-myoverview.html',
|
||||
styleUrls: ['myoverview.scss'],
|
||||
})
|
||||
export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy {
|
||||
export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy, OnChanges {
|
||||
|
||||
filteredCourses: CoreEnrolledCourseDataWithOptions[] = [];
|
||||
filteredCourses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = [];
|
||||
|
||||
prefetchCoursesData: CorePrefetchStatusInfo = {
|
||||
icon: '',
|
||||
|
@ -84,7 +91,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
searchEnabled = false;
|
||||
|
||||
protected currentSite!: CoreSite;
|
||||
protected allCourses: CoreEnrolledCourseDataWithOptions[] = [];
|
||||
protected allCourses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = [];
|
||||
protected prefetchIconsInitialized = false;
|
||||
protected isDirty = false;
|
||||
protected isDestroyed = false;
|
||||
|
@ -94,15 +101,21 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
protected gradePeriodAfter = 0;
|
||||
protected gradePeriodBefore = 0;
|
||||
protected today = 0;
|
||||
protected firstLoadWatcher?: PageLoadWatcher;
|
||||
protected loadsManager: PageLoadsManager;
|
||||
|
||||
constructor() {
|
||||
constructor(@Optional() loadsManager?: PageLoadsManager) {
|
||||
super('AddonBlockMyOverviewComponent');
|
||||
|
||||
this.loadsManager = loadsManager ?? new PageLoadsManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.firstLoadWatcher = this.loadsManager.startComponentLoad(this);
|
||||
|
||||
// Refresh the enabled flags if enabled.
|
||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
||||
|
@ -159,6 +172,16 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (this.loaded && changes.block) {
|
||||
// Block was re-fetched, load content.
|
||||
this.reloadContent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
|
@ -226,35 +249,66 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
* @inheritdoc
|
||||
*/
|
||||
protected async fetchContent(): Promise<void> {
|
||||
const config = this.block.configsRecord;
|
||||
const loadWatcher = this.firstLoadWatcher ?? this.loadsManager.startComponentLoad(this);
|
||||
this.firstLoadWatcher = undefined;
|
||||
|
||||
const showCategories = config?.displaycategories?.value == '1';
|
||||
await Promise.all([
|
||||
this.loadAllCourses(loadWatcher),
|
||||
this.loadGracePeriod(loadWatcher),
|
||||
]);
|
||||
|
||||
this.allCourses = await CoreCoursesHelper.getUserCoursesWithOptions(
|
||||
this.sort.selected,
|
||||
undefined,
|
||||
undefined,
|
||||
showCategories,
|
||||
{
|
||||
readingStrategy: this.isDirty ? CoreSitesReadingStrategy.PREFER_NETWORK : undefined,
|
||||
},
|
||||
this.loadSort();
|
||||
this.loadLayouts(this.block.configsRecord?.layouts?.value.split(','));
|
||||
|
||||
await this.loadFilters(this.block.configsRecord, loadWatcher);
|
||||
|
||||
this.isDirty = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all courses.
|
||||
*
|
||||
* @param loadWatcher To manage the requests.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadAllCourses(loadWatcher: PageLoadWatcher): Promise<void> {
|
||||
const showCategories = this.block.configsRecord?.displaycategories?.value === '1';
|
||||
|
||||
this.allCourses = await loadWatcher.watchRequest(
|
||||
CoreCoursesHelper.getUserCoursesWithOptionsObservable({
|
||||
sort: this.sort.selected,
|
||||
loadCategoryNames: showCategories,
|
||||
readingStrategy: this.isDirty ? CoreSitesReadingStrategy.PREFER_NETWORK : loadWatcher.getReadingStrategy(),
|
||||
}),
|
||||
this.coursesHaveMeaningfulChanges.bind(this),
|
||||
);
|
||||
|
||||
this.hasCourses = this.allCourses.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load grace period.
|
||||
*
|
||||
* @param loadWatcher To manage the requests.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadGracePeriod(loadWatcher: PageLoadWatcher): Promise<void> {
|
||||
this.hasCourses = this.allCourses.length > 0;
|
||||
|
||||
try {
|
||||
this.gradePeriodAfter = parseInt(await this.currentSite.getConfig('coursegraceperiodafter', this.isDirty), 10);
|
||||
this.gradePeriodBefore = parseInt(await this.currentSite.getConfig('coursegraceperiodbefore', this.isDirty), 10);
|
||||
const siteConfig = await loadWatcher.watchRequest(
|
||||
this.currentSite.getConfigObservable(
|
||||
undefined,
|
||||
this.isDirty ? CoreSitesReadingStrategy.PREFER_NETWORK : loadWatcher.getReadingStrategy(),
|
||||
),
|
||||
);
|
||||
|
||||
this.gradePeriodAfter = parseInt(siteConfig.coursegraceperiodafter, 10);
|
||||
this.gradePeriodBefore = parseInt(siteConfig.coursegraceperiodbefore, 10);
|
||||
} catch {
|
||||
this.gradePeriodAfter = 0;
|
||||
this.gradePeriodBefore = 0;
|
||||
}
|
||||
|
||||
this.loadSort();
|
||||
this.loadLayouts(config?.layouts?.value.split(','));
|
||||
await this.loadFilters(config);
|
||||
|
||||
this.isDirty = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -279,9 +333,12 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
* Load filters.
|
||||
*
|
||||
* @param config Block configuration.
|
||||
* @param loadWatcher To manage the requests.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadFilters(
|
||||
config?: Record<string, { name: string; value: string; type: string }>,
|
||||
loadWatcher?: PageLoadWatcher,
|
||||
): Promise<void> {
|
||||
if (!this.hasCourses) {
|
||||
return;
|
||||
|
@ -320,7 +377,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
this.saveFilters('all');
|
||||
}
|
||||
|
||||
await this.filterCourses();
|
||||
await this.filterCourses(loadWatcher);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -369,18 +426,14 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
protected async refreshCourseList(data: CoreCoursesMyCoursesUpdatedEventData): Promise<void> {
|
||||
if (data.action == CoreCoursesProvider.ACTION_ENROL) {
|
||||
// Always update if user enrolled in a course.
|
||||
this.loaded = false;
|
||||
|
||||
return this.refreshContent();
|
||||
return this.refreshContent(true);
|
||||
}
|
||||
|
||||
const course = this.allCourses.find((course) => course.id == data.courseId);
|
||||
if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED) {
|
||||
if (!course) {
|
||||
// Not found, use WS update.
|
||||
this.loaded = false;
|
||||
|
||||
return this.refreshContent();
|
||||
return this.refreshContent(true);
|
||||
}
|
||||
|
||||
if (data.state == CoreCoursesProvider.STATE_FAVOURITE) {
|
||||
|
@ -398,9 +451,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
if (data.action == CoreCoursesProvider.ACTION_VIEW && data.courseId != CoreSites.getCurrentSiteHomeId()) {
|
||||
if (!course) {
|
||||
// Not found, use WS update.
|
||||
this.loaded = false;
|
||||
|
||||
return this.refreshContent();
|
||||
return this.refreshContent(true);
|
||||
}
|
||||
|
||||
course.lastaccess = CoreTimeUtils.timestamp();
|
||||
|
@ -457,8 +508,11 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
|
||||
/**
|
||||
* Set selected courses filter.
|
||||
*
|
||||
* @param loadWatcher To manage the requests.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async filterCourses(): Promise<void> {
|
||||
protected async filterCourses(loadWatcher?: PageLoadWatcher): Promise<void> {
|
||||
let timeFilter = this.filters.timeFilterSelected;
|
||||
|
||||
this.filteredCourses = this.allCourses;
|
||||
|
@ -473,7 +527,15 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
this.loaded = false;
|
||||
|
||||
try {
|
||||
const courses = await CoreCourses.getEnrolledCoursesByCustomField(customFilterName, customFilterValue);
|
||||
const courses = loadWatcher ?
|
||||
await loadWatcher.watchRequest(
|
||||
CoreCourses.getEnrolledCoursesByCustomFieldObservable(customFilterName, customFilterValue, {
|
||||
readingStrategy: loadWatcher.getReadingStrategy(),
|
||||
}),
|
||||
this.customFilterCoursesHaveMeaningfulChanges.bind(this),
|
||||
)
|
||||
:
|
||||
await CoreCourses.getEnrolledCoursesByCustomField(customFilterName, customFilterValue);
|
||||
|
||||
// Get the courses information from allincludinghidden to get the max info about the course.
|
||||
const courseIds = courses.map((course) => course.id);
|
||||
|
@ -642,6 +704,62 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
|||
CoreNavigator.navigateToSitePath('courses/list', { params : { mode: 'search' } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare if the WS data has meaningful changes for the user.
|
||||
*
|
||||
* @param previousCourses Previous courses.
|
||||
* @param newCourses New courses.
|
||||
* @return Whether it has meaningful changes.
|
||||
*/
|
||||
protected coursesHaveMeaningfulChanges(
|
||||
previousCourses: CoreEnrolledCourseDataWithExtraInfoAndOptions[],
|
||||
newCourses: CoreEnrolledCourseDataWithExtraInfoAndOptions[],
|
||||
): boolean {
|
||||
if (previousCourses.length !== newCourses.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
previousCourses = Array.from(previousCourses)
|
||||
.sort((a, b) => a.fullname.toLowerCase().localeCompare(b.fullname.toLowerCase()));
|
||||
newCourses = Array.from(newCourses).sort((a, b) => a.fullname.toLowerCase().localeCompare(b.fullname.toLowerCase()));
|
||||
|
||||
for (let i = 0; i < previousCourses.length; i++) {
|
||||
const prevCourse = previousCourses[i];
|
||||
const newCourse = newCourses[i];
|
||||
|
||||
if (
|
||||
prevCourse.progress !== newCourse.progress ||
|
||||
prevCourse.categoryname !== newCourse.categoryname ||
|
||||
(prevCourse.displayname ?? prevCourse.fullname) !== (newCourse.displayname ?? newCourse.fullname)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare if the WS data has meaningful changes for the user.
|
||||
*
|
||||
* @param previousCourses Previous courses.
|
||||
* @param newCourses New courses.
|
||||
* @return Whether it has meaningful changes.
|
||||
*/
|
||||
protected customFilterCoursesHaveMeaningfulChanges(
|
||||
previousCourses: CoreCourseSummaryData[],
|
||||
newCourses: CoreCourseSummaryData[],
|
||||
): boolean {
|
||||
if (previousCourses.length !== newCourses.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const previousIds = previousCourses.map(course => course.id).sort();
|
||||
const newIds = newCourses.map(course => course.id).sort();
|
||||
|
||||
return previousIds.some((previousId, index) => previousId !== newIds[index]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
|
||||
<div class="mark-all-as-read" slot="fixed" collapsible-footer appearOnBottom>
|
||||
<ion-chip *ngIf="notifications.loaded && canMarkAllNotificationsAsRead" [disabled]="loadingMarkAllNotificationsAsRead"
|
||||
color="primary" (click)="markAllNotificationsAsRead()">
|
||||
color="info" class="clickable fab-chip" (click)="markAllNotificationsAsRead()">
|
||||
<ion-icon name="fas-eye" aria-hidden="true" *ngIf="!loadingMarkAllNotificationsAsRead"></ion-icon>
|
||||
<ion-spinner [attr.aria-label]="'core.loading' | translate" *ngIf="loadingMarkAllNotificationsAsRead">
|
||||
</ion-spinner>
|
||||
|
|
|
@ -56,9 +56,7 @@ ion-item {
|
|||
pointer-events: none;
|
||||
|
||||
ion-chip.ion-color {
|
||||
pointer-events: all;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, .4);
|
||||
|
||||
ion-spinner {
|
||||
width: 16px;
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
// (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 { CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { AsyncComponent } from './async-component';
|
||||
import { PageLoadsManager } from './page-loads-manager';
|
||||
import { CorePromisedValue } from './promised-value';
|
||||
import { WSObservable } from './site';
|
||||
|
||||
/**
|
||||
* Class to watch requests from a page load (including requests from page sub-components).
|
||||
*/
|
||||
export class PageLoadWatcher {
|
||||
|
||||
protected hasChanges = false;
|
||||
protected ongoingRequests = 0;
|
||||
protected components = new Set<AsyncComponent>();
|
||||
protected loadedTimeout?: number;
|
||||
|
||||
constructor(
|
||||
protected loadsManager: PageLoadsManager,
|
||||
protected updateInBackground: boolean,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Whether this load watcher can update data in background.
|
||||
*
|
||||
* @return Whether this load watcher can update data in background.
|
||||
*/
|
||||
canUpdateInBackground(): boolean {
|
||||
return this.updateInBackground;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this load watcher had meaningful changes received in background.
|
||||
*
|
||||
* @return Whether this load watcher had meaningful changes received in background.
|
||||
*/
|
||||
hasMeaningfulChanges(): boolean {
|
||||
return this.hasChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set has meaningful changes to true.
|
||||
*/
|
||||
markMeaningfulChanges(): void {
|
||||
this.hasChanges = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch a component, waiting for it to be ready.
|
||||
*
|
||||
* @param component Component instance.
|
||||
*/
|
||||
async watchComponent(component: AsyncComponent): Promise<void> {
|
||||
this.components.add(component);
|
||||
clearTimeout(this.loadedTimeout);
|
||||
|
||||
try {
|
||||
await component.ready();
|
||||
} finally {
|
||||
this.components.delete(component);
|
||||
this.checkHasLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reading strategy to use.
|
||||
*
|
||||
* @return Reading strategy to use.
|
||||
*/
|
||||
getReadingStrategy(): CoreSitesReadingStrategy | undefined {
|
||||
return this.updateInBackground ? CoreSitesReadingStrategy.STALE_WHILE_REVALIDATE : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch a WS request, handling the different values it can return, calling the hasMeaningfulChanges callback if needed to
|
||||
* detect if there are new meaningful changes in the page load, and completing the page load when all requests have
|
||||
* finished and all components are ready.
|
||||
*
|
||||
* @param observable Observable of the request.
|
||||
* @param hasMeaningfulChanges Callback to check if there are meaningful changes if data was updated in background.
|
||||
* @return First value of the observable.
|
||||
*/
|
||||
watchRequest<T>(
|
||||
observable: WSObservable<T>,
|
||||
hasMeaningfulChanges?: (previousValue: T, newValue: T) => boolean,
|
||||
): Promise<T> {
|
||||
const promisedValue = new CorePromisedValue<T>();
|
||||
let subscription: Subscription | null = null;
|
||||
let firstValue: T | undefined;
|
||||
this.ongoingRequests++;
|
||||
clearTimeout(this.loadedTimeout);
|
||||
|
||||
const complete = async () => {
|
||||
this.ongoingRequests--;
|
||||
this.checkHasLoaded();
|
||||
|
||||
// Subscription variable might not be set because the observable completed immediately. Wait for next tick.
|
||||
await CoreUtils.nextTick();
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
|
||||
subscription = observable.subscribe({
|
||||
next: value => {
|
||||
if (!firstValue) {
|
||||
firstValue = value;
|
||||
promisedValue.resolve(value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Second value, it means data was updated in background. Compare data.
|
||||
if (hasMeaningfulChanges?.(firstValue, value)) {
|
||||
this.hasChanges = true;
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
promisedValue.reject(error);
|
||||
complete();
|
||||
},
|
||||
complete: () => complete(),
|
||||
});
|
||||
|
||||
return promisedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the load has finished.
|
||||
*/
|
||||
protected checkHasLoaded(): void {
|
||||
if (this.ongoingRequests !== 0 || this.components.size !== 0) {
|
||||
// Load not finished.
|
||||
return;
|
||||
}
|
||||
|
||||
// It seems load has finished. Wait to make sure no new component has been rendered and started loading.
|
||||
// If a new component or a new request starts the timeout will be cancelled, no need to double check it.
|
||||
clearTimeout(this.loadedTimeout);
|
||||
this.loadedTimeout = window.setTimeout(() => {
|
||||
// Loading finished.
|
||||
this.loadsManager.onPageLoaded(this);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
// (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 { CoreRefreshButtonModalComponent } from '@components/refresh-button-modal/refresh-button-modal';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { Subject } from 'rxjs';
|
||||
import { AsyncComponent } from './async-component';
|
||||
import { PageLoadWatcher } from './page-load-watcher';
|
||||
|
||||
/**
|
||||
* Class to manage requests in a page and its components.
|
||||
*/
|
||||
export class PageLoadsManager {
|
||||
|
||||
onRefreshPage = new Subject<void>();
|
||||
|
||||
protected initialPath?: string;
|
||||
protected currentLoadWatcher?: PageLoadWatcher;
|
||||
protected ongoingLoadWatchers = new Set<PageLoadWatcher>();
|
||||
|
||||
/**
|
||||
* Start a page load, creating a new load watcher and watching the page.
|
||||
*
|
||||
* @param page Page instance.
|
||||
* @param staleWhileRevalidate Whether to use stale while revalidate strategy.
|
||||
* @return Load watcher to use.
|
||||
*/
|
||||
startPageLoad(page: AsyncComponent, staleWhileRevalidate: boolean): PageLoadWatcher {
|
||||
this.initialPath = this.initialPath ?? CoreNavigator.getCurrentPath();
|
||||
this.currentLoadWatcher = new PageLoadWatcher(this, staleWhileRevalidate);
|
||||
this.ongoingLoadWatchers.add(this.currentLoadWatcher);
|
||||
|
||||
this.currentLoadWatcher.watchComponent(page);
|
||||
|
||||
return this.currentLoadWatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a component load, adding it to currrent load watcher (if it exists) and watching the component.
|
||||
*
|
||||
* @param component Component instance.
|
||||
* @return Load watcher to use.
|
||||
*/
|
||||
startComponentLoad(component: AsyncComponent): PageLoadWatcher {
|
||||
// If a component is loading data without the page loading data, probably the component is reloading/refreshing.
|
||||
// In that case, create a load watcher instance but don't store it in currentLoadWatcher because it's not a page load.
|
||||
const loadWatcher = this.currentLoadWatcher ?? new PageLoadWatcher(this, false);
|
||||
|
||||
loadWatcher.watchComponent(component);
|
||||
|
||||
return loadWatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* A load has finished, remove its watcher from ongoing watchers and notify if needed.
|
||||
*
|
||||
* @param loadWatcher Load watcher related to the load that finished.
|
||||
*/
|
||||
onPageLoaded(loadWatcher: PageLoadWatcher): void {
|
||||
if (!this.ongoingLoadWatchers.has(loadWatcher)) {
|
||||
// Watcher not in list, it probably finished already.
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeLoadWatcher(loadWatcher);
|
||||
|
||||
if (!loadWatcher.hasMeaningfulChanges()) {
|
||||
// No need to notify.
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there is another ongoing load watcher using update in background.
|
||||
// If so, wait for the other one to finish before notifying to prevent being notified twice.
|
||||
const ongoingLoadWatcher = this.getAnotherOngoingUpdateInBackgroundWatcher(loadWatcher);
|
||||
if (ongoingLoadWatcher) {
|
||||
ongoingLoadWatcher.markMeaningfulChanges();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.initialPath === CoreNavigator.getCurrentPath()) {
|
||||
// User hasn't changed page, notify them.
|
||||
this.notifyUser();
|
||||
} else {
|
||||
// User left the page, just update the data.
|
||||
this.onRefreshPage.next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ongoing load watcher that supports updating in background and is not the one passed as a parameter.
|
||||
*
|
||||
* @param loadWatcher Load watcher to ignore.
|
||||
* @return Ongoing load watcher, undefined if none found.
|
||||
*/
|
||||
protected getAnotherOngoingUpdateInBackgroundWatcher(loadWatcher: PageLoadWatcher): PageLoadWatcher | undefined {
|
||||
for (const ongoingLoadWatcher of this.ongoingLoadWatchers) {
|
||||
if (ongoingLoadWatcher.canUpdateInBackground() && loadWatcher !== ongoingLoadWatcher) {
|
||||
return ongoingLoadWatcher;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a load watcher from the list.
|
||||
*
|
||||
* @param loadWatcher Load watcher to remove.
|
||||
*/
|
||||
protected removeLoadWatcher(loadWatcher: PageLoadWatcher): void {
|
||||
this.ongoingLoadWatchers.delete(loadWatcher);
|
||||
if (loadWatcher === this.currentLoadWatcher) {
|
||||
delete this.currentLoadWatcher;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the user, asking him if he wants to update the data.
|
||||
*/
|
||||
protected async notifyUser(): Promise<void> {
|
||||
await CoreDomUtils.openModal<boolean>({
|
||||
component: CoreRefreshButtonModalComponent,
|
||||
cssClass: 'core-modal-no-background',
|
||||
closeOnNavigate: true,
|
||||
});
|
||||
|
||||
this.onRefreshPage.next();
|
||||
}
|
||||
|
||||
}
|
|
@ -63,6 +63,7 @@ import { CoreSwipeSlidesComponent } from './swipe-slides/swipe-slides';
|
|||
import { CoreSwipeNavigationTourComponent } from './swipe-navigation-tour/swipe-navigation-tour';
|
||||
import { CoreMessageComponent } from './message/message';
|
||||
import { CoreGroupSelectorComponent } from './group-selector/group-selector';
|
||||
import { CoreRefreshButtonModalComponent } from './refresh-button-modal/refresh-button-modal';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -108,6 +109,7 @@ import { CoreGroupSelectorComponent } from './group-selector/group-selector';
|
|||
CoreSpacerComponent,
|
||||
CoreHorizontalScrollControlsComponent,
|
||||
CoreSwipeNavigationTourComponent,
|
||||
CoreRefreshButtonModalComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -160,6 +162,7 @@ import { CoreGroupSelectorComponent } from './group-selector/group-selector';
|
|||
CoreSpacerComponent,
|
||||
CoreHorizontalScrollControlsComponent,
|
||||
CoreSwipeNavigationTourComponent,
|
||||
CoreRefreshButtonModalComponent,
|
||||
],
|
||||
})
|
||||
export class CoreComponentsModule {}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<ion-chip class="clickable fab-chip" color="info" (click)="closeModal()">
|
||||
<ion-icon name="fas-sync" aria-hidden="true"></ion-icon>
|
||||
{{ 'core.refresh' | translate }}
|
||||
</ion-chip>
|
|
@ -0,0 +1,7 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host {
|
||||
ion-chip {
|
||||
@include margin(auto, auto, calc(12px + var(--bottom-tabs-size, 0px)), auto);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { ModalController } from '@singletons';
|
||||
|
||||
/**
|
||||
* Modal that displays a refresh button.
|
||||
*/
|
||||
@Component({
|
||||
templateUrl: 'refresh-button-modal.html',
|
||||
styleUrls: ['refresh-button-modal.scss'],
|
||||
})
|
||||
export class CoreRefreshButtonModalComponent {
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*/
|
||||
closeModal(): void {
|
||||
ModalController.dismiss();
|
||||
}
|
||||
|
||||
}
|
|
@ -21,6 +21,8 @@ import { CoreCourseBlock } from '../../course/services/course';
|
|||
import { Params } from '@angular/router';
|
||||
import { ContextLevel } from '@/core/constants';
|
||||
import { CoreNavigationOptions } from '@services/navigator';
|
||||
import { AsyncComponent } from '@classes/async-component';
|
||||
import { CorePromisedValue } from '@classes/promised-value';
|
||||
|
||||
/**
|
||||
* Template class to easily create components for blocks.
|
||||
|
@ -28,7 +30,7 @@ import { CoreNavigationOptions } from '@services/navigator';
|
|||
@Component({
|
||||
template: '',
|
||||
})
|
||||
export abstract class CoreBlockBaseComponent implements OnInit, ICoreBlockComponent {
|
||||
export abstract class CoreBlockBaseComponent implements OnInit, ICoreBlockComponent, AsyncComponent {
|
||||
|
||||
@Input() title!: string; // The block title.
|
||||
@Input() block!: CoreCourseBlock; // The block to render.
|
||||
|
@ -38,8 +40,9 @@ export abstract class CoreBlockBaseComponent implements OnInit, ICoreBlockCompon
|
|||
@Input() linkParams?: Params; // Link params to go when clicked.
|
||||
@Input() navOptions?: CoreNavigationOptions; // Navigation options.
|
||||
|
||||
loaded = false; // If the component has been loaded.
|
||||
loaded = false; // If false, the UI should display a loading.
|
||||
protected fetchContentDefaultError = ''; // Default error to show when loading contents.
|
||||
protected onReadyPromise = new CorePromisedValue<void>();
|
||||
|
||||
protected logger: CoreLogger;
|
||||
|
||||
|
@ -65,9 +68,14 @@ export abstract class CoreBlockBaseComponent implements OnInit, ICoreBlockCompon
|
|||
/**
|
||||
* Perform the refresh content function.
|
||||
*
|
||||
* @param showLoading Whether to show loading.
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
protected async refreshContent(): Promise<void> {
|
||||
protected async refreshContent(showLoading?: boolean): Promise<void> {
|
||||
if (showLoading) {
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
// Wrap the call in a try/catch so the workflow isn't interrupted if an error occurs.
|
||||
try {
|
||||
await this.invalidateContent();
|
||||
|
@ -102,6 +110,7 @@ export abstract class CoreBlockBaseComponent implements OnInit, ICoreBlockCompon
|
|||
}
|
||||
|
||||
this.loaded = true;
|
||||
this.onReadyPromise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -113,6 +122,28 @@ export abstract class CoreBlockBaseComponent implements OnInit, ICoreBlockCompon
|
|||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload content without invalidating data.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async reloadContent(): Promise<void> {
|
||||
if (!this.loaded) {
|
||||
// Content being loaded, don't do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
this.loaded = false;
|
||||
await this.loadContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ready(): Promise<void> {
|
||||
return await this.onReadyPromise;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
|
||||
import { AddonBlockMyOverviewComponent } from '@addons/block/myoverview/components/myoverview/myoverview';
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { AsyncComponent } from '@classes/async-component';
|
||||
import { PageLoadsManager } from '@classes/page-loads-manager';
|
||||
import { CorePromisedValue } from '@classes/promised-value';
|
||||
import { CoreBlockComponent } from '@features/block/components/block/block';
|
||||
import { CoreCourseBlock } from '@features/course/services/course';
|
||||
import { CoreCoursesDashboard, CoreCoursesDashboardProvider } from '@features/courses/services/dashboard';
|
||||
|
@ -23,6 +26,7 @@ import { CoreSites } from '@services/sites';
|
|||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { CoreCourses } from '../../services/courses';
|
||||
|
||||
/**
|
||||
|
@ -32,8 +36,12 @@ import { CoreCourses } from '../../services/courses';
|
|||
selector: 'page-core-courses-my',
|
||||
templateUrl: 'my.html',
|
||||
styleUrls: ['my.scss'],
|
||||
providers: [{
|
||||
provide: PageLoadsManager,
|
||||
useClass: PageLoadsManager,
|
||||
}],
|
||||
})
|
||||
export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy {
|
||||
export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy, AsyncComponent {
|
||||
|
||||
@ViewChild(CoreBlockComponent) block!: CoreBlockComponent;
|
||||
|
||||
|
@ -47,8 +55,10 @@ export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy {
|
|||
hasSideBlocks = false;
|
||||
|
||||
protected updateSiteObserver: CoreEventObserver;
|
||||
protected onReadyPromise = new CorePromisedValue<void>();
|
||||
protected loadsManagerSubscription: Subscription;
|
||||
|
||||
constructor() {
|
||||
constructor(protected loadsManager: PageLoadsManager) {
|
||||
// Refresh the enabled flags if site is updated.
|
||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
||||
|
@ -57,6 +67,11 @@ export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy {
|
|||
}, CoreSites.getCurrentSiteId());
|
||||
|
||||
this.userId = CoreSites.getCurrentSiteUserId();
|
||||
|
||||
this.loadsManagerSubscription = this.loadsManager.onRefreshPage.subscribe(() => {
|
||||
this.loaded = false;
|
||||
this.loadContent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -70,19 +85,27 @@ export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy {
|
|||
|
||||
this.loadSiteName();
|
||||
|
||||
this.loadContent();
|
||||
this.loadContent(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load my overview block instance.
|
||||
* Load data.
|
||||
*
|
||||
* @param firstLoad Whether it's the first load.
|
||||
*/
|
||||
protected async loadContent(): Promise<void> {
|
||||
protected async loadContent(firstLoad = false): Promise<void> {
|
||||
const loadWatcher = this.loadsManager.startPageLoad(this, !!firstLoad);
|
||||
const available = await CoreCoursesDashboard.isAvailable();
|
||||
const disabled = await CoreCourses.isMyCoursesDisabled();
|
||||
|
||||
if (available && !disabled) {
|
||||
try {
|
||||
const blocks = await CoreCoursesDashboard.getDashboardBlocks(undefined, undefined, this.myPageCourses);
|
||||
const blocks = await loadWatcher.watchRequest(
|
||||
CoreCoursesDashboard.getDashboardBlocksObservable({
|
||||
myPage: this.myPageCourses,
|
||||
readingStrategy: loadWatcher.getReadingStrategy(),
|
||||
}),
|
||||
);
|
||||
|
||||
// My overview block should always be in main blocks, but check side blocks too just in case.
|
||||
this.loadedBlock = blocks.mainBlocks.concat(blocks.sideBlocks).find((block) => block.name == 'myoverview');
|
||||
|
@ -106,6 +129,7 @@ export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.loaded = true;
|
||||
this.onReadyPromise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -138,7 +162,7 @@ export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy {
|
|||
|
||||
// Invalidate the blocks.
|
||||
if (this.myOverviewBlock) {
|
||||
promises.push(CoreUtils.ignoreErrors(this.myOverviewBlock.doRefresh()));
|
||||
promises.push(CoreUtils.ignoreErrors(this.myOverviewBlock.invalidateContent()));
|
||||
}
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
|
@ -153,6 +177,14 @@ export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy {
|
|||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.updateSiteObserver?.off();
|
||||
this.loadsManagerSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ready(): Promise<void> {
|
||||
return await this.onReadyPromise;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -227,7 +227,7 @@ export class CoreCoursesHelperProvider {
|
|||
filter?: string,
|
||||
loadCategoryNames: boolean = false,
|
||||
options: CoreSitesCommonWSOptions = {},
|
||||
): Promise<CoreEnrolledCourseDataWithOptions[]> {
|
||||
): Promise<CoreEnrolledCourseDataWithExtraInfoAndOptions[]> {
|
||||
return firstValueFrom(this.getUserCoursesWithOptionsObservable({
|
||||
sort,
|
||||
slice,
|
||||
|
@ -245,7 +245,8 @@ export class CoreCoursesHelperProvider {
|
|||
*/
|
||||
getUserCoursesWithOptionsObservable(
|
||||
options: CoreCoursesGetWithOptionsOptions = {},
|
||||
): Observable<CoreEnrolledCourseDataWithOptions[]> {
|
||||
): Observable<CoreEnrolledCourseDataWithExtraInfoAndOptions[]> {
|
||||
|
||||
return CoreCourses.getUserCoursesObservable(options).pipe(
|
||||
chainRequests(options.readingStrategy, (courses, newReadingStrategy) => {
|
||||
if (courses.length <= 0) {
|
||||
|
|
|
@ -65,6 +65,35 @@ export class CoreObject {
|
|||
return Object.keys(object).length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object including only certain keys.
|
||||
*
|
||||
* @param obj Object.
|
||||
* @param keysOrRegex If array is supplied, keys to include. Otherwise, regular expression used to filter keys.
|
||||
* @return New object with only the specified keys.
|
||||
*/
|
||||
static only<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>;
|
||||
static only<T>(obj: T, regex: RegExp): Partial<T>;
|
||||
static only<T, K extends keyof T>(obj: T, keysOrRegex: K[] | RegExp): Pick<T, K> | Partial<T> {
|
||||
const newObject: Partial<T> = {};
|
||||
|
||||
if (Array.isArray(keysOrRegex)) {
|
||||
for (const key of keysOrRegex) {
|
||||
newObject[key] = obj[key];
|
||||
}
|
||||
} else {
|
||||
const originalKeys = Object.keys(obj);
|
||||
|
||||
for (const key of originalKeys) {
|
||||
if (key.match(keysOrRegex)) {
|
||||
newObject[key] = obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new object without the specified keys.
|
||||
*
|
||||
|
|
|
@ -99,6 +99,53 @@ describe('CoreObject singleton', () => {
|
|||
expect(CoreObject.isEmpty({ foo: 1 })).toEqual(false);
|
||||
});
|
||||
|
||||
it('creates a copy of an object with certain properties (using a list)', () => {
|
||||
const originalObject = {
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
baz: 3,
|
||||
};
|
||||
|
||||
expect(CoreObject.only(originalObject, [])).toEqual({});
|
||||
expect(CoreObject.only(originalObject, ['foo'])).toEqual({
|
||||
foo: 1,
|
||||
});
|
||||
expect(CoreObject.only(originalObject, ['foo', 'baz'])).toEqual({
|
||||
foo: 1,
|
||||
baz: 3,
|
||||
});
|
||||
expect(CoreObject.only(originalObject, ['foo', 'bar', 'baz'])).toEqual(originalObject);
|
||||
expect(originalObject).toEqual({
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
baz: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a copy of an object with certain properties (using a regular expression)', () => {
|
||||
const originalObject = {
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
baz: 3,
|
||||
};
|
||||
|
||||
expect(CoreObject.only(originalObject, /.*/)).toEqual(originalObject);
|
||||
expect(CoreObject.only(originalObject, /^ba.*/)).toEqual({
|
||||
bar: 2,
|
||||
baz: 3,
|
||||
});
|
||||
expect(CoreObject.only(originalObject, /(foo|bar)/)).toEqual({
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
});
|
||||
expect(CoreObject.only(originalObject, /notfound/)).toEqual({});
|
||||
expect(originalObject).toEqual({
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
baz: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a copy of an object without certain properties', () => {
|
||||
const originalObject = {
|
||||
foo: 1,
|
||||
|
|
|
@ -126,6 +126,7 @@ type GetObservablesReturnTypes<T> = { [key in keyof T]: T[key] extends Observabl
|
|||
*/
|
||||
type ZipObservableData<T = unknown> = {
|
||||
values: T[];
|
||||
hasValueForIndex: boolean[];
|
||||
completed: boolean;
|
||||
subscription?: Subscription;
|
||||
};
|
||||
|
@ -159,7 +160,7 @@ export function zipIncludingComplete<T extends Observable<unknown>[]>(
|
|||
}
|
||||
|
||||
// Check if any observable still doesn't have data for the index.
|
||||
const notReady = observablesData.some(data => !data.completed && data.values[nextIndex] === undefined);
|
||||
const notReady = observablesData.some(data => !data.completed && !data.hasValueForIndex[nextIndex]);
|
||||
if (notReady) {
|
||||
return;
|
||||
}
|
||||
|
@ -177,15 +178,22 @@ export function zipIncludingComplete<T extends Observable<unknown>[]>(
|
|||
}
|
||||
};
|
||||
|
||||
// Before subscribing, initialize the data for all observables.
|
||||
observables.forEach((observable, obsIndex) => {
|
||||
const observableData: ZipObservableData = {
|
||||
observablesData[obsIndex] = {
|
||||
values: [],
|
||||
hasValueForIndex: [],
|
||||
completed: false,
|
||||
};
|
||||
});
|
||||
|
||||
observables.forEach((observable, obsIndex) => {
|
||||
const observableData = observablesData[obsIndex];
|
||||
|
||||
observableData.subscription = observable.subscribe({
|
||||
next: (value) => {
|
||||
observableData.values.push(value);
|
||||
observableData.hasValueForIndex.push(true);
|
||||
treatEmitted();
|
||||
},
|
||||
error: (error) => {
|
||||
|
@ -198,8 +206,6 @@ export function zipIncludingComplete<T extends Observable<unknown>[]>(
|
|||
treatEmitted(true);
|
||||
},
|
||||
});
|
||||
|
||||
observablesData[obsIndex] = observableData;
|
||||
});
|
||||
|
||||
// When unsubscribing, unsubscribe from all observables.
|
||||
|
|
|
@ -1116,17 +1116,23 @@ ion-button.chip {
|
|||
}
|
||||
|
||||
ion-chip {
|
||||
line-height: 1.1;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
min-height: 24px;
|
||||
height: auto;
|
||||
|
||||
// Chips are not currently clickable.
|
||||
&.ion-activatable {
|
||||
// Chips are not currently clickable, only if specified explicitly.
|
||||
&.ion-activatable:not(.clickable) {
|
||||
cursor: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
&.fab-chip {
|
||||
padding: 8px 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, .4);
|
||||
}
|
||||
|
||||
&.ion-color {
|
||||
background: var(--ion-color-tint);
|
||||
|
@ -1135,6 +1141,10 @@ ion-chip {
|
|||
border-color: var(--ion-color-base);
|
||||
color: var(--ion-color-base);
|
||||
}
|
||||
&.fab-chip {
|
||||
background: var(--ion-color);
|
||||
color: var(--ion-color-contrast);
|
||||
}
|
||||
|
||||
&.ion-color-light,
|
||||
&.ion-color-medium,
|
||||
|
@ -1739,3 +1749,12 @@ ion-header.no-title {
|
|||
video::-webkit-media-text-track-display {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
ion-modal.core-modal-no-background {
|
||||
--background: transparent;
|
||||
pointer-events: none;
|
||||
|
||||
ion-backdrop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue