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

View File

@ -15,9 +15,10 @@
import { AddonBlockTimeline } from '@addons/block/timeline/services/timeline'; import { AddonBlockTimeline } from '@addons/block/timeline/services/timeline';
import { AddonCalendarEvent } from '@addons/calendar/services/calendar'; import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
import { CoreCourse } from '@features/course/services/course'; import { CoreCourse } from '@features/course/services/course';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper'; import { CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
import { CoreTimeUtils } from '@services/utils/time'; import { CoreTimeUtils } from '@services/utils/time';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
/** /**
* A collection of events displayed in the timeline block. * A collection of events displayed in the timeline block.
@ -28,12 +29,8 @@ export class AddonBlockTimelineSection {
overdue: boolean; overdue: boolean;
dateRange: AddonBlockTimelineDateRange; dateRange: AddonBlockTimelineDateRange;
course?: CoreEnrolledCourseDataWithOptions; course?: CoreEnrolledCourseDataWithOptions;
data$: BehaviorSubject<{
events: AddonBlockTimelineDayEvents[]; private dataSubject$: BehaviorSubject<AddonBlockTimelineSectionData>;
lastEventId?: number;
canLoadMore: boolean;
loadingMore: boolean;
}>;
constructor( constructor(
search: string | null, search: string | null,
@ -47,30 +44,42 @@ export class AddonBlockTimelineSection {
this.overdue = overdue; this.overdue = overdue;
this.dateRange = dateRange; this.dateRange = dateRange;
this.course = course; this.course = course;
this.data$ = new BehaviorSubject({ this.dataSubject$ = new BehaviorSubject({
events: courseEvents ? this.reduceEvents(courseEvents, overdue, dateRange) : [], events: [],
lastEventId: canLoadMore, lastEventId: canLoadMore,
canLoadMore: typeof canLoadMore !== 'undefined', canLoadMore: typeof canLoadMore !== 'undefined',
loadingMore: false, 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. * Load more events.
*/ */
async loadMore(): Promise<void> { async loadMore(): Promise<void> {
this.data$.next({ this.dataSubject$.next({
...this.data$.value, ...this.dataSubject$.value,
loadingMore: true, loadingMore: true,
}); });
const lastEventId = this.data$.value.lastEventId; const lastEventId = this.dataSubject$.value.lastEventId;
const { events, canLoadMore } = this.course const { events, canLoadMore } = this.course
? await AddonBlockTimeline.getActionEventsByCourse(this.course.id, lastEventId, this.search ?? '') ? await AddonBlockTimeline.getActionEventsByCourse(this.course.id, lastEventId, this.search ?? '')
: await AddonBlockTimeline.getActionEventsByTimesort(lastEventId, this.search ?? ''); : await AddonBlockTimeline.getActionEventsByTimesort(lastEventId, this.search ?? '');
this.data$.next({ this.dataSubject$.next({
events: this.data$.value.events.concat(this.reduceEvents(events, this.overdue, this.dateRange)), events: this.dataSubject$.value.events.concat(await this.reduceEvents(events, this.overdue, this.dateRange)),
lastEventId: canLoadMore, lastEventId: canLoadMore,
canLoadMore: canLoadMore !== undefined, canLoadMore: canLoadMore !== undefined,
loadingMore: false, loadingMore: false,
@ -85,32 +94,35 @@ export class AddonBlockTimelineSection {
* @param dateRange Date range to filter events. * @param dateRange Date range to filter events.
* @returns Day events list. * @returns Day events list.
*/ */
private reduceEvents( private async reduceEvents(
events: AddonCalendarEvent[], events: AddonCalendarEvent[],
overdue: boolean, overdue: boolean,
{ from, to }: AddonBlockTimelineDateRange, { from, to }: AddonBlockTimelineDateRange,
): AddonBlockTimelineDayEvents[] { ): Promise<AddonBlockTimelineDayEvents[]> {
const filterDates: AddonBlockTimelineFilterDates = { const filterDates: AddonBlockTimelineFilterDates = {
now: CoreTimeUtils.timestamp(), now: CoreTimeUtils.timestamp(),
midnight: AddonBlockTimeline.getDayStart(), midnight: AddonBlockTimeline.getDayStart(),
start: AddonBlockTimeline.getDayStart(from), start: AddonBlockTimeline.getDayStart(from),
end: typeof to === 'number' ? AddonBlockTimeline.getDayStart(to) : undefined, end: typeof to === 'number' ? AddonBlockTimeline.getDayStart(to) : undefined,
}; };
const eventsByDates = events const timelineEvents = await Promise.all(
.filter((event) => this.filterEvent(event, overdue, filterDates)) events
.map((event) => this.mapToTimelineEvent(event, filterDates.now)) .filter((event) => this.filterEvent(event, overdue, filterDates))
.reduce((filteredEvents, event) => { .map((event) => this.mapToTimelineEvent(event, filterDates.now)),
const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort); );
filteredEvents[dayTimestamp] = filteredEvents[dayTimestamp] ?? { const eventsByDates = timelineEvents.reduce((filteredEvents, event) => {
dayTimestamp, const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort);
events: [],
} as AddonBlockTimelineDayEvents;
filteredEvents[dayTimestamp].events.push(event); filteredEvents[dayTimestamp] = filteredEvents[dayTimestamp] ?? {
dayTimestamp,
events: [],
} as AddonBlockTimelineDayEvents;
return filteredEvents; filteredEvents[dayTimestamp].events.push(event);
}, {} as Record<string, AddonBlockTimelineDayEvents>);
return filteredEvents;
}, {} as Record<string, AddonBlockTimelineDayEvents>);
return Object.values(eventsByDates); return Object.values(eventsByDates);
} }
@ -151,20 +163,30 @@ export class AddonBlockTimelineSection {
* @param now Current time. * @param now Current time.
* @returns Timeline event. * @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; const modulename = event.modulename || event.icon.component;
return { return {
...event, ...event,
modulename, modulename,
overdue: event.timesort < now, overdue: event.timesort < now,
iconUrl: CoreCourse.getModuleIconSrc(event.icon.component), iconUrl: await CoreCourseModuleDelegate.getModuleIconSrc(event.icon.component, event.icon.iconurl),
iconTitle: CoreCourse.translateModuleName(modulename), iconTitle: CoreCourse.translateModuleName(modulename),
} as AddonBlockTimelineEvent; } as AddonBlockTimelineEvent;
} }
} }
/**
* Section data.
*/
export type AddonBlockTimelineSectionData = {
events: AddonBlockTimelineDayEvents[];
lastEventId?: number;
canLoadMore: boolean;
loadingMore: boolean;
};
/** /**
* Timestamps to use during event filtering. * 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 { CoreCourses } from '@features/courses/services/courses';
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
import { BehaviorSubject, combineLatest, Observable, of, Subject } from 'rxjs'; 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 { AddonBlockTimelineDateRange, AddonBlockTimelineSection } from '@addons/block/timeline/classes/section';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { formControlValue, resolved } from '@/core/utils/rxjs'; import { formControlValue, resolved } from '@/core/utils/rxjs';
@ -198,6 +198,7 @@ export class AddonBlockTimelineComponent implements OnInit, ICoreBlockComponent
} }
}), }),
resolved(), resolved(),
mergeAll(),
catchError(error => { catchError(error => {
// An error ocurred in the function, log the error and just resolve the observable so the workflow continues. // An error ocurred in the function, log the error and just resolve the observable so the workflow continues.
this.logger.error(error); this.logger.error(error);
@ -205,7 +206,7 @@ export class AddonBlockTimelineComponent implements OnInit, ICoreBlockComponent
// Error getting data, fail. // Error getting data, fail.
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true); CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true);
return of([]); return of([] as AddonBlockTimelineSection[]);
}), }),
share(), share(),
tap(() => (this.loaded = true)), tap(() => (this.loaded = true)),
@ -224,12 +225,12 @@ export class AddonBlockTimelineComponent implements OnInit, ICoreBlockComponent
search: string | null, search: string | null,
overdue: boolean, overdue: boolean,
dateRange: AddonBlockTimelineDateRange, dateRange: AddonBlockTimelineDateRange,
): Promise<AddonBlockTimelineSection[]> { ): Promise<Observable<AddonBlockTimelineSection[]>> {
const section = new AddonBlockTimelineSection(search, overdue, dateRange); const section = new AddonBlockTimelineSection(search, overdue, dateRange);
await section.loadMore(); 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, overdue: boolean,
dateRange: AddonBlockTimelineDateRange, dateRange: AddonBlockTimelineDateRange,
courses: CoreEnrolledCourseDataWithOptions[], courses: CoreEnrolledCourseDataWithOptions[],
): Promise<AddonBlockTimelineSection[]> { ): Promise<Observable<AddonBlockTimelineSection[]>> {
// Do not filter courses by date because they can contain activities due. // Do not filter courses by date because they can contain activities due.
const courseIds = courses.map(course => course.id); const courseIds = courses.map(course => course.id);
const gracePeriod = await this.getCoursesGracePeriod(); const gracePeriod = await this.getCoursesGracePeriod();
const courseEvents = await AddonBlockTimeline.getActionEventsByCourses(courseIds, search ?? ''); const courseEvents = await AddonBlockTimeline.getActionEventsByCourses(courseIds, search ?? '');
return courses return combineLatest(
.filter( courses
course => .filter(
!course.hidden && course =>
!CoreCoursesHelper.isPastCourse(course, gracePeriod.after) && !course.hidden &&
!CoreCoursesHelper.isFutureCourse(course, gracePeriod.after, gracePeriod.before) && !CoreCoursesHelper.isPastCourse(course, gracePeriod.after) &&
courseEvents[course.id].events.length > 0, !CoreCoursesHelper.isFutureCourse(course, gracePeriod.after, gracePeriod.before) &&
) courseEvents[course.id].events.length > 0,
.map(course => new AddonBlockTimelineSection( )
search, .map(course => {
overdue, const section = new AddonBlockTimelineSection(
dateRange, search,
course, overdue,
courseEvents[course.id].events, dateRange,
courseEvents[course.id].canLoadMore, course,
)) courseEvents[course.id].events,
.filter(section => section.data$.value.events.length > 0); 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.eventsFormated = day.eventsFormated || [];
day.filteredEvents = day.filteredEvents || []; day.filteredEvents = day.filteredEvents || [];
// Format online events. // 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); day.eventsFormated = day.eventsFormated.concat(onlineEventsFormatted);

View File

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

View File

@ -672,7 +672,7 @@ class AddonCalendarDaySlidesItemsManagerSource extends CoreSwipeSlidesDynamicIte
try { try {
// Don't pass courseId and categoryId, we'll filter them locally. // 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()); 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) { } catch (error) {
// Allow navigating to non-cached days in offline (behave as if using emergency cache). // Allow navigating to non-cached days in offline (behave as if using emergency cache).
if (CoreNetwork.isOnline()) { if (CoreNetwork.isOnline()) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<img *ngIf="!isLocalUrl" [src]="icon" [alt]="showAlt ? modNameTranslated : ''" [attr.role]="!showAlt ? 'presentation' : null" <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()"> [componentId]="linkIconWithComponent ? componentId : null" (error)="loadFallbackIcon()">
<img *ngIf="isLocalUrl" [src]="icon" [alt]="showAlt ? modNameTranslated : ''" [attr.role]="!showAlt ? 'presentation' : null" <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; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
&.no-filter {
--filter: none;
}
} }
:host-context(ion-item) { :host-context(ion-item) {

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { CoreConstants, ModPurpose } from '@/core/constants'; 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 { CoreCourse } from '@features/course/services/course';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreSites } from '@services/sites'; 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() modname?: string; // The module name. Used also as component if set.
@Input() componentId?: number; // Component Id for external icons. @Input() componentId?: number; // Component Id for external icons.
@Input() modicon?: string; // Module icon url or local url. @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() showAlt = true; // Show alt otherwise it's only presentation icon.
@Input() purpose: ModPurpose = ModPurpose.MOD_PURPOSE_OTHER; // Purpose of the module. @Input() purpose: ModPurpose = ModPurpose.MOD_PURPOSE_OTHER; // Purpose of the module.
@Output() failedLoading = new EventEmitter<void>();
icon = ''; icon = '';
modNameTranslated = ''; modNameTranslated = '';
isLocalUrl = true; isLocalUrl = true;
@ -122,6 +125,8 @@ export class CoreModIconComponent implements OnInit, OnChanges {
} }
this.icon = path + moduleName + '.svg'; 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 forCoursePage?: boolean, // eslint-disable-line @typescript-eslint/no-unused-vars
): Promise<CoreCourseModuleHandlerData> | CoreCourseModuleHandlerData { ): Promise<CoreCourseModuleHandlerData> | CoreCourseModuleHandlerData {
return { return {
icon: CoreCourse.getModuleIconSrc(module.modname, module.modicon), icon: this.getIconSrc(module, module.modicon),
title: module.name, title: module.name,
class: 'addon-mod_' + module.modname + '-handler', class: 'addon-mod_' + module.modname + '-handler',
showDownloadButton: true, showDownloadButton: true,
@ -78,4 +78,15 @@ export class CoreModuleHandlerBase implements Partial<CoreCourseModuleHandler> {
await CoreNavigator.navigateToSitePath(this.pageName + routeParams, options); 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. * Get the icon src for the module.
* *
* @param module Module to get the icon from. * @param module Module to get the icon from.
* @param modicon The mod icon string.
* @returns The icon src. * @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. * 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. * @returns Promise resolved with the icon src.
*/ */
async getModuleIconSrc(modname: string, modicon?: string, module?: CoreCourseModuleData): Promise<string> { 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" <img *ngIf="row.image && !row.itemmodule" [src]="row.image" slot="start" class="core-module-icon"
[alt]="row.iconAlt" /> [alt]="row.iconAlt" />
<core-mod-icon *ngIf="row.image && row.itemmodule" [modicon]="row.image" slot="start" <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> </core-mod-icon>
<span [innerHTML]="row.gradeitem"></span> <span [innerHTML]="row.gradeitem"></span>
</th> </th>

View File

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