Merge pull request #3579 from NoelDeMartin/MOBILE-4193

MOBILE-4193 core: Consolidate module icons sources
main
Dani Palou 2023-03-16 09:48:47 +01:00 committed by GitHub
commit 8f6320e012
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 214 additions and 90 deletions

View File

@ -18,6 +18,7 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourse } from '@features/course/services/course';
import { CoreSiteWSPreSets } from '@classes/site';
import { makeSingleton } from '@singletons';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
const ROOT_CACHE_KEY = 'AddonBlockRecentlyAccessedItems:';
@ -54,15 +55,15 @@ export class AddonBlockRecentlyAccessedItemsProvider {
const cmIds: number[] = [];
items = items.map((item) => {
items = await Promise.all(items.map(async (item) => {
const modicon = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'src');
item.iconUrl = CoreCourse.getModuleIconSrc(item.modname, modicon || undefined);
item.iconUrl = await CoreCourseModuleDelegate.getModuleIconSrc(item.modname, modicon || undefined);
item.iconTitle = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'title');
cmIds.push(item.cmid);
return item;
});
}));
// Check if the viewed module should be updated for each activity.
const lastViewedMap = await CoreCourse.getCertainModulesViewed(cmIds, site.getId());

View File

@ -15,9 +15,10 @@
import { AddonBlockTimeline } from '@addons/block/timeline/services/timeline';
import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
import { CoreTimeUtils } from '@services/utils/time';
import { BehaviorSubject } from 'rxjs';
import { BehaviorSubject, Observable } from 'rxjs';
/**
* A collection of events displayed in the timeline block.
@ -28,12 +29,8 @@ export class AddonBlockTimelineSection {
overdue: boolean;
dateRange: AddonBlockTimelineDateRange;
course?: CoreEnrolledCourseDataWithOptions;
data$: BehaviorSubject<{
events: AddonBlockTimelineDayEvents[];
lastEventId?: number;
canLoadMore: boolean;
loadingMore: boolean;
}>;
private dataSubject$: BehaviorSubject<AddonBlockTimelineSectionData>;
constructor(
search: string | null,
@ -47,30 +44,42 @@ export class AddonBlockTimelineSection {
this.overdue = overdue;
this.dateRange = dateRange;
this.course = course;
this.data$ = new BehaviorSubject({
events: courseEvents ? this.reduceEvents(courseEvents, overdue, dateRange) : [],
this.dataSubject$ = new BehaviorSubject({
events: [],
lastEventId: canLoadMore,
canLoadMore: typeof canLoadMore !== 'undefined',
loadingMore: false,
});
if (courseEvents) {
// eslint-disable-next-line promise/catch-or-return
this.reduceEvents(courseEvents, overdue, dateRange).then(events => this.dataSubject$.next({
...this.dataSubject$.value,
events,
}));
}
}
get data$(): Observable<AddonBlockTimelineSectionData> {
return this.dataSubject$;
}
/**
* Load more events.
*/
async loadMore(): Promise<void> {
this.data$.next({
...this.data$.value,
this.dataSubject$.next({
...this.dataSubject$.value,
loadingMore: true,
});
const lastEventId = this.data$.value.lastEventId;
const lastEventId = this.dataSubject$.value.lastEventId;
const { events, canLoadMore } = this.course
? await AddonBlockTimeline.getActionEventsByCourse(this.course.id, lastEventId, this.search ?? '')
: await AddonBlockTimeline.getActionEventsByTimesort(lastEventId, this.search ?? '');
this.data$.next({
events: this.data$.value.events.concat(this.reduceEvents(events, this.overdue, this.dateRange)),
this.dataSubject$.next({
events: this.dataSubject$.value.events.concat(await this.reduceEvents(events, this.overdue, this.dateRange)),
lastEventId: canLoadMore,
canLoadMore: canLoadMore !== undefined,
loadingMore: false,
@ -85,32 +94,35 @@ export class AddonBlockTimelineSection {
* @param dateRange Date range to filter events.
* @returns Day events list.
*/
private reduceEvents(
private async reduceEvents(
events: AddonCalendarEvent[],
overdue: boolean,
{ from, to }: AddonBlockTimelineDateRange,
): AddonBlockTimelineDayEvents[] {
): Promise<AddonBlockTimelineDayEvents[]> {
const filterDates: AddonBlockTimelineFilterDates = {
now: CoreTimeUtils.timestamp(),
midnight: AddonBlockTimeline.getDayStart(),
start: AddonBlockTimeline.getDayStart(from),
end: typeof to === 'number' ? AddonBlockTimeline.getDayStart(to) : undefined,
};
const eventsByDates = events
.filter((event) => this.filterEvent(event, overdue, filterDates))
.map((event) => this.mapToTimelineEvent(event, filterDates.now))
.reduce((filteredEvents, event) => {
const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort);
const timelineEvents = await Promise.all(
events
.filter((event) => this.filterEvent(event, overdue, filterDates))
.map((event) => this.mapToTimelineEvent(event, filterDates.now)),
);
filteredEvents[dayTimestamp] = filteredEvents[dayTimestamp] ?? {
dayTimestamp,
events: [],
} as AddonBlockTimelineDayEvents;
const eventsByDates = timelineEvents.reduce((filteredEvents, event) => {
const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort);
filteredEvents[dayTimestamp].events.push(event);
filteredEvents[dayTimestamp] = filteredEvents[dayTimestamp] ?? {
dayTimestamp,
events: [],
} as AddonBlockTimelineDayEvents;
return filteredEvents;
}, {} as Record<string, AddonBlockTimelineDayEvents>);
filteredEvents[dayTimestamp].events.push(event);
return filteredEvents;
}, {} as Record<string, AddonBlockTimelineDayEvents>);
return Object.values(eventsByDates);
}
@ -151,20 +163,30 @@ export class AddonBlockTimelineSection {
* @param now Current time.
* @returns Timeline event.
*/
private mapToTimelineEvent(event: AddonCalendarEvent, now: number): AddonBlockTimelineEvent {
private async mapToTimelineEvent(event: AddonCalendarEvent, now: number): Promise<AddonBlockTimelineEvent> {
const modulename = event.modulename || event.icon.component;
return {
...event,
modulename,
overdue: event.timesort < now,
iconUrl: CoreCourse.getModuleIconSrc(event.icon.component),
iconUrl: await CoreCourseModuleDelegate.getModuleIconSrc(event.icon.component, event.icon.iconurl),
iconTitle: CoreCourse.translateModuleName(modulename),
} as AddonBlockTimelineEvent;
}
}
/**
* Section data.
*/
export type AddonBlockTimelineSectionData = {
events: AddonBlockTimelineDayEvents[];
lastEventId?: number;
canLoadMore: boolean;
loadingMore: boolean;
};
/**
* Timestamps to use during event filtering.
*/

View File

@ -22,7 +22,7 @@ import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/
import { CoreCourses } from '@features/courses/services/courses';
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
import { BehaviorSubject, combineLatest, Observable, of, Subject } from 'rxjs';
import { catchError, distinctUntilChanged, map, share, tap } from 'rxjs/operators';
import { catchError, distinctUntilChanged, map, share, tap, mergeAll } from 'rxjs/operators';
import { AddonBlockTimelineDateRange, AddonBlockTimelineSection } from '@addons/block/timeline/classes/section';
import { FormControl } from '@angular/forms';
import { formControlValue, resolved } from '@/core/utils/rxjs';
@ -198,6 +198,7 @@ export class AddonBlockTimelineComponent implements OnInit, ICoreBlockComponent
}
}),
resolved(),
mergeAll(),
catchError(error => {
// An error ocurred in the function, log the error and just resolve the observable so the workflow continues.
this.logger.error(error);
@ -205,7 +206,7 @@ export class AddonBlockTimelineComponent implements OnInit, ICoreBlockComponent
// Error getting data, fail.
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true);
return of([]);
return of([] as AddonBlockTimelineSection[]);
}),
share(),
tap(() => (this.loaded = true)),
@ -224,12 +225,12 @@ export class AddonBlockTimelineComponent implements OnInit, ICoreBlockComponent
search: string | null,
overdue: boolean,
dateRange: AddonBlockTimelineDateRange,
): Promise<AddonBlockTimelineSection[]> {
): Promise<Observable<AddonBlockTimelineSection[]>> {
const section = new AddonBlockTimelineSection(search, overdue, dateRange);
await section.loadMore();
return section.data$.value.events.length > 0 ? [section] : [];
return section.data$.pipe(map(({ events }) => events.length > 0 ? [section] : []));
}
/**
@ -246,29 +247,38 @@ export class AddonBlockTimelineComponent implements OnInit, ICoreBlockComponent
overdue: boolean,
dateRange: AddonBlockTimelineDateRange,
courses: CoreEnrolledCourseDataWithOptions[],
): Promise<AddonBlockTimelineSection[]> {
): Promise<Observable<AddonBlockTimelineSection[]>> {
// Do not filter courses by date because they can contain activities due.
const courseIds = courses.map(course => course.id);
const gracePeriod = await this.getCoursesGracePeriod();
const courseEvents = await AddonBlockTimeline.getActionEventsByCourses(courseIds, search ?? '');
return courses
.filter(
course =>
!course.hidden &&
!CoreCoursesHelper.isPastCourse(course, gracePeriod.after) &&
!CoreCoursesHelper.isFutureCourse(course, gracePeriod.after, gracePeriod.before) &&
courseEvents[course.id].events.length > 0,
)
.map(course => new AddonBlockTimelineSection(
search,
overdue,
dateRange,
course,
courseEvents[course.id].events,
courseEvents[course.id].canLoadMore,
))
.filter(section => section.data$.value.events.length > 0);
return combineLatest(
courses
.filter(
course =>
!course.hidden &&
!CoreCoursesHelper.isPastCourse(course, gracePeriod.after) &&
!CoreCoursesHelper.isFutureCourse(course, gracePeriod.after, gracePeriod.before) &&
courseEvents[course.id].events.length > 0,
)
.map(course => {
const section = new AddonBlockTimelineSection(
search,
overdue,
dateRange,
course,
courseEvents[course.id].events,
courseEvents[course.id].canLoadMore,
);
return section.data$.pipe(map(({ events }) => events.length > 0 ? section : null));
}),
).pipe(
map(sections => sections.filter(
(section: AddonBlockTimelineSection | null): section is AddonBlockTimelineSection => !!section,
)),
);
}
/**

View File

@ -551,7 +551,9 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI
day.eventsFormated = day.eventsFormated || [];
day.filteredEvents = day.filteredEvents || [];
// Format online events.
const onlineEventsFormatted = day.events.map((event) => AddonCalendarHelper.formatEventData(event));
const onlineEventsFormatted = await Promise.all(
day.events.map((event) => AddonCalendarHelper.formatEventData(event)),
);
day.eventsFormated = day.eventsFormated.concat(onlineEventsFormatted);

View File

@ -165,7 +165,7 @@ export class AddonCalendarUpcomingEventsComponent implements OnInit, DoCheck, On
async fetchEvents(): Promise<void> {
// Don't pass courseId and categoryId, we'll filter them locally.
const result = await AddonCalendar.getUpcomingEvents();
this.onlineEvents = result.events.map((event) => AddonCalendarHelper.formatEventData(event));
this.onlineEvents = await Promise.all(result.events.map((event) => AddonCalendarHelper.formatEventData(event)));
// Merge the online events with offline data.
this.events = this.mergeEvents();
// Filter events by course.

View File

@ -672,7 +672,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
try {
// Don't pass courseId and categoryId, we'll filter them locally.
result = await AddonCalendar.getDayEvents(day.moment.year(), day.moment.month() + 1, day.moment.date());
preloadedDay.onlineEvents = result.events.map((event) => AddonCalendarHelper.formatEventData(event));
preloadedDay.onlineEvents = await Promise.all(result.events.map((event) => AddonCalendarHelper.formatEventData(event)));
} catch (error) {
// Allow navigating to non-cached days in offline (behave as if using emergency cache).
if (CoreNetwork.isOnline()) {

View File

@ -197,7 +197,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
// Get the event data.
if (this.eventId >= 0) {
const event = await AddonCalendar.getEventById(this.eventId);
this.event = AddonCalendarHelper.formatEventData(event);
this.event = await AddonCalendarHelper.formatEventData(event);
}
try {

View File

@ -37,6 +37,7 @@ import { AddonCalendarOfflineEventDBRecord } from './database/calendar-offline';
import { CoreCategoryData } from '@features/courses/services/courses';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreReminders, CoreRemindersService } from '@features/reminders/services/reminders';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
/**
* Context levels enumeration.
@ -164,9 +165,9 @@ export class AddonCalendarHelperProvider {
* @param event Event to format.
* @returns The formatted event to display.
*/
formatEventData(
async formatEventData(
event: AddonCalendarEvent | AddonCalendarEventBase | AddonCalendarGetEventsEvent,
): AddonCalendarEventToDisplay {
): Promise<AddonCalendarEventToDisplay> {
const eventFormatted: AddonCalendarEventToDisplay = {
...event,
@ -182,7 +183,10 @@ export class AddonCalendarHelperProvider {
};
if (event.modulename) {
eventFormatted.eventIcon = CoreCourse.getModuleIconSrc(event.modulename);
eventFormatted.eventIcon = await CoreCourseModuleDelegate.getModuleIconSrc(
event.modulename,
'icon' in event ? event.icon.iconurl : undefined,
);
eventFormatted.moduleIcon = eventFormatted.eventIcon;
eventFormatted.iconTitle = CoreCourse.translateModuleName(event.modulename);
}

View File

@ -1809,6 +1809,7 @@ export type AddonCalendarEventBase = {
key: string; // Key.
component: string; // Component.
alttext: string; // Alttext.
iconurl?: string; // @since 4.2. Icon image url.
};
category?: {
id: number; // Id.

View File

@ -52,7 +52,7 @@ export class AddonModLabelModuleHandlerService extends CoreModuleHandlerBase imp
module.description = '';
return {
icon: '',
icon: this.getIconSrc(),
title,
a11yTitle: '',
class: 'addon-mod-label-handler',
@ -74,5 +74,12 @@ export class AddonModLabelModuleHandlerService extends CoreModuleHandlerBase imp
return true;
}
/**
* @inheritdoc
*/
getIconSrc(): string {
return '';
}
}
export const AddonModLabelModuleHandler = makeSingleton(AddonModLabelModuleHandlerService);

View File

@ -22,6 +22,7 @@ import { AddonModLtiHelper } from '../lti-helper';
import { AddonModLtiIndexComponent } from '../../components/index';
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
import { CoreCourse } from '@features/course/services/course';
import { CoreSites } from '@services/sites';
/**
* Handler to support LTI modules.
@ -58,10 +59,6 @@ export class AddonModLtiModuleHandlerService extends CoreModuleHandlerBase imple
): Promise<CoreCourseModuleHandlerData> {
const data = await super.getData(module, courseId, sectionId, forCoursePage);
data.showDownloadButton = false;
// Handle custom icons.
data.icon = module.modicon;
data.buttons = [{
icon: 'fas-external-link-alt',
label: 'addon.mod_lti.launchactivity',
@ -83,6 +80,26 @@ export class AddonModLtiModuleHandlerService extends CoreModuleHandlerBase imple
return AddonModLtiIndexComponent;
}
/**
* @inheritdoc
*/
getIconSrc(module?: CoreCourseModuleData | undefined, modicon?: string | undefined): string | undefined {
return module?.modicon ?? modicon ?? CoreCourse.getModuleIconSrc(this.modName);
}
/**
* @inheritdoc
*/
iconIsShape(module?: CoreCourseModuleData | undefined, modicon?: string | undefined): boolean | undefined {
const iconUrl = module?.modicon ?? modicon;
if (!iconUrl) {
return true;
}
return iconUrl.startsWith(CoreSites.getRequiredCurrentSite().siteUrl);
}
}
export const AddonModLtiModuleHandler = makeSingleton(AddonModLtiModuleHandlerService);

View File

@ -1,5 +1,5 @@
<img *ngIf="!isLocalUrl" [src]="icon" [alt]="showAlt ? modNameTranslated : ''" [attr.role]="!showAlt ? 'presentation' : null"
class="core-module-icon" core-external-content [component]="linkIconWithComponent ? modname : null"
class="core-module-icon" [class.no-filter]="noFilter" core-external-content [component]="linkIconWithComponent ? modname : null"
[componentId]="linkIconWithComponent ? componentId : null" (error)="loadFallbackIcon()">
<img *ngIf="isLocalUrl" [src]="icon" [alt]="showAlt ? modNameTranslated : ''" [attr.role]="!showAlt ? 'presentation' : null"
class="core-module-icon" (error)="loadFallbackIcon()">
class="core-module-icon" [class.no-filter]="noFilter" (error)="loadFallbackIcon()">

View File

@ -42,6 +42,11 @@ img {
white-space: nowrap;
overflow: hidden;
}
&.no-filter {
--filter: none;
}
}
:host-context(ion-item) {

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { CoreConstants, ModPurpose } from '@/core/constants';
import { Component, ElementRef, Input, OnChanges, OnInit, SimpleChange } from '@angular/core';
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChange } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreSites } from '@services/sites';
@ -34,9 +34,12 @@ export class CoreModIconComponent implements OnInit, OnChanges {
@Input() modname?: string; // The module name. Used also as component if set.
@Input() componentId?: number; // Component Id for external icons.
@Input() modicon?: string; // Module icon url or local url.
@Input() noFilter?: boolean; // Whether to disable filters.
@Input() showAlt = true; // Show alt otherwise it's only presentation icon.
@Input() purpose: ModPurpose = ModPurpose.MOD_PURPOSE_OTHER; // Purpose of the module.
@Output() failedLoading = new EventEmitter<void>();
icon = '';
modNameTranslated = '';
isLocalUrl = true;
@ -122,6 +125,8 @@ export class CoreModIconComponent implements OnInit, OnChanges {
}
this.icon = path + moduleName + '.svg';
this.failedLoading.emit();
}
}

View File

@ -41,7 +41,7 @@ export class CoreModuleHandlerBase implements Partial<CoreCourseModuleHandler> {
forCoursePage?: boolean, // eslint-disable-line @typescript-eslint/no-unused-vars
): Promise<CoreCourseModuleHandlerData> | CoreCourseModuleHandlerData {
return {
icon: CoreCourse.getModuleIconSrc(module.modname, module.modicon),
icon: this.getIconSrc(module, module.modicon),
title: module.name,
class: 'addon-mod_' + module.modname + '-handler',
showDownloadButton: true,
@ -78,4 +78,15 @@ export class CoreModuleHandlerBase implements Partial<CoreCourseModuleHandler> {
await CoreNavigator.navigateToSitePath(this.pageName + routeParams, options);
}
/**
* @inheritdoc
*/
getIconSrc(module?: CoreCourseModuleData, modicon?: string): Promise<string | undefined> | string | undefined {
if (!module) {
return modicon;
}
return CoreCourse.getModuleIconSrc(module.name, modicon);
}
}

View File

@ -82,9 +82,19 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler {
* Get the icon src for the module.
*
* @param module Module to get the icon from.
* @param modicon The mod icon string.
* @returns The icon src.
*/
getIconSrc?(module?: CoreCourseModuleData): Promise<string | undefined> | string | undefined;
getIconSrc?(module?: CoreCourseModuleData, modicon?: string): Promise<string | undefined> | string | undefined;
/**
* Check whether the icon should be treated as a shape or a rich image.
*
* @param module Module to get the icon from.
* @param modicon The mod icon string.
* @returns Whether the icon should be treated as a shape.
*/
iconIsShape?(module?: CoreCourseModuleData, modicon?: string): Promise<boolean | undefined> | boolean | undefined;
/**
* Check if this type of module supports a certain feature.
@ -390,9 +400,21 @@ export class CoreCourseModuleDelegateService extends CoreDelegate<CoreCourseModu
* @returns Promise resolved with the icon src.
*/
async getModuleIconSrc(modname: string, modicon?: string, module?: CoreCourseModuleData): Promise<string> {
const icon = await this.executeFunctionOnEnabled<Promise<string>>(modname, 'getIconSrc', [module]);
const icon = await this.executeFunctionOnEnabled<Promise<string>>(modname, 'getIconSrc', [module, modicon]);
return icon || CoreCourse.getModuleIconSrc(modname, modicon) || '';
return icon ?? CoreCourse.getModuleIconSrc(modname, modicon) ?? '';
}
/**
* Get whether the icon for the given module should be treated as a shape or a rich image.
*
* @param modname The name of the module type.
* @param modicon The mod icon string.
* @param module The module to use.
* @returns Whether the icon should be treated as a shape.
*/
async moduleIconIsShape(modname: string, modicon?: string, module?: CoreCourseModuleData): Promise<boolean | undefined> {
return await this.executeFunctionOnEnabled<Promise<boolean>>(modname, 'iconIsShape', [module, modicon]);
}
/**

View File

@ -48,7 +48,8 @@
<img *ngIf="row.image && !row.itemmodule" [src]="row.image" slot="start" class="core-module-icon"
[alt]="row.iconAlt" />
<core-mod-icon *ngIf="row.image && row.itemmodule" [modicon]="row.image" slot="start"
[modname]="row.itemmodule">
[modname]="row.itemmodule" [noFilter]="row.imageIsShape === false"
(failedLoading)="failedLoadingRowImage(row)">
</core-mod-icon>
<span [innerHTML]="row.gradeitem"></span>
</th>

View File

@ -206,7 +206,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
*/
private async fetchGrades(): Promise<void> {
const table = await CoreGrades.getCourseGradesTable(this.courseId, this.userId);
const formattedTable = CoreGradesHelper.formatGradesTable(table);
const formattedTable = await CoreGradesHelper.formatGradesTable(table);
this.title = formattedTable.rows[0]?.gradeitem ?? Translate.instant('core.grades.grades');
this.columns = formattedTable.columns;
@ -239,4 +239,13 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
infiniteComplete && infiniteComplete();
}
/**
* Handle row image failed loading.
*
* @param row Row data.
*/
failedLoadingRowImage(row: CoreGradesFormattedTableRow): void {
delete row.imageIsShape;
}
}

View File

@ -37,6 +37,7 @@ import { makeSingleton, Translate } from '@singletons';
import { CoreError } from '@classes/errors/error';
import { CoreCourseHelper } from '@features/course/services/course-helper';
import { CoreAppProvider } from '@services/app';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
export const GRADES_PAGE_NAME = 'grades';
@ -73,7 +74,7 @@ export class CoreGradesHelperProvider {
let content = String(column.content);
if (name == 'itemname') {
this.setRowIconAndType(row, content);
await this.setRowIconAndType(row, content);
row.link = this.getModuleLink(content);
row.rowclass += column.class.indexOf('hidden') >= 0 ? ' hidden' : '';
@ -102,7 +103,10 @@ export class CoreGradesHelperProvider {
* @param useLegacyLayout Whether to use the layout before 4.1.
* @returns Formatted row object.
*/
protected formatGradeRowForTable(tableRow: CoreGradesTableRow, useLegacyLayout: boolean): CoreGradesFormattedTableRow {
protected async formatGradeRowForTable(
tableRow: CoreGradesTableRow,
useLegacyLayout: boolean,
): Promise<CoreGradesFormattedTableRow> {
const row: CoreGradesFormattedTableRow = {};
if (!useLegacyLayout && 'leader' in tableRow) {
@ -132,7 +136,7 @@ export class CoreGradesHelperProvider {
row.colspan = itemNameColumn.colspan;
row.rowspan = tableRow.leader?.rowspan || 1;
this.setRowIconAndType(row, content);
await this.setRowIconAndType(row, content);
this.setRowStyleClasses(row, itemNameColumn.class);
row.rowclass += itemNameColumn.class.indexOf('hidden') >= 0 ? ' hidden' : '';
row.rowclass += itemNameColumn.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
@ -203,7 +207,7 @@ export class CoreGradesHelperProvider {
* @param table JSON object representing a table with data.
* @returns Formatted HTML table.
*/
formatGradesTable(table: CoreGradesTable): CoreGradesFormattedTable {
async formatGradesTable(table: CoreGradesTable): Promise<CoreGradesFormattedTable> {
const maxDepth = table.maxdepth;
const formatted: CoreGradesFormattedTable = {
columns: [],
@ -223,7 +227,7 @@ export class CoreGradesHelperProvider {
feedback: false,
contributiontocoursetotal: false,
};
formatted.rows = this.formatGradesTableRows(table.tabledata);
formatted.rows = await this.formatGradesTableRows(table.tabledata);
// Get a row with some info.
let normalRow = formatted.rows.find(
@ -261,9 +265,9 @@ export class CoreGradesHelperProvider {
* @param rows Unformatted rows.
* @returns Formatted rows.
*/
protected formatGradesTableRows(rows: CoreGradesTableRow[]): CoreGradesFormattedTableRow[] {
protected async formatGradesTableRows(rows: CoreGradesTableRow[]): Promise<CoreGradesFormattedTableRow[]> {
const useLegacyLayout = !CoreSites.getRequiredCurrentSite().isVersionGreaterEqualThan('4.1');
const formattedRows = rows.map(row => this.formatGradeRowForTable(row, useLegacyLayout));
const formattedRows = await Promise.all(rows.map(row => this.formatGradeRowForTable(row, useLegacyLayout)));
if (!useLegacyLayout) {
for (let index = 0; index < formattedRows.length - 1; index++) {
@ -652,7 +656,7 @@ export class CoreGradesHelperProvider {
* @param row Row.
* @param text Row content.
*/
protected setRowIconAndType(row: CoreGradesFormattedRowCommonData, text: string): void {
protected async setRowIconAndType(row: CoreGradesFormattedRowCommonData, text: string): Promise<void> {
text = text.replace('%2F', '/').replace('%2f', '/');
if (text.indexOf('/agg_mean') > -1) {
row.itemtype = 'agg_mean';
@ -680,14 +684,16 @@ export class CoreGradesHelperProvider {
row.iconAlt = Translate.instant('core.grades.calculatedgrade');
} else if (text.indexOf('/mod/') > -1) {
const module = text.match(/mod\/([^/]*)\//);
if (module?.[1] !== undefined) {
const modname = module?.[1];
if (modname !== undefined) {
const modicon = CoreDomUtils.convertToElement(text).querySelector('img')?.getAttribute('src') ?? undefined;
row.itemtype = 'mod';
row.itemmodule = module[1];
row.itemmodule = modname;
row.iconAlt = CoreCourse.translateModuleName(row.itemmodule) || '';
row.image = CoreCourse.getModuleIconSrc(
module[1],
CoreDomUtils.convertToElement(text).querySelector('img')?.getAttribute('src') ?? undefined,
);
row.image = await CoreCourseModuleDelegate.getModuleIconSrc(modname, modicon);
row.imageIsShape = await CoreCourseModuleDelegate.moduleIconIsShape(modname, modicon);
}
} else {
if (row.rowspan && row.rowspan > 1) {
@ -800,6 +806,7 @@ export type CoreGradesFormattedRowCommonData = {
rowclass?: string;
itemtype?: string;
image?: string;
imageIsShape?: boolean;
itemmodule?: string;
iconAlt?: string;
rowspan?: number;