From c24bb2e5cc9e638915b769259d6a0d18bf6cd06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 8 Nov 2021 16:19:08 +0100 Subject: [PATCH] MOBILE-3806 myoverview: Renew filter interface --- scripts/langindex.json | 7 +- .../components/components.module.ts | 2 + .../filteroptions/filteroptions.html | 56 ++ .../components/filteroptions/filteroptions.ts | 44 + .../myoverview/addon-block-myoverview.html | 132 ++- .../components/myoverview/myoverview.ts | 825 +++++++++--------- src/addons/block/myoverview/lang.json | 4 +- .../core-courses-course-list-item.html | 4 +- src/core/lang.json | 1 + 9 files changed, 587 insertions(+), 488 deletions(-) create mode 100644 src/addons/block/myoverview/components/filteroptions/filteroptions.html create mode 100644 src/addons/block/myoverview/components/filteroptions/filteroptions.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index a8d9ddc2b..db1744aee 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -40,11 +40,13 @@ "addon.block_learningplans.pluginname": "block_lp", "addon.block_myoverview.all": "block_myoverview", "addon.block_myoverview.allincludinghidden": "block_myoverview", - "addon.block_myoverview.favourites": "block_myoverview", + "addon.block_myoverview.aria:hiddencourses": "block_myoverview", + "addon.block_myoverview.card": "block_myoverview", + "addon.block_myoverview.favouritesonly": "local_moodlemobileapp", "addon.block_myoverview.future": "block_myoverview", - "addon.block_myoverview.hiddencourses": "block_myoverview", "addon.block_myoverview.inprogress": "block_myoverview", "addon.block_myoverview.lastaccessed": "block_myoverview", + "addon.block_myoverview.list": "block_myoverview", "addon.block_myoverview.nocourses": "block_myoverview", "addon.block_myoverview.past": "block_myoverview", "addon.block_myoverview.pluginname": "block_myoverview", @@ -1400,6 +1402,7 @@ "core.allparticipants": "moodle", "core.answer": "moodle", "core.answered": "quiz", + "core.applyfilters": "user", "core.areyousure": "moodle", "core.back": "moodle", "core.block.blocks": "moodle", diff --git a/src/addons/block/myoverview/components/components.module.ts b/src/addons/block/myoverview/components/components.module.ts index c1829bd61..49e53a2f4 100644 --- a/src/addons/block/myoverview/components/components.module.ts +++ b/src/addons/block/myoverview/components/components.module.ts @@ -17,10 +17,12 @@ import { NgModule } from '@angular/core'; import { CoreSharedModule } from '@/core/shared.module'; import { CoreCoursesComponentsModule } from '@features/courses/components/components.module'; import { AddonBlockMyOverviewComponent } from './myoverview/myoverview'; +import { AddonBlockMyOverviewFilterOptionsComponent } from './filteroptions/filteroptions'; @NgModule({ declarations: [ AddonBlockMyOverviewComponent, + AddonBlockMyOverviewFilterOptionsComponent, ], imports: [ CoreSharedModule, diff --git a/src/addons/block/myoverview/components/filteroptions/filteroptions.html b/src/addons/block/myoverview/components/filteroptions/filteroptions.html new file mode 100644 index 000000000..414d1fbd4 --- /dev/null +++ b/src/addons/block/myoverview/components/filteroptions/filteroptions.html @@ -0,0 +1,56 @@ + + +

{{ 'core.courses.filtermycourses' | translate }}

+ + + + + +
+
+ + + + + {{'addon.block_myoverview.allincludinghidden' | translate}} + + + + {{'addon.block_myoverview.inprogress' | translate}} + + + + {{'addon.block_myoverview.future' | translate}} + + + + {{'addon.block_myoverview.past' | translate}} + + + + + + + + {{customOption.name}} + + + + + + + {{ 'addon.block_myoverview.favouritesonly' | translate }} + + + + + {{ 'addon.block_myoverview.aria:hiddencourses' | translate }} + + + + + + + {{ 'core.applyfilters' | translate }} + + diff --git a/src/addons/block/myoverview/components/filteroptions/filteroptions.ts b/src/addons/block/myoverview/components/filteroptions/filteroptions.ts new file mode 100644 index 000000000..93b9d753a --- /dev/null +++ b/src/addons/block/myoverview/components/filteroptions/filteroptions.ts @@ -0,0 +1,44 @@ +// (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, Input } from '@angular/core'; +import { ModalController } from '@singletons'; +import { AddonBlockMyOverviewFilterOptions } from '../myoverview/myoverview'; + +/** + * Component to render a my overview filter options. + */ +@Component({ + selector: 'addon-block-myoverview-filter-options', + templateUrl: 'filteroptions.html', +}) +export class AddonBlockMyOverviewFilterOptionsComponent { + + @Input() options!: AddonBlockMyOverviewFilterOptions; + + /** + * Appl filters. + */ + apply(): void { + ModalController.dismiss(this.options); + } + + /** + * Close modal. + */ + closeModal(): void { + ModalController.dismiss(); + } + +} diff --git a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html index 422f1b3e7..121ae723b 100644 --- a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html +++ b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html @@ -4,105 +4,81 @@
-
- + - - - {{prefetchCoursesData[timeSelectorFilter].badge}} + + {{prefetchCoursesData.badge}} - +
- - - - - - - - -
-
- - - - {{ 'addon.block_myoverview.allincludinghidden' | translate }} - - - {{ 'addon.block_myoverview.all' | translate }} - - - {{ 'addon.block_myoverview.inprogress' | translate }} - - - {{ 'addon.block_myoverview.future' | translate }} - - - {{ 'addon.block_myoverview.past' | translate }} - - - - {{ customOption.name }} - - - - {{ 'addon.block_myoverview.favourites' | translate }} - - - {{ 'addon.block_myoverview.hiddencourses' | translate }} - - -
-
- - - - {{ 'addon.block_myoverview.'+layout | translate }} + + + + + + + + + + + + + + {{'addon.block_myoverview.title' | translate}} - - -
- - - - + + {{'addon.block_myoverview.shortname' | translate}} + + + {{'addon.block_myoverview.lastaccessed' | translate}} + + + + + + + + {{ 'addon.block_myoverview.'+layout | translate }} + + + + + + + + + + + + -
- +
+ + [showDownload]="downloadCourseEnabled && downloadEnabled" [layout]="layouts.selected"> diff --git a/src/addons/block/myoverview/components/myoverview/myoverview.ts b/src/addons/block/myoverview/components/myoverview/myoverview.ts index 0f3cf060f..a3970be95 100644 --- a/src/addons/block/myoverview/components/myoverview/myoverview.ts +++ b/src/addons/block/myoverview/components/myoverview/myoverview.ts @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Input, OnDestroy, ViewChild, OnChanges, SimpleChange } from '@angular/core'; -import { IonSearchbar } from '@ionic/angular'; +import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChange } from '@angular/core'; +import { ModalOptions } from '@ionic/core'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; @@ -27,8 +27,10 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; +import { AddonBlockMyOverviewFilterOptionsComponent } from '../filteroptions/filteroptions'; +import { IonSearchbar } from '@ionic/angular'; -const FILTER_PRIORITY = ['all', 'allincludinghidden', 'inprogress', 'future', 'past', 'favourite', 'hidden', 'custom']; +const FILTER_PRIORITY: AddonBlockMyOverviewTimeFilters[] = ['all', 'inprogress', 'future', 'past']; /** * Component to render a my overview block. @@ -39,107 +41,62 @@ const FILTER_PRIORITY = ['all', 'allincludinghidden', 'inprogress', 'future', 'p }) export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy { - @ViewChild('searchbar') searchbar?: IonSearchbar; @Input() downloadEnabled = false; - courses = { - filter: '', - all: [], - allincludinghidden: [], - past: [], - inprogress: [], - future: [], - favourite: [], - hidden: [], - custom: [], // Leave it empty to avoid download all those courses. - }; - - customFilter: { - name: string; - value: string; - }[] = []; - - timeSelectorFilter = 'inprogress'; - sort = 'fullname'; - currentSite?: CoreSite; filteredCourses: CoreEnrolledCourseDataWithOptions[] = []; - prefetchCoursesData: Record = { - all: { - icon: '', - statusTranslatable: 'core.loading', - status: '', - loading: true, - }, - allincludinghidden: { - icon: '', - statusTranslatable: 'core.loading', - status: '', - loading: true, - }, - inprogress: { - icon: '', - statusTranslatable: 'core.loading', - status: '', - loading: true, - }, - past: { - icon: '', - statusTranslatable: 'core.loading', - status: '', - loading: true, - }, - future: { - icon: '', - statusTranslatable: 'core.loading', - status: '', - loading: true, - }, - favourite: { - icon: '', - statusTranslatable: 'core.loading', - status: '', - loading: true, - }, - hidden: { - icon: '', - statusTranslatable: 'core.loading', - status: '', - loading: true, - }, - custom: { - icon: '', - statusTranslatable: '', - status: '', - loading: false, - }, // Leave it empty to avoid download all those courses. + + prefetchCoursesData: CorePrefetchStatusInfo = { + icon: '', + statusTranslatable: 'core.loading', + status: '', + loading: true, }; - showFilters = { // Options are show, disabled, hidden. - all: 'show', - allincludinghidden: 'show', - past: 'show', - inprogress: 'show', - future: 'show', - favourite: 'show', - hidden: 'show', - custom: 'hidden', - }; - - showFilter = false; - showTimeSelectorFilter = false; - showSortFilter = false; downloadCourseEnabled = false; downloadCoursesEnabled = false; - showSortByShortName = false; - layouts: AddonBlockMyOverviewLayouts[] = []; - selectedLayout: AddonBlockMyOverviewLayouts = 'card'; + filters: AddonBlockMyOverviewFilterOptions = { + enabled: false, + show: { // Options are visible, disabled, hidden. + all: true, + past: true, + inprogress: true, + future: true, + favourite: true, + hidden: true, + custom: false, + }, + timeFilterSelected: 'inprogress', + favouriteSelected: false, + hiddenSelected: false, + customFilters: [], + count: 0, + }; + filterModalOptions: ModalOptions = { + component: AddonBlockMyOverviewFilterOptionsComponent, + }; + + layouts: AddonBlockMyOverviewLayoutOptions = { + options: [], + selected: 'card', + }; + + sort: AddonBlockMyOverviewSortOptions = { + shortnameEnabled: false, + selected: 'fullname', + enabled: false, + }; + + textFilter = ''; + hasCourses = false; + + protected currentSite!: CoreSite; + protected allCourses: CoreEnrolledCourseDataWithOptions[] = []; protected prefetchIconsInitialized = false; protected isDestroyed = false; protected coursesObserver?: CoreEventObserver; protected updateSiteObserver?: CoreEventObserver; - protected courseIds: number[] = []; protected fetchContentDefaultError = 'Error getting my overview data.'; constructor() { @@ -147,7 +104,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem } /** - * Component being initialized. + * @inheritdoc */ async ngOnInit(): Promise { // Refresh the enabled flags if enabled. @@ -164,10 +121,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem this.coursesObserver = CoreEvents.on( CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, (data) => { - - if (this.shouldRefreshOnUpdatedEvent(data)) { - this.refreshCourseList(); - } + this.refreshCourseList(data); }, CoreSites.getCurrentSiteId(), ); @@ -175,25 +129,62 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem this.currentSite = CoreSites.getRequiredCurrentSite(); const promises: Promise[] = []; - promises.push(this.currentSite.getLocalSiteConfig('AddonBlockMyOverviewSort', this.sort).then((value) => { - this.sort = value; - return; - })); promises.push(this.currentSite.getLocalSiteConfig( - 'AddonBlockMyOverviewFilter', - this.timeSelectorFilter, + 'AddonBlockMyOverviewSort', + this.sort.selected, ).then((value) => { - this.timeSelectorFilter = value; + this.sort.selected = value; return; })); promises.push(this.currentSite.getLocalSiteConfig( 'AddonBlockMyOverviewLayout', - this.selectedLayout, + this.layouts.selected, ).then((value) => { - this.selectedLayout = value; + this.layouts.selected = value; + + return; + })); + + // Wait for the migration. + await this.currentSite.getLocalSiteConfig( + 'AddonBlockMyOverviewFilter', + this.filters.timeFilterSelected, + ).then(async (value) => { + if (FILTER_PRIORITY.includes(value as AddonBlockMyOverviewTimeFilters)) { + this.filters.timeFilterSelected = value as AddonBlockMyOverviewTimeFilters; + + return; + } + + // Migrate setting. + this.filters.hiddenSelected = value == 'allincludinghidden' || value == 'hidden'; + + if (value == 'favourite') { + this.filters.favouriteSelected = true; + } else { + this.filters.favouriteSelected = false; + } + + return await this.saveFilters('all'); + }); + + promises.push(this.currentSite.getLocalSiteConfig( + 'AddonBlockMyOverviewFavouriteFilter', + this.filters.favouriteSelected ? 1 : 0, + ).then((value) => { + this.filters.favouriteSelected = value == 1; + + return; + })); + + promises.push(this.currentSite.getLocalSiteConfig( + 'AddonBlockMyOverviewHiddenFilter', + this.filters.hiddenSelected ? 1 : 0, + ).then((value) => { + this.filters.hiddenSelected = value == 1; return; })); @@ -204,7 +195,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem } /** - * Detect changes on input properties. + * @inheritdoc */ ngOnChanges(changes: {[name: string]: SimpleChange}): void { if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) { @@ -214,21 +205,35 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem } /** - * Perform the invalidate content function. - * - * @return Resolved when done. + * @inheritdoc */ protected async invalidateContent(): Promise { + const courseIds = this.allCourses.map((course) => course.id); + + await this.invalidateCourses(courseIds); + } + + /** + * Helper function to invalidate only selected courses. + * + * @param courseIds Course Id array. + * @return Promise resolved when done. + */ + protected async invalidateCourses(courseIds: number[]): Promise { const promises: Promise[] = []; // Invalidate course completion data. promises.push(CoreCourses.invalidateUserCourses().finally(() => - CoreUtils.allPromises(this.courseIds.map((courseId) => + CoreUtils.allPromises(courseIds.map((courseId) => AddonCourseCompletion.invalidateCourseCompletion(courseId))))); - promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions()); - if (this.courseIds.length > 0) { - promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIds.join(','))); + if (courseIds.length == 1) { + promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(courseIds[0])); + } else { + promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions()); + } + if (courseIds.length > 0) { + promises.push(CoreCourses.invalidateCoursesByField('ids', courseIds.join(','))); } await CoreUtils.allPromises(promises).finally(() => { @@ -242,101 +247,88 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem protected async fetchContent(refresh?: boolean): Promise { const config = this.block.configsRecord; - this.loadLayouts(config?.layouts?.value.split(',')); - const showCategories = config?.displaycategories?.value == '1'; - const courses = await CoreCoursesHelper.getUserCoursesWithOptions(this.sort, undefined, undefined, showCategories, { - readingStrategy: refresh ? CoreSitesReadingStrategy.PREFER_NETWORK : undefined, - }); + this.allCourses = await CoreCoursesHelper.getUserCoursesWithOptions( + this.sort.selected, + undefined, + undefined, + showCategories, + { + readingStrategy: refresh ? CoreSitesReadingStrategy.PREFER_NETWORK : undefined, + }, + ); + + this.hasCourses = this.allCourses.length > 0; + + this.loadSort(); + this.loadLayouts(config?.layouts?.value.split(',')); + this.loadFilters(config); + } + + /** + * Load sort. + */ + protected loadSort(): void { + const sampleCourse = this.allCourses[0]; // Check to show sort by short name only if the text is visible. - if (courses.length > 0) { - const sampleCourse = courses[0]; - this.showSortByShortName = !!sampleCourse.displayname && !!sampleCourse.shortname && - sampleCourse.fullname != sampleCourse.displayname; - } + this.sort.shortnameEnabled = !!sampleCourse?.displayname && !!sampleCourse?.shortname && + sampleCourse?.fullname != sampleCourse?.displayname; // Rollback to sort by full name if user is sorting by short name then Moodle web change the config. - if (!this.showSortByShortName && this.sort === 'shortname') { - this.switchSort('fullname'); + if (!this.sort.shortnameEnabled && this.sort.selected === 'shortname') { + this.saveSort('fullname'); } - this.courseIds = courses.map((course) => course.id); + this.sort.enabled = sampleCourse?.lastaccess !== undefined; + } - this.showSortFilter = courses.length > 0 && typeof courses[0].lastaccess != 'undefined'; + /** + * Load filters. + * + * @param config Block configuration. + */ + protected loadFilters( + config?: Record, + ): void { + this.textFilter = ''; - this.initCourseFilters(courses); + const sampleCourse = this.allCourses[0]; - this.courses.filter = ''; - this.showFilter = false; + // Do not show hidden if config it's not present (before 3.8) but if hidden is enabled. + this.filters.show.hidden = + config?.displaygroupingallincludinghidden?.value == '1' || + sampleCourse.hidden !== undefined && (!config || config.displaygroupinghidden?.value == '1'); - this.showFilters.all = this.getShowFilterValue( - !config || config.displaygroupingall?.value == '1', - this.courses.all.length === 0, - ); - // Do not show allincludinghiddenif config it's not present (before 3.8). - this.showFilters.allincludinghidden = - this.getShowFilterValue( - config?.displaygroupingallincludinghidden?.value == '1', - this.courses.allincludinghidden.length === 0, - ); + this.filters.show.all = !config || config.displaygroupingall?.value == '1'; + this.filters.show.inprogress = !config || config.displaygroupinginprogress?.value == '1'; + this.filters.show.past = !config || config.displaygroupingpast?.value == '1'; + this.filters.show.future = !config || config.displaygroupingfuture?.value == '1'; - this.showFilters.inprogress = this.getShowFilterValue( - !config || config.displaygroupinginprogress?.value == '1', - this.courses.inprogress.length === 0, - ); - this.showFilters.past = this.getShowFilterValue( - !config || config.displaygroupingpast?.value == '1', - this.courses.past.length === 0, - ); - this.showFilters.future = this.getShowFilterValue( - !config || config.displaygroupingfuture?.value == '1', - this.courses.future.length === 0, - ); + this.filters.show.favourite = sampleCourse.isfavourite !== undefined && + (!config || config.displaygroupingstarred?.value == '1' || config.displaygroupingfavourites?.value == '1'); - this.showTimeSelectorFilter = courses.length > 0 && (this.courses.past.length > 0 || this.courses.future.length > 0 || - typeof courses[0].enddate != 'undefined'); + this.filters.show.custom = config?.displaygroupingcustomfield?.value == '1' && !!config?.customfieldsexport?.value; - this.showFilters.hidden = this.getShowFilterValue( - this.showTimeSelectorFilter && typeof courses[0].hidden != 'undefined' && - (!config || config.displaygroupinghidden?.value == '1'), - this.courses.hidden.length === 0, - ); + this.filters.customFilters = this.filters.show.custom + ? CoreTextUtils.parseJSON(config?.customfieldsexport?.value || '[]', []) + : []; - this.showFilters.favourite = this.getShowFilterValue( - this.showTimeSelectorFilter && typeof courses[0].isfavourite != 'undefined' && - (!config || config.displaygroupingstarred?.value == '1' || config.displaygroupingfavourites?.value == '1'), - this.courses.favourite.length === 0, - ); + // Check if any selector is shown and not disabled. + this.filters.enabled = Object.keys(this.filters.show).some((key) => this.filters.show[key]); - this.showFilters.custom = this.getShowFilterValue( - this.showTimeSelectorFilter && config?.displaygroupingcustomfield?.value == '1' && !!config?.customfieldsexport?.value, - false, - ); - if (this.showFilters.custom == 'show') { - this.customFilter = CoreTextUtils.parseJSON(config?.customfieldsexport?.value || '[]', []); - } else { - this.customFilter = []; + if (!this.filters.enabled) { + // All filters disabled, display all the courses. + this.filters.show.all = true; + this.saveFilters('all'); } - if (this.showTimeSelectorFilter) { - // Check if any selector is shown and not disabled. - this.showTimeSelectorFilter = Object.keys(this.showFilters).some((key) => this.showFilters[key] == 'show'); + this.filterModalOptions.componentProps = { + options: Object.assign({}, this.filters), + }; - if (!this.showTimeSelectorFilter) { - // All filters disabled, display all the courses. - this.showFilters.all = 'show'; - } - } - - if (!this.showTimeSelectorFilter) { - // No selector, display all the courses. - this.timeSelectorFilter = 'all'; - } - this.setCourseFilter(this.timeSelectorFilter); - - this.initPrefetchCoursesIcons(); + this.filterCourses(); } /** @@ -344,8 +336,14 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem * * @param layouts Config available layouts. */ - protected loadLayouts(layouts: string[] = []): void { - this.layouts = []; + protected loadLayouts(layouts?: string[]): void { + this.layouts.options = []; + + if (layouts === undefined) { + this.layouts.options = ['card', 'list']; + + return; + } layouts.forEach((layout) => { if (layout == '') { @@ -353,95 +351,71 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem } const validLayout: AddonBlockMyOverviewLayouts = layout == 'summary' ? 'list' : layout as AddonBlockMyOverviewLayouts; - if (!this.layouts.includes(validLayout)) { - this.layouts.push(validLayout); + if (!this.layouts.options.includes(validLayout)) { + this.layouts.options.push(validLayout); } }); // If no layout is available use card. - if (this.layouts.length == 0) { - this.layouts = ['card']; + if (this.layouts.options.length == 0) { + this.layouts.options = ['card']; } - if (!this.layouts.includes(this.selectedLayout)) { - this.selectedLayout = this.layouts[0]; + if (!this.layouts.options.includes(this.layouts.selected)) { + this.layouts.selected = this.layouts.options[0]; } } /** - * Helper function to help with filter values. - * - * @param showCondition If true, filter will be shown. - * @param disabledCondition If true, and showCondition is also met, it will be shown as disabled. - * @return show / disabled / hidden value. - */ - protected getShowFilterValue(showCondition: boolean, disabledCondition: boolean): string { - return showCondition ? (disabledCondition ? 'disabled' : 'show') : 'hidden'; - } - - /** - * Whether list should be refreshed based on a EVENT_MY_COURSES_UPDATED event. + * Refresh course list based on a EVENT_MY_COURSES_UPDATED event. * * @param data Event data. - * @return Whether to refresh. + * @return Promise resolved when done. */ - protected shouldRefreshOnUpdatedEvent(data: CoreCoursesMyCoursesUpdatedEventData): boolean { + protected async refreshCourseList(data: CoreCoursesMyCoursesUpdatedEventData): Promise { if (data.action == CoreCoursesProvider.ACTION_ENROL) { // Always update if user enrolled in a course. - return true; + return await this.refreshContent(); } + const course = this.allCourses.find((course) => course.id == data.courseId); if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED) { - // Update list when course state changes (favourite, hidden). - return true; + if (!course) { + // Not found, use WS update. + return await this.refreshContent(); + } + + if (data.state == CoreCoursesProvider.STATE_FAVOURITE) { + course.isfavourite = !!data.value; + } + + if (data.state == CoreCoursesProvider.STATE_HIDDEN) { + course.hidden = !!data.value; + } + + await this.invalidateCourses([course.id]); + await this.filterCourses(); } if (data.action == CoreCoursesProvider.ACTION_VIEW && data.courseId != CoreSites.getCurrentSiteHomeId()) { - // User viewed a course. If it isn't the most recent accessed course, update the list. - let recentAccessedCourse: CoreEnrolledCourseDataWithOptions | undefined; - if (this.sort == 'lastaccess') { - recentAccessedCourse = this.courses.allincludinghidden[0]; - } else { - recentAccessedCourse = Array.from(this.courses.allincludinghidden) - .sort((a, b) => (b.lastaccess || 0) - (a.lastaccess || 0))[0]; + if (!course) { + // Not found, use WS update. + return await this.refreshContent(); } - if (recentAccessedCourse && data.courseId != recentAccessedCourse.id) { - return true; - } - } + course.lastaccess = CoreTimeUtils.timestamp(); - return false; - } - - /** - * The filter has changed. - * - * @param Received Event. - */ - filterChanged(event: Event): void { - const target = event?.target || null; - - const newValue = target?.value.trim().toLowerCase(); - if (!newValue || this.courses.allincludinghidden.length <= 0) { - this.filteredCourses = this.courses.allincludinghidden; - } else { - // Use displayname if available, or fullname if not. - if (this.courses.allincludinghidden.length > 0 && - typeof this.courses.allincludinghidden[0].displayname != 'undefined') { - this.filteredCourses = this.courses.allincludinghidden.filter((course) => - course.displayname && course.displayname.toLowerCase().indexOf(newValue) > -1); - } else { - this.filteredCourses = this.courses.allincludinghidden.filter((course) => - course.fullname.toLowerCase().indexOf(newValue) > -1); - } + await this.invalidateCourses([course.id]); + await this.filterCourses(); } } /** * Initialize the prefetch icon for selected courses. + * + * @return Promise resolved when done. */ - protected initPrefetchCoursesIcons(): void { + protected async initPrefetchCoursesIcons(): Promise { if (this.prefetchIconsInitialized || !this.downloadEnabled) { // Already initialized. return; @@ -449,10 +423,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem this.prefetchIconsInitialized = true; - Object.keys(this.prefetchCoursesData).forEach(async (filter) => { - this.prefetchCoursesData[filter] = - await CoreCourseHelper.initPrefetchCoursesIcons(this.courses[filter], this.prefetchCoursesData[filter]); - }); + this.prefetchCoursesData = await CoreCourseHelper.initPrefetchCoursesIcons(this.filteredCourses, this.prefetchCoursesData); } /** @@ -461,206 +432,218 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem * @return Promise resolved when done. */ async prefetchCourses(): Promise { - const selected = this.timeSelectorFilter; - const initialIcon = this.prefetchCoursesData[selected].icon; + const initialIcon = this.prefetchCoursesData.icon; try { - await CoreCourseHelper.prefetchCourses(this.courses[selected], this.prefetchCoursesData[selected]); + await CoreCourseHelper.prefetchCourses(this.filteredCourses, this.prefetchCoursesData); } catch (error) { if (!this.isDestroyed) { CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); - this.prefetchCoursesData[selected].icon = initialIcon; + this.prefetchCoursesData.icon = initialIcon; } } } /** - * Refresh the list of courses. + * Text filter changed. * - * @return Promise resolved when done. + * @param target Searchbar element. */ - protected async refreshCourseList(): Promise { - CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED); + filterTextChanged(target: IonSearchbar): void { + this.textFilter = target.value || ''; - await this.loadContent(true); - } - - /** - * The layout have changed. - * - * @param layout New layout. - */ - layoutChanged(layout: AddonBlockMyOverviewLayouts): void { - this.selectedLayout = layout; - - this.currentSite?.setLocalSiteConfig('AddonBlockMyOverviewLayout', layout); + this.filterCourses(); } /** * Set selected courses filter. - * - * @param filter Filter name to set. */ - protected async setCourseFilter(filter: string): Promise { - this.timeSelectorFilter = filter; + protected async filterCourses(): Promise { + this.filters.count = 0; - if (this.showFilters.custom == 'show' && filter.startsWith('custom-') && - typeof this.customFilter[filter.substr(7)] != 'undefined') { + let timeFilter = this.filters.timeFilterSelected; - const filterName = this.block.configsRecord!.customfiltergrouping.value; - const filterValue = this.customFilter[filter.substr(7)].value; + // Filter is not active, take the first active or all. + if (!this.filters.show[timeFilter]) { + timeFilter = FILTER_PRIORITY.find((name) => this.filters.show[name]) || 'all'; + + this.saveFilters(timeFilter); + } + + if (timeFilter !== 'all') { + this.filters.count++; + } + + this.filteredCourses = this.allCourses; + + const customFilterName = this.block.configsRecord?.customfiltergrouping.value; + const customFilterValue = this.filters.customSelected; + if (customFilterName && this.filters.show.custom && customFilterValue !== undefined) { + this.filters.count++; this.loaded = false; try { - const courses = await CoreCourses.getEnrolledCoursesByCustomField(filterName, filterValue); + const courses = 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); - this.filteredCourses = this.courses.allincludinghidden.filter((allCourse) => - courseIds.indexOf(allCourse.id) !== -1); + this.filteredCourses = this.filteredCourses.filter((course) => courseIds.includes(course.id)); } catch (error) { CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError); } finally { this.loaded = true; } + } + const onlyFavourite = this.filters.show.favourite && this.filters.favouriteSelected; + if (onlyFavourite) { + this.filters.count++; + } + + const showHidden = this.filters.show.hidden && this.filters.hiddenSelected; + if (showHidden) { + this.filters.count++; + } + + // Time filter, favourite and hidden. + const today = CoreTimeUtils.timestamp(); + this.filteredCourses = this.filteredCourses.filter((course) => { + let include = timeFilter == 'all'; + + if (!include) { + if ((course.enddate && course.enddate < today) || course.completed) { + // Courses that have already ended. + include = timeFilter == 'past'; + } else if (course.startdate && course.startdate > today) { + // Courses that have not started yet. + include = timeFilter == 'future'; + } else { + // Courses still in progress. + include = timeFilter == 'inprogress'; + } + } + + if (onlyFavourite) { + include = include && !!course.isfavourite; + } + + if (!showHidden) { + include = include && !course.hidden; + } + + return include; + }); + + // Text filter. + const value = this.textFilter.trim().toLowerCase(); + if (value != '' && this.filteredCourses.length > 0) { + // Use displayname if available, or fullname if not. + if (this.filteredCourses[0].displayname !== undefined) { + this.filteredCourses = this.filteredCourses.filter((course) => + course.displayname && course.displayname.toLowerCase().indexOf(value) > -1); + } else { + this.filteredCourses = this.filteredCourses.filter((course) => + course.fullname.toLowerCase().indexOf(value) > -1); + } + } + + this.sortCourses(this.sort.selected); + + // Refresh prefetch data (if enabled). + this.prefetchIconsInitialized = false; + this.initPrefetchCoursesIcons(); + } + + /** + * Sort courses + * + * @param sort Sort by value. + */ + sortCourses(sort: string): void { + if (!this.sort.enabled) { return; } - // Only save the filter if not a custom one. - this.currentSite?.setLocalSiteConfig('AddonBlockMyOverviewFilter', filter); - - if (this.showFilters[filter] == 'show') { - this.filteredCourses = this.courses[filter]; - } else { - const activeFilter = FILTER_PRIORITY.find((name) => this.showFilters[name] == 'show'); - - if (activeFilter) { - this.setCourseFilter(activeFilter); - } - } - } - - /** - * The time selector courses filter have changed. - * - * @param filter New filter - */ - timeSelectorChanged(filter: string): void { - this.timeSelectorFilter = filter; - this.setCourseFilter(this.timeSelectorFilter); - } - - /** - * Init courses filters. - * - * @param courses Courses to filter. - */ - initCourseFilters(courses: CoreEnrolledCourseDataWithOptions[]): void { - this.courses.allincludinghidden = courses; - - if (this.showSortFilter) { - if (this.sort == 'lastaccess') { - courses.sort((a, b) => (b.lastaccess || 0) - (a.lastaccess || 0)); - } else if (this.sort == 'fullname') { - courses.sort((a, b) => { - const compareA = a.fullname.toLowerCase(); - const compareB = b.fullname.toLowerCase(); - - return compareA.localeCompare(compareB); - }); - } else if (this.sort == 'shortname') { - courses.sort((a, b) => { - const compareA = a.shortname.toLowerCase(); - const compareB = b.shortname.toLowerCase(); - - return compareA.localeCompare(compareB); - }); - } + if (this.sort.selected != sort) { + this.saveSort(sort); } - this.courses.all = []; - this.courses.past = []; - this.courses.inprogress = []; - this.courses.future = []; - this.courses.favourite = []; - this.courses.hidden = []; + if (this.sort.selected == 'lastaccess') { + this.filteredCourses.sort((a, b) => (b.lastaccess || 0) - (a.lastaccess || 0)); + } else if (this.sort.selected == 'fullname') { + this.filteredCourses.sort((a, b) => { + const compareA = a.fullname.toLowerCase(); + const compareB = b.fullname.toLowerCase(); - const today = CoreTimeUtils.timestamp(); - courses.forEach((course) => { - if (course.hidden) { - this.courses.hidden.push(course); - } else { - this.courses.all.push(course); + return compareA.localeCompare(compareB); + }); + } else if (this.sort.selected == 'shortname') { + this.filteredCourses.sort((a, b) => { + const compareA = a.shortname.toLowerCase(); + const compareB = b.shortname.toLowerCase(); - if ((course.enddate && course.enddate < today) || course.completed) { - // Courses that have already ended. - this.courses.past.push(course); - } else if (course.startdate && course.startdate > today) { - // Courses that have not started yet. - this.courses.future.push(course); - } else { - // Courses still in progress. - this.courses.inprogress.push(course); - } - - if (course.isfavourite) { - this.courses.favourite.push(course); - } - } - }); - - this.setCourseFilter(this.timeSelectorFilter); - } - - /** - * The selected courses sort filter have changed. - * - * @param sort New sorting. - */ - switchSort(sort: string): void { - this.sort = sort; - this.currentSite?.setLocalSiteConfig('AddonBlockMyOverviewSort', this.sort); - this.initCourseFilters(this.courses.allincludinghidden); - } - - /** - * Show or hide the filter. - */ - switchFilter(): void { - this.showFilter = !this.showFilter; - this.courses.filter = ''; - - if (this.showFilter) { - this.filteredCourses = this.courses.allincludinghidden; - } else { - this.setCourseFilter(this.timeSelectorFilter); - } - } - - /** - * Popover closed after clicking switch filter. - */ - switchFilterClosed(): void { - if (this.showFilter) { - setTimeout(() => { - this.searchbar?.setFocus(); + return compareA.localeCompare(compareB); }); } } /** - * If switch button that enables the filter input is shown or not. + * Saves filters value. * - * @return If switch button that enables the filter input is shown or not. + * @param timeFilter New time filter. + * @return Promise resolved when done. */ - showFilterSwitchButton(): boolean { - return this.loaded && this.courses.allincludinghidden && this.courses.allincludinghidden.length > 5; + async saveFilters(timeFilter: AddonBlockMyOverviewTimeFilters): Promise { + this.filters.timeFilterSelected = timeFilter; + + this.filterModalOptions.componentProps = { + options: Object.assign({}, this.filters), + }; + + await Promise.all([ + this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewFilter', this.filters.timeFilterSelected), + this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewFavouriteFilter', this.filters.favouriteSelected ? 1 : 0), + this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewHiddenFilter', this.filters.hiddenSelected ? 1 : 0), + ]); } /** - * Component being destroyed. + * Saves layout value. + * + * @param layout New layout. + * @return Promise resolved when done. + */ + async saveLayout(layout: AddonBlockMyOverviewLayouts): Promise { + this.layouts.selected = layout; + + await this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewLayout', this.layouts.selected); + } + + /** + * Saves sort courses value. + * + * @param sort New sorting. + * @return Promise resolved when done. + */ + async saveSort(sort: string): Promise { + this.sort.selected = sort; + + await this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewSort', this.sort.selected); + } + + /** + * Opens display Options modal. + * + * @return Promise resolved when done. + */ + filterOptionsChanged(modalData: AddonBlockMyOverviewFilterOptions): void { + this.filters = modalData; + this.saveFilters(this.filters.timeFilterSelected); + this.filterCourses(); + } + + /** + * @inheritdoc */ ngOnDestroy(): void { this.isDestroyed = true; @@ -671,3 +654,37 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem } type AddonBlockMyOverviewLayouts = 'card'|'list'; +type AddonBlockMyOverviewTimeFilters = 'all'|'inprogress'|'future'|'past'; + +export type AddonBlockMyOverviewFilterOptions = { + enabled: boolean; + show: { + all: boolean; + inprogress: boolean; + future: boolean; + past: boolean; + favourite: boolean; + hidden: boolean; + custom: boolean; + }; + timeFilterSelected: AddonBlockMyOverviewTimeFilters; + favouriteSelected: boolean; + hiddenSelected: boolean; + customFilters: { + name: string; + value: string; + }[]; + customSelected?: string; + count: number; +}; + +type AddonBlockMyOverviewLayoutOptions = { + options: AddonBlockMyOverviewLayouts[]; + selected: AddonBlockMyOverviewLayouts; +}; + +type AddonBlockMyOverviewSortOptions = { + shortnameEnabled: boolean; + selected: string; + enabled: boolean; +}; diff --git a/src/addons/block/myoverview/lang.json b/src/addons/block/myoverview/lang.json index 19cbb6144..9b0831599 100644 --- a/src/addons/block/myoverview/lang.json +++ b/src/addons/block/myoverview/lang.json @@ -1,10 +1,10 @@ { "all": "All (except removed from view)", "allincludinghidden": "All", + "aria:hiddencourses": "Show courses removed from view", "card": "Card", - "favourites": "Starred", + "favouritesonly": "Show starred courses only", "future": "Future", - "hiddencourses": "Removed from view", "inprogress": "In progress", "lastaccessed": "Last accessed", "list": "List", diff --git a/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html b/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html index 772b58c0b..73b78fa5a 100644 --- a/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html +++ b/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html @@ -9,7 +9,7 @@ - +

@@ -32,7 +32,7 @@

- + diff --git a/src/core/lang.json b/src/core/lang.json index 99182daa6..793e0da74 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -7,6 +7,7 @@ "all": "All", "allgroups": "All groups", "allparticipants": "All participants", + "applyfilters": "Apply filters", "answer": "Answer", "answered": "Answered", "areyousure": "Are you sure?",