Merge pull request #3579 from NoelDeMartin/MOBILE-4193
MOBILE-4193 core: Consolidate module icons sourcesmain
commit
8f6320e012
|
@ -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());
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()">
|
||||
|
|
|
@ -42,6 +42,11 @@ img {
|
|||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.no-filter {
|
||||
--filter: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
:host-context(ion-item) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue