MOBILE-3881 timeline: Apply new design on timeline block

main
Pau Ferrer Ocaña 2021-11-02 11:07:28 +01:00
parent 375301b788
commit 774dc32bfd
11 changed files with 186 additions and 137 deletions

View File

@ -28,7 +28,7 @@
} }
.userpicture { .userpicture {
vertical-align: text-bottom; border-radius: 50%;
} }
} }

View File

@ -15,8 +15,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreCoursesComponentsModule } from '@features/courses/components/components.module';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
import { AddonBlockTimelineComponent } from './timeline/timeline'; import { AddonBlockTimelineComponent } from './timeline/timeline';
import { AddonBlockTimelineEventsComponent } from './events/events'; import { AddonBlockTimelineEventsComponent } from './events/events';
@ -28,8 +26,6 @@ import { AddonBlockTimelineEventsComponent } from './events/events';
], ],
imports: [ imports: [
CoreSharedModule, CoreSharedModule,
CoreCoursesComponentsModule,
CoreCourseComponentsModule,
], ],
exports: [ exports: [
AddonBlockTimelineComponent, AddonBlockTimelineComponent,

View File

@ -1,50 +1,61 @@
<ion-item lines="none" *ngIf="course">
<ion-label class="ion-text-wrap">
<h3>
<span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span>
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"></core-format-text>
</h3>
</ion-label>
</ion-item>
<ion-item-group *ngFor="let dayEvents of filteredEvents"> <ion-item-group *ngFor="let dayEvents of filteredEvents">
<ion-item-divider [color]="dayEvents.color"> <ion-item lines="none">
<ion-label><h3>{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}</h3></ion-label> <ion-label>
</ion-item-divider> <h4 [class.core-bold]="!course">{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}</h4>
</ion-label>
</ion-item>
<ng-container *ngFor="let event of dayEvents.events"> <ng-container *ngFor="let event of dayEvents.events">
<ion-item class="ion-text-wrap core-course-module-handler item-media" detail="false" (click)="action($event, event.url)" <ion-item class="addon-block-timeline-activity" detail="false" (click)="action($event, event.url)" [attr.aria-label]="event.name"
[attr.aria-label]="event.name" button> button lines="full">
<core-mod-icon *ngIf="event.iconUrl" slot="start" [modicon]="event.iconUrl" [componentId]="event.instance" <ion-label>
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding">
<ion-col class="addon-block-timeline-activity-main ion-no-padding">
<ion-row class="ion-justify-content-between ion-align-items-center ion-nowrap ion-no-padding">
<ion-col class="addon-block-timeline-activity-time ion-no-padding">
<ion-badge color="light">{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</ion-badge>
<core-mod-icon *ngIf="event.iconUrl" [modicon]="event.iconUrl" [componentId]="event.instance"
[modname]="event.modulename"> [modname]="event.modulename">
</core-mod-icon> </core-mod-icon>
<ion-label> </ion-col>
<ion-col class="addon-block-timeline-activity-name ion-no-padding">
<p class="item-heading"> <p class="item-heading">
<core-format-text [text]="event.name" contextLevel="module" [contextInstanceId]="event.id" <core-format-text [text]="event.activityname || event.name" contextLevel="module"
[courseId]="event.course && event.course.id"> [contextInstanceId]="event.id" [courseId]="event.course && event.course.id">
</core-format-text> </core-format-text>
<ion-badge *ngIf="event.overdue" color="danger">{{ 'addon.block_timeline.overdue' | translate }}
</ion-badge>
</p> </p>
<p *ngIf="showCourse && event.course"> <p *ngIf="(showCourse && event.course) || event.activitystr">
<span *ngIf="showCourse && event.course">
<core-format-text [text]="event.course.fullnamedisplay" contextLevel="course" <core-format-text [text]="event.course.fullnamedisplay" contextLevel="course"
[contextInstanceId]="event.course.id"> [contextInstanceId]="event.course.id">
</core-format-text> ·
</span>
<core-format-text [text]="event.activitystr" contextLevel="module" [contextInstanceId]="event.id">
</core-format-text> </core-format-text>
</p> </p>
</ion-col>
<ion-button fill="clear" class="ion-hide-md-up ion-text-wrap" (click)="action($event, event.action.url)" </ion-row>
[title]="event.action.name" [disabled]="!event.action.actionable" *ngIf="event.action"> </ion-col>
{{event.action.name}} <ion-col class="addon-block-timeline-activity-action ion-no-padding">
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">{{event.action.itemcount}} <ion-button fill="clear" (click)="action($event, event.action.url)" [title]="event.action.name"
</ion-badge> [disabled]="!event.action.actionable" *ngIf="event.action">
</ion-button>
</ion-label>
<div slot="end" class="events-info">
<div>
<ion-badge color="light">{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</ion-badge>
</div>
<ion-button
class="ion-hide-md-down"
fill="clear"
(click)="action($event, event.action.url)"
[title]="event.action.name"
[disabled]="!event.action.actionable" *ngIf="event.action"
>
{{event.action.name}} {{event.action.name}}
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount"> <ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">
{{event.action.itemcount}} {{event.action.itemcount}}
</ion-badge> </ion-badge>
</ion-button> </ion-button>
</div> </ion-col>
</ion-row>
</ion-label>
</ion-item> </ion-item>
</ng-container> </ng-container>
</ion-item-group> </ion-item-group>
@ -57,6 +68,10 @@
<ion-spinner *ngIf="loadingMore" [attr.aria-label]="'core.loading' | translate"></ion-spinner> <ion-spinner *ngIf="loadingMore" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
</div> </div>
<core-empty-box *ngIf="empty" image="assets/img/icons/activities.svg" [message]="'addon.block_timeline.noevents' | translate" <ion-item lines="none" *ngIf="empty && course">
inline="true"> <ion-label class="ion-text-wrap">
</core-empty-box> <p>{{'addon.block_timeline.noevents' | translate}}</p>
</ion-label>
</ion-item>
<core-empty-box *ngIf="empty && !course" image="assets/img/icons/activities.svg" inline="true"
[message]="'addon.block_timeline.noevents' | translate"></core-empty-box>

View File

@ -1,6 +1,41 @@
.events-info { @import "~theme/globals";
display: flex;
flex-direction: column; h3 {
text-align: end; font-weight: bold;
padding: 10px 0; font-size: 18px;
}
h4 {
font-size: 15px;
}
h4.core-bold {
font-weight: bold;
}
.addon-block-timeline-activity ion-badge {
@include margin-horizontal(0.25rem, 0.5rem);
}
.addon-block-timeline-activity core-mod-icon {
--margin-end: 0.5rem;
}
.addon-block-timeline-activity-time,
.addon-block-timeline-activity-action {
flex-grow: 0;
}
.addon-block-timeline-activity-main,
.addon-block-timeline-activity-name {
flex-grow: 1;
p {
overflow: hidden;
text-overflow: ellipsis;
}
}
.addon-block-timeline-activity-name {
flex-grow: 1;
overflow: hidden;
} }

View File

@ -17,11 +17,11 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time'; import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourse } from '@features/course/services/course'; import { CoreCourse } from '@features/course/services/course';
import moment from 'moment'; import moment from 'moment';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
import { AddonCalendarEvent } from '@addons/calendar/services/calendar'; import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
import { CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
/** /**
* Directive to render a list of events in course overview. * Directive to render a list of events in course overview.
@ -34,34 +34,37 @@ import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
export class AddonBlockTimelineEventsComponent implements OnChanges { export class AddonBlockTimelineEventsComponent implements OnChanges {
@Input() events: AddonBlockTimelineEvent[] = []; // The events to render. @Input() events: AddonBlockTimelineEvent[] = []; // The events to render.
@Input() showCourse?: boolean | string; // Whether to show the course name. @Input() course?: CoreEnrolledCourseDataWithOptions; // Whether to show the course name.
@Input() from = 0; // Number of days from today to offset the events. @Input() from = 0; // Number of days from today to offset the events.
@Input() to?: number; // Number of days from today to limit the events to. If not defined, no limit. @Input() to?: number; // Number of days from today to limit the events to. If not defined, no limit.
@Input() canLoadMore?: boolean; // Whether more events can be loaded. @Input() canLoadMore = false; // Whether more events can be loaded.
@Output() loadMore: EventEmitter<void>; // Notify that more events should be loaded. @Output() loadMore = new EventEmitter(); // Notify that more events should be loaded.
showCourse = false; // Whether to show the course name.
empty = true; empty = true;
loadingMore = false; loadingMore = false;
filteredEvents: AddonBlockTimelineEventFilteredEvent[] = []; filteredEvents: AddonBlockTimelineEventFilteredEvent[] = [];
constructor() {
this.loadMore = new EventEmitter();
}
/** /**
* Detect changes on input properties. * @inheritdoc
*/ */
async ngOnChanges(changes: {[name: string]: SimpleChange}): Promise<void> { async ngOnChanges(changes: {[name: string]: SimpleChange}): Promise<void> {
this.showCourse = CoreUtils.isTrueOrOne(this.showCourse); this.showCourse = !this.course;
if (changes.events || changes.from || changes.to) { if (changes.events || changes.from || changes.to) {
if (this.events && this.events.length > 0) { if (this.events && this.events.length > 0) {
const filteredEvents = await this.filterEventsByTime(this.from, this.to); const filteredEvents = await this.filterEventsByTime(this.from, this.to);
this.empty = !filteredEvents || filteredEvents.length <= 0; this.empty = !filteredEvents || filteredEvents.length <= 0;
const now = CoreTimeUtils.timestamp();
const eventsByDay: Record<number, AddonCalendarEvent[]> = {}; const eventsByDay: Record<number, AddonCalendarEvent[]> = {};
filteredEvents.forEach((event) => { filteredEvents.forEach((event) => {
const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort); const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort);
// Already calculated on 4.0 onwards but this will be live.
event.overdue = event.timesort < now;
if (eventsByDay[dayTimestamp]) { if (eventsByDay[dayTimestamp]) {
eventsByDay[dayTimestamp].push(event); eventsByDay[dayTimestamp].push(event);
} else { } else {
@ -69,15 +72,13 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
} }
}); });
const todaysMidnight = CoreTimeUtils.getMidnightForTimestamp(); this.filteredEvents = Object.keys(eventsByDay).map((key) => {
this.filteredEvents = [];
Object.keys(eventsByDay).forEach((key) => {
const dayTimestamp = parseInt(key); const dayTimestamp = parseInt(key);
this.filteredEvents.push({
color: dayTimestamp < todaysMidnight ? 'danger' : 'light', return {
dayTimestamp, dayTimestamp,
events: eventsByDay[dayTimestamp], events: eventsByDay[dayTimestamp],
}); };
}); });
} else { } else {
this.empty = true; this.empty = true;
@ -94,7 +95,7 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
*/ */
protected async filterEventsByTime(start: number, end?: number): Promise<AddonBlockTimelineEvent[]> { protected async filterEventsByTime(start: number, end?: number): Promise<AddonBlockTimelineEvent[]> {
start = moment().add(start, 'days').startOf('day').unix(); start = moment().add(start, 'days').startOf('day').unix();
end = typeof end != 'undefined' ? moment().add(end, 'days').startOf('day').unix() : end; end = end !== undefined ? moment().add(end, 'days').startOf('day').unix() : end;
return await Promise.all(this.events.filter((event) => { return await Promise.all(this.events.filter((event) => {
if (end) { if (end) {
@ -122,12 +123,12 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
/** /**
* Action clicked. * Action clicked.
* *
* @param e Click event. * @param event Click event.
* @param url Url of the action. * @param url Url of the action.
*/ */
async action(e: Event, url: string): Promise<void> { async action(event: Event, url: string): Promise<void> {
e.preventDefault(); event.preventDefault();
e.stopPropagation(); event.stopPropagation();
// Fix URL format. // Fix URL format.
url = CoreTextUtils.decodeHTMLEntities(url); url = CoreTextUtils.decodeHTMLEntities(url);
@ -137,7 +138,7 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
try { try {
const treated = await CoreContentLinksHelper.handleLink(url); const treated = await CoreContentLinksHelper.handleLink(url);
if (!treated) { if (!treated) {
return CoreSites.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(url); return CoreSites.getRequiredCurrentSite().openInBrowserWithAutoLoginIfSameSite(url);
} }
} finally { } finally {
modal.dismiss(); modal.dismiss();
@ -154,5 +155,4 @@ type AddonBlockTimelineEvent = AddonCalendarEvent & {
type AddonBlockTimelineEventFilteredEvent = { type AddonBlockTimelineEventFilteredEvent = {
events: AddonBlockTimelineEvent[]; events: AddonBlockTimelineEvent[];
dayTimestamp: number; dayTimestamp: number;
color: string;
}; };

View File

@ -1,5 +1,7 @@
<ion-item-divider sticky="true"> <ion-item-divider sticky="true">
<ion-label><h2>{{ 'addon.block_timeline.pluginname' | translate }}</h2></ion-label> <ion-label>
<h2>{{ 'addon.block_timeline.pluginname' | translate }}</h2>
</ion-label>
<core-context-menu slot="end"> <core-context-menu slot="end">
<core-context-menu-item *ngIf="loaded" [priority]="900" [content]="'addon.block_timeline.sortbydates' | translate" <core-context-menu-item *ngIf="loaded" [priority]="900" [content]="'addon.block_timeline.sortbydates' | translate"
(action)="switchSort('sortbydates')" [iconAction]="sort == 'sortbydates' ? 'far-dot-circle' : 'far-circle'"> (action)="switchSort('sortbydates')" [iconAction]="sort == 'sortbydates' ? 'far-dot-circle' : 'far-circle'">
@ -18,7 +20,7 @@
<ion-select-option class="ion-text-wrap" value="overdue"> <ion-select-option class="ion-text-wrap" value="overdue">
{{ 'addon.block_timeline.overdue' | translate }} {{ 'addon.block_timeline.overdue' | translate }}
</ion-select-option> </ion-select-option>
<ion-select-option class="ion-text-wrap" disabled value="disabled"> <ion-select-option class="ion-text-wrap core-select-option-title" disabled value="disabled">
{{ 'addon.block_timeline.duedate' | translate }} {{ 'addon.block_timeline.duedate' | translate }}
</ion-select-option> </ion-select-option>
<ion-select-option class="ion-text-wrap" value="next7days"> <ion-select-option class="ion-text-wrap" value="next7days">
@ -36,21 +38,14 @@
</core-combobox> </core-combobox>
</div> </div>
<core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'" [fullscreen]="false"> <core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'" [fullscreen]="false">
<addon-block-timeline-events [events]="timeline.events" showCourse="true" [canLoadMore]="timeline.canLoadMore" <addon-block-timeline-events [events]="timeline.events" [canLoadMore]="timeline.canLoadMore" (loadMore)="loadMore()"
(loadMore)="loadMoreTimeline()" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events> [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
</core-loading> </core-loading>
<core-loading [hideUntil]="timelineCourses.loaded" [hidden]="sort != 'sortbycourses'" <core-loading [hideUntil]="timelineCourses.loaded" [hidden]="sort != 'sortbycourses'" [fullscreen]="false" class="safe-area-page">
[fullscreen]="false" class="safe-area-padding"> <ng-container *ngFor="let course of timelineCourses.courses">
<ion-grid class="ion-no-padding"> <addon-block-timeline-events [events]="course.events" [canLoadMore]="course.canLoadMore" (loadMore)="loadMore(course)"
<ion-row class="ion-no-padding"> [course]="course" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
<ion-col *ngFor="let course of timelineCourses.courses" class="ion-no-padding" size="12" size-md="6"> </ng-container>
<core-courses-course-progress [course]="course">
<addon-block-timeline-events [events]="course.events" [canLoadMore]="course.canLoadMore"
(loadMore)="loadMoreCourse(course)" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
</core-courses-course-progress>
</ion-col>
</ion-row>
</ion-grid>
<core-empty-box *ngIf="timelineCourses.courses.length == 0" image="assets/img/icons/courses.svg" inline="true" <core-empty-box *ngIf="timelineCourses.courses.length == 0" image="assets/img/icons/courses.svg" inline="true"
[message]="'addon.block_timeline.nocoursesinprogress' | translate"></core-empty-box> [message]="'addon.block_timeline.nocoursesinprogress' | translate"></core-empty-box>
</core-loading> </core-loading>

View File

@ -24,6 +24,7 @@ import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/
import { CoreSite } from '@classes/site'; import { CoreSite } from '@classes/site';
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 { CoreNavigator } from '@services/navigator';
/** /**
* Component to render a timeline block. * Component to render a timeline block.
@ -36,7 +37,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
sort = 'sortbydates'; sort = 'sortbydates';
filter = 'next30days'; filter = 'next30days';
currentSite?: CoreSite; currentSite!: CoreSite;
timeline: { timeline: {
events: AddonCalendarEvent[]; events: AddonCalendarEvent[];
loaded: boolean; loaded: boolean;
@ -66,10 +67,18 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
} }
/** /**
* Component being initialized. * @inheritdoc
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
try {
this.currentSite = CoreSites.getRequiredCurrentSite(); this.currentSite = CoreSites.getRequiredCurrentSite();
} catch (error) {
CoreDomUtils.showErrorModal(error);
CoreNavigator.back();
return;
}
this.filter = await this.currentSite.getLocalSiteConfig('AddonBlockTimelineFilter', this.filter); this.filter = await this.currentSite.getLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
this.switchFilter(this.filter); this.switchFilter(this.filter);
@ -117,28 +126,21 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
} }
} }
/**
* Load more events.
*/
async loadMoreTimeline(): Promise<void> {
try {
await this.fetchMyOverviewTimeline(this.timeline.canLoadMore);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError);
}
}
/** /**
* Load more events. * Load more events.
* *
* @param course Course. * @param course Course. If defined, it will update the course events, timeline otherwise.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async loadMoreCourse(course: AddonBlockTimelineCourse): Promise<void> { async loadMore(course?: AddonBlockTimelineCourse): Promise<void> {
try { try {
if (course) {
const courseEvents = await AddonBlockTimeline.getActionEventsByCourse(course.id, course.canLoadMore); const courseEvents = await AddonBlockTimeline.getActionEventsByCourse(course.id, course.canLoadMore);
course.events = course.events?.concat(courseEvents.events); course.events = course.events?.concat(courseEvents.events);
course.canLoadMore = courseEvents.canLoadMore; course.canLoadMore = courseEvents.canLoadMore;
} else {
await this.fetchMyOverviewTimeline(this.timeline.canLoadMore);
}
} catch (error) { } catch (error) {
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError); CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError);
} }
@ -188,12 +190,12 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
*/ */
switchFilter(filter: string): void { switchFilter(filter: string): void {
this.filter = filter; this.filter = filter;
this.currentSite?.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter); this.currentSite.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
switch (this.filter) { switch (this.filter) {
case 'overdue': case 'overdue':
this.dataFrom = -14; this.dataFrom = -14;
this.dataTo = 0; this.dataTo = 1;
break; break;
case 'next7days': case 'next7days':
this.dataFrom = 0; this.dataFrom = 0;
@ -226,7 +228,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
*/ */
switchSort(sort: string): void { switchSort(sort: string): void {
this.sort = sort; this.sort = sort;
this.currentSite?.setLocalSiteConfig('AddonBlockTimelineSort', this.sort); this.currentSite.setLocalSiteConfig('AddonBlockTimelineSort', this.sort);
if (!this.timeline.loaded && this.sort == 'sortbydates') { if (!this.timeline.loaded && this.sort == 'sortbydates') {
this.fetchContent(); this.fetchContent();
@ -237,7 +239,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
} }
type AddonBlockTimelineCourse = CoreEnrolledCourseDataWithOptions & { export type AddonBlockTimelineCourse = CoreEnrolledCourseDataWithOptions & {
events?: AddonCalendarEvent[]; events?: AddonCalendarEvent[];
canLoadMore?: number; canLoadMore?: number;
}; };

View File

@ -1707,14 +1707,19 @@ export type AddonCalendarEventBase = {
userid?: number; // Userid. userid?: number; // Userid.
repeatid?: number; // Repeatid. repeatid?: number; // Repeatid.
eventcount?: number; // Eventcount. eventcount?: number; // Eventcount.
component?: string; // Component.
modulename?: string; // Modulename. modulename?: string; // Modulename.
activityname?: string; // Activityname.
activitystr?: string; // Activitystr.
instance?: number; // Instance. instance?: number; // Instance.
eventtype: AddonCalendarEventType; // Eventtype. eventtype: AddonCalendarEventType; // Eventtype.
timestart: number; // Timestart. timestart: number; // Timestart.
timeduration: number; // Timeduration. timeduration: number; // Timeduration.
timesort: number; // Timesort. timesort: number; // Timesort.
timeusermidnight: number; // Timeusermidnight.
visible: number; // Visible. visible: number; // Visible.
timemodified: number; // Timemodified. timemodified: number; // Timemodified.
overdue?: boolean; // Overdue.
icon: { icon: {
key: string; // Key. key: string; // Key.
component: string; // Component. component: string; // Component.

View File

@ -7,4 +7,8 @@
ion-item-divider { ion-item-divider {
min-height: var(--item-divider-min-height); min-height: var(--item-divider-min-height);
} }
::ng-deep core-loading {
--loading-inline-min-height: 44px;
}
} }

View File

@ -5,10 +5,8 @@
</div> </div>
<ion-item button lines="none" (click)="openCourse()" [attr.aria-label]="course.displayname || course.fullname" <ion-item button lines="none" (click)="openCourse()" [attr.aria-label]="course.displayname || course.fullname"
class="core-course-header" [class.item-disabled]="course.visible == 0" class="core-course-header" [class.item-disabled]="course.visible == 0"
[class.core-course-only-title]="!showAll || progress < 0 && completionUserTracked === false" [class.core-course-only-title]="!showAll || progress < 0 && completionUserTracked === false" detail="false">
detail="false"> <ion-label class="ion-text-wrap core-course-title"
<ion-label
class="ion-text-wrap core-course-title"
[class.core-course-with-buttons]="courseOptionMenuEnabled || (downloadCourseEnabled && showDownload)" [class.core-course-with-buttons]="courseOptionMenuEnabled || (downloadCourseEnabled && showDownload)"
[class.core-course-with-spinner]="(downloadCourseEnabled && prefetchCourseData.icon == 'spinner') || showSpinner"> [class.core-course-with-spinner]="(downloadCourseEnabled && prefetchCourseData.icon == 'spinner') || showSpinner">
<p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)" <p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)"
@ -19,8 +17,7 @@
</span> </span>
<span *ngIf="course.categoryname && course.displayname && course.shortname && course.fullname != course.displayname" <span *ngIf="course.categoryname && course.displayname && course.shortname && course.fullname != course.displayname"
class="core-course-category"> | </span> class="core-course-category"> | </span>
<span *ngIf="course.displayname && course.shortname && course.fullname != course.displayname" <span *ngIf="course.displayname && course.shortname && course.fullname != course.displayname" class="core-course-shortname">
class="core-course-shortname">
<core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id"> <core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text> </core-format-text>
</span> </span>
@ -35,12 +32,8 @@
</ion-label> </ion-label>
<div class="core-button-spinner" *ngIf="downloadCourseEnabled && !courseOptionMenuEnabled && showDownload" slot="end"> <div class="core-button-spinner" *ngIf="downloadCourseEnabled && !courseOptionMenuEnabled && showDownload" slot="end">
<core-download-refresh <core-download-refresh [status]="prefetchCourseData.status" [enabled]="downloadCourseEnabled"
[status]="prefetchCourseData.status" [statusTranslatable]="prefetchCourseData.statusTranslatable" canTrustDownload="false" [loading]="prefetchCourseData.loading"
[enabled]="downloadCourseEnabled"
[statusTranslatable]="prefetchCourseData.statusTranslatable"
canTrustDownload="false"
[loading]="prefetchCourseData.loading"
(action)="prefetchCourse()"></core-download-refresh> (action)="prefetchCourse()"></core-download-refresh>
</div> </div>
@ -50,9 +43,8 @@
[attr.aria-label]="'core.loading' | translate"></ion-spinner> [attr.aria-label]="'core.loading' | translate"></ion-spinner>
<!-- Downloaded icon. --> <!-- Downloaded icon. -->
<ion-icon *ngIf="downloadCourseEnabled && prefetchCourseData.downloadSucceeded && !showSpinner" <ion-icon *ngIf="downloadCourseEnabled && prefetchCourseData.downloadSucceeded && !showSpinner" class="core-icon-downloaded"
class="core-icon-downloaded" name="cloud-done" color="success" role="status" name="cloud-done" color="success" role="status" [attr.aria-label]="'core.downloaded' | translate"></ion-icon>
[attr.aria-label]="'core.downloaded' | translate"></ion-icon>
<!-- Options menu. --> <!-- Options menu. -->
<ion-button fill="clear" color="dark" (click)="showCourseOptionsMenu($event)" *ngIf="!showSpinner" <ion-button fill="clear" color="dark" (click)="showCourseOptionsMenu($event)" *ngIf="!showSpinner"
@ -61,11 +53,9 @@
</ion-button> </ion-button>
</div> </div>
</ion-item> </ion-item>
<ion-item *ngIf="showAll && progress >= 0 && completionUserTracked !== false" lines="none" <ion-item *ngIf="showAll && progress >= 0 && completionUserTracked !== false" lines="none" class="core-course-progress">
class="core-course-progress">
<ion-label> <ion-label>
<core-progress-bar [progress]="progress" a11yText="core.courses.aria:courseprogress"></core-progress-bar> <core-progress-bar [progress]="progress" a11yText="core.courses.aria:courseprogress"></core-progress-bar>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ng-content></ng-content>
</ion-card> </ion-card>

View File

@ -788,6 +788,13 @@ ion-select::part(icon) {
opacity: 1; opacity: 1;
} }
ion-select-popover ion-item.core-select-option-title {
cursor: pointer;
ion-radio {
display: none;
}
}
ion-searchbar { ion-searchbar {
.searchbar-search-icon.ios { .searchbar-search-icon.ios {
top: 4px; top: 4px;