MOBILE-3625 blocks: Timeline block
parent
15bb71b49b
commit
807a50a1df
|
@ -38,6 +38,7 @@ import { AddonBlockStarredCoursesModule } from './starredcourses/starredcourses.
|
||||||
import { AddonBlockTagsModule } from './tags/tags.module';
|
import { AddonBlockTagsModule } from './tags/tags.module';
|
||||||
import { AddonBlockActivityModulesModule } from './activitymodules/activitymodules.module';
|
import { AddonBlockActivityModulesModule } from './activitymodules/activitymodules.module';
|
||||||
import { AddonBlockRecentlyAccessedItemsModule } from './recentlyaccesseditems/recentlyaccesseditems.module';
|
import { AddonBlockRecentlyAccessedItemsModule } from './recentlyaccesseditems/recentlyaccesseditems.module';
|
||||||
|
import { AddonBlockTimelineModule } from './timeline/timeline.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [],
|
declarations: [],
|
||||||
|
@ -66,6 +67,7 @@ import { AddonBlockRecentlyAccessedItemsModule } from './recentlyaccesseditems/r
|
||||||
AddonBlockTagsModule,
|
AddonBlockTagsModule,
|
||||||
AddonBlockActivityModulesModule,
|
AddonBlockActivityModulesModule,
|
||||||
AddonBlockRecentlyAccessedItemsModule,
|
AddonBlockRecentlyAccessedItemsModule,
|
||||||
|
AddonBlockTimelineModule,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
exports: [],
|
exports: [],
|
||||||
|
|
|
@ -20,15 +20,15 @@
|
||||||
(onClosed)="switchFilterClosed()"></core-context-menu-item>
|
(onClosed)="switchFilterClosed()"></core-context-menu-item>
|
||||||
<core-context-menu-item *ngIf="loaded && showSortFilter" [priority]="900"
|
<core-context-menu-item *ngIf="loaded && showSortFilter" [priority]="900"
|
||||||
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.title' | translate)}}"
|
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.title' | translate)}}"
|
||||||
(action)="switchSort('fullname')" [iconAction]="sort == 'fullname' ? 'far-check-circle' : 'far-circle'">
|
(action)="switchSort('fullname')" [iconAction]="sort == 'fullname' ? 'far-dot-circle' : 'far-circle'">
|
||||||
</core-context-menu-item>
|
</core-context-menu-item>
|
||||||
<core-context-menu-item *ngIf="loaded && showSortFilter && showSortByShortName" [priority]="800"
|
<core-context-menu-item *ngIf="loaded && showSortFilter && showSortByShortName" [priority]="800"
|
||||||
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.shortname' | translate)}}"
|
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.shortname' | translate)}}"
|
||||||
(action)="switchSort('shortname')" [iconAction]="sort == 'shortname' ? 'far-check-circle' : 'far-circle'">
|
(action)="switchSort('shortname')" [iconAction]="sort == 'shortname' ? 'far-dot-circle' : 'far-circle'">
|
||||||
</core-context-menu-item>
|
</core-context-menu-item>
|
||||||
<core-context-menu-item *ngIf="loaded && showSortFilter" [priority]="700"
|
<core-context-menu-item *ngIf="loaded && showSortFilter" [priority]="700"
|
||||||
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.lastaccessed' | translate)}}"
|
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.lastaccessed' | translate)}}"
|
||||||
(action)="switchSort('lastaccess')" [iconAction]="sort == 'lastaccess' ? 'far-check-circle' : 'far-circle'">
|
(action)="switchSort('lastaccess')" [iconAction]="sort == 'lastaccess' ? 'far-dot-circle' : 'far-circle'">
|
||||||
</core-context-menu-item>
|
</core-context-menu-item>
|
||||||
</core-context-menu>
|
</core-context-menu>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
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 { AddonBlockTimelineEventsComponent } from './events/events';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonBlockTimelineComponent,
|
||||||
|
AddonBlockTimelineEventsComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreSharedModule,
|
||||||
|
CoreCoursesComponentsModule,
|
||||||
|
CoreCourseComponentsModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonBlockTimelineComponent,
|
||||||
|
AddonBlockTimelineEventsComponent,
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AddonBlockTimelineComponent,
|
||||||
|
AddonBlockTimelineEventsComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonBlockTimelineComponentsModule {}
|
|
@ -0,0 +1,55 @@
|
||||||
|
<ion-item-group *ngFor="let dayEvents of filteredEvents">
|
||||||
|
<ion-item-divider [color]="dayEvents.color">
|
||||||
|
<ion-label><h2>{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}</h2></ion-label>
|
||||||
|
</ion-item-divider>
|
||||||
|
<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)"
|
||||||
|
[title]="event.name">
|
||||||
|
<img slot="start" [src]="event.iconUrl" alt="" role="presentation" *ngIf="event.iconUrl" class="core-module-icon">
|
||||||
|
<ion-label>
|
||||||
|
<h2>
|
||||||
|
<core-format-text [text]="event.name" contextLevel="module" [contextInstanceId]="event.id"
|
||||||
|
[courseId]="event.course && event.course.id">
|
||||||
|
</core-format-text>
|
||||||
|
</h2>
|
||||||
|
<p *ngIf="showCourse && event.course">
|
||||||
|
<core-format-text [text]="event.course.fullnamedisplay" contextLevel="course"
|
||||||
|
[contextInstanceId]="event.course.id">
|
||||||
|
</core-format-text>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ion-button fill="clear" class="ion-hide-md-up" (click)="action($event, event.action.url)"
|
||||||
|
[title]="event.action.name" [disabled]="!event.action.actionable" *ngIf="event.action">
|
||||||
|
{{event.action.name}}
|
||||||
|
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">{{event.action.itemcount}}
|
||||||
|
</ion-badge>
|
||||||
|
</ion-button>
|
||||||
|
</ion-label>
|
||||||
|
|
||||||
|
<ion-grid slot="end">
|
||||||
|
<ion-row class="ion-justify-content-end">
|
||||||
|
<ion-badge color="light">{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</ion-badge>
|
||||||
|
</ion-row>
|
||||||
|
<ion-row class="ion-justify-content-end">
|
||||||
|
<ion-button fill="clear" class="ion-hide-md-down" (click)="action($event, event.action.url)"
|
||||||
|
[title]="event.action.name" [disabled]="!event.action.actionable" *ngIf="event.action">
|
||||||
|
{{event.action.name}}
|
||||||
|
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">{{event.action.itemcount}}
|
||||||
|
</ion-badge>
|
||||||
|
</ion-button>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
</ion-item-group>
|
||||||
|
|
||||||
|
<div class="ion-padding ion-text-center" *ngIf="canLoadMore && !empty">
|
||||||
|
<!-- Button and spinner to show more attempts. -->
|
||||||
|
<ion-button expand="block" (click)="loadMoreEvents()" color="light" *ngIf="!loadingMore">
|
||||||
|
{{ 'core.loadmore' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
<ion-spinner *ngIf="loadingMore"></ion-spinner>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<core-empty-box *ngIf="empty" image="assets/img/icons/activities.svg" [message]="'addon.block_timeline.noevents' | translate"
|
||||||
|
[inline]="!showCourse"></core-empty-box>
|
|
@ -0,0 +1,154 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Component, Input, Output, OnChanges, EventEmitter, SimpleChange } from '@angular/core';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||||
|
import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directive to render a list of events in course overview.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-block-timeline-events',
|
||||||
|
templateUrl: 'addon-block-timeline-events.html',
|
||||||
|
})
|
||||||
|
export class AddonBlockTimelineEventsComponent implements OnChanges {
|
||||||
|
|
||||||
|
@Input() events: AddonBlockTimelineEvent[] = []; // The events to render.
|
||||||
|
@Input() showCourse?: boolean | string; // Whether to show the course name.
|
||||||
|
@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() canLoadMore?: boolean; // Whether more events can be loaded.
|
||||||
|
@Output() loadMore: EventEmitter<void>; // Notify that more events should be loaded.
|
||||||
|
|
||||||
|
empty = true;
|
||||||
|
loadingMore = false;
|
||||||
|
filteredEvents: AddonBlockTimelineEventFilteredEvent[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.loadMore = new EventEmitter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect changes on input properties.
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
|
||||||
|
this.showCourse = CoreUtils.instance.isTrueOrOne(this.showCourse);
|
||||||
|
|
||||||
|
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: Record<number, AddonCalendarEvent[]> = {};
|
||||||
|
filteredEvents.forEach((event) => {
|
||||||
|
const dayTimestamp = CoreTimeUtils.instance.getMidnightForTimestamp(event.timesort);
|
||||||
|
if (eventsByDay[dayTimestamp]) {
|
||||||
|
eventsByDay[dayTimestamp].push(event);
|
||||||
|
} else {
|
||||||
|
eventsByDay[dayTimestamp] = [event];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const todaysMidnight = CoreTimeUtils.instance.getMidnightForTimestamp();
|
||||||
|
this.filteredEvents = [];
|
||||||
|
Object.keys(eventsByDay).forEach((key) => {
|
||||||
|
const dayTimestamp = parseInt(key);
|
||||||
|
this.filteredEvents.push({
|
||||||
|
color: dayTimestamp < todaysMidnight ? 'danger' : 'light',
|
||||||
|
dayTimestamp,
|
||||||
|
events: eventsByDay[dayTimestamp],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.empty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the events by time.
|
||||||
|
*
|
||||||
|
* @param start Number of days to start getting events from today. E.g. -1 will get events from yesterday.
|
||||||
|
* @param end Number of days after the start.
|
||||||
|
* @return Filtered events.
|
||||||
|
*/
|
||||||
|
protected filterEventsByTime(start: number, end?: number): AddonBlockTimelineEvent[] {
|
||||||
|
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) {
|
||||||
|
return start <= event.timesort && event.timesort < end;
|
||||||
|
}
|
||||||
|
|
||||||
|
return start <= event.timesort;
|
||||||
|
}).map((event) => {
|
||||||
|
event.iconUrl = CoreCourse.instance.getModuleIconSrc(event.icon.component);
|
||||||
|
|
||||||
|
return event;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load more events clicked.
|
||||||
|
*/
|
||||||
|
loadMoreEvents(): void {
|
||||||
|
this.loadingMore = true;
|
||||||
|
this.loadMore.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action clicked.
|
||||||
|
*
|
||||||
|
* @param e Click event.
|
||||||
|
* @param url Url of the action.
|
||||||
|
*/
|
||||||
|
async action(e: Event, url: string): Promise<void> {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Fix URL format.
|
||||||
|
url = CoreTextUtils.instance.decodeHTMLEntities(url);
|
||||||
|
|
||||||
|
const modal = await CoreDomUtils.instance.showModalLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const treated = await CoreContentLinksHelper.instance.handleLink(url);
|
||||||
|
if (!treated) {
|
||||||
|
return CoreSites.instance.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(url);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
modal.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddonBlockTimelineEvent = AddonCalendarEvent & {
|
||||||
|
iconUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AddonBlockTimelineEventFilteredEvent = {
|
||||||
|
events: AddonBlockTimelineEvent[];
|
||||||
|
dayTimestamp: number;
|
||||||
|
color: string;
|
||||||
|
};
|
|
@ -0,0 +1,44 @@
|
||||||
|
<ion-item-divider sticky="true">
|
||||||
|
<ion-label><h2>{{ 'addon.block_timeline.pluginname' | translate }}</h2></ion-label>
|
||||||
|
<core-context-menu slot="end">
|
||||||
|
<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'">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="loaded" [priority]="800" [content]="'addon.block_timeline.sortbycourses' | translate"
|
||||||
|
(action)="switchSort('sortbycourses')" [iconAction]="sort == 'sortbycourses' ? 'far-dot-circle' : 'far-circle'">
|
||||||
|
</core-context-menu-item>
|
||||||
|
</core-context-menu>
|
||||||
|
</ion-item-divider>
|
||||||
|
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||||
|
<div class="ion-padding safe-padding-horizontal">
|
||||||
|
<ion-select class="ion-text-start core-button-select" [(ngModel)]="filter" (ngModelChange)="switchFilter()"
|
||||||
|
interface="popover">
|
||||||
|
<ion-select-option value="all">{{ 'core.all' | translate }}</ion-select-option>
|
||||||
|
<ion-select-option value="overdue">{{ 'addon.block_timeline.overdue' | translate }}</ion-select-option>
|
||||||
|
<ion-select-option disabled value="disabled">{{ 'addon.block_timeline.duedate' | translate }}</ion-select-option>
|
||||||
|
<ion-select-option value="next7days">{{ 'addon.block_timeline.next7days' | translate }}</ion-select-option>
|
||||||
|
<ion-select-option value="next30days">{{ 'addon.block_timeline.next30days' | translate }}</ion-select-option>
|
||||||
|
<ion-select-option value="next3months">{{ 'addon.block_timeline.next3months' | translate }}</ion-select-option>
|
||||||
|
<ion-select-option value="next6months">{{ 'addon.block_timeline.next6months' | translate }}</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</div>
|
||||||
|
<core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'" class="core-loading-center">
|
||||||
|
<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]="timelineCourses.loaded" [hidden]="sort != 'sortbycourses'"
|
||||||
|
class="core-loading-center safe-area-page">
|
||||||
|
<ion-grid class="ion-no-padding">
|
||||||
|
<ion-row class="ion-no-padding">
|
||||||
|
<ion-col *ngFor="let course of timelineCourses.courses" class="ion-no-padding" size="12" size-md="6">
|
||||||
|
<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"
|
||||||
|
[message]="'addon.block_timeline.nocoursesinprogress' | translate"></core-empty-box>
|
||||||
|
</core-loading>
|
||||||
|
</core-loading>
|
|
@ -0,0 +1,240 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
|
||||||
|
import { AddonBlockTimeline } from '../../services/timeline';
|
||||||
|
import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
|
||||||
|
import { CoreSite } from '@classes/site';
|
||||||
|
import { CoreCourses } from '@features/courses/services/courses';
|
||||||
|
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render a timeline block.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-block-timeline',
|
||||||
|
templateUrl: 'addon-block-timeline.html',
|
||||||
|
})
|
||||||
|
export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implements OnInit {
|
||||||
|
|
||||||
|
sort = 'sortbydates';
|
||||||
|
filter = 'next30days';
|
||||||
|
currentSite?: CoreSite;
|
||||||
|
timeline: {
|
||||||
|
events: AddonCalendarEvent[];
|
||||||
|
loaded: boolean;
|
||||||
|
canLoadMore?: number;
|
||||||
|
} = {
|
||||||
|
events: <AddonCalendarEvent[]> [],
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
timelineCourses: {
|
||||||
|
courses: AddonBlockTimelineCourse[];
|
||||||
|
loaded: boolean;
|
||||||
|
canLoadMore?: number;
|
||||||
|
} = {
|
||||||
|
courses: [],
|
||||||
|
loaded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
dataFrom?: number;
|
||||||
|
dataTo?: number;
|
||||||
|
|
||||||
|
protected courseIds: number[] = [];
|
||||||
|
protected fetchContentDefaultError = 'Error getting timeline data.';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('AddonBlockTimelineComponent');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
this.currentSite = CoreSites.instance.getCurrentSite();
|
||||||
|
|
||||||
|
this.filter = await this.currentSite!.getLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
|
||||||
|
this.switchFilter();
|
||||||
|
|
||||||
|
this.sort = await this.currentSite!.getLocalSiteConfig('AddonBlockTimelineSort', this.sort);
|
||||||
|
|
||||||
|
super.ngOnInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the invalidate content function.
|
||||||
|
*
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
protected invalidateContent(): Promise<void> {
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
promises.push(AddonBlockTimeline.instance.invalidateActionEventsByTimesort());
|
||||||
|
promises.push(AddonBlockTimeline.instance.invalidateActionEventsByCourses());
|
||||||
|
promises.push(CoreCourses.instance.invalidateUserCourses());
|
||||||
|
promises.push(CoreCourseOptionsDelegate.instance.clearAndInvalidateCoursesOptions());
|
||||||
|
if (this.courseIds.length > 0) {
|
||||||
|
promises.push(CoreCourses.instance.invalidateCoursesByField('ids', this.courseIds.join(',')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreUtils.instance.allPromises(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the courses for my overview.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchContent(): Promise<void> {
|
||||||
|
if (this.sort == 'sortbydates') {
|
||||||
|
return this.fetchMyOverviewTimeline().finally(() => {
|
||||||
|
this.timeline.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sort == 'sortbycourses') {
|
||||||
|
return this.fetchMyOverviewTimelineByCourses().finally(() => {
|
||||||
|
this.timelineCourses.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load more events.
|
||||||
|
*/
|
||||||
|
async loadMoreTimeline(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.fetchMyOverviewTimeline(this.timeline.canLoadMore);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load more events.
|
||||||
|
*
|
||||||
|
* @param course Course.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async loadMoreCourse(course: AddonBlockTimelineCourse): Promise<void> {
|
||||||
|
try {
|
||||||
|
const courseEvents = await AddonBlockTimeline.instance.getActionEventsByCourse(course.id, course.canLoadMore);
|
||||||
|
course.events = course.events?.concat(courseEvents.events);
|
||||||
|
course.canLoadMore = courseEvents.canLoadMore;
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the timeline.
|
||||||
|
*
|
||||||
|
* @param afterEventId The last event id.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchMyOverviewTimeline(afterEventId?: number): Promise<void> {
|
||||||
|
const events = await AddonBlockTimeline.instance.getActionEventsByTimesort(afterEventId);
|
||||||
|
|
||||||
|
this.timeline.events = events.events;
|
||||||
|
this.timeline.canLoadMore = events.canLoadMore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the timeline by courses.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchMyOverviewTimelineByCourses(): Promise<void> {
|
||||||
|
const courses = await CoreCoursesHelper.instance.getUserCoursesWithOptions();
|
||||||
|
const today = CoreTimeUtils.instance.timestamp();
|
||||||
|
|
||||||
|
this.timelineCourses.courses = courses.filter((course) =>
|
||||||
|
(course.startdate || 0) <= today && (!course.enddate || course.enddate >= today));
|
||||||
|
|
||||||
|
if (this.timelineCourses.courses.length > 0) {
|
||||||
|
this.courseIds = this.timelineCourses.courses.map((course) => course.id);
|
||||||
|
|
||||||
|
const courseEvents = await AddonBlockTimeline.instance.getActionEventsByCourses(this.courseIds);
|
||||||
|
|
||||||
|
this.timelineCourses.courses.forEach((course) => {
|
||||||
|
course.events = courseEvents[course.id].events;
|
||||||
|
course.canLoadMore = courseEvents[course.id].canLoadMore;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change timeline filter being viewed.
|
||||||
|
*/
|
||||||
|
switchFilter(): void {
|
||||||
|
this.currentSite?.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @param sort New sorting.
|
||||||
|
*/
|
||||||
|
switchSort(sort: string): void {
|
||||||
|
this.sort = sort;
|
||||||
|
this.currentSite?.setLocalSiteConfig('AddonBlockTimelineSort', this.sort);
|
||||||
|
|
||||||
|
if (!this.timeline.loaded && this.sort == 'sortbydates') {
|
||||||
|
this.fetchContent();
|
||||||
|
} else if (!this.timelineCourses.loaded && this.sort == 'sortbycourses') {
|
||||||
|
this.fetchContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddonBlockTimelineCourse = CoreEnrolledCourseDataWithOptions & {
|
||||||
|
events?: AddonCalendarEvent[];
|
||||||
|
canLoadMore?: number;
|
||||||
|
};
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"duedate": "Due date",
|
||||||
|
"next30days": "Next 30 days",
|
||||||
|
"next3months": "Next 3 months",
|
||||||
|
"next6months": "Next 6 months",
|
||||||
|
"next7days": "Next 7 days",
|
||||||
|
"nocoursesinprogress": "No in-progress courses",
|
||||||
|
"noevents": "No upcoming activities due",
|
||||||
|
"overdue": "Overdue",
|
||||||
|
"pluginname": "Timeline",
|
||||||
|
"sortbycourses": "Sort by courses",
|
||||||
|
"sortbydates": "Sort by dates"
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreBlockHandlerData } from '@features/block/services/block-delegate';
|
||||||
|
import { CoreCourses } from '@features/courses/services/courses';
|
||||||
|
import { AddonBlockTimelineComponent } from '@addons/block/timeline/components/timeline/timeline';
|
||||||
|
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonBlockTimeline } from './timeline';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block handler.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonBlockTimelineHandlerService extends CoreBlockBaseHandler {
|
||||||
|
|
||||||
|
name = 'AddonBlockTimeline';
|
||||||
|
blockName = 'timeline';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
const enabled = await AddonBlockTimeline.instance.isAvailable();
|
||||||
|
const currentSite = CoreSites.instance.getCurrentSite();
|
||||||
|
|
||||||
|
return enabled && ((currentSite && currentSite.isVersionGreaterEqualThan('3.6')) ||
|
||||||
|
!CoreCourses.instance.isMyCoursesDisabledInSite());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data needed to render the block.
|
||||||
|
*
|
||||||
|
* @return Data or promise resolved with the data.
|
||||||
|
*/
|
||||||
|
getDisplayData(): CoreBlockHandlerData {
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: 'addon.block_timeline.pluginname',
|
||||||
|
class: 'addon-block-timeline',
|
||||||
|
component: AddonBlockTimelineComponent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AddonBlockTimelineHandler extends makeSingleton(AddonBlockTimelineHandlerService) {}
|
|
@ -0,0 +1,290 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreCoursesDashboard } from '@features/courses/services/dashboard';
|
||||||
|
import {
|
||||||
|
AddonCalendarEvents,
|
||||||
|
AddonCalendarEventsGroupedByCourse,
|
||||||
|
AddonCalendarEvent,
|
||||||
|
AddonCalendarGetActionEventsByCourseWSParams,
|
||||||
|
AddonCalendarGetActionEventsByTimesortWSParams,
|
||||||
|
AddonCalendarGetActionEventsByCoursesWSParams,
|
||||||
|
} from '@addons/calendar/services/calendar';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreSiteWSPreSets } from '@classes/site';
|
||||||
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
|
||||||
|
// Cache key was maintained from block myoverview when blocks were splitted.
|
||||||
|
const ROOT_CACHE_KEY = 'myoverview:';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that provides some features regarding course overview.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonBlockTimelineProvider {
|
||||||
|
|
||||||
|
static readonly EVENTS_LIMIT = 20;
|
||||||
|
static readonly EVENTS_LIMIT_PER_COURSE = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calendar action events for the given course.
|
||||||
|
*
|
||||||
|
* @param courseId Only events in this course.
|
||||||
|
* @param afterEventId The last seen event id.
|
||||||
|
* @param siteId Site ID. If not defined, use current site.
|
||||||
|
* @return Promise resolved when the info is retrieved.
|
||||||
|
*/
|
||||||
|
async getActionEventsByCourse(
|
||||||
|
courseId: number,
|
||||||
|
afterEventId?: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<{ events: AddonCalendarEvent[]; canLoadMore?: number }> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const time = moment().subtract(14, 'days').unix(); // Check two weeks ago.
|
||||||
|
|
||||||
|
const data: AddonCalendarGetActionEventsByCourseWSParams = {
|
||||||
|
timesortfrom: time,
|
||||||
|
courseid: courseId,
|
||||||
|
limitnum: AddonBlockTimelineProvider.EVENTS_LIMIT_PER_COURSE,
|
||||||
|
};
|
||||||
|
if (afterEventId) {
|
||||||
|
data.aftereventid = afterEventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getActionEventsByCourseCacheKey(courseId),
|
||||||
|
};
|
||||||
|
|
||||||
|
const courseEvents = await site.read<AddonCalendarEvents>(
|
||||||
|
'core_calendar_get_action_events_by_course',
|
||||||
|
data,
|
||||||
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (courseEvents && courseEvents.events) {
|
||||||
|
return this.treatCourseEvents(courseEvents, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CoreError('No events returned on core_calendar_get_action_events_by_course.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for get calendar action events for the given course value WS call.
|
||||||
|
*
|
||||||
|
* @param courseId Only events in this course.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getActionEventsByCourseCacheKey(courseId: number): string {
|
||||||
|
return this.getActionEventsByCoursesCacheKey() + ':' + courseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calendar action events for a given list of courses.
|
||||||
|
*
|
||||||
|
* @param courseIds Course IDs.
|
||||||
|
* @param siteId Site ID. If not defined, use current site.
|
||||||
|
* @return Promise resolved when the info is retrieved.
|
||||||
|
*/
|
||||||
|
async getActionEventsByCourses(
|
||||||
|
courseIds: number[],
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<{[courseId: string]: { events: AddonCalendarEvent[]; canLoadMore: number } }> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const time = moment().subtract(14, 'days').unix(); // Check two weeks ago.
|
||||||
|
|
||||||
|
const data: AddonCalendarGetActionEventsByCoursesWSParams = {
|
||||||
|
timesortfrom: time,
|
||||||
|
courseids: courseIds,
|
||||||
|
limitnum: AddonBlockTimelineProvider.EVENTS_LIMIT_PER_COURSE,
|
||||||
|
};
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getActionEventsByCoursesCacheKey(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const events = await site.read<AddonCalendarEventsGroupedByCourse>(
|
||||||
|
'core_calendar_get_action_events_by_courses',
|
||||||
|
data,
|
||||||
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (events && events.groupedbycourse) {
|
||||||
|
const courseEvents = {};
|
||||||
|
|
||||||
|
events.groupedbycourse.forEach((course) => {
|
||||||
|
courseEvents[course.courseid] = this.treatCourseEvents(course, time);
|
||||||
|
});
|
||||||
|
|
||||||
|
return courseEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CoreError('No events returned on core_calendar_get_action_events_by_courses.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for get calendar action events for a given list of courses value WS call.
|
||||||
|
*
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getActionEventsByCoursesCacheKey(): string {
|
||||||
|
return ROOT_CACHE_KEY + 'bycourse';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calendar action events based on the timesort value.
|
||||||
|
*
|
||||||
|
* @param afterEventId The last seen event id.
|
||||||
|
* @param siteId Site ID. If not defined, use current site.
|
||||||
|
* @return Promise resolved when the info is retrieved.
|
||||||
|
*/
|
||||||
|
async getActionEventsByTimesort(
|
||||||
|
afterEventId?: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<{ events: AddonCalendarEvent[]; canLoadMore?: number }> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const timesortfrom = moment().subtract(14, 'days').unix(); // Check two weeks ago.
|
||||||
|
const limitnum = AddonBlockTimelineProvider.EVENTS_LIMIT;
|
||||||
|
|
||||||
|
const data: AddonCalendarGetActionEventsByTimesortWSParams = {
|
||||||
|
timesortfrom,
|
||||||
|
limitnum,
|
||||||
|
};
|
||||||
|
if (afterEventId) {
|
||||||
|
data.aftereventid = afterEventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getActionEventsByTimesortCacheKey(afterEventId, limitnum),
|
||||||
|
getCacheUsingCacheKey: true,
|
||||||
|
uniqueCacheKey: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await site.read<AddonCalendarEvents>(
|
||||||
|
'core_calendar_get_action_events_by_timesort',
|
||||||
|
data,
|
||||||
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result && result.events) {
|
||||||
|
const canLoadMore = result.events.length >= limitnum ? result.lastid : undefined;
|
||||||
|
|
||||||
|
// Filter events by time in case it uses cache.
|
||||||
|
const events = result.events.filter((element) => element.timesort >= timesortfrom);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
canLoadMore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CoreError('No events returned on core_calendar_get_action_events_by_timesort.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get prefix cache key for calendar action events based on the timesort value WS calls.
|
||||||
|
*
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getActionEventsByTimesortPrefixCacheKey(): string {
|
||||||
|
return ROOT_CACHE_KEY + 'bytimesort:';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for get calendar action events based on the timesort value WS call.
|
||||||
|
*
|
||||||
|
* @param afterEventId The last seen event id.
|
||||||
|
* @param limit Limit num of the call.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getActionEventsByTimesortCacheKey(afterEventId?: number, limit?: number): string {
|
||||||
|
afterEventId = afterEventId || 0;
|
||||||
|
limit = limit || 0;
|
||||||
|
|
||||||
|
return this.getActionEventsByTimesortPrefixCacheKey() + afterEventId + ':' + limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates get calendar action events for a given list of courses WS call.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID to invalidate. If not defined, use current site.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateActionEventsByCourses(siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKeyStartingWith(this.getActionEventsByCoursesCacheKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates get calendar action events based on the timesort value WS call.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID to invalidate. If not defined, use current site.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateActionEventsByTimesort(siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKeyStartingWith(this.getActionEventsByTimesortPrefixCacheKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not My Overview is available for a certain site.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with true if available, resolved with false or rejected otherwise.
|
||||||
|
*/
|
||||||
|
async isAvailable(siteId?: string): Promise<boolean> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
// First check if dashboard is disabled.
|
||||||
|
if (CoreCoursesDashboard.instance.isDisabledInSite(site)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return site.wsAvailable('core_calendar_get_action_events_by_courses') &&
|
||||||
|
site.wsAvailable('core_calendar_get_action_events_by_timesort');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles course events, filtering and treating if more can be loaded.
|
||||||
|
*
|
||||||
|
* @param course Object containing response course events info.
|
||||||
|
* @param timeFrom Current time to filter events from.
|
||||||
|
* @return Object with course events and last loaded event id if more can be loaded.
|
||||||
|
*/
|
||||||
|
protected treatCourseEvents(
|
||||||
|
course: AddonCalendarEvents,
|
||||||
|
timeFrom: number,
|
||||||
|
): { events: AddonCalendarEvent[]; canLoadMore?: number } {
|
||||||
|
|
||||||
|
const canLoadMore: number | undefined =
|
||||||
|
course.events.length >= AddonBlockTimelineProvider.EVENTS_LIMIT_PER_COURSE ? course.lastid : undefined;
|
||||||
|
|
||||||
|
// Filter events by time in case it uses cache.
|
||||||
|
course.events = course.events.filter((element) => element.timesort >= timeFrom);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: course.events,
|
||||||
|
canLoadMore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AddonBlockTimeline extends makeSingleton(AddonBlockTimelineProvider) {}
|
|
@ -0,0 +1,38 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CoreBlockDelegate } from '@features/block/services/block-delegate';
|
||||||
|
import { AddonBlockTimelineComponentsModule } from './components/components.module';
|
||||||
|
import { AddonBlockTimelineHandler } from './services/block-handler';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
IonicModule,
|
||||||
|
AddonBlockTimelineComponentsModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
useValue: () => {
|
||||||
|
CoreBlockDelegate.instance.registerHandler(AddonBlockTimelineHandler.instance);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonBlockTimelineModule {}
|
|
@ -1749,6 +1749,7 @@ export class AddonCalendar extends makeSingleton(AddonCalendarProvider) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data returned by calendar's events_exporter.
|
* Data returned by calendar's events_exporter.
|
||||||
|
* Data returned by core_calendar_get_action_events_by_course and core_calendar_get_action_events_by_timesort WS.
|
||||||
*/
|
*/
|
||||||
export type AddonCalendarEvents = {
|
export type AddonCalendarEvents = {
|
||||||
events: AddonCalendarEvent[]; // Events.
|
events: AddonCalendarEvent[]; // Events.
|
||||||
|
@ -1756,13 +1757,47 @@ export type AddonCalendarEvents = {
|
||||||
lastid: number; // Lastid.
|
lastid: number; // Lastid.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of core_calendar_get_action_events_by_courses WS.
|
||||||
|
*/
|
||||||
|
export type AddonCalendarGetActionEventsByCoursesWSParams = {
|
||||||
|
courseids: number[];
|
||||||
|
timesortfrom?: number; // Time sort from.
|
||||||
|
timesortto?: number; // Time sort to.
|
||||||
|
limitnum?: number; // Limit number.
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data returned by calendar's events_grouped_by_course_exporter.
|
* Data returned by calendar's events_grouped_by_course_exporter.
|
||||||
|
* Data returned by core_calendar_get_action_events_by_courses WS.
|
||||||
*/
|
*/
|
||||||
export type AddonCalendarEventsGroupedByCourse = {
|
export type AddonCalendarEventsGroupedByCourse = {
|
||||||
groupedbycourse: AddonCalendarEventsSameCourse[]; // Groupped by course.
|
groupedbycourse: AddonCalendarEventsSameCourse[]; // Groupped by course.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of core_calendar_get_action_events_by_course WS.
|
||||||
|
*/
|
||||||
|
export type AddonCalendarGetActionEventsByCourseWSParams = {
|
||||||
|
courseid: number; // Course id.
|
||||||
|
timesortfrom?: number; // Time sort from.
|
||||||
|
timesortto?: number; // Time sort to.
|
||||||
|
aftereventid?: number; // The last seen event id.
|
||||||
|
limitnum?: number; // Limit number.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of core_calendar_get_action_events_by_timesort WS.
|
||||||
|
*/
|
||||||
|
export type AddonCalendarGetActionEventsByTimesortWSParams = {
|
||||||
|
timesortfrom?: number; // Time sort from.
|
||||||
|
timesortto?: number; // Time sort to.
|
||||||
|
aftereventid?: number; // The last seen event id.
|
||||||
|
limitnum?: number; // Limit number.
|
||||||
|
limittononsuspendedevents?: boolean; // Limit the events to courses the user is not suspended in.
|
||||||
|
userid?: number; // The user id.
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data returned by calendar's events_same_course_exporter.
|
* Data returned by calendar's events_same_course_exporter.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,178 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
viewBox="157 -1509 148 125"
|
||||||
|
preserveAspectRatio="xMinYMid meet"
|
||||||
|
version="1.1"
|
||||||
|
id="svg23"
|
||||||
|
sodipodi:docname="activities.svg"
|
||||||
|
inkscape:version="0.92.1 r15371">
|
||||||
|
<metadata
|
||||||
|
id="metadata27">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1016"
|
||||||
|
id="namedview25"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="5.981125"
|
||||||
|
inkscape:cx="38.889548"
|
||||||
|
inkscape:cy="62.5"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="27"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Group_42" />
|
||||||
|
<defs
|
||||||
|
id="defs7">
|
||||||
|
<style
|
||||||
|
id="style2">
|
||||||
|
.cls-1 {
|
||||||
|
clip-path: url(#clip-Activities);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #c4c8cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<clipPath
|
||||||
|
id="clip-Activities">
|
||||||
|
<rect
|
||||||
|
x="157"
|
||||||
|
y="-1509"
|
||||||
|
width="148"
|
||||||
|
height="125"
|
||||||
|
id="rect4" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
id="Activities"
|
||||||
|
class="cls-1"
|
||||||
|
clip-path="url(#clip-Activities)">
|
||||||
|
<g
|
||||||
|
id="Group_42"
|
||||||
|
data-name="Group 42"
|
||||||
|
transform="translate(-268 -1985)">
|
||||||
|
<ellipse
|
||||||
|
id="Ellipse_37"
|
||||||
|
data-name="Ellipse 37"
|
||||||
|
class="cls-2"
|
||||||
|
cx="74"
|
||||||
|
cy="14.785"
|
||||||
|
rx="74"
|
||||||
|
ry="14.785"
|
||||||
|
transform="translate(425 571.43)"
|
||||||
|
style="fill:#000000;fill-opacity:0.06666667" />
|
||||||
|
<rect
|
||||||
|
id="Rectangle_80"
|
||||||
|
data-name="Rectangle 80"
|
||||||
|
class="cls-3"
|
||||||
|
width="94.182"
|
||||||
|
height="110.215"
|
||||||
|
transform="translate(451.909 476)" />
|
||||||
|
<g
|
||||||
|
id="Group_41"
|
||||||
|
data-name="Group 41"
|
||||||
|
transform="translate(467.043 493)">
|
||||||
|
<rect
|
||||||
|
id="Rectangle_81"
|
||||||
|
data-name="Rectangle 81"
|
||||||
|
class="cls-4"
|
||||||
|
width="44.456"
|
||||||
|
height="5.625"
|
||||||
|
transform="translate(21.16 0.549)" />
|
||||||
|
<rect
|
||||||
|
id="Rectangle_82"
|
||||||
|
data-name="Rectangle 82"
|
||||||
|
class="cls-4"
|
||||||
|
width="33.342"
|
||||||
|
height="5.625"
|
||||||
|
transform="translate(21.16 11.652)" />
|
||||||
|
<rect
|
||||||
|
id="Rectangle_83"
|
||||||
|
data-name="Rectangle 83"
|
||||||
|
class="cls-4"
|
||||||
|
width="44.456"
|
||||||
|
height="5.625"
|
||||||
|
transform="translate(21.16 30.772)" />
|
||||||
|
<rect
|
||||||
|
id="Rectangle_84"
|
||||||
|
data-name="Rectangle 84"
|
||||||
|
class="cls-4"
|
||||||
|
width="33.342"
|
||||||
|
height="5.625"
|
||||||
|
transform="translate(21.16 41.875)" />
|
||||||
|
<rect
|
||||||
|
id="Rectangle_85"
|
||||||
|
data-name="Rectangle 85"
|
||||||
|
class="cls-4"
|
||||||
|
width="44.456"
|
||||||
|
height="5.625"
|
||||||
|
transform="translate(21.16 61.291)" />
|
||||||
|
<rect
|
||||||
|
id="Rectangle_86"
|
||||||
|
data-name="Rectangle 86"
|
||||||
|
class="cls-4"
|
||||||
|
width="33.342"
|
||||||
|
height="5.625"
|
||||||
|
transform="translate(21.16 72.393)" />
|
||||||
|
<ellipse
|
||||||
|
id="Ellipse_38"
|
||||||
|
data-name="Ellipse 38"
|
||||||
|
class="cls-4"
|
||||||
|
cx="7.007"
|
||||||
|
cy="7"
|
||||||
|
rx="7.007"
|
||||||
|
ry="7"
|
||||||
|
transform="translate(0 0)" />
|
||||||
|
<ellipse
|
||||||
|
id="Ellipse_39"
|
||||||
|
data-name="Ellipse 39"
|
||||||
|
class="cls-4"
|
||||||
|
cx="7.007"
|
||||||
|
cy="7"
|
||||||
|
rx="7.007"
|
||||||
|
ry="7"
|
||||||
|
transform="translate(0 31)" />
|
||||||
|
<ellipse
|
||||||
|
id="Ellipse_40"
|
||||||
|
data-name="Ellipse 40"
|
||||||
|
class="cls-4"
|
||||||
|
cx="7.007"
|
||||||
|
cy="7"
|
||||||
|
rx="7.007"
|
||||||
|
ry="7"
|
||||||
|
transform="translate(0 61)" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
Loading…
Reference in New Issue