forked from CIT/Vmeda.Online
		
	
						commit
						bcca120bea
					
				
							
								
								
									
										38
									
								
								src/addons/block/activitymodules/activitymodules.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/addons/block/activitymodules/activitymodules.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -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 {}
 | 
			
		||||
@ -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<void> {
 | 
			
		||||
        await CoreCourse.instance.invalidateSections(this.instanceId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch the data to render the block.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchContent(): Promise<void> {
 | 
			
		||||
        const sections = await CoreCourse.instance.getSections(this.getCourseId(), false, true);
 | 
			
		||||
 | 
			
		||||
        this.entries = [];
 | 
			
		||||
        const archetypes: Record<string, number> = {};
 | 
			
		||||
        const modIcons: Record<string, string> = {};
 | 
			
		||||
        let modFullNames: Record<string, string> = {};
 | 
			
		||||
        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<number>(
 | 
			
		||||
                        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;
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,11 @@
 | 
			
		||||
<ion-item-divider sticky="true">
 | 
			
		||||
    <ion-label>
 | 
			
		||||
        <h2>{{ 'addon.block_activitymodules.pluginname' | translate }}</h2>
 | 
			
		||||
    </ion-label>
 | 
			
		||||
</ion-item-divider>
 | 
			
		||||
<core-loading [hideUntil]="loaded" class="core-loading-center">
 | 
			
		||||
    <ion-item class="ion-text-wrap item-media" *ngFor="let entry of entries" detail="false" (click)="gotoCoureListModType(entry)">
 | 
			
		||||
        <img slot="start" [src]="entry.icon" alt="" role="presentation" class="core-module-icon">
 | 
			
		||||
        <ion-label>{{ entry.name }}</ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
</core-loading>
 | 
			
		||||
@ -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 {}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/addons/block/activitymodules/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/addons/block/activitymodules/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
    "pluginname": "Activities"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										46
									
								
								src/addons/block/activitymodules/services/block-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/addons/block/activitymodules/services/block-handler.ts
									
									
									
									
									
										Normal file
									
								
							@ -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) {}
 | 
			
		||||
@ -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: [],
 | 
			
		||||
 | 
			
		||||
@ -20,15 +20,15 @@
 | 
			
		||||
            (onClosed)="switchFilterClosed()"></core-context-menu-item>
 | 
			
		||||
        <core-context-menu-item *ngIf="loaded && showSortFilter" [priority]="900"
 | 
			
		||||
            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 *ngIf="loaded && showSortFilter && showSortByShortName" [priority]="800"
 | 
			
		||||
            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 *ngIf="loaded && showSortFilter" [priority]="700"
 | 
			
		||||
            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>
 | 
			
		||||
</ion-item-divider>
 | 
			
		||||
 | 
			
		||||
@ -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 {}
 | 
			
		||||
@ -0,0 +1,29 @@
 | 
			
		||||
<ion-item-divider sticky="true">
 | 
			
		||||
    <ion-label><h2>{{ 'addon.block_recentlyaccesseditems.pluginname' | translate }}</h2></ion-label>
 | 
			
		||||
</ion-item-divider>
 | 
			
		||||
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
 | 
			
		||||
    <div class="core-horizontal-scroll" *ngIf="items && items.length > 0">
 | 
			
		||||
        <div *ngFor="let item of items">
 | 
			
		||||
            <ion-card>
 | 
			
		||||
                <ion-item class="core-course-module-handler item-media ion-text-wrap" detail="false" (click)="action($event, item)"
 | 
			
		||||
                    [title]="item.name">
 | 
			
		||||
                    <img slot="start" [src]="item.iconUrl" alt="" role="presentation" *ngIf="item.iconUrl" class="core-module-icon">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>
 | 
			
		||||
                            <core-format-text [text]="item.name" contextLevel="module" [contextInstanceId]="item.cmid"
 | 
			
		||||
                                [courseId]="item.courseid"></core-format-text>
 | 
			
		||||
                        </h2>
 | 
			
		||||
                        <p>
 | 
			
		||||
                            <core-format-text [text]="item.coursename" contextLevel="course" [contextInstanceId]="item.courseid">
 | 
			
		||||
                            </core-format-text>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
            </ion-card>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <core-empty-box *ngIf="items.length <= 0" image="assets/img/icons/activities.svg"
 | 
			
		||||
        [message]="'addon.block_recentlyaccesseditems.noitems' | translate"></core-empty-box>
 | 
			
		||||
 | 
			
		||||
</core-loading>
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
@import "~theme/globals";
 | 
			
		||||
 | 
			
		||||
:host {
 | 
			
		||||
    .core-horizontal-scroll > div {
 | 
			
		||||
        @include horizontal_scroll_item(80%, 250px, 300px);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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<void> {
 | 
			
		||||
        await AddonBlockRecentlyAccessedItems.instance.invalidateRecentItems();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch the data to render the block.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchContent(): Promise<void> {
 | 
			
		||||
        this.items = await AddonBlockRecentlyAccessedItems.instance.getRecentItems();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Event clicked.
 | 
			
		||||
     *
 | 
			
		||||
     * @param e Click event.
 | 
			
		||||
     * @param item Activity item info.
 | 
			
		||||
     */
 | 
			
		||||
    async action(e: Event, item: AddonBlockRecentlyAccessedItemsItem): Promise<void> {
 | 
			
		||||
        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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								src/addons/block/recentlyaccesseditems/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/addons/block/recentlyaccesseditems/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
{
 | 
			
		||||
    "noitems": "No recent items",
 | 
			
		||||
    "pluginname": "Recently accessed items"
 | 
			
		||||
}
 | 
			
		||||
@ -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 {}
 | 
			
		||||
@ -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) {}
 | 
			
		||||
@ -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<AddonBlockRecentlyAccessedItemsItem[]> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        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.
 | 
			
		||||
};
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <!--<core-course-module *ngFor="let module of mainMenuBlock.modules" [module]="module" [courseId]="siteHomeId"
 | 
			
		||||
            [downloadEnabled]="downloadEnabled" [section]="mainMenuBlock"></core-course-module>-->
 | 
			
		||||
        <core-course-module *ngFor="let module of mainMenuBlock.modules" [module]="module" [courseId]="siteHomeId"
 | 
			
		||||
            [downloadEnabled]="downloadEnabled" [section]="mainMenuBlock"></core-course-module>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
</core-loading>
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										51
									
								
								src/addons/block/timeline/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/addons/block/timeline/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
			
		||||
							
								
								
									
										154
									
								
								src/addons/block/timeline/components/events/events.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								src/addons/block/timeline/components/events/events.ts
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
			
		||||
							
								
								
									
										240
									
								
								src/addons/block/timeline/components/timeline/timeline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								src/addons/block/timeline/components/timeline/timeline.ts
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										13
									
								
								src/addons/block/timeline/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/addons/block/timeline/lang.json
									
									
									
									
									
										Normal file
									
								
							@ -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"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										62
									
								
								src/addons/block/timeline/services/block-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/addons/block/timeline/services/block-handler.ts
									
									
									
									
									
										Normal file
									
								
							@ -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) {}
 | 
			
		||||
							
								
								
									
										290
									
								
								src/addons/block/timeline/services/timeline.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								src/addons/block/timeline/services/timeline.ts
									
									
									
									
									
										Normal file
									
								
							@ -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) {}
 | 
			
		||||
							
								
								
									
										38
									
								
								src/addons/block/timeline/timeline.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/addons/block/timeline/timeline.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -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 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.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										178
									
								
								src/assets/img/icons/activities.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/assets/img/icons/activities.svg
									
									
									
									
									
										Normal file
									
								
							@ -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  | 
@ -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.
 | 
			
		||||
 | 
			
		||||
@ -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),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@
 | 
			
		||||
        <core-loading [hideUntil]="dataLoaded">
 | 
			
		||||
                <ion-list>
 | 
			
		||||
                    <!-- Site home main contents. -->
 | 
			
		||||
                    <!-- @todo <ng-container *ngIf="section && section.hasContent">
 | 
			
		||||
                    <ng-container *ngIf="section && section.hasContent">
 | 
			
		||||
                        <ion-item class="ion-text-wrap" *ngIf="section.summary">
 | 
			
		||||
                            <core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="siteHomeId">
 | 
			
		||||
                            </core-format-text>
 | 
			
		||||
@ -28,7 +28,7 @@
 | 
			
		||||
 | 
			
		||||
                        <core-course-module *ngFor="let module of section.modules" [module]="module" [courseId]="siteHomeId"
 | 
			
		||||
                            [downloadEnabled]="downloadEnabled" [section]="section"></core-course-module>
 | 
			
		||||
                    </ng-container> -->
 | 
			
		||||
                    </ng-container>
 | 
			
		||||
 | 
			
		||||
                    <!-- Site home items: news, categories, courses, etc. -->
 | 
			
		||||
                    <ng-container *ngIf="items.length > 0">
 | 
			
		||||
@ -71,11 +71,8 @@
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #news>
 | 
			
		||||
    <ion-item>
 | 
			
		||||
        <ion-label>News (TODO)</ion-label>
 | 
			
		||||
    </ion-item>
 | 
			
		||||
    <!-- @todo <core-course-module class="core-sitehome-news" *ngIf="newsForumModule" [module]="module" [courseId]="siteHomeId">
 | 
			
		||||
    </core-course-module> -->
 | 
			
		||||
    <core-course-module class="core-sitehome-news" *ngIf="newsForumModule" [module]="newsForumModule" [courseId]="siteHomeId">
 | 
			
		||||
    </core-course-module>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #categories>
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user