MOBILE-2616 timeline: Filter events by days

main
Pau Ferrer Ocaña 2018-10-11 16:03:55 +02:00
parent d2383c72d2
commit f66c8375ea
14 changed files with 173 additions and 90 deletions

View File

@ -17,11 +17,14 @@
"addon.block_myoverview.nocoursesinprogress": "block_myoverview",
"addon.block_myoverview.nocoursespast": "block_myoverview",
"addon.block_myoverview.past": "block_myoverview",
"addon.block_timeline.duedate": "block_timeline",
"addon.block_timeline.next30days": "block_timeline",
"addon.block_timeline.next3months": "block_timeline",
"addon.block_timeline.next6months": "block_timeline",
"addon.block_timeline.next7days": "block_timeline",
"addon.block_timeline.nocoursesinprogress": "block_timeline",
"addon.block_timeline.noevents": "block_timeline",
"addon.block_timeline.recentlyoverdue": "local_moodlemobileapp",
"addon.block_timeline.overdue": "block_timeline",
"addon.block_timeline.sortbycourses": "block_timeline",
"addon.block_timeline.sortbydates": "block_timeline",
"addon.calendar.calendar": "calendar",
@ -1058,6 +1061,7 @@
"core.accounts": "admin",
"core.add": "moodle",
"core.agelocationverification": "moodle",
"core.all": "moodle",
"core.allparticipants": "moodle",
"core.android": "local_moodlemobileapp",
"core.answer": "moodle",

View File

@ -1,49 +1,25 @@
<ng-template #eventTemplate let-event="event">
<a ion-item core-link text-wrap detail-none capture="true" class="core-course-module-handler item-media" [href]="event.url" [title]="event.name">
<img item-start [src]="event.iconUrl" core-external-content alt="" role="presentation" *ngIf="event.iconUrl" class="core-module-icon">
<h2><core-format-text [text]="event.name"></core-format-text></h2>
<p>{{event.timesort * 1000 | coreFormatDate:"dfmediumdate" }} <core-format-text *ngIf="showCourse" [text]="event.course.fullnamedisplay"></core-format-text></p>
<button ion-button clear item-end class="hidden-phone" (click)="action($event, event.action.url)" [title]="event.action.name" [disabled]="!event.action.actionable" *ngIf="event.action">
{{event.action.name}}
<ion-badge item-end margin-start *ngIf="event.action.showitemcount">{{event.action.itemcount}}</ion-badge>
</button>
<ion-badge class="hidden-tablet" item-end *ngIf="event.action.showitemcount">{{event.action.itemcount}}</ion-badge>
</a>
</ng-template>
<ion-item-group *ngIf="recentlyOverdue.length > 0">
<ion-item-divider color="danger">{{ 'addon.block_timeline.recentlyoverdue' | translate }}</ion-item-divider>
<ng-container *ngFor="let event of recentlyOverdue">
<ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container>
</ng-container>
</ion-item-group>
<ion-item-group *ngIf="next7Days.length > 0">
<ion-item-divider color="light">{{ 'addon.block_timeline.next7days' | translate }}</ion-item-divider>
<ng-container *ngFor="let event of next7Days">
<ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container>
</ng-container>
</ion-item-group>
<ion-item-group *ngIf="next30Days.length > 0">
<ion-item-divider color="light">{{ 'addon.block_timeline.next30days' | translate }}</ion-item-divider>
<ng-container *ngFor="let event of next30Days">
<ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container>
</ng-container>
</ion-item-group>
<ion-item-group *ngIf="future.length > 0">
<ion-item-divider color="light">{{ 'addon.block_myoverview.future' | translate }}</ion-item-divider>
<ng-container *ngFor="let event of future">
<ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container>
<ion-item-group *ngFor="let dayEvents of filteredEvents">
<ion-item-divider [color]="dayEvents.color">
<h2>{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"LL" }}</h2>
</ion-item-divider>
<ng-container *ngFor="let event of dayEvents.events">
<a ion-item text-wrap detail-none class="core-course-module-handler item-media" (click)="action($event, event)" [title]="event.action.actionable ? event.action.name: event.name">
<img item-start [src]="event.iconUrl" core-external-content alt="" role="presentation" *ngIf="event.iconUrl" class="core-module-icon">
<h2><core-format-text [text]="event.name"></core-format-text></h2>
<p *ngIf="showCourse">
<core-format-text [text]="event.course.fullnamedisplay"></core-format-text>
</p>
<ion-badge color="light" item-end>{{event.timesort * 1000 | coreFormatDate:"LT" }}</ion-badge>
</a>
</ng-container>
</ion-item-group>
<div padding text-center *ngIf="canLoadMore && !empty">
<!-- Button and spinner to show more attempts. -->
<button *ngIf="!loadingMore" ion-button block (click)="loadMoreEvents()">{{ 'core.loadmore' | translate }}</button>
<button ion-button block (click)="loadMoreEvents()" color="light" *ngIf="!loadingMore">
{{ 'core.loadmore' | translate }}
</button>
<ion-spinner *ngIf="loadingMore"></ion-spinner>
</div>
<core-empty-box *ngIf="empty && showCourse" image="assets/img/icons/activities.svg" [message]="'addon.block_timeline.noevents' | translate"></core-empty-box>
<core-empty-box *ngIf="empty && !showCourse" [message]="'addon.block_timeline.noevents' | translate"></core-empty-box>
<core-empty-box *ngIf="empty" image="assets/img/icons/activities.svg" [message]="'addon.block_timeline.noevents' | translate" [inline]="!showCourse"></core-empty-box>

View File

@ -17,6 +17,7 @@ import { NavController } from 'ionic-angular';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
@ -30,23 +31,21 @@ import * as moment from 'moment';
templateUrl: 'addon-block-timeline-events.html'
})
export class AddonBlockTimelineEventsComponent implements OnChanges {
@Input() events: any[]; // The events to render.
@Input() events = []; // The events to render.
@Input() showCourse?: boolean | string; // Whether to show the course name.
@Input() from: number; // 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() canLoadMore?: boolean; // Whether more events can be loaded.
@Output() loadMore: EventEmitter<void>; // Notify that more events should be loaded.
empty: boolean;
loadingMore: boolean;
recentlyOverdue: any[] = [];
today: any[] = [];
next7Days: any[] = [];
next30Days: any[] = [];
future: any[] = [];
filteredEvents = [];
constructor(@Optional() private navCtrl: NavController, private utils: CoreUtilsProvider,
private textUtils: CoreTextUtilsProvider, private domUtils: CoreDomUtilsProvider,
private sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider,
private contentLinksHelper: CoreContentLinksHelperProvider) {
private contentLinksHelper: CoreContentLinksHelperProvider, private timeUtils: CoreTimeUtilsProvider) {
this.loadMore = new EventEmitter();
}
@ -56,8 +55,34 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
this.showCourse = this.utils.isTrueOrOne(this.showCourse);
if (changes.events) {
this.updateEvents();
if (changes.events || changes.from || changes.to) {
if (this.events && this.events.length > 0) {
const filteredEvents = this.filterEventsByTime(this.from, this.to);
this.empty = !filteredEvents || filteredEvents.length <= 0;
const eventsByDay = {};
filteredEvents.forEach((event) => {
const dayTimestamp = this.timeUtils.getMidnightForTimestamp(event.timesort);
if (eventsByDay[dayTimestamp]) {
eventsByDay[dayTimestamp].push(event);
} else {
eventsByDay[dayTimestamp] = [event];
}
});
const todaysMidnight = this.timeUtils.getMidnightForTimestamp();
this.filteredEvents = [];
Object.keys(eventsByDay).forEach((key) => {
const dayTimestamp = parseInt(key);
this.filteredEvents.push({
color: dayTimestamp < todaysMidnight ? 'danger' : 'light',
dayTimestamp: dayTimestamp,
events: eventsByDay[dayTimestamp]
});
});
} else {
this.empty = true;
}
}
}
@ -69,8 +94,8 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
* @return {any[]} Filtered events.
*/
protected filterEventsByTime(start: number, end?: number): any[] {
start = moment().add(start, 'days').unix();
end = typeof end != 'undefined' ? moment().add(end, 'days').unix() : end;
start = moment().add(start, 'days').startOf('day').unix();
end = typeof end != 'undefined' ? moment().add(end, 'days').startOf('day').unix() : end;
return this.events.filter((event) => {
if (end) {
@ -85,20 +110,6 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
});
}
/**
* Update the events displayed.
*/
protected updateEvents(): void {
this.empty = !this.events || this.events.length <= 0;
if (!this.empty) {
this.recentlyOverdue = this.filterEventsByTime(-14, 0);
this.today = this.filterEventsByTime(0, 1);
this.next7Days = this.filterEventsByTime(1, 7);
this.next30Days = this.filterEventsByTime(7, 30);
this.future = this.filterEventsByTime(30);
}
}
/**
* Load more events clicked.
*/
@ -110,15 +121,20 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
/**
* Action clicked.
*
* @param {Event} e Click event.
* @param {string} url Url of the action.
* @param {Event} e Click event.
* @param {any} event Calendar event info.
*/
action(e: Event, url: string): void {
action(e: Event, event: any): void {
e.preventDefault();
e.stopPropagation();
let url;
// Fix URL format.
url = this.textUtils.decodeHTMLEntities(url);
if (event.action.actionable) {
// Fix URL format.
url = this.textUtils.decodeHTMLEntities(event.action.url);
} else {
url = this.textUtils.decodeHTMLEntities(event.url);
}
const modal = this.domUtils.showModalLoading();
this.contentLinksHelper.handleLink(url, undefined, this.navCtrl).then((treated) => {

View File

@ -1,18 +1,31 @@
<div padding [hidden]="!loaded">
<ion-select [(ngModel)]="sort" (ngModelChange)="switchSort()" interface="popover" class="core-button-select">
<ion-option value="sortbydates">{{ 'addon.block_timeline.sortbydates' | translate }}</ion-option>
<ion-option value="sortbycourses">{{ 'addon.block_timeline.sortbycourses' | translate }}</ion-option>
</ion-select>
<div padding [hidden]="!loaded" ion-row>
<ion-col>
<ion-select text-start [(ngModel)]="filter" (ngModelChange)="switchFilter()" interface="popover" class="core-button-select">
<ion-option value="all">{{ 'core.all' | translate }}</ion-option>
<ion-option value="overdue">{{ 'addon.block_timeline.overdue' | translate }}</ion-option>
<ion-option disabled value="disabled">{{ 'addon.block_timeline.duedate' | translate }}</ion-option>
<ion-option value="next7days">{{ 'addon.block_timeline.next7days' | translate }}</ion-option>
<ion-option value="next30days">{{ 'addon.block_timeline.next30days' | translate }}</ion-option>
<ion-option value="next3months">{{ 'addon.block_timeline.next3months' | translate }}</ion-option>
<ion-option value="next6months">{{ 'addon.block_timeline.next6months' | translate }}</ion-option>
</ion-select>
</ion-col>
<ion-col>
<ion-select text-start [(ngModel)]="sort" (ngModelChange)="switchSort()" interface="popover" class="core-button-select">
<ion-option value="sortbydates">{{ 'addon.block_timeline.sortbydates' | translate }}</ion-option>
<ion-option value="sortbycourses">{{ 'addon.block_timeline.sortbycourses' | translate }}</ion-option>
</ion-select>
</ion-col>
</div>
<core-loading [hideUntil]="loaded && timeline.loaded" [hidden]="sort != 'sortbydates'" class="core-loading-center">
<addon-block-timeline-events [events]="timeline.events" showCourse="true" [canLoadMore]="timeline.canLoadMore" (loadMore)="loadMoreTimeline()"></addon-block-timeline-events>
<addon-block-timeline-events [events]="timeline.events" showCourse="true" [canLoadMore]="timeline.canLoadMore" (loadMore)="loadMoreTimeline()" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
</core-loading>
<core-loading [hideUntil]="loaded && timelineCourses.loaded" [hidden]="sort != 'sortbycourses'" class="core-loading-center">
<ion-grid no-padding>
<ion-row no-padding>
<ion-col *ngFor="let course of timelineCourses.courses" no-padding col-12 col-md-6>
<core-courses-course-progress [course]="course">
<addon-block-timeline-events [events]="course.events" [canLoadMore]="course.canLoadMore" (loadMore)="loadMoreCourse(course)"></addon-block-timeline-events>
<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>

View File

@ -30,6 +30,7 @@ import { AddonBlockTimelineProvider } from '../../providers/timeline';
})
export class AddonBlockTimelineComponent extends AddonBlockComponent implements OnInit {
sort = 'sortbydates';
filter = 'next30days';
timeline = {
events: [],
loaded: false,
@ -40,6 +41,8 @@ export class AddonBlockTimelineComponent extends AddonBlockComponent implements
loaded: false,
canLoadMore: false
};
dataFrom: number;
dataTo: number;
protected courseIds = [];
protected fetchContentDefaultError = 'Error getting timeline data.';
@ -55,6 +58,7 @@ export class AddonBlockTimelineComponent extends AddonBlockComponent implements
* Component being initialized.
*/
ngOnInit(): void {
this.switchFilter();
super.ngOnInit();
}
@ -159,6 +163,39 @@ export class AddonBlockTimelineComponent extends AddonBlockComponent implements
});
}
/**
* Change timeline filter being viewed.
*/
switchFilter(): void {
switch (this.filter) {
case 'overdue':
this.dataFrom = -14;
this.dataTo = 0;
break;
case 'next7days':
this.dataFrom = 0;
this.dataTo = 7;
break;
case 'next30days':
this.dataFrom = 0;
this.dataTo = 30;
break;
case 'next3months':
this.dataFrom = 0;
this.dataTo = 90;
break;
case 'next6months':
this.dataFrom = 0;
this.dataTo = 180;
break;
default:
case 'all':
this.dataFrom = -14;
this.dataTo = undefined;
break;
}
}
/**
* Change timeline sort being viewed.
*/

View File

@ -1,9 +1,12 @@
{
"next30days": "Next 30 days",
"next7days": "Next 7 days",
"duedate": "Due date",
"nocoursesinprogress": "No in progress courses",
"noevents": "No upcoming activities due",
"recentlyoverdue": "Recently overdue",
"next30days": "Next 30 days",
"next7days": "Next 7 days",
"next3months": "Next 3 months",
"next6months": "Next 6 months",
"overdue": "Overdue",
"sortbycourses": "Sort by courses",
"sortbydates": "Sort by dates"
}

View File

@ -417,12 +417,7 @@ ion-app.app-root {
margin: 0;
}
// Ionic fix. Button can occupy all page if not.
ion-select {
position: relative;
}
ion-col ion-select {
ion-col ion-select:not([text-start]) {
@include float(end);
max-width: none;
width: 100%;
@ -432,7 +427,12 @@ ion-app.app-root {
}
}
.item-radio-disabled ion-radio[ng-reflect-value="disabled"]{
display: none;
}
ion-select {
position: relative; // Ionic fix. Button can occupy all page if not.
color: $core-select-placeholder-color;
align-self: start;

View File

@ -17,11 +17,14 @@
"addon.block_myoverview.nocoursesinprogress": "No in progress courses",
"addon.block_myoverview.nocoursespast": "No past courses",
"addon.block_myoverview.past": "Past",
"addon.block_timeline.duedate": "Due date",
"addon.block_timeline.next30days": "Next 30 days",
"addon.block_timeline.next3months": "Next 3 months",
"addon.block_timeline.next6months": "Next 6 months",
"addon.block_timeline.next7days": "Next 7 days",
"addon.block_timeline.nocoursesinprogress": "No in progress courses",
"addon.block_timeline.noevents": "No upcoming activities due",
"addon.block_timeline.recentlyoverdue": "Recently overdue",
"addon.block_timeline.overdue": "Overdue",
"addon.block_timeline.sortbycourses": "Sort by courses",
"addon.block_timeline.sortbydates": "Sort by dates",
"addon.calendar.calendar": "Calendar",
@ -1058,6 +1061,7 @@
"core.accounts": "Accounts",
"core.add": "Add",
"core.agelocationverification": "Age and location verification",
"core.all": "All",
"core.allparticipants": "All participants",
"core.android": "Android",
"core.answer": "Answer",

View File

@ -1,4 +1,4 @@
<div class="core-empty-box" [class.core-empty-box-inline]="!image && !icon">
<div class="core-empty-box" [class.core-empty-box-inline]="(!image && !icon) || inline">
<div class="core-empty-box-content" padding>
<img *ngIf="image && !icon" [src]="image" role="presentation">
<core-icon *ngIf="icon" [name]="icon"></core-icon>

View File

@ -33,6 +33,7 @@ ion-app.app-root core-empty-box {
img {
height: 125px;
width: 145px;
margin: 0 auto;
}
p {
font-size: 120%;

View File

@ -28,6 +28,8 @@ export class CoreEmptyBoxComponent {
@Input() message: string; // Message to display.
@Input() icon?: string; // Name of the icon to use.
@Input() image?: string; // Image source. If an icon is provided, image won't be used.
@Input() inline?: boolean; // If this has to be shown inline instead of occupying whole page.
// If image or icon is not supplied, it's true by default.
constructor() {
// Nothing to do.

View File

@ -48,6 +48,15 @@ ion-app.app-root core-courses-course-progress {
.label {
@include margin(0, 0, 0, null);
}
ion-item-divider .label-md {
@extend .label-md;
}
ion-item-divider .label-wp {
@extend .label-wp;
}
ion-item-divider .label-ios {
@extend .label-ios;
}
}
button {

View File

@ -1,6 +1,7 @@
{
"accounts": "Accounts",
"add": "Add",
"all": "All",
"agelocationverification": "Age and location verification",
"allparticipants": "All participants",
"android": "Android",

View File

@ -163,4 +163,21 @@ export class CoreTimeUtilsProvider {
getLocalizedDateFormat(localizedFormat: any): string {
return moment.localeData().longDateFormat(localizedFormat);
}
/**
* For a given timestamp get the midnight value in the user's timezone.
*
* The calculation is performed relative to the user's midnight timestamp
* for today to ensure that timezones are preserved.
*
* @param {number} [timestamp] The timestamp to calculate from. If not defined, return today's midnight.
* @return {number} The midnight value of the user's timestamp.
*/
getMidnightForTimestamp(timestamp?: number): number {
if (timestamp) {
return moment(timestamp * 1000).startOf('day').unix();
} else {
return moment().startOf('day').unix();
}
}
}