diff --git a/src/addons/block/activitymodules/activitymodules.module.ts b/src/addons/block/activitymodules/activitymodules.module.ts new file mode 100644 index 000000000..a94a20a03 --- /dev/null +++ b/src/addons/block/activitymodules/activitymodules.module.ts @@ -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 { AddonBlockActivityModulesHandler } from './services/block-handler'; +import { AddonBlockActivityModulesComponentsModule } from './components/components.module'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockActivityModulesComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockActivityModulesHandler.instance); + }, + }, + ], +}) +export class AddonBlockActivityModulesModule {} diff --git a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts new file mode 100644 index 000000000..edcb45111 --- /dev/null +++ b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts @@ -0,0 +1,149 @@ +// (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 { CoreCourse } from '@features/course/services/course'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; +import { CoreSites } from '@services/sites'; +import { ContextLevel, CoreConstants } from '@/core/constants'; +import { Translate } from '@singletons'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreNavigator } from '@services/navigator'; + +/** + * Component to render an "activity modules" block. + */ +@Component({ + selector: 'addon-block-activitymodules', + templateUrl: 'addon-block-activitymodules.html', +}) +export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent implements OnInit { + + entries: AddonBlockActivityModuleEntry[] = []; + + protected fetchContentDefaultError = 'Error getting activity modules data.'; + + constructor() { + super('AddonBlockActivityModulesComponent'); + } + + /** + * Perform the invalidate content function. + * + * @return Resolved when done. + */ + protected async invalidateContent(): Promise { + await CoreCourse.instance.invalidateSections(this.instanceId); + } + + /** + * Fetch the data to render the block. + * + * @return Promise resolved when done. + */ + protected async fetchContent(): Promise { + const sections = await CoreCourse.instance.getSections(this.getCourseId(), false, true); + + this.entries = []; + const archetypes: Record = {}; + const modIcons: Record = {}; + let modFullNames: Record = {}; + sections.forEach((section) => { + if (!section.modules) { + return; + } + + section.modules.forEach((mod) => { + if (mod.uservisible === false || !CoreCourse.instance.moduleHasView(mod) || + typeof modFullNames[mod.modname] != 'undefined') { + // Ignore this module. + return; + } + + // Get the archetype of the module type. + if (typeof archetypes[mod.modname] == 'undefined') { + archetypes[mod.modname] = CoreCourseModuleDelegate.instance.supportsFeature( + mod.modname, + CoreConstants.FEATURE_MOD_ARCHETYPE, + CoreConstants.MOD_ARCHETYPE_OTHER, + ); + } + + // Get the full name of the module type. + if (archetypes[mod.modname] == CoreConstants.MOD_ARCHETYPE_RESOURCE) { + // All resources are gathered in a single "Resources" option. + if (!modFullNames['resources']) { + modFullNames['resources'] = Translate.instance.instant('core.resources'); + } + } else { + modFullNames[mod.modname] = mod.modplural; + } + modIcons[mod.modname] = mod.modicon; + }); + }); + // Sort the modnames alphabetically. + modFullNames = CoreUtils.instance.sortValues(modFullNames); + for (const modName in modFullNames) { + let icon: string; + + if (modName === 'resources') { + icon = CoreCourse.instance.getModuleIconSrc('page', modIcons['page']); + } else { + icon = CoreCourseModuleDelegate.instance.getModuleIconSrc(modName, modIcons[modName]); + } + + this.entries.push({ + icon: icon, + name: modFullNames[modName], + modName, + }); + } + } + + /** + * Obtain the appropiate course id for the block. + * + * @return Course id. + */ + protected getCourseId(): number { + if (this.contextLevel == ContextLevel.COURSE) { + return this.instanceId; + } + + return CoreSites.instance.getCurrentSiteHomeId(); + } + + /** + * Navigate to the activity list. + * + * @param entry Selected entry. + */ + gotoCoureListModType(entry: AddonBlockActivityModuleEntry): void { + CoreNavigator.instance.navigateToSitePath('course/list-mod-type', { + params: { + courseId: this.getCourseId(), + modName: entry.modName, + title: entry.name, + }, + }); + } + +} + +type AddonBlockActivityModuleEntry = { + icon: string; + name: string; + modName: string; +}; diff --git a/src/addons/block/activitymodules/components/activitymodules/addon-block-activitymodules.html b/src/addons/block/activitymodules/components/activitymodules/addon-block-activitymodules.html new file mode 100644 index 000000000..ea3b50dea --- /dev/null +++ b/src/addons/block/activitymodules/components/activitymodules/addon-block-activitymodules.html @@ -0,0 +1,11 @@ + + +

{{ 'addon.block_activitymodules.pluginname' | translate }}

+
+
+ + + + {{ entry.name }} + + diff --git a/src/addons/block/activitymodules/components/components.module.ts b/src/addons/block/activitymodules/components/components.module.ts new file mode 100644 index 000000000..3449dbce6 --- /dev/null +++ b/src/addons/block/activitymodules/components/components.module.ts @@ -0,0 +1,43 @@ +// (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 { FormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreSharedModule } from '@/core/shared.module'; + +import { AddonBlockActivityModulesComponent } from './activitymodules/activitymodules'; + +@NgModule({ + declarations: [ + AddonBlockActivityModulesComponent, + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreSharedModule, + ], + exports: [ + AddonBlockActivityModulesComponent, + ], + entryComponents: [ + AddonBlockActivityModulesComponent, + ], +}) +export class AddonBlockActivityModulesComponentsModule {} diff --git a/src/addons/block/activitymodules/lang.json b/src/addons/block/activitymodules/lang.json new file mode 100644 index 000000000..7f1c7ab21 --- /dev/null +++ b/src/addons/block/activitymodules/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Activities" +} diff --git a/src/addons/block/activitymodules/services/block-handler.ts b/src/addons/block/activitymodules/services/block-handler.ts new file mode 100644 index 000000000..a6f7885a2 --- /dev/null +++ b/src/addons/block/activitymodules/services/block-handler.ts @@ -0,0 +1,46 @@ +// (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 { CoreBlockHandlerData } from '@features/block/services/block-delegate'; +import { AddonBlockActivityModulesComponent } from '../components/activitymodules/activitymodules'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockActivityModulesHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockActivityModules'; + blockName = 'activity_modules'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_activitymodules.pluginname', + class: 'addon-block-activitymodules', + component: AddonBlockActivityModulesComponent, + }; + } + +} + +export class AddonBlockActivityModulesHandler extends makeSingleton(AddonBlockActivityModulesHandlerService) {} diff --git a/src/addons/block/block.module.ts b/src/addons/block/block.module.ts index 420c0e632..0d8ff2d47 100644 --- a/src/addons/block/block.module.ts +++ b/src/addons/block/block.module.ts @@ -36,6 +36,9 @@ import { AddonBlockSelfCompletionModule } from './selfcompletion/selfcompletion. import { AddonBlockSiteMainMenuModule } from './sitemainmenu/sitemainmenu.module'; import { AddonBlockStarredCoursesModule } from './starredcourses/starredcourses.module'; import { AddonBlockTagsModule } from './tags/tags.module'; +import { AddonBlockActivityModulesModule } from './activitymodules/activitymodules.module'; +import { AddonBlockRecentlyAccessedItemsModule } from './recentlyaccesseditems/recentlyaccesseditems.module'; +import { AddonBlockTimelineModule } from './timeline/timeline.module'; @NgModule({ declarations: [], @@ -62,6 +65,9 @@ import { AddonBlockTagsModule } from './tags/tags.module'; AddonBlockSiteMainMenuModule, AddonBlockStarredCoursesModule, AddonBlockTagsModule, + AddonBlockActivityModulesModule, + AddonBlockRecentlyAccessedItemsModule, + AddonBlockTimelineModule, ], providers: [], exports: [], diff --git a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html index acf647cf3..f9576466d 100644 --- a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html +++ b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html @@ -20,15 +20,15 @@ (onClosed)="switchFilterClosed()"> + (action)="switchSort('fullname')" [iconAction]="sort == 'fullname' ? 'far-dot-circle' : 'far-circle'"> + (action)="switchSort('shortname')" [iconAction]="sort == 'shortname' ? 'far-dot-circle' : 'far-circle'"> + (action)="switchSort('lastaccess')" [iconAction]="sort == 'lastaccess' ? 'far-dot-circle' : 'far-circle'"> diff --git a/src/addons/block/recentlyaccesseditems/components/components.module.ts b/src/addons/block/recentlyaccesseditems/components/components.module.ts new file mode 100644 index 000000000..9aa9ce8a5 --- /dev/null +++ b/src/addons/block/recentlyaccesseditems/components/components.module.ts @@ -0,0 +1,43 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { CoreCoursesComponentsModule } from '@features/courses/components/components.module'; + +import { AddonBlockRecentlyAccessedItemsComponent } from './recentlyaccesseditems/recentlyaccesseditems'; + +@NgModule({ + declarations: [ + AddonBlockRecentlyAccessedItemsComponent, + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreSharedModule, + CoreCoursesComponentsModule, + ], + exports: [ + AddonBlockRecentlyAccessedItemsComponent, + ], + entryComponents: [ + AddonBlockRecentlyAccessedItemsComponent, + ], +}) +export class AddonBlockRecentlyAccessedItemsComponentsModule {} diff --git a/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/addon-block-recentlyaccesseditems.html b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/addon-block-recentlyaccesseditems.html new file mode 100644 index 000000000..2008fbce0 --- /dev/null +++ b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/addon-block-recentlyaccesseditems.html @@ -0,0 +1,29 @@ + +

{{ 'addon.block_recentlyaccesseditems.pluginname' | translate }}

+
+ +
+
+ + + + +

+ +

+

+ + +

+
+
+
+
+
+ + + +
diff --git a/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.scss b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.scss new file mode 100644 index 000000000..fc9edbddf --- /dev/null +++ b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.scss @@ -0,0 +1,7 @@ +@import "~theme/globals"; + +:host { + .core-horizontal-scroll > div { + @include horizontal_scroll_item(80%, 250px, 300px); + } +} diff --git a/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.ts b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.ts new file mode 100644 index 000000000..65c55f24e --- /dev/null +++ b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.ts @@ -0,0 +1,86 @@ +// (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 { CoreSites } from '@services/sites'; +import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; +import { + AddonBlockRecentlyAccessedItems, + AddonBlockRecentlyAccessedItemsItem, +} from '../../services/recentlyaccesseditems'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; + +/** + * Component to render a recently accessed items block. + */ +@Component({ + selector: 'addon-block-recentlyaccesseditems', + templateUrl: 'addon-block-recentlyaccesseditems.html', + styleUrls: ['recentlyaccesseditems.scss'], +}) +export class AddonBlockRecentlyAccessedItemsComponent extends CoreBlockBaseComponent implements OnInit { + + items: AddonBlockRecentlyAccessedItemsItem[] = []; + + protected fetchContentDefaultError = 'Error getting recently accessed items data.'; + + constructor() { + super('AddonBlockRecentlyAccessedItemsComponent'); + } + + + /** + * Perform the invalidate content function. + * + * @return Resolved when done. + */ + protected async invalidateContent(): Promise { + await AddonBlockRecentlyAccessedItems.instance.invalidateRecentItems(); + } + + /** + * Fetch the data to render the block. + * + * @return Promise resolved when done. + */ + protected async fetchContent(): Promise { + this.items = await AddonBlockRecentlyAccessedItems.instance.getRecentItems(); + } + + /** + * Event clicked. + * + * @param e Click event. + * @param item Activity item info. + */ + async action(e: Event, item: AddonBlockRecentlyAccessedItemsItem): Promise { + e.preventDefault(); + e.stopPropagation(); + + const url = CoreTextUtils.instance.decodeHTMLEntities(item.viewurl); + 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(); + } + } + +} diff --git a/src/addons/block/recentlyaccesseditems/lang.json b/src/addons/block/recentlyaccesseditems/lang.json new file mode 100644 index 000000000..47311d281 --- /dev/null +++ b/src/addons/block/recentlyaccesseditems/lang.json @@ -0,0 +1,4 @@ +{ + "noitems": "No recent items", + "pluginname": "Recently accessed items" +} diff --git a/src/addons/block/recentlyaccesseditems/recentlyaccesseditems.module.ts b/src/addons/block/recentlyaccesseditems/recentlyaccesseditems.module.ts new file mode 100644 index 000000000..a25826da9 --- /dev/null +++ b/src/addons/block/recentlyaccesseditems/recentlyaccesseditems.module.ts @@ -0,0 +1,40 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockRecentlyAccessedItemsComponentsModule } from './components/components.module'; +import { AddonBlockRecentlyAccessedItemsHandler } from './services/block-handler'; + +@NgModule({ + imports: [ + IonicModule, + CoreSharedModule, + AddonBlockRecentlyAccessedItemsComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockRecentlyAccessedItemsHandler.instance); + }, + }, + ], +}) +export class AddonBlockRecentlyAccessedItemsModule {} diff --git a/src/addons/block/recentlyaccesseditems/services/block-handler.ts b/src/addons/block/recentlyaccesseditems/services/block-handler.ts new file mode 100644 index 000000000..cfc9ddf00 --- /dev/null +++ b/src/addons/block/recentlyaccesseditems/services/block-handler.ts @@ -0,0 +1,50 @@ +// (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 { CoreBlockHandlerData } from '@features/block/services/block-delegate'; +import { AddonBlockRecentlyAccessedItemsComponent } from '../components/recentlyaccesseditems/recentlyaccesseditems'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable( { providedIn: 'root' }) +export class AddonBlockRecentlyAccessedItemsHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockRecentlyAccessedItems'; + blockName = 'recentlyaccesseditems'; + + /** + * Returns the data needed to render the block. + * + * @param injector Injector. + * @param block The block to render. + * @param contextLevel The context where the block will be used. + * @param instanceId The instance ID associated with the context level. + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData{ + + return { + title: 'addon.block_recentlyaccesseditems.pluginname', + class: 'addon-block-recentlyaccesseditems', + component: AddonBlockRecentlyAccessedItemsComponent, + }; + } + +} + +export class AddonBlockRecentlyAccessedItemsHandler extends makeSingleton(AddonBlockRecentlyAccessedItemsHandlerService) {} diff --git a/src/addons/block/recentlyaccesseditems/services/recentlyaccesseditems.ts b/src/addons/block/recentlyaccesseditems/services/recentlyaccesseditems.ts new file mode 100644 index 000000000..42376d2c5 --- /dev/null +++ b/src/addons/block/recentlyaccesseditems/services/recentlyaccesseditems.ts @@ -0,0 +1,102 @@ +// (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 { CoreDomUtils } from '@services/utils/dom'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreSiteWSPreSets } from '@classes/site'; +import { makeSingleton } from '@singletons'; + +const ROOT_CACHE_KEY = 'AddonBlockRecentlyAccessedItems:'; + +/** + * Service that provides some features regarding recently accessed items. + */ +@Injectable( { providedIn: 'root' }) +export class AddonBlockRecentlyAccessedItemsProvider { + + /** + * Get cache key for get last accessed items value WS call. + * + * @return Cache key. + */ + protected getRecentItemsCacheKey(): string { + return ROOT_CACHE_KEY + ':recentitems'; + } + + /** + * Get last accessed items. + * + * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved when the info is retrieved. + */ + async getRecentItems(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getRecentItemsCacheKey(), + }; + + const items: AddonBlockRecentlyAccessedItemsItem[] = + await site.read('block_recentlyaccesseditems_get_recent_items', undefined, preSets); + + return items.map((item) => { + const modicon = item.icon && CoreDomUtils.instance.getHTMLElementAttribute(item.icon, 'src'); + + item.iconUrl = CoreCourse.instance.getModuleIconSrc(item.modname, modicon || undefined); + + return item; + }); + } + + /** + * Invalidates get last accessed items WS call. + * + * @param siteId Site ID to invalidate. If not defined, use current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateRecentItems(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getRecentItemsCacheKey()); + } + +} +export class AddonBlockRecentlyAccessedItems extends makeSingleton(AddonBlockRecentlyAccessedItemsProvider) {} + + +/** + * Result of WS block_recentlyaccesseditems_get_recent_items. + */ +export type AddonBlockRecentlyAccessedItemsItem = { + id: number; // Id. + courseid: number; // Courseid. + cmid: number; // Cmid. + userid: number; // Userid. + modname: string; // Modname. + name: string; // Name. + coursename: string; // Coursename. + timeaccess: number; // Timeaccess. + viewurl: string; // Viewurl. + courseviewurl: string; // Courseviewurl. + icon: string; // Icon. +} & AddonBlockRecentlyAccessedItemsItemCalculatedData; + +/** + * Calculated data for recently accessed item. + */ +export type AddonBlockRecentlyAccessedItemsItemCalculatedData = { + iconUrl: string; // Icon URL. Calculated by the app. +}; diff --git a/src/addons/block/sitemainmenu/components/components.module.ts b/src/addons/block/sitemainmenu/components/components.module.ts index 52c1968e8..81ebe3f25 100644 --- a/src/addons/block/sitemainmenu/components/components.module.ts +++ b/src/addons/block/sitemainmenu/components/components.module.ts @@ -18,7 +18,7 @@ import { IonicModule } from '@ionic/angular'; import { TranslateModule } from '@ngx-translate/core'; import { CoreSharedModule } from '@/core/shared.module'; -// import { CoreCourseComponentsModule } from '@features/course/components/components.module'; +import { CoreCourseComponentsModule } from '@features/course/components/components.module'; import { AddonBlockSiteMainMenuComponent } from './sitemainmenu/sitemainmenu'; @@ -32,7 +32,7 @@ import { AddonBlockSiteMainMenuComponent } from './sitemainmenu/sitemainmenu'; IonicModule, TranslateModule.forChild(), CoreSharedModule, - // CoreCourseComponentsModule, + CoreCourseComponentsModule, ], exports: [ AddonBlockSiteMainMenuComponent, diff --git a/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html b/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html index fa047f050..8f9f3989a 100644 --- a/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html +++ b/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html @@ -12,7 +12,7 @@ - + diff --git a/src/addons/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts b/src/addons/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts index f572b5067..5f8c2a85b 100644 --- a/src/addons/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts +++ b/src/addons/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts @@ -91,7 +91,7 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl const items = config.frontpageloggedin.split(','); const hasNewsItem = items.find((item) => parseInt(item, 10) == FrontPageItemNames['NEWS_ITEMS']); - const result = await CoreCourseHelper.instance.addHandlerDataForModules( + const result = CoreCourseHelper.instance.addHandlerDataForModules( [mainMenuBlock], this.siteHomeId, undefined, diff --git a/src/addons/block/timeline/components/components.module.ts b/src/addons/block/timeline/components/components.module.ts new file mode 100644 index 000000000..d2f4620cd --- /dev/null +++ b/src/addons/block/timeline/components/components.module.ts @@ -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 {} diff --git a/src/addons/block/timeline/components/events/addon-block-timeline-events.html b/src/addons/block/timeline/components/events/addon-block-timeline-events.html new file mode 100644 index 000000000..67db5d061 --- /dev/null +++ b/src/addons/block/timeline/components/events/addon-block-timeline-events.html @@ -0,0 +1,55 @@ + + +

{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}

+
+ + + + +

+ + +

+

+ + +

+ + + {{event.action.name}} + {{event.action.itemcount}} + + +
+ + + + {{event.timesort * 1000 | coreFormatDate:"strftimetime24" }} + + + + {{event.action.name}} + {{event.action.itemcount}} + + + + +
+
+
+ +
+ + + {{ 'core.loadmore' | translate }} + + +
+ + diff --git a/src/addons/block/timeline/components/events/events.ts b/src/addons/block/timeline/components/events/events.ts new file mode 100644 index 000000000..0f5a3f97e --- /dev/null +++ b/src/addons/block/timeline/components/events/events.ts @@ -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; // 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 = {}; + 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 { + 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; +}; diff --git a/src/addons/block/timeline/components/timeline/addon-block-timeline.html b/src/addons/block/timeline/components/timeline/addon-block-timeline.html new file mode 100644 index 000000000..188b94023 --- /dev/null +++ b/src/addons/block/timeline/components/timeline/addon-block-timeline.html @@ -0,0 +1,44 @@ + +

{{ 'addon.block_timeline.pluginname' | translate }}

+ + + + + + +
+ +
+ + {{ 'core.all' | translate }} + {{ 'addon.block_timeline.overdue' | translate }} + {{ 'addon.block_timeline.duedate' | translate }} + {{ 'addon.block_timeline.next7days' | translate }} + {{ 'addon.block_timeline.next30days' | translate }} + {{ 'addon.block_timeline.next3months' | translate }} + {{ 'addon.block_timeline.next6months' | translate }} + +
+ + + + + + + + + + + + + + + +
diff --git a/src/addons/block/timeline/components/timeline/timeline.ts b/src/addons/block/timeline/components/timeline/timeline.ts new file mode 100644 index 000000000..4f25fcfea --- /dev/null +++ b/src/addons/block/timeline/components/timeline/timeline.ts @@ -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: [], + 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 { + 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 { + const promises: Promise[] = []; + + 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 { + 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 { + 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 { + 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 { + 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 { + 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; +}; diff --git a/src/addons/block/timeline/lang.json b/src/addons/block/timeline/lang.json new file mode 100644 index 000000000..fbd482f47 --- /dev/null +++ b/src/addons/block/timeline/lang.json @@ -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" +} \ No newline at end of file diff --git a/src/addons/block/timeline/services/block-handler.ts b/src/addons/block/timeline/services/block-handler.ts new file mode 100644 index 000000000..277fd6dc7 --- /dev/null +++ b/src/addons/block/timeline/services/block-handler.ts @@ -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 { + 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) {} diff --git a/src/addons/block/timeline/services/timeline.ts b/src/addons/block/timeline/services/timeline.ts new file mode 100644 index 000000000..3848dbd1a --- /dev/null +++ b/src/addons/block/timeline/services/timeline.ts @@ -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( + '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( + '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( + '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 { + 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 { + 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 { + 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) {} diff --git a/src/addons/block/timeline/timeline.module.ts b/src/addons/block/timeline/timeline.module.ts new file mode 100644 index 000000000..3c024d20c --- /dev/null +++ b/src/addons/block/timeline/timeline.module.ts @@ -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 {} diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts index 4644dded1..fe7e1d6a1 100644 --- a/src/addons/calendar/services/calendar.ts +++ b/src/addons/calendar/services/calendar.ts @@ -1749,6 +1749,7 @@ export class AddonCalendar extends makeSingleton(AddonCalendarProvider) {} /** * 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 = { events: AddonCalendarEvent[]; // Events. @@ -1756,13 +1757,47 @@ export type AddonCalendarEvents = { 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 core_calendar_get_action_events_by_courses WS. */ export type AddonCalendarEventsGroupedByCourse = { 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. */ diff --git a/src/assets/img/icons/activities.svg b/src/assets/img/icons/activities.svg new file mode 100644 index 000000000..56243a53c --- /dev/null +++ b/src/assets/img/icons/activities.svg @@ -0,0 +1,178 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/core/features/block/classes/base-block-component.ts b/src/core/features/block/classes/base-block-component.ts index aac65bba4..20c020c08 100644 --- a/src/core/features/block/classes/base-block-component.ts +++ b/src/core/features/block/classes/base-block-component.ts @@ -20,6 +20,7 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreCourseBlock } from '../../course/services/course'; import { IonRefresher } from '@ionic/angular'; import { Params } from '@angular/router'; +import { ContextLevel } from '@/core/constants'; /** * Template class to easily create components for blocks. @@ -31,7 +32,7 @@ export abstract class CoreBlockBaseComponent implements OnInit { @Input() title!: string; // The block title. @Input() block!: CoreCourseBlock; // The block to render. - @Input() contextLevel!: string; // The context where the block will be used. + @Input() contextLevel!: ContextLevel; // The context where the block will be used. @Input() instanceId!: number; // The instance ID associated with the context level. @Input() link?: string; // Link to go when clicked. @Input() linkParams?: Params; // Link params to go when clicked. diff --git a/src/core/features/course/course-lazy.module.ts b/src/core/features/course/course-lazy.module.ts index fa42b82c0..a6b626437 100644 --- a/src/core/features/course/course-lazy.module.ts +++ b/src/core/features/course/course-lazy.module.ts @@ -32,7 +32,7 @@ const routes: Routes = [ }, { path: 'list-mod-type', - loadChildren: () => import('./pages/list-mod-type/list-mod-type').then( m => m.CoreCourseListModTypePage), + loadChildren: () => import('./pages/list-mod-type/list-mod-type.module').then(m => m.CoreCourseListModTypePageModule), }, ]; diff --git a/src/core/features/courses/components/course-progress/course-progress.scss b/src/core/features/courses/components/course-progress/course-progress.scss index 50dc956eb..f73f02035 100644 --- a/src/core/features/courses/components/course-progress/course-progress.scss +++ b/src/core/features/courses/components/course-progress/course-progress.scss @@ -1,3 +1,5 @@ +@import "~theme/globals"; + :host { ion-card { display: flex; @@ -107,21 +109,8 @@ } } -// @todo :host-context(.core-horizontal-scroll) { - flex: 0 0 80%; - min-width: 250px; - max-width: 300px; - align-self: stretch; - display: block; - - [text-wrap] .label { - h2, p { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } + @include horizontal_scroll_item(80%, 250px, 300px); ion-card { .core-course-thumb { diff --git a/src/core/features/sitehome/pages/index/index.html b/src/core/features/sitehome/pages/index/index.html index 5711d4f37..bbee0488a 100644 --- a/src/core/features/sitehome/pages/index/index.html +++ b/src/core/features/sitehome/pages/index/index.html @@ -20,7 +20,7 @@ - + @@ -71,11 +71,8 @@ - - News (TODO) - - + + diff --git a/src/core/features/sitehome/pages/index/index.module.ts b/src/core/features/sitehome/pages/index/index.module.ts index 2122eafe3..a6a93449d 100644 --- a/src/core/features/sitehome/pages/index/index.module.ts +++ b/src/core/features/sitehome/pages/index/index.module.ts @@ -20,6 +20,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreSharedModule } from '@/core/shared.module'; import { CoreBlockComponentsModule } from '@/core/features/block/components/components.module'; +import { CoreCourseComponentsModule } from '@features/course/components/components.module'; import { CoreSiteHomeIndexPage } from '.'; @@ -38,6 +39,7 @@ const routes: Routes = [ TranslateModule.forChild(), CoreSharedModule, CoreBlockComponentsModule, + CoreCourseComponentsModule, ], declarations: [ CoreSiteHomeIndexPage, diff --git a/src/theme/globals.mixins.scss b/src/theme/globals.mixins.scss index 21c70a4eb..45233a3a1 100644 --- a/src/theme/globals.mixins.scss +++ b/src/theme/globals.mixins.scss @@ -185,3 +185,32 @@ } } } + +@mixin horizontal_scroll_item($width, $min-width, $max-width) { + flex: 0 0 $width; + min-width: $min-width; + max-width: $max-width; + align-self: stretch; + display: block; + + ion-card { + height: calc(100% - 20px); + width: calc(100% - 20px); + margin-top: 10px; + margin-bottom: 10px; + + @media (max-width: 360px) { + margin-left: 6px; + margin-right: 6px; + width: calc(100% - 12px); + } + } + + .ion-text-wrap ion-label { + h2, p { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } +}