diff --git a/scripts/langindex.json b/scripts/langindex.json index a8d9ddc2b..df6d554e3 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", @@ -1553,6 +1556,7 @@ "core.courses.password": "local_moodlemobileapp", "core.courses.paymentrequired": "moodle", "core.courses.paypalaccepted": "enrol_paypal", + "core.courses.refreshcourses": "local_moodlemobileapp", "core.courses.reload": "moodle", "core.courses.removefromfavourites": "block_myoverview", "core.courses.search": "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 2394d5a8b..6c3d9ffa6 100644 --- a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html +++ b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html @@ -4,97 +4,82 @@
-
- -
-
- - - - {{ '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.title' | translate}} + + + {{'addon.block_myoverview.shortname' | translate}} + + + {{'addon.block_myoverview.lastaccessed' | translate}} + + + + + + + + {{ 'addon.block_myoverview.'+layout | translate }} + + + + + + + + + + + + -
- +
+ - - - + + + diff --git a/src/addons/block/myoverview/components/myoverview/myoverview.ts b/src/addons/block/myoverview/components/myoverview/myoverview.ts index 1d71653d0..d99e22201 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,104 +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; - }[] = []; - - selectedFilter = 'inprogress'; - sort = 'fullname'; - currentSite?: CoreSite; filteredCourses: CoreEnrolledCourseDataWithOptions[] = []; - prefetchCoursesData = { - 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; - showSelectorFilter = false; - showSortFilter = false; downloadCourseEnabled = false; downloadCoursesEnabled = false; - showSortByShortName = false; + 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() { @@ -144,7 +104,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem } /** - * Component being initialized. + * @inheritdoc */ async ngOnInit(): Promise { // Refresh the enabled flags if enabled. @@ -161,29 +121,73 @@ 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(), ); - this.currentSite = CoreSites.getCurrentSite(); + this.currentSite = CoreSites.getRequiredCurrentSite(); const promises: Promise[] = []; - if (this.currentSite) { - promises.push(this.currentSite.getLocalSiteConfig('AddonBlockMyOverviewSort', this.sort).then((value) => { - this.sort = value; + + promises.push(this.currentSite.getLocalSiteConfig( + 'AddonBlockMyOverviewSort', + this.sort.selected, + ).then((value) => { + this.sort.selected = value; + + return; + })); + + promises.push(this.currentSite.getLocalSiteConfig( + 'AddonBlockMyOverviewLayout', + this.layouts.selected, + ).then((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; - })); - promises.push(this.currentSite.getLocalSiteConfig('AddonBlockMyOverviewFilter', this.selectedFilter).then((value) => { - this.selectedFilter = value; + } - 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; + })); Promise.all(promises).finally(() => { super.ngOnInit(); @@ -191,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) { @@ -201,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(() => { @@ -231,173 +249,173 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem 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.showSortFilter = courses.length > 0 && typeof courses[0].lastaccess != 'undefined'; - - this.initCourseFilters(courses); - - this.courses.filter = ''; - this.showFilter = false; - - 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.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.showSelectorFilter = courses.length > 0 && (this.courses.past.length > 0 || this.courses.future.length > 0 || - typeof courses[0].enddate != 'undefined'); - - this.showFilters.hidden = this.getShowFilterValue( - this.showSelectorFilter && typeof courses[0].hidden != 'undefined' && - (!config || config.displaygroupinghidden?.value == '1'), - this.courses.hidden.length === 0, - ); - - this.showFilters.favourite = this.getShowFilterValue( - this.showSelectorFilter && typeof courses[0].isfavourite != 'undefined' && - (!config || config.displaygroupingstarred?.value == '1' || config.displaygroupingfavourites?.value == '1'), - this.courses.favourite.length === 0, - ); - - this.showFilters.custom = this.getShowFilterValue( - this.showSelectorFilter && 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.showSelectorFilter) { - // Check if any selector is shown and not disabled. - this.showSelectorFilter = Object.keys(this.showFilters).some((key) => this.showFilters[key] == 'show'); - - if (!this.showSelectorFilter) { - // All filters disabled, display all the courses. - this.showFilters.all = 'show'; - } - } - - if (!this.showSelectorFilter) { - // No selector, display all the courses. - this.selectedFilter = 'all'; - } - this.setCourseFilter(this.selectedFilter); - - this.initPrefetchCoursesIcons(); + this.sort.enabled = sampleCourse?.lastaccess !== undefined; } /** - * Helper function to help with filter values. + * Load filters. * - * @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. + * @param config Block configuration. */ - protected getShowFilterValue(showCondition: boolean, disabledCondition: boolean): string { - return showCondition ? (disabledCondition ? 'disabled' : 'show') : 'hidden'; + protected loadFilters( + config?: Record, + ): void { + this.textFilter = ''; + + const sampleCourse = this.allCourses[0]; + + // 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.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.filters.show.favourite = sampleCourse.isfavourite !== undefined && + (!config || config.displaygroupingstarred?.value == '1' || config.displaygroupingfavourites?.value == '1'); + + this.filters.show.custom = config?.displaygroupingcustomfield?.value == '1' && !!config?.customfieldsexport?.value; + + this.filters.customFilters = this.filters.show.custom + ? CoreTextUtils.parseJSON(config?.customfieldsexport?.value || '[]', []) + : []; + + // Check if any selector is shown and not disabled. + this.filters.enabled = Object.keys(this.filters.show).some((key) => this.filters.show[key]); + + if (!this.filters.enabled) { + // All filters disabled, display all the courses. + this.filters.show.all = true; + this.saveFilters('all'); + } + + this.filterModalOptions.componentProps = { + options: Object.assign({}, this.filters), + }; + + this.filterCourses(); } /** - * Whether list should be refreshed based on a EVENT_MY_COURSES_UPDATED event. + * Load block layouts. + * + * @param layouts Config available layouts. + */ + protected loadLayouts(layouts?: string[]): void { + this.layouts.options = []; + + if (layouts === undefined) { + this.layouts.options = ['card', 'list']; + + return; + } + + layouts.forEach((layout) => { + if (layout == '') { + return; + } + + const validLayout: AddonBlockMyOverviewLayouts = layout == 'summary' ? 'list' : layout as AddonBlockMyOverviewLayouts; + if (!this.layouts.options.includes(validLayout)) { + this.layouts.options.push(validLayout); + } + }); + + // If no layout is available use card. + if (this.layouts.options.length == 0) { + this.layouts.options = ['card']; + } + + if (!this.layouts.options.includes(this.layouts.selected)) { + this.layouts.selected = this.layouts.options[0]; + } + } + + /** + * 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 { + async initPrefetchCoursesIcons(): Promise { if (this.prefetchIconsInitialized || !this.downloadEnabled) { // Already initialized. return; @@ -405,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); } /** @@ -417,195 +432,218 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem * @return Promise resolved when done. */ async prefetchCourses(): Promise { - const selected = this.selectedFilter; - 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 selected courses filter have changed. - * - * @param filter New filter - */ - selectedChanged(filter: string): void { - this.selectedFilter = filter; - this.setCourseFilter(this.selectedFilter); + this.filterCourses(); } /** * Set selected courses filter. - * - * @param filter Filter name to set. */ - protected async setCourseFilter(filter: string): Promise { - this.selectedFilter = 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); - } - } - } - - /** - * 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.selectedFilter); - } - - /** - * 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.selectedFilter); - } - } - - /** - * 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; @@ -614,3 +652,39 @@ 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 7bca82636..9b0831599 100644 --- a/src/addons/block/myoverview/lang.json +++ b/src/addons/block/myoverview/lang.json @@ -1,11 +1,13 @@ { "all": "All (except removed from view)", "allincludinghidden": "All", - "favourites": "Starred", + "aria:hiddencourses": "Show courses removed from view", + "card": "Card", + "favouritesonly": "Show starred courses only", "future": "Future", - "hiddencourses": "Removed from view", "inprogress": "In progress", "lastaccessed": "Last accessed", + "list": "List", "nocourses": "No courses", "past": "Past", "pluginname": "Course overview", diff --git a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html index d4a503b64..6f10e6e92 100644 --- a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html +++ b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html @@ -3,20 +3,6 @@

{{ 'addon.block_recentlyaccessedcourses.pluginname' | translate }}

-
- - - - - {{prefetchCoursesData.badge}} - - -
-
@@ -25,16 +11,13 @@ -
+
- +
diff --git a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts index 4d31b6553..b6aef6614 100644 --- a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts +++ b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, OnDestroy, Input, OnChanges, SimpleChange } from '@angular/core'; +import { Component, OnInit, OnDestroy, Input } from '@angular/core'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; import { @@ -21,13 +21,11 @@ import { CoreCourses, CoreCourseSummaryData, } from '@features/courses/services/courses'; -import { CoreCourseSearchedDataWithExtraInfoAndOptions, CoreCoursesHelper } from '@features/courses/services/courses-helper'; -import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; +import { CoreCourseSearchedDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; import { CoreUtils } from '@services/utils/utils'; -import { CoreDomUtils } from '@services/utils/dom'; /** * Component to render a recent courses block. @@ -36,24 +34,15 @@ import { CoreDomUtils } from '@services/utils/dom'; selector: 'addon-block-recentlyaccessedcourses', templateUrl: 'addon-block-recentlyaccessedcourses.html', }) -export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy { +export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy { @Input() downloadEnabled = false; courses: (Omit & CoreCourseSearchedDataWithExtraInfoAndOptions)[] = []; - prefetchCoursesData: CorePrefetchStatusInfo = { - icon: '', - statusTranslatable: 'core.loading', - status: '', - loading: true, - badge: '', - }; downloadCourseEnabled = false; - downloadCoursesEnabled = false; scrollElementId!: string; - protected prefetchIconsInitialized = false; protected isDestroyed = false; protected coursesObserver?: CoreEventObserver; protected updateSiteObserver?: CoreEventObserver; @@ -74,12 +63,10 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom // Refresh the enabled flags if enabled. this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); - this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); // Refresh the enabled flags if site is updated. this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); - this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); }, CoreSites.getCurrentSiteId()); @@ -94,16 +81,6 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom super.ngOnInit(); } - /** - * @inheritdoc - */ - ngOnChanges(changes: {[name: string]: SimpleChange}): void { - if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) { - // Download all courses is enabled now, initialize it. - this.initPrefetchCoursesIcons(); - } - } - /** * @inheritdoc */ @@ -136,9 +113,7 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom promises.push(CoreCourses.invalidateCoursesByField('ids', courseIds.join(','))); } - await CoreUtils.allPromises(promises).finally(() => { - this.prefetchIconsInitialized = false; - }); + await CoreUtils.allPromises(promises); } /** @@ -170,24 +145,6 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom course.categoryname = ''; } }); - - await CoreCoursesHelper.loadCoursesColorAndImage(courses); - - this.initPrefetchCoursesIcons(); - } - - /** - * Initialize the prefetch icon for selected courses. - */ - protected async initPrefetchCoursesIcons(): Promise { - if (this.prefetchIconsInitialized || !this.downloadEnabled) { - // Already initialized. - return; - } - - this.prefetchIconsInitialized = true; - - this.prefetchCoursesData = await CoreCourseHelper.initPrefetchCoursesIcons(this.courses, this.prefetchCoursesData); } /** @@ -221,26 +178,6 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom data.state == CoreCoursesProvider.STATE_FAVOURITE && course) { course.isfavourite = !!data.value; await this.invalidateCourses([course.id]); - - this.initPrefetchCoursesIcons(); - } - } - - /** - * Prefetch all the shown courses. - * - * @return Promise resolved when done. - */ - async prefetchCourses(): Promise { - const initialIcon = this.prefetchCoursesData.icon; - - try { - await CoreCourseHelper.prefetchCourses(this.courses, this.prefetchCoursesData); - } catch (error) { - if (!this.isDestroyed) { - CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); - this.prefetchCoursesData.icon = initialIcon; - } } } diff --git a/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html b/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html index 32fbdc9fb..3697b8439 100644 --- a/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html +++ b/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html @@ -3,20 +3,6 @@

{{ 'addon.block_starredcourses.pluginname' | translate }}

-
- - - - - {{prefetchCoursesData.badge}} - - -
-
@@ -25,17 +11,13 @@ -
+
- +
diff --git a/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts b/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts index 766c695c7..38fd77081 100644 --- a/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts +++ b/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts @@ -12,17 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, OnDestroy, Input, OnChanges, SimpleChange } from '@angular/core'; +import { Component, OnInit, OnDestroy, Input } from '@angular/core'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData, CoreCourses } from '@features/courses/services/courses'; import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } 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 { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; import { CoreUtils } from '@services/utils/utils'; -import { CoreDomUtils } from '@services/utils/dom'; /** * Component to render a starred courses block. @@ -31,24 +29,15 @@ import { CoreDomUtils } from '@services/utils/dom'; selector: 'addon-block-starredcourses', templateUrl: 'addon-block-starredcourses.html', }) -export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy { +export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy { @Input() downloadEnabled = false; courses: CoreEnrolledCourseDataWithOptions [] = []; - prefetchCoursesData: CorePrefetchStatusInfo = { - icon: '', - statusTranslatable: 'core.loading', - status: '', - loading: true, - badge: '', - }; downloadCourseEnabled = false; - downloadCoursesEnabled = false; scrollElementId!: string; - protected prefetchIconsInitialized = false; protected isDestroyed = false; protected coursesObserver?: CoreEventObserver; protected updateSiteObserver?: CoreEventObserver; @@ -69,12 +58,10 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im // Refresh the enabled flags if enabled. this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); - this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); // Refresh the enabled flags if site is updated. this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); - this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); }, CoreSites.getCurrentSiteId()); this.coursesObserver = CoreEvents.on( @@ -89,16 +76,6 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im super.ngOnInit(); } - /** - * @inheritdoc - */ - ngOnChanges(changes: {[name: string]: SimpleChange}): void { - if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) { - // Download all courses is enabled now, initialize it. - this.initPrefetchCoursesIcons(); - } - } - /** * @inheritdoc */ @@ -131,9 +108,7 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im promises.push(CoreCourses.invalidateCoursesByField('ids', courseIds.join(','))); } - await CoreUtils.allPromises(promises).finally(() => { - this.prefetchIconsInitialized = false; - }); + await CoreUtils.allPromises(promises); } /** @@ -145,8 +120,6 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im // @TODO: Sort won't coincide with website because timemodified is not informed. this.courses = await CoreCoursesHelper.getUserCoursesWithOptions('timemodified', 0, 'isfavourite', showCategories); - - this.initPrefetchCoursesIcons(); } /** @@ -182,43 +155,10 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im } await this.invalidateCourses([course.id]); - this.initPrefetchCoursesIcons(); } } - /** - * Initialize the prefetch icon for selected courses. - */ - protected async initPrefetchCoursesIcons(): Promise { - if (this.prefetchIconsInitialized || !this.downloadEnabled) { - // Already initialized. - return; - } - - this.prefetchIconsInitialized = true; - - this.prefetchCoursesData = await CoreCourseHelper.initPrefetchCoursesIcons(this.courses, this.prefetchCoursesData); - } - - /** - * Prefetch all the shown courses. - * - * @return Promise resolved when done. - */ - async prefetchCourses(): Promise { - const initialIcon = this.prefetchCoursesData.icon; - - try { - return CoreCourseHelper.prefetchCourses(this.courses, this.prefetchCoursesData); - } catch (error) { - if (!this.isDestroyed) { - CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); - this.prefetchCoursesData.icon = initialIcon; - } - } - } - /** * @inheritdoc */ diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index b87b0ab95..768470957 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -429,7 +429,7 @@ export class CoreCourseHelperProvider { const siteId = CoreSites.getCurrentSiteId(); // Confirm the download without checking size because it could take a while. - await CoreDomUtils.showConfirm(Translate.instant('core.areyousure')); + await CoreDomUtils.showConfirm(Translate.instant('core.areyousure'), Translate.instant('core.courses.downloadcourses')); const total = courses.length; let count = 0; @@ -1209,7 +1209,7 @@ export class CoreCourseHelperProvider { const status = await this.determineCoursesStatus(courses); - prefetch = this.getCoursePrefetchStatusInfo(status); + prefetch = this.getCoursesPrefetchStatusInfo(status); if (prefetch.loading) { // It seems all courses are being downloaded, show a download button instead. @@ -1381,6 +1381,33 @@ export class CoreCourseHelperProvider { return prefetchStatus; } + /** + * Get a courses status icon and the langkey to use as a title from status. + * + * @param status Courses status. + * @return Prefetch status info. + */ + getCoursesPrefetchStatusInfo(status: string): CorePrefetchStatusInfo { + const prefetchStatus: CorePrefetchStatusInfo = { + status: status, + icon: this.getPrefetchStatusIcon(status, false), + statusTranslatable: '', + loading: false, + }; + + if (status == CoreConstants.DOWNLOADED) { + // Always show refresh icon, we cannot know if there's anything new in course options. + prefetchStatus.statusTranslatable = 'core.courses.refreshcourses'; + } else if (status == CoreConstants.DOWNLOADING) { + prefetchStatus.statusTranslatable = 'core.downloading'; + prefetchStatus.loading = true; + } else { + prefetchStatus.statusTranslatable = 'core.courses.downloadcourses'; + } + + return prefetchStatus; + } + /** * Get the icon given the status and if trust the download status. * 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 425d8e3f3..91e596ed5 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 @@ -1,47 +1,106 @@ - - - - + + + + + -

- - -

-

- - - - | - - - - -

-

- + + + + + + + + + + + + + + +

+

- - - - +
-
- +
+ +
+ + + + + + + + + + +
+ +
+ +
+
+ + + + +
+
- + +
+ + + + + + + + + + +
+
+ + +

+ + {{ 'core.courses.aria:coursecategory' | translate }} + + + | + + + + +

+

+ + + {{ 'core.courses.aria:favourite' | translate }} + {{ 'core.courses.aria:coursename' | translate }} + + +

+
diff --git a/src/core/features/courses/components/course-list-item/course-list-item.scss b/src/core/features/courses/components/course-list-item/course-list-item.scss index 4807e1b59..928f993e2 100644 --- a/src/core/features/courses/components/course-list-item/course-list-item.scss +++ b/src/core/features/courses/components/course-list-item/course-list-item.scss @@ -1,13 +1,12 @@ @import "~theme/globals"; -:host { +.core-course-list-item { .course-icon { color: white; background: var(--gray-light); padding: 8px; font-size: 24px; border-radius: 50%; - margin-inline-end: 16px; -webkit-transition: all 50ms ease-in-out; transition: all 50ms ease-in-out; } @@ -22,4 +21,177 @@ -webkit-transition: all 50ms ease-in-out; transition: all 50ms ease-in-out; } + + .core-course-thumb { + @include margin(12px, 16px, 12px, null); + align-self: flex-start; + } + + .core-course-summary { + margin-top: 12px; + } +} + +.item-heading ion-icon { + margin-right: 4px; + color: var(--core-star-color); +} + +ion-card { + --vertical-margin: 12px; + + display: flex; + flex-direction: column; + align-self: stretch; + height: calc(100% - var(--vertical-margin) - var(--vertical-margin)); + margin-top: var(--vertical-margin); + margin-bottom: var(--vertical-margin); + + @for $i from 0 to length($core-course-image-background) { + &[course-color="#{$i}"] .core-course-thumb { + background: var(--core-course-color-#{$i}); + } + } + + ion-row { + min-height: var(--a11y-min-target-size); + ion-col .core-button-spinner { + min-width: calc(var(--a11y-min-target-size) + 16px); + } + } + + .core-course-thumb { + padding-top: 40%; + width: 100%; + overflow: hidden; + cursor: pointer; + pointer-events: auto; + position: relative; + background-position: center; + background-size: cover; + -webkit-transition: all 50ms ease-in-out; + transition: all 50ms ease-in-out; + + &.core-course-color-img { + background: var(--ion-item-background); + } + + img { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + margin: auto; + } + } + + @if ($core-course-hide-thumb-on-cards) { + .core-course-thumb { + display: none; + } + } + + @if ($core-course-thumb-on-cards-background) { + .core-course-thumb { + background: $core-course-thumb-on-cards-background !important; + } + } + + .core-course-additional-info { + margin-bottom: 8px; + } + + .core-course-header { + flex-grow: 1; + display: flex; + flex-direction: column; + + --inner-padding-end: 0px; + + &::part(native) { + flex-grow: 1; + align-items: self-start; + } + + &.core-course-only-title { + &::part(native) { + flex-grow: 1; + } + + } + + .core-course-title { + margin: 12px 0; + flex-grow: 1; + width: 100%; + max-width: 100%; + } + + .core-button-spinner { + margin: 0; + } + .core-button-spinner ion-spinner { + vertical-align: top; // the better option for most scenarios + vertical-align: -webkit-baseline-middle; // the best for those that support it + } + + .core-button-spinner .core-icon-downloaded { + font-size: 28.8px; + margin-top: 8px; + vertical-align: top; + } + + .item-button[icon-only] { + min-width: 50px; + width: 50px; + } + } + + @if ($core-course-hide-progress-on-cards) { + .core-course-progress { + display: none; + } + } +} + +button { + z-index: 1; +} + +:host-context(.core-horizontal-scroll) { + @include horizontal_scroll_item(80%, 250px, 300px); + + ion-card { + .core-course-thumb { + padding-top: 30%; + } + + ion-item.core-course-header { + --padding-start: 4px; + + .core-course-title { + margin: 7px 0; + + .item-heading ion-icon { + margin-right: 2px; + } + } + .core-button-spinner { + min-height: 40px; + min-width: 40px; + + ion-spinner { + width: 20px; + height: 20px; + } + } + .item-button[icon-only] { + min-width: 40px; + width: 40px; + padding: 8px; + } + + } + } } diff --git a/src/core/features/courses/components/course-list-item/course-list-item.ts b/src/core/features/courses/components/course-list-item/course-list-item.ts index 6d4da5a97..70304420b 100644 --- a/src/core/features/courses/components/course-list-item/course-list-item.ts +++ b/src/core/features/courses/components/course-list-item/course-list-item.ts @@ -12,15 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreConstants } from '@/core/constants'; import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; import { CoreCourseProvider, CoreCourse } from '@features/course/services/course'; import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; +import { CoreUser } from '@features/user/services/user'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; +import { Translate } from '@singletons'; import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events'; -import { CoreCourseListItem, CoreCourses } from '../../services/courses'; -import { CoreCoursesHelper } from '../../services/courses-helper'; +import { CoreCourseListItem, CoreCourses, CoreCoursesProvider } from '../../services/courses'; +import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper'; +import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu'; /** * This directive is meant to display an item for a list of courses. @@ -37,10 +41,10 @@ import { CoreCoursesHelper } from '../../services/courses-helper'; export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, OnChanges { @Input() course!: CoreCourseListItem; // The course to render. - @Input() showDownload = false; // If true, will show download button. + @Input() layout: 'listwithenrol'|'summarycard'|'list'|'card' = 'listwithenrol'; - icons: CoreCoursesEnrolmentIcons[] = []; + enrolmentIcons: CoreCoursesEnrolmentIcons[] = []; isEnrolled = false; prefetchCourseData: CorePrefetchStatusInfo = { icon: '', @@ -49,8 +53,16 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On loading: true, }; - protected courseStatusObserver?: CoreEventObserver; + showSpinner = false; + downloadCourseEnabled = false; + courseOptionMenuEnabled = false; + progress = -1; + completionUserTracked: boolean | undefined = false; + + protected courseStatus = CoreConstants.NOT_DOWNLOADED; protected isDestroyed = false; + protected courseStatusObserver?: CoreEventObserver; + protected siteUpdatedObserver?: CoreEventObserver; /** * @inheritdoc @@ -58,48 +70,71 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On async ngOnInit(): Promise { CoreCoursesHelper.loadCourseColorAndImage(this.course); - this.isEnrolled = this.course.progress !== undefined; + // Assume is enroled if mode is not listwithenrol. + this.isEnrolled = this.layout != 'listwithenrol' || this.course.progress !== undefined; if (!this.isEnrolled) { try { const course = await CoreCourses.getUserCourse(this.course.id); - this.course.progress = course.progress; - this.course.completionusertracked = course.completionusertracked; + this.course = Object.assign(this.course, course); + this.updateCourseFields(); this.isEnrolled = true; - - if (this.showDownload) { - this.initPrefetchCourse(); - } } catch { this.isEnrolled = false; } } - if (!this.isEnrolled) { - this.icons = []; + if (this.isEnrolled) { + if (this.showDownload) { + this.initPrefetchCourse(); + } + + this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); + + if (this.downloadCourseEnabled) { + this.initPrefetchCourse(); + } + + // This field is only available from 3.6 onwards. + this.courseOptionMenuEnabled = (this.layout != 'listwithenrol' && this.layout != 'summarycard') && + this.course.isfavourite !== undefined; + + // Refresh the enabled flag if site is updated. + this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { + const wasEnabled = this.downloadCourseEnabled; + + this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); + + if (!wasEnabled && this.downloadCourseEnabled) { + // Download course is enabled now, initialize it. + this.initPrefetchCourse(); + } + }, CoreSites.getCurrentSiteId()); + } else if ('enrollmentmethods' in this.course) { + this.enrolmentIcons = []; this.course.enrollmentmethods.forEach((instance) => { if (instance === 'self') { - this.icons.push({ + this.enrolmentIcons.push({ label: 'core.courses.selfenrolment', icon: 'fas-key', }); } else if (instance === 'guest') { - this.icons.push({ + this.enrolmentIcons.push({ label: 'core.courses.allowguests', icon: 'fas-unlock', }); } else if (instance === 'paypal') { - this.icons.push({ + this.enrolmentIcons.push({ label: 'core.courses.paypalaccepted', icon: 'fab-paypal', }); } }); - if (this.icons.length == 0) { - this.icons.push({ + if (this.enrolmentIcons.length == 0) { + this.enrolmentIcons.push({ label: 'core.courses.notenrollable', icon: 'fas-lock', }); @@ -114,6 +149,16 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On if (this.showDownload && this.isEnrolled) { this.initPrefetchCourse(); } + + this.updateCourseFields(); + } + + /** + * Helper function to update course fields. + */ + protected updateCourseFields(): void { + this.progress = 'progress' in this.course ? this.course.progress || -1 : -1; + this.completionUserTracked = 'completionusertracked' in this.course && this.course.completionusertracked; } /** @@ -179,6 +224,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On protected updateCourseStatus(status: string): void { const statusData = CoreCourseHelper.getCoursePrefetchStatusInfo(status); + this.courseStatus = status; this.prefetchCourseData.status = statusData.status; this.prefetchCourseData.icon = statusData.icon; this.prefetchCourseData.statusTranslatable = statusData.statusTranslatable; @@ -188,11 +234,11 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On /** * Prefetch the course. * - * @param e Click event. + * @param event Click event. */ - async prefetchCourse(e?: Event): Promise { - e?.preventDefault(); - e?.stopPropagation(); + async prefetchCourse(event?: Event): Promise { + event?.preventDefault(); + event?.stopPropagation(); try { await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course); @@ -203,12 +249,149 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On } } + /** + * Delete course stored data. + */ + async deleteCourseStoredData(): Promise { + try { + await CoreDomUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); + } catch (error) { + if (!CoreDomUtils.isCanceledError(error)) { + throw error; + } + + return; + } + + const modal = await CoreDomUtils.showModalLoading(); + + try { + await CoreCourseHelper.deleteCourseFiles(this.course.id); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, Translate.instant('core.errordeletefile')); + } finally { + modal.dismiss(); + } + } + + /** + * Show the context menu. + * + * @param event Click Event. + */ + async showCourseOptionsMenu(event: Event): Promise { + event.preventDefault(); + event.stopPropagation(); + + const popoverData = await CoreDomUtils.openPopover({ + component: CoreCoursesCourseOptionsMenuComponent, + componentProps: { + course: this.course, + prefetch: this.prefetchCourseData, + }, + event: event, + }); + + switch (popoverData) { + case 'download': + if (!this.prefetchCourseData.loading) { + this.prefetchCourse(event); + } + break; + case 'delete': + if (this.courseStatus == CoreConstants.DOWNLOADED || this.courseStatus == CoreConstants.OUTDATED) { + this.deleteCourseStoredData(); + } + break; + case 'hide': + this.setCourseHidden(true); + break; + case 'show': + this.setCourseHidden(false); + break; + case 'favourite': + this.setCourseFavourite(true); + break; + case 'unfavourite': + this.setCourseFavourite(false); + break; + default: + break; + } + + } + + /** + * Hide/Unhide the course from the course list. + * + * @param hide True to hide and false to show. + */ + protected async setCourseHidden(hide: boolean): Promise { + this.showSpinner = true; + + // We should use null to unset the preference. + try { + await CoreUser.updateUserPreference( + 'block_myoverview_hidden_course_' + this.course.id, + hide ? '1' : undefined, + ); + + this.course.hidden = hide; + + ( this.course).hidden = hide; + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { + courseId: this.course.id, + course: this.course, + action: CoreCoursesProvider.ACTION_STATE_CHANGED, + state: CoreCoursesProvider.STATE_HIDDEN, + value: hide, + }, CoreSites.getCurrentSiteId()); + + } catch (error) { + if (!this.isDestroyed) { + CoreDomUtils.showErrorModalDefault(error, 'Error changing course visibility.'); + } + } finally { + this.showSpinner = false; + } + } + + /** + * Favourite/Unfavourite the course from the course list. + * + * @param favourite True to favourite and false to unfavourite. + */ + protected async setCourseFavourite(favourite: boolean): Promise { + this.showSpinner = true; + + try { + await CoreCourses.setFavouriteCourse(this.course.id, favourite); + + this.course.isfavourite = favourite; + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { + courseId: this.course.id, + course: this.course, + action: CoreCoursesProvider.ACTION_STATE_CHANGED, + state: CoreCoursesProvider.STATE_FAVOURITE, + value: favourite, + }, CoreSites.getCurrentSiteId()); + + } catch (error) { + if (!this.isDestroyed) { + CoreDomUtils.showErrorModalDefault(error, 'Error changing course favourite attribute.'); + } + } finally { + this.showSpinner = false; + } + } + /** * @inheritdoc */ ngOnDestroy(): void { this.isDestroyed = true; this.courseStatusObserver?.off(); + this.siteUpdatedObserver?.off(); } } diff --git a/src/core/features/courses/components/course-progress/course-progress.scss b/src/core/features/courses/components/course-progress/course-progress.scss index 07024b58f..28a70f437 100644 --- a/src/core/features/courses/components/course-progress/course-progress.scss +++ b/src/core/features/courses/components/course-progress/course-progress.scss @@ -151,9 +151,3 @@ } } } - -:host-context(body.version-3-1) { - .core-course-thumb{ - display: none; - } -} diff --git a/src/core/features/courses/components/course-progress/course-progress.ts b/src/core/features/courses/components/course-progress/course-progress.ts index ece9a9b50..7b1e37c7b 100644 --- a/src/core/features/courses/components/course-progress/course-progress.ts +++ b/src/core/features/courses/components/course-progress/course-progress.ts @@ -35,6 +35,8 @@ import { CoreUser } from '@features/user/services/user'; * * * + * + * @deprecated since 4.0 Use core-courses-course-list-item instead. */ @Component({ selector: 'core-courses-course-progress', @@ -175,7 +177,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy, On try { await CoreDomUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); } catch (error) { - if (CoreDomUtils.isCanceledError(error)) { + if (!CoreDomUtils.isCanceledError(error)) { throw error; } @@ -221,7 +223,6 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy, On component: CoreCoursesCourseOptionsMenuComponent, componentProps: { course: this.course, - courseStatus: this.courseStatus, prefetch: this.prefetchCourseData, }, event: e, @@ -234,7 +235,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy, On } break; case 'delete': - if (this.courseStatus == 'downloaded' || this.courseStatus == 'outdated') { + if (this.courseStatus == CoreConstants.DOWNLOADED || this.courseStatus == CoreConstants.OUTDATED) { this.deleteCourse(); } break; diff --git a/src/core/features/courses/courses.module.ts b/src/core/features/courses/courses.module.ts index 925858cc0..f4f801622 100644 --- a/src/core/features/courses/courses.module.ts +++ b/src/core/features/courses/courses.module.ts @@ -60,7 +60,7 @@ const mainMenuHomeSiblingRoutes: Routes = [ const mainMenuTabRoutes: Routes = [ { path: CoreCoursesMyCoursesMainMenuHandlerService.PAGE_NAME, - loadChildren: () => import('./pages/list/list.module').then(m => m.CoreCoursesListPageModule), + loadChildren: () => import('./pages/my/my.module').then(m => m.CoreCoursesMyCoursesPageModule), }, ]; diff --git a/src/core/features/courses/lang.json b/src/core/features/courses/lang.json index 47924a0bd..70b5a0b79 100644 --- a/src/core/features/courses/lang.json +++ b/src/core/features/courses/lang.json @@ -32,6 +32,7 @@ "password": "Enrolment key", "paymentrequired": "This course requires a payment for entry.", "paypalaccepted": "PayPal payments accepted", + "refreshcourses": "Refresh courses", "reload": "Reload", "removefromfavourites": "Unstar this course", "search": "Search", diff --git a/src/core/features/courses/pages/categories/categories.html b/src/core/features/courses/pages/categories/categories.html index 7ecd32f4b..495001569 100644 --- a/src/core/features/courses/pages/categories/categories.html +++ b/src/core/features/courses/pages/categories/categories.html @@ -10,11 +10,10 @@ - + [content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()" iconAction="toggle" + [(toggle)]="downloadEnabled"> + @@ -28,12 +27,12 @@

- + +

+ [contextInstanceId]="currentCategory.id">

@@ -45,8 +44,7 @@
- +

diff --git a/src/core/features/courses/pages/my/my.html b/src/core/features/courses/pages/my/my.html new file mode 100644 index 000000000..83e679d1a --- /dev/null +++ b/src/core/features/courses/pages/my/my.html @@ -0,0 +1,36 @@ + + + + + +

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

+ + + + + + + + + + + +
+
+ + + + + + + + + + diff --git a/src/core/features/courses/pages/my/my.module.ts b/src/core/features/courses/pages/my/my.module.ts new file mode 100644 index 000000000..a86c67dff --- /dev/null +++ b/src/core/features/courses/pages/my/my.module.ts @@ -0,0 +1,63 @@ +// (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 { Injector, NgModule } from '@angular/core'; +import { RouterModule, ROUTES, Routes } from '@angular/router'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreBlockComponentsModule } from '@features/block/components/components.module'; + +import { CoreCoursesMyCoursesPage } from './my'; +import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; +import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; + +function buildRoutes(injector: Injector): Routes { + return [ + { + path: '', + component: CoreCoursesMyCoursesPage, + }, + { + path: 'list', + loadChildren: () => + import('../list/list.module') + .then(m => m.CoreCoursesListPageModule), + }, + ...buildTabMainRoutes(injector, { + redirectTo: '', + pathMatch: 'full', + }), + ]; +} + +@NgModule({ + imports: [ + CoreSharedModule, + CoreBlockComponentsModule, + CoreMainMenuComponentsModule, + ], + providers: [ + { + provide: ROUTES, + multi: true, + deps: [Injector], + useFactory: buildRoutes, + }, + ], + declarations: [ + CoreCoursesMyCoursesPage, + ], + exports: [RouterModule], +}) +export class CoreCoursesMyCoursesPageModule { } diff --git a/src/core/features/courses/pages/my/my.scss b/src/core/features/courses/pages/my/my.scss new file mode 100644 index 000000000..05e886016 --- /dev/null +++ b/src/core/features/courses/pages/my/my.scss @@ -0,0 +1,3 @@ +:host ::ng-deep ion-item-divider { + display: none !important; +} diff --git a/src/core/features/courses/pages/my/my.ts b/src/core/features/courses/pages/my/my.ts new file mode 100644 index 000000000..9c98a25a4 --- /dev/null +++ b/src/core/features/courses/pages/my/my.ts @@ -0,0 +1,112 @@ +// (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 { AddonBlockMyOverviewComponent } from '@addons/block/myoverview/components/myoverview/myoverview'; +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { CoreBlockComponent } from '@features/block/components/block/block'; +import { IonRefresher } from '@ionic/angular'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreCourses } from '../../services/courses'; + +/** + * Page that shows a my courses. + */ +@Component({ + selector: 'page-core-courses-my', + templateUrl: 'my.html', + styleUrls: ['my.scss'], +}) +export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy { + + @ViewChild(CoreBlockComponent) block!: CoreBlockComponent; + + searchEnabled = false; + downloadCoursesEnabled = false; + userId: number; + myOverviewBlock?: AddonBlockMyOverviewComponent; + + protected updateSiteObserver: CoreEventObserver; + + constructor() { + // Refresh the enabled flags if site is updated. + this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { + this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite(); + this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); + }, CoreSites.getCurrentSiteId()); + + this.userId = CoreSites.getCurrentSiteUserId(); + } + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite(); + this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite(); + + this.loadBlock(); + + } + + /** + * Load my overview block instance. + */ + protected loadBlock(): void { + setTimeout(() => { + if (!this.block) { + return this.loadBlock(); + } + + this.myOverviewBlock = this.block?.dynamicComponent?.instance as AddonBlockMyOverviewComponent; + }, 500); + } + + /** + * Open page to manage courses storage. + */ + manageCoursesStorage(): void { + CoreNavigator.navigateToSitePath('/storage'); + } + + /** + * Go to search courses. + */ + async openSearch(): Promise { + CoreNavigator.navigateToSitePath('/list', { params : { mode: 'search' } }); + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + */ + async refresh(refresher?: IonRefresher): Promise { + if (this.block) { + await CoreUtils.ignoreErrors(this.block.doRefresh()); + } + + refresher?.complete(); + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.updateSiteObserver?.off(); + } + +} diff --git a/src/core/features/courses/services/courses-helper.ts b/src/core/features/courses/services/courses-helper.ts index 89034e716..3cf1a014d 100644 --- a/src/core/features/courses/services/courses-helper.ts +++ b/src/core/features/courses/services/courses-helper.ts @@ -32,6 +32,8 @@ import { AddonCourseCompletion } from '@/addons/coursecompletion/services/course @Injectable({ providedIn: 'root' }) export class CoreCoursesHelperProvider { + protected courseSiteColors: Record = {}; + /** * Get the courses to display the course picker popover. If a courseId is specified, it will also return its categoryId. * @@ -72,11 +74,10 @@ export class CoreCoursesHelperProvider { * @param courseByField Course returned by core_course_get_courses_by_field. * @param addCategoryName Whether add category name or not. */ - loadCourseExtraInfo( + protected loadCourseExtraInfo( course: CoreEnrolledCourseDataWithExtraInfo, courseByField: CoreCourseSearchedData, addCategoryName: boolean = false, - colors?: (string | undefined)[], ): void { if (courseByField) { course.displayname = courseByField.displayname; @@ -85,18 +86,8 @@ export class CoreCoursesHelperProvider { } else { delete course.displayname; } - - this.loadCourseColorAndImage(course, colors); } - /** - * Given a list of courses returned by core_enrol_get_users_courses, load some extra data using the WebService - * core_course_get_courses_by_field if available. - * - * @param courses List of courses. - * @param loadCategoryNames Whether load category names or not. - * @return Promise resolved when done. - */ /** * Loads the color of courses or the thumb image. * @@ -107,11 +98,8 @@ export class CoreCoursesHelperProvider { if (!courses.length) { return; } - const colors = await this.loadCourseSiteColors(); - courses.forEach((course) => { - this.loadCourseColorAndImage(course, colors); - }); + await Promise.all(courses.map((course) => this.loadCourseColorAndImage(course))); } /** @@ -131,32 +119,19 @@ export class CoreCoursesHelperProvider { let coursesInfo = {}; let courseInfoAvailable = false; - const promises: Promise[] = []; - let colors: (string | undefined)[] = []; - - promises.push(this.loadCourseSiteColors().then((loadedColors) => { - colors = loadedColors; - - return; - })); - if (loadCategoryNames || (courses[0].overviewfiles === undefined && courses[0].displayname === undefined)) { const courseIds = courses.map((course) => course.id).join(','); courseInfoAvailable = true; // Get the extra data for the courses. - promises.push(CoreCourses.getCoursesByField('ids', courseIds).then((coursesInfos) => { - coursesInfo = CoreUtils.arrayToObject(coursesInfos, 'id'); + const coursesInfosArray = await CoreCourses.getCoursesByField('ids', courseIds); - return; - })); + coursesInfo = CoreUtils.arrayToObject(coursesInfosArray, 'id'); } - await Promise.all(promises); - courses.forEach((course) => { - this.loadCourseExtraInfo(course, courseInfoAvailable ? coursesInfo[course.id] : course, loadCategoryNames, colors); + this.loadCourseExtraInfo(course, courseInfoAvailable ? coursesInfo[course.id] : course, loadCategoryNames); }); } @@ -166,18 +141,30 @@ export class CoreCoursesHelperProvider { * @return course colors RGB. */ protected async loadCourseSiteColors(): Promise<(string | undefined)[]> { - const site = CoreSites.getCurrentSite(); + const site = CoreSites.getRequiredCurrentSite(); + const siteId = site.getId(); + + if (this.courseSiteColors[siteId] !== undefined) { + return this.courseSiteColors[siteId]; + } + + if (!site.isVersionGreaterEqualThan('3.8')) { + this.courseSiteColors[siteId] = []; + + return []; + } + const colors: (string | undefined)[] = []; - if (site?.isVersionGreaterEqualThan('3.8')) { - try { - const configs = await site.getConfig(); - for (let x = 0; x < 10; x++) { - colors[x] = configs['core_admin_coursecolor' + (x + 1)] || undefined; - } - } catch { - // Ignore errors. + try { + const configs = await site.getConfig(); + for (let x = 0; x < 10; x++) { + colors[x] = configs['core_admin_coursecolor' + (x + 1)] || undefined; } + + this.courseSiteColors[siteId] = colors; + } catch { + // Ignore errors. } return colors; @@ -187,19 +174,18 @@ export class CoreCoursesHelperProvider { * Loads the color of the course or the thumb image. * * @param course Course data. - * @param colors Colors loaded. */ - async loadCourseColorAndImage(course: CoreCourseWithImageAndColor, colors?: (string | undefined)[]): Promise { - if (!colors) { - colors = await this.loadCourseSiteColors(); - } - + async loadCourseColorAndImage(course: CoreCourseWithImageAndColor): Promise { if (course.overviewfiles && course.overviewfiles[0]) { course.courseImage = course.overviewfiles[0].fileurl; - } else { - course.colorNumber = course.id % 10; - course.color = colors.length ? colors[course.colorNumber] : undefined; + + return; } + + const colors = await this.loadCourseSiteColors(); + + course.colorNumber = course.id % 10; + course.color = colors.length ? colors[course.colorNumber] : undefined; } /** diff --git a/src/core/features/courses/services/courses.ts b/src/core/features/courses/services/courses.ts index fa9a973c9..c28874747 100644 --- a/src/core/features/courses/services/courses.ts +++ b/src/core/features/courses/services/courses.ts @@ -19,7 +19,7 @@ import { makeSingleton } from '@singletons'; import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { CoreEvents } from '@singletons/events'; import { CoreWSError } from '@classes/errors/wserror'; -import { CoreCourseWithImageAndColor } from './courses-helper'; +import { CoreCourseAnyCourseDataWithExtraInfoAndOptions, CoreCourseWithImageAndColor } from './courses-helper'; const ROOT_CACHE_KEY = 'mmCourses:'; @@ -1384,7 +1384,10 @@ export type CoreCourseSearchedData = CoreCourseBasicSearchedData & { /** * Course to render as list item. */ -export type CoreCourseListItem = CoreCourseSearchedData & CoreCourseWithImageAndColor & { +export type CoreCourseListItem = ((CoreCourseSearchedData & CoreCourseWithImageAndColor) | +CoreCourseAnyCourseDataWithExtraInfoAndOptions) & { + isfavourite?: boolean; // If the user marked this course a favourite. + hidden?: boolean; // If the user hide the course from the dashboard. completionusertracked?: boolean; // If the user is completion tracked. progress?: number | null; // Progress percentage. }; diff --git a/src/core/features/courses/services/handlers/my-courses-mainmenu.ts b/src/core/features/courses/services/handlers/my-courses-mainmenu.ts index 8c2c51878..4d27add17 100644 --- a/src/core/features/courses/services/handlers/my-courses-mainmenu.ts +++ b/src/core/features/courses/services/handlers/my-courses-mainmenu.ts @@ -26,7 +26,7 @@ import { CoreDashboardHomeHandler } from './dashboard-home'; @Injectable({ providedIn: 'root' }) export class CoreCoursesMyCoursesMainMenuHandlerService implements CoreMainMenuHandler { - static readonly PAGE_NAME = 'courses'; + static readonly PAGE_NAME = 'my'; name = 'CoreCoursesMyCourses'; priority = 900; @@ -35,13 +35,20 @@ export class CoreCoursesMyCoursesMainMenuHandlerService implements CoreMainMenuH * @inheritdoc */ async isEnabled(): Promise { - const siteId = CoreSites.getCurrentSiteId(); + const site = CoreSites.getRequiredCurrentSite(); + + const siteId = site.getId(); const disabled = await CoreCourses.isMyCoursesDisabled(siteId); if (disabled) { return false; } + if (site.isVersionGreaterEqualThan('4.0')) { + return true; + } + + // Dashboard cannot be disabled on 3.5 or 3.6 so it will never show this tab. const dashboardEnabled = await CoreDashboardHomeHandler.isEnabledForSite(siteId); const siteHomeEnabled = await CoreSiteHomeHomeHandler.isEnabledForSite(siteId); 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?", diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index c4c8a8d00..c7150accd 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -1163,3 +1163,7 @@ iframe { display: none !important; } } + +ion-grid.core-no-grid > ion-row { + display: block; +} \ No newline at end of file