From 3356659a24fca5fd905ef82b0f047a7733f9039d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 3 Dec 2020 12:35:19 +0100 Subject: [PATCH 01/11] MOBILE-3617 lang: Update other component languages --- scripts/lang_functions.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/lang_functions.php b/scripts/lang_functions.php index f09bba4df..3f1011955 100644 --- a/scripts/lang_functions.php +++ b/scripts/lang_functions.php @@ -398,6 +398,9 @@ function override_component_lang_files($keys, $translations) { case 'assets': $path .= $type.'/'.$component.'.json'; break; + default: + $path .= $type.'/lang.json'; + break; } From 7d1d318afc0a06530e73b6639217806a9118e683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 7 Dec 2020 17:04:31 +0100 Subject: [PATCH 02/11] MOBILE-3608 blocks: Migrate basic block structure --- src/core/components/icon/icon.scss | 4 + src/core/features/block/block.module.ts | 24 +++ .../block/classes/base-block-component.ts | 143 ++++++++++++ .../block/classes/base-block-handler.ts | 61 ++++++ .../block/components/block/block.scss | 30 +++ .../features/block/components/block/block.ts | 163 ++++++++++++++ .../block/components/block/core-block.html | 4 + .../block/components/components.module.ts | 52 +++++ .../core-block-course-blocks.html | 14 ++ .../course-blocks/course-blocks.scss | 58 +++++ .../components/course-blocks/course-blocks.ts | 111 ++++++++++ .../core-block-only-title.html | 4 + .../only-title-block/only-title-block.scss | 5 + .../only-title-block/only-title-block.ts | 49 +++++ .../core-block-pre-rendered.html | 25 +++ .../pre-rendered-block/pre-rendered-block.ts | 44 ++++ src/core/features/block/lang.json | 3 + .../features/block/services/block-delegate.ts | 203 ++++++++++++++++++ .../features/block/services/block-helper.ts | 60 ++++++ .../block/services/handlers/default-block.ts | 27 +++ .../classes/module-list-handler.ts | 2 - src/core/features/course/services/course.ts | 5 + .../features/sitehome/pages/index/index.html | 4 +- .../sitehome/pages/index/index.module.ts | 2 + src/theme/app.scss | 11 +- src/theme/variables.scss | 5 + 26 files changed, 1103 insertions(+), 10 deletions(-) create mode 100644 src/core/features/block/block.module.ts create mode 100644 src/core/features/block/classes/base-block-component.ts create mode 100644 src/core/features/block/classes/base-block-handler.ts create mode 100644 src/core/features/block/components/block/block.scss create mode 100644 src/core/features/block/components/block/block.ts create mode 100644 src/core/features/block/components/block/core-block.html create mode 100644 src/core/features/block/components/components.module.ts create mode 100644 src/core/features/block/components/course-blocks/core-block-course-blocks.html create mode 100644 src/core/features/block/components/course-blocks/course-blocks.scss create mode 100644 src/core/features/block/components/course-blocks/course-blocks.ts create mode 100644 src/core/features/block/components/only-title-block/core-block-only-title.html create mode 100644 src/core/features/block/components/only-title-block/only-title-block.scss create mode 100644 src/core/features/block/components/only-title-block/only-title-block.ts create mode 100644 src/core/features/block/components/pre-rendered-block/core-block-pre-rendered.html create mode 100644 src/core/features/block/components/pre-rendered-block/pre-rendered-block.ts create mode 100644 src/core/features/block/lang.json create mode 100644 src/core/features/block/services/block-delegate.ts create mode 100644 src/core/features/block/services/block-helper.ts create mode 100644 src/core/features/block/services/handlers/default-block.ts diff --git a/src/core/components/icon/icon.scss b/src/core/components/icon/icon.scss index 93c842fce..097de22a6 100644 --- a/src/core/components/icon/icon.scss +++ b/src/core/components/icon/icon.scss @@ -2,6 +2,10 @@ margin: 0; } +:host-context([dir=rtl]).icon-flip-rtl { + transform: scaleX(-1); +} + :host-context(ion-item.md) ion-icon { &[slot] { font-size: 1.6em; diff --git a/src/core/features/block/block.module.ts b/src/core/features/block/block.module.ts new file mode 100644 index 000000000..8092c0dbe --- /dev/null +++ b/src/core/features/block/block.module.ts @@ -0,0 +1,24 @@ +// (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 { CoreBlockDefaultHandler } from './services/handlers/default-block'; + +@NgModule({ + providers: [ + CoreBlockDefaultHandler, + ], +}) +export class CoreBlockModule { +} diff --git a/src/core/features/block/classes/base-block-component.ts b/src/core/features/block/classes/base-block-component.ts new file mode 100644 index 000000000..aac65bba4 --- /dev/null +++ b/src/core/features/block/classes/base-block-component.ts @@ -0,0 +1,143 @@ +// (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 { OnInit, Input, Component, Optional, Inject } from '@angular/core'; +import { CoreLogger } from '@singletons/logger'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreCourseBlock } from '../../course/services/course'; +import { IonRefresher } from '@ionic/angular'; +import { Params } from '@angular/router'; + +/** + * Template class to easily create components for blocks. + */ +@Component({ + template: '', +}) +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() 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. + + loaded = false; // If the component has been loaded. + protected fetchContentDefaultError = ''; // Default error to show when loading contents. + + protected logger: CoreLogger; + + constructor(@Optional() @Inject('') loggerName: string = 'AddonBlockComponent') { + this.logger = CoreLogger.getInstance(loggerName); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + if (this.block.configs && this.block.configs.length > 0) { + this.block.configs.map((config) => { + config.value = CoreTextUtils.instance.parseJSON(config.value); + + return config; + }); + + this.block.configsRecord = CoreUtils.instance.arrayToObject(this.block.configs, 'name'); + } + + await this.loadContent(); + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + * @param done Function to call when done. + * @param showErrors If show errors to the user of hide them. + * @return Promise resolved when done. + */ + async doRefresh(refresher?: CustomEvent, done?: () => void, showErrors: boolean = false): Promise { + if (this.loaded) { + return this.refreshContent(showErrors).finally(() => { + refresher?.detail.complete(); + done && done(); + }); + } + } + + /** + * Perform the refresh content function. + * + * @param showErrors Wether to show errors to the user or hide them. + * @return Resolved when done. + */ + protected async refreshContent(showErrors: boolean = false): Promise { + // Wrap the call in a try/catch so the workflow isn't interrupted if an error occurs. + try { + await this.invalidateContent(); + } catch (ex) { + // An error ocurred in the function, log the error and just resolve the promise so the workflow continues. + this.logger.error(ex); + } + + await this.loadContent(true, showErrors); + } + + /** + * Perform the invalidate content function. + * + * @return Resolved when done. + */ + protected async invalidateContent(): Promise { + return; + } + + /** + * Loads the component contents and shows the corresponding error. + * + * @param refresh Whether we're refreshing data. + * @param showErrors Wether to show errors to the user or hide them. + * @return Promise resolved when done. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected async loadContent(refresh?: boolean, showErrors: boolean = false): Promise { + // Wrap the call in a try/catch so the workflow isn't interrupted if an error occurs. + try { + await this.fetchContent(refresh); + } catch (error) { + // An error ocurred in the function, log the error and just resolve the promise so the workflow continues. + this.logger.error(error); + + // Error getting data, fail. + CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError, true); + } + + this.loaded = true; + } + + /** + * Download the component contents. + * + * @param refresh Whether we're refreshing data. + * @return Promise resolved when done. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected async fetchContent(refresh: boolean = false): Promise { + return; + } + +} diff --git a/src/core/features/block/classes/base-block-handler.ts b/src/core/features/block/classes/base-block-handler.ts new file mode 100644 index 000000000..89c0fef81 --- /dev/null +++ b/src/core/features/block/classes/base-block-handler.ts @@ -0,0 +1,61 @@ +// (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 { CoreCourseBlock } from '@features/course/services/course'; +import { CoreBlockPreRenderedComponent } from '../components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockHandler, CoreBlockHandlerData } from '../services/block-delegate'; + +/** + * Base handler for blocks. + * + * This class is needed because parent classes cannot have @Injectable in Angular v6, so the default handler cannot be a + * parent class. + */ +export class CoreBlockBaseHandler implements CoreBlockHandler { + + name = 'CoreBlockBase'; + blockName = 'base'; + + /** + * Whether or not the handler is enabled on a site level. + * + * @return True or promise resolved with true if enabled. + */ + async isEnabled(): Promise { + return true; + } + + /** + * Returns the data needed to render the block. + * + * @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( + block: CoreCourseBlock, // eslint-disable-line @typescript-eslint/no-unused-vars + contextLevel: string, // eslint-disable-line @typescript-eslint/no-unused-vars + instanceId: number, // eslint-disable-line @typescript-eslint/no-unused-vars + ): CoreBlockHandlerData | Promise { + + // To be overridden. + return { + title: '', + class: '', + component: CoreBlockPreRenderedComponent, + }; + } + +} diff --git a/src/core/features/block/components/block/block.scss b/src/core/features/block/components/block/block.scss new file mode 100644 index 000000000..3acf40fdd --- /dev/null +++ b/src/core/features/block/components/block/block.scss @@ -0,0 +1,30 @@ +:host { + // @todo + position: relative; + display: block; + + core-loading.core-loading-center { + display: block; + + .core-loading-container { + margin-top: 10px; + position: relative; + } + } + + core-empty-box .core-empty-box { + position: relative; + z-index: initial; + //@include position(initial, initial, null, initial); + height: auto; + } + + ion-item-divider { + //@include padding-horizontal(null, 0px); + min-height: 60px; + } + + ion-item-divider .core-button-spinner { + margin: 0; + } +} diff --git a/src/core/features/block/components/block/block.ts b/src/core/features/block/components/block/block.ts new file mode 100644 index 000000000..595c283ad --- /dev/null +++ b/src/core/features/block/components/block/block.ts @@ -0,0 +1,163 @@ +// (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, OnInit, ViewChild, OnDestroy, DoCheck, KeyValueDiffers, KeyValueDiffer, Type } from '@angular/core'; +import { CoreBlockDelegate } from '../../services/block-delegate'; +import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; +import { Subscription } from 'rxjs'; +import { CoreCourseBlock } from '@/core/features/course/services/course'; +import { IonRefresher } from '@ionic/angular'; + +/** + * Component to render a block. + */ +@Component({ + selector: 'core-block', + templateUrl: 'core-block.html', + styleUrls: ['block.scss'], +}) +export class CoreBlockComponent implements OnInit, OnDestroy, DoCheck { + + @ViewChild(CoreDynamicComponent) dynamicComponent?: CoreDynamicComponent; + + @Input() block!: CoreCourseBlock; // The block to render. + @Input() contextLevel!: string; // The context where the block will be used. + @Input() instanceId!: number; // The instance ID associated with the context level. + @Input() extraData: any; // Any extra data to be passed to the block. + + componentClass?: Type; // The class of the component to render. + data: any = {}; // Data to pass to the component. + class?: string; // CSS class to apply to the block. + loaded = false; + + blockSubscription?: Subscription; + + protected differ: KeyValueDiffer; // To detect changes in the data input. + + constructor( + differs: KeyValueDiffers, + ) { + this.differ = differs.find([]).create(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + if (!this.block) { + this.loaded = true; + + return; + } + + if (this.block.visible) { + // Get the data to render the block. + this.initBlock(); + } + } + + /** + * Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays). + */ + ngDoCheck(): void { + if (this.data) { + // Check if there's any change in the extraData object. + const changes = this.differ.diff(this.extraData); + if (changes) { + this.data = Object.assign(this.data, this.extraData || {}); + } + } + } + + /** + * Get block display data and initialises the block once this is available. If the block is not + * supported at the moment, try again if the available blocks are updated (because it comes + * from a site plugin). + */ + async initBlock(): Promise { + try { + const data = await CoreBlockDelegate.instance.getBlockDisplayData(this.block, this.contextLevel, this.instanceId); + + if (!data) { + // Block not supported, don't render it. But, site plugins might not have finished loading. + // Subscribe to the observable in block delegate that will tell us if blocks are updated. + // We can retry init later if that happens. + this.blockSubscription = CoreBlockDelegate.instance.blocksUpdateObservable.subscribe( + (): void => { + this.blockSubscription?.unsubscribe(); + delete this.blockSubscription; + this.initBlock(); + }, + ); + + return; + } + + this.class = data.class; + this.componentClass = data.component; + + // Set up the data needed by the block component. + this.data = Object.assign({ + title: data.title, + block: this.block, + contextLevel: this.contextLevel, + instanceId: this.instanceId, + link: data.link || null, + linkParams: data.linkParams || null, + }, this.extraData || {}, data.componentData || {}); + } catch { + // Ignore errors. + } + + this.loaded = true; + } + + /** + * On destroy of the component, clear up any subscriptions. + */ + ngOnDestroy(): void { + this.blockSubscription?.unsubscribe(); + delete this.blockSubscription; + } + + /** + * Refresh the data. + * + * @param refresher Refresher. Please pass this only if the refresher should finish when this function finishes. + * @param done Function to call when done. + * @param showErrors If show errors to the user of hide them. + * @return Promise resolved when done. + */ + async doRefresh( + refresher?: CustomEvent, + done?: () => void, + showErrors: boolean = false, + ): Promise { + if (this.dynamicComponent) { + await this.dynamicComponent.callComponentFunction('doRefresh', [refresher, done, showErrors]); + } + } + + /** + * Invalidate some data. + * + * @return Promise resolved when done. + */ + async invalidate(): Promise { + if (this.dynamicComponent) { + await this.dynamicComponent.callComponentFunction('invalidateContent'); + } + } + +} diff --git a/src/core/features/block/components/block/core-block.html b/src/core/features/block/components/block/core-block.html new file mode 100644 index 000000000..1c1e3a215 --- /dev/null +++ b/src/core/features/block/components/block/core-block.html @@ -0,0 +1,4 @@ + +
+ +
diff --git a/src/core/features/block/components/components.module.ts b/src/core/features/block/components/components.module.ts new file mode 100644 index 000000000..70aec8124 --- /dev/null +++ b/src/core/features/block/components/components.module.ts @@ -0,0 +1,52 @@ +// (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 { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreBlockComponent } from './block/block'; +import { CoreBlockOnlyTitleComponent } from './only-title-block/only-title-block'; +import { CoreBlockPreRenderedComponent } from './pre-rendered-block/pre-rendered-block'; +import { CoreBlockCourseBlocksComponent } from './course-blocks/course-blocks'; +import { CoreComponentsModule } from '@components/components.module'; + +@NgModule({ + declarations: [ + CoreBlockComponent, + CoreBlockOnlyTitleComponent, + CoreBlockPreRenderedComponent, + CoreBlockCourseBlocksComponent, + ], + imports: [ + CommonModule, + IonicModule, + CoreDirectivesModule, + TranslateModule.forChild(), + CoreComponentsModule, + ], + exports: [ + CoreBlockComponent, + CoreBlockOnlyTitleComponent, + CoreBlockPreRenderedComponent, + CoreBlockCourseBlocksComponent, + ], + entryComponents: [ + CoreBlockOnlyTitleComponent, + CoreBlockPreRenderedComponent, + CoreBlockCourseBlocksComponent, + ], +}) +export class CoreBlockComponentsModule {} diff --git a/src/core/features/block/components/course-blocks/core-block-course-blocks.html b/src/core/features/block/components/course-blocks/core-block-course-blocks.html new file mode 100644 index 000000000..a875f46e4 --- /dev/null +++ b/src/core/features/block/components/course-blocks/core-block-course-blocks.html @@ -0,0 +1,14 @@ +
+ +
+ +
+ + + + + + + + +
diff --git a/src/core/features/block/components/course-blocks/course-blocks.scss b/src/core/features/block/components/course-blocks/course-blocks.scss new file mode 100644 index 000000000..96774170f --- /dev/null +++ b/src/core/features/block/components/course-blocks/course-blocks.scss @@ -0,0 +1,58 @@ +:host { + &.core-no-blocks .core-course-blocks-content { + height: auto; + } + + &.core-has-blocks { + @media (min-width: 768px) { + display: flex; + + flex-direction: row; + flex-wrap: nowrap; + + .core-course-blocks-content { + box-shadow: none !important; + flex-grow: 1; + max-width: 100%; + // @todo @include core-split-area-start(); + } + + div.core-course-blocks-side { + max-width: var(--side-blocks-max-width); + min-width: var(--side-blocks-min-width); + box-shadow: -4px 0px 16px rgba(0, 0, 0, 0.18); + // @todo @include core-split-area-end(); + } + + .core-course-blocks-content, + div.core-course-blocks-side { + position: relative; + height: 100%; + + .core-loading-center, + core-loading.core-loading-loaded { + position: initial; + } + } + } + + @media (max-width: 767.98px) { + // Disable scroll on individual columns. + div.core-course-blocks-side { + height: auto; + + &.core-hide-blocks { + display: none; + } + } + } + } +} + +:host-context([dir="rtl"]).core-has-blocks { + @media (min-width: 768px) { + div.core-course-blocks-side { + box-shadow: 4px 0px 16px rgba(0, 0, 0, 0.18); + } + } +} diff --git a/src/core/features/block/components/course-blocks/course-blocks.ts b/src/core/features/block/components/course-blocks/course-blocks.ts new file mode 100644 index 000000000..c95ab480c --- /dev/null +++ b/src/core/features/block/components/course-blocks/course-blocks.ts @@ -0,0 +1,111 @@ +// (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, ViewChildren, Input, OnInit, QueryList, ElementRef } from '@angular/core'; +import { IonContent } from '@ionic/angular'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreCourse, CoreCourseBlock } from '@features/course/services/course'; +import { CoreBlockHelper } from '../../services/block-helper'; +import { CoreBlockComponent } from '../block/block'; + +/** + * Component that displays the list of course blocks. + */ +@Component({ + selector: 'core-block-course-blocks', + templateUrl: 'core-block-course-blocks.html', + styleUrls: ['course-blocks.scss'], +}) +export class CoreBlockCourseBlocksComponent implements OnInit { + + @Input() courseId!: number; + @Input() hideBlocks = false; + @Input() hideBottomBlocks = false; + @Input() downloadEnabled = false; + + @ViewChildren(CoreBlockComponent) blocksComponents?: QueryList; + + dataLoaded = false; + blocks: CoreCourseBlock[] = []; + + protected element: HTMLElement; + + constructor( + element: ElementRef, + protected content: IonContent, + ) { + this.element = element.nativeElement; + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + this.element.classList.add('core-no-blocks'); + this.loadContent().finally(() => { + this.dataLoaded = true; + }); + } + + /** + * Invalidate blocks data. + * + * @return Promise resolved when done. + */ + async invalidateBlocks(): Promise { + const promises: Promise[] = []; + + if (CoreBlockHelper.instance.canGetCourseBlocks()) { + promises.push(CoreCourse.instance.invalidateCourseBlocks(this.courseId)); + } + + // Invalidate the blocks. + this.blocksComponents?.forEach((blockComponent) => { + promises.push(blockComponent.invalidate().catch(() => { + // Ignore errors. + })); + }); + + await Promise.all(promises); + } + + /** + * Convenience function to fetch the data. + * + * @return Promise resolved when done. + */ + async loadContent(): Promise { + + try { + this.blocks = await CoreBlockHelper.instance.getCourseBlocks(this.courseId); + } catch (error) { + CoreDomUtils.instance.showErrorModal(error); + + this.blocks = []; + } + + const scrollElement = await this.content.getScrollElement(); + if (!this.hideBlocks && this.blocks.length > 0) { + this.element.classList.add('core-has-blocks'); + this.element.classList.remove('core-no-blocks'); + + scrollElement.classList.add('core-course-block-with-blocks'); + } else { + this.element.classList.remove('core-has-blocks'); + this.element.classList.add('core-no-blocks'); + scrollElement.classList.remove('core-course-block-with-blocks'); + } + } + +} diff --git a/src/core/features/block/components/only-title-block/core-block-only-title.html b/src/core/features/block/components/only-title-block/core-block-only-title.html new file mode 100644 index 000000000..c919a85a1 --- /dev/null +++ b/src/core/features/block/components/only-title-block/core-block-only-title.html @@ -0,0 +1,4 @@ + +

{{ title | translate }}

+ +
diff --git a/src/core/features/block/components/only-title-block/only-title-block.scss b/src/core/features/block/components/only-title-block/only-title-block.scss new file mode 100644 index 000000000..b129c351b --- /dev/null +++ b/src/core/features/block/components/only-title-block/only-title-block.scss @@ -0,0 +1,5 @@ +:host { + ion-item-divider { + cursor: pointer; + } +} diff --git a/src/core/features/block/components/only-title-block/only-title-block.ts b/src/core/features/block/components/only-title-block/only-title-block.ts new file mode 100644 index 000000000..3e2ec6d20 --- /dev/null +++ b/src/core/features/block/components/only-title-block/only-title-block.ts @@ -0,0 +1,49 @@ +// (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 { OnInit, Component } from '@angular/core'; +import { CoreBlockBaseComponent } from '../../classes/base-block-component'; +import { CoreNavHelper } from '@services/nav-helper'; + +/** + * Component to render blocks with only a title and link. + */ +@Component({ + selector: 'core-block-only-title', + templateUrl: 'core-block-only-title.html', + styleUrls: ['only-title-block.scss'], +}) +export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent implements OnInit { + + constructor() { + super('CoreBlockOnlyTitleComponent'); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + await super.ngOnInit(); + + this.fetchContentDefaultError = 'Error getting ' + this.block.contents?.title + ' data.'; + } + + /** + * Go to the block page. + */ + gotoBlock(): void { + CoreNavHelper.instance.goInSite(this.link!, this.linkParams!, undefined, true); + } + +} diff --git a/src/core/features/block/components/pre-rendered-block/core-block-pre-rendered.html b/src/core/features/block/components/pre-rendered-block/core-block-pre-rendered.html new file mode 100644 index 000000000..c0c1ed41e --- /dev/null +++ b/src/core/features/block/components/pre-rendered-block/core-block-pre-rendered.html @@ -0,0 +1,25 @@ + + +

+ + +

+
+
+ + + + + + + + + + + + + + diff --git a/src/core/features/block/components/pre-rendered-block/pre-rendered-block.ts b/src/core/features/block/components/pre-rendered-block/pre-rendered-block.ts new file mode 100644 index 000000000..ee874bef3 --- /dev/null +++ b/src/core/features/block/components/pre-rendered-block/pre-rendered-block.ts @@ -0,0 +1,44 @@ +// (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 { OnInit, Component } from '@angular/core'; +import { CoreBlockBaseComponent } from '../../classes/base-block-component'; + +/** + * Component to render blocks with pre-rendered HTML. + */ +@Component({ + selector: 'core-block-pre-rendered', + templateUrl: 'core-block-pre-rendered.html', +}) +export class CoreBlockPreRenderedComponent extends CoreBlockBaseComponent implements OnInit { + + courseId?: number; + + constructor() { + super('CoreBlockPreRenderedComponent'); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + await super.ngOnInit(); + + this.courseId = this.contextLevel == 'course' ? this.instanceId : undefined; + + this.fetchContentDefaultError = 'Error getting ' + this.block.contents?.title + ' data.'; + } + +} diff --git a/src/core/features/block/lang.json b/src/core/features/block/lang.json new file mode 100644 index 000000000..9b136b8ee --- /dev/null +++ b/src/core/features/block/lang.json @@ -0,0 +1,3 @@ +{ + "blocks": "Blocks" +} \ No newline at end of file diff --git a/src/core/features/block/services/block-delegate.ts b/src/core/features/block/services/block-delegate.ts new file mode 100644 index 000000000..d46f34b1d --- /dev/null +++ b/src/core/features/block/services/block-delegate.ts @@ -0,0 +1,203 @@ +// (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, Type } from '@angular/core'; +import { CoreSites } from '@services/sites'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CoreSite } from '@classes/site'; +import { Subject } from 'rxjs'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { Params } from '@angular/router'; +import { makeSingleton } from '@singletons'; + +/** + * Interface that all blocks must implement. + */ +export interface CoreBlockHandler extends CoreDelegateHandler { + /** + * Name of the block the handler supports. E.g. 'activity_modules'. + */ + blockName: string; + + /** + * Returns the data needed to render the block. + * + * @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?( + block: CoreCourseBlock, + contextLevel: string, + instanceId: number, + ): CoreBlockHandlerData | Promise; +} + +/** + * Data needed to render a block. It's returned by the handler. + */ +export interface CoreBlockHandlerData { + /** + * Title to display for the block. + */ + title: string; + + /** + * Class to add to the displayed block. + */ + class?: string; + + /** + * The component to render the contents of the block. + * It's recommended to return the class of the component, but you can also return an instance of the component. + */ + component: Type; + + /** + * Data to pass to the component. All the properties in this object will be passed to the component as inputs. + */ + componentData?: Record; + + /** + * Link to go when showing only title. + */ + link?: string; + + /** + * Params of the link. + */ + linkParams?: Params; +} + +/** + * Delegate to register block handlers. + */ +@Injectable({ providedIn: 'root' }) +export class CoreBlockDelegateService extends CoreDelegate { + + protected handlerNameProperty = 'blockName'; + + protected featurePrefix = 'CoreBlockDelegate_'; + + blocksUpdateObservable: Subject; + + constructor() { + super('CoreBlockDelegate', true); + + this.blocksUpdateObservable = new Subject(); + } + + /** + * Check if blocks are disabled in a certain site. + * + * @param site Site. If not defined, use current site. + * @return Whether it's disabled. + */ + areBlocksDisabledInSite(site?: CoreSite): boolean { + site = site || CoreSites.instance.getCurrentSite(); + + return !!site && site.isFeatureDisabled('NoDelegate_SiteBlocks'); + } + + /** + * Check if blocks are disabled in a certain site for courses. + * + * @param site Site. If not defined, use current site. + * @return Whether it's disabled. + */ + areBlocksDisabledInCourses(site?: CoreSite): boolean { + site = site || CoreSites.instance.getCurrentSite(); + + return !!site && site.isFeatureDisabled('NoDelegate_CourseBlocks'); + } + + /** + * Check if blocks are disabled in a certain site. + * + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + async areBlocksDisabled(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return this.areBlocksDisabledInSite(site); + } + + /** + * Get the display data for a certain 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 Promise resolved with the display data. + */ + async getBlockDisplayData( + block: CoreCourseBlock, + contextLevel: string, + instanceId: number, + ): Promise { + return this.executeFunctionOnEnabled( + block.name, + 'getDisplayData', + [block, contextLevel, instanceId], + ); + } + + /** + * Check if any of the blocks in a list is supported. + * + * @param blocks The list of blocks. + * @return Whether any of the blocks is supported. + */ + hasSupportedBlock(blocks: CoreCourseBlock[]): boolean { + blocks = blocks || []; + + return !!blocks.find((block) => this.isBlockSupported(block.name)); + } + + /** + * Check if a block is supported. + * + * @param name Block "name". E.g. 'activity_modules'. + * @return Whether it's supported. + */ + isBlockSupported(name: string): boolean { + return this.hasHandler(name, true); + } + + /** + * Check if feature is enabled or disabled in the site, depending on the feature prefix and the handler name. + * + * @param handler Handler to check. + * @param site Site to check. + * @return Whether is enabled or disabled in site. + */ + protected isFeatureDisabled(handler: CoreBlockHandler, site: CoreSite): boolean { + return this.areBlocksDisabledInSite(site) || super.isFeatureDisabled(handler, site); + } + + /** + * Called when there are new block handlers available. Informs anyone who subscribed to the + * observable. + */ + updateData(): void { + this.blocksUpdateObservable.next(); + } + +} + +export class CoreBlockDelegate extends makeSingleton(CoreBlockDelegateService) {} + diff --git a/src/core/features/block/services/block-helper.ts b/src/core/features/block/services/block-helper.ts new file mode 100644 index 000000000..80c5261a6 --- /dev/null +++ b/src/core/features/block/services/block-helper.ts @@ -0,0 +1,60 @@ +// (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 { CoreCourse, CoreCourseBlock } from '@features/course/services/course'; +import { CoreBlockDelegate } from './block-delegate'; +import { makeSingleton } from '@singletons'; + +/** + * Service that provides helper functions for blocks. + */ +@Injectable({ providedIn: 'root' }) +export class CoreBlockHelperProvider { + + /** + * Return if it get course blocks options is enabled for the current site. + * + * @return true if enabled, false otherwise. + */ + canGetCourseBlocks(): boolean { + return CoreCourse.instance.canGetCourseBlocks() && !CoreBlockDelegate.instance.areBlocksDisabledInCourses(); + } + + /** + * Returns the list of blocks for the selected course. + * + * @param courseId Course ID. + * @return List of supported blocks. + */ + async getCourseBlocks(courseId: number): Promise { + const canGetBlocks = this.canGetCourseBlocks(); + + if (!canGetBlocks) { + return []; + } + + const blocks = await CoreCourse.instance.getCourseBlocks(courseId); + const hasSupportedBlock = CoreBlockDelegate.instance.hasSupportedBlock(blocks); + if (!hasSupportedBlock) { + return []; + } + + return blocks; + } + +} + +export class CoreBlockHelper extends makeSingleton(CoreBlockHelperProvider) {} + diff --git a/src/core/features/block/services/handlers/default-block.ts b/src/core/features/block/services/handlers/default-block.ts new file mode 100644 index 000000000..f4cb57d9d --- /dev/null +++ b/src/core/features/block/services/handlers/default-block.ts @@ -0,0 +1,27 @@ +// (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 { CoreBlockBaseHandler } from '../../classes/base-block-handler'; + +/** + * Default handler used when a block type doesn't have a specific implementation. + */ +@Injectable() +export class CoreBlockDefaultHandler extends CoreBlockBaseHandler { + + name = 'CoreBlockDefault'; + blockName = 'default'; + +} diff --git a/src/core/features/contentlinks/classes/module-list-handler.ts b/src/core/features/contentlinks/classes/module-list-handler.ts index e6e2d663e..e0359ae50 100644 --- a/src/core/features/contentlinks/classes/module-list-handler.ts +++ b/src/core/features/contentlinks/classes/module-list-handler.ts @@ -31,8 +31,6 @@ export class CoreContentLinksModuleListHandler extends CoreContentLinksHandlerBa /** * Construct the handler. * - * @param linkHelper The CoreContentLinksHelperProvider instance. - * @param translate The TranslateService instance. * @param addon Name of the addon as it's registered in course delegate. It'll be used to check if it's disabled. * @param modName Name of the module (assign, book, ...). */ diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 02bdef195..537f2d6aa 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -1301,6 +1301,11 @@ export type CoreCourseBlock = { value: string; // JSON encoded representation of the config value. type: string; // Type (instance or plugin). }[]; + configsRecord?: Record; }; /** diff --git a/src/core/features/sitehome/pages/index/index.html b/src/core/features/sitehome/pages/index/index.html index e816db78e..f89fa3a0b 100644 --- a/src/core/features/sitehome/pages/index/index.html +++ b/src/core/features/sitehome/pages/index/index.html @@ -16,7 +16,7 @@ (ionRefresh)="doRefresh($event)"> - + @@ -56,7 +56,7 @@ - + diff --git a/src/core/features/sitehome/pages/index/index.module.ts b/src/core/features/sitehome/pages/index/index.module.ts index e1871fec1..b0a5457d4 100644 --- a/src/core/features/sitehome/pages/index/index.module.ts +++ b/src/core/features/sitehome/pages/index/index.module.ts @@ -20,6 +20,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CoreComponentsModule } from '@components/components.module'; +import { CoreBlockComponentsModule } from '@/core/features/block/components/components.module'; import { CoreSiteHomeIndexPage } from '.'; @@ -38,6 +39,7 @@ const routes: Routes = [ TranslateModule.forChild(), CoreDirectivesModule, CoreComponentsModule, + CoreBlockComponentsModule, ], declarations: [ CoreSiteHomeIndexPage, diff --git a/src/theme/app.scss b/src/theme/app.scss index 106fcab25..07a599401 100644 --- a/src/theme/app.scss +++ b/src/theme/app.scss @@ -40,11 +40,6 @@ ion-icon { } } -[dir=rtl] ion-icon.icon-flip-rtl { - -webkit-transform: scale(-1, 1); - transform: scale(-1, 1); -} - // Ionic alert. ion-alert.core-alert-network-error .alert-head { position: relative; @@ -77,7 +72,11 @@ ion-alert.core-nohead { // Ionic item divider. ion-item-divider { --background: var(--gray-lighter); - border: 0; + .item-detail-icon { + font-size: 20px; + opacity: 0.25; + padding-inline-end: 16px; + } } // Ionic list. diff --git a/src/theme/variables.scss b/src/theme/variables.scss index 2c7b87910..25dd057bc 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -151,6 +151,11 @@ --background: var(--custom-progress-background, var(--gray-lighter)); } + core-block-course-blocks { + --side-blocks-max-width: var(--custom-side-blocks-max-width, 30%); + --side-blocks-min-width: var(--custom-side-blocks-min-width, 280px); + } + --selected-item-color: var(--custom-selected-item-color, var(--core-color)); --selected-item-border-width: var(--custom-selected-item-border-width, 5px); From c3372e8076caab9f24b1dc48bb323e9281f72c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 7 Dec 2020 15:31:39 +0100 Subject: [PATCH 03/11] MOBILE-3608 blocks: Add sitehome and dashboard blocks --- src/core/features/courses/courses.module.ts | 2 +- .../courses/pages/dashboard/dashboard.html | 19 ++- .../pages/dashboard/dashboard.module.ts | 2 + .../courses/pages/dashboard/dashboard.ts | 90 ++++++++++- .../features/courses/services/dashboard.ts | 140 ++++++++++++++++++ .../services/handlers/dashboard-home.ts | 8 +- ...{my-courses.home.ts => my-courses-home.ts} | 9 +- .../features/sitehome/pages/index/index.ts | 13 +- 8 files changed, 264 insertions(+), 19 deletions(-) create mode 100644 src/core/features/courses/services/dashboard.ts rename src/core/features/courses/services/handlers/{my-courses.home.ts => my-courses-home.ts} (84%) diff --git a/src/core/features/courses/courses.module.ts b/src/core/features/courses/courses.module.ts index 3f5014a11..e47bfb9f7 100644 --- a/src/core/features/courses/courses.module.ts +++ b/src/core/features/courses/courses.module.ts @@ -19,7 +19,7 @@ import { CoreMainMenuHomeRoutingModule } from '@features/mainmenu/pages/home/hom import { CoreMainMenuHomeDelegate } from '@features/mainmenu/services/home-delegate'; import { CoreDashboardHomeHandler, CoreDashboardHomeHandlerService } from './services/handlers/dashboard-home'; -import { CoreCoursesMyCoursesHomeHandler, CoreCoursesMyCoursesHomeHandlerService } from './services/handlers/my-courses.home'; +import { CoreCoursesMyCoursesHomeHandler, CoreCoursesMyCoursesHomeHandlerService } from './services/handlers/my-courses-home'; const mainMenuHomeChildrenRoutes: Routes = [ { diff --git a/src/core/features/courses/pages/dashboard/dashboard.html b/src/core/features/courses/pages/dashboard/dashboard.html index 53ab4c47d..5e0488613 100644 --- a/src/core/features/courses/pages/dashboard/dashboard.html +++ b/src/core/features/courses/pages/dashboard/dashboard.html @@ -12,8 +12,19 @@ - - -
Dashboard
-
+ + + + + + + + + + + + + +
diff --git a/src/core/features/courses/pages/dashboard/dashboard.module.ts b/src/core/features/courses/pages/dashboard/dashboard.module.ts index 5ba48f8f2..40924f020 100644 --- a/src/core/features/courses/pages/dashboard/dashboard.module.ts +++ b/src/core/features/courses/pages/dashboard/dashboard.module.ts @@ -20,6 +20,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreComponentsModule } from '@components/components.module'; import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreBlockComponentsModule } from '@features/block/components/components.module'; import { CoreCoursesDashboardPage } from './dashboard'; @@ -38,6 +39,7 @@ const routes: Routes = [ TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, + CoreBlockComponentsModule, ], declarations: [ CoreCoursesDashboardPage, diff --git a/src/core/features/courses/pages/dashboard/dashboard.ts b/src/core/features/courses/pages/dashboard/dashboard.ts index 997f5e5ac..cc93164a6 100644 --- a/src/core/features/courses/pages/dashboard/dashboard.ts +++ b/src/core/features/courses/pages/dashboard/dashboard.ts @@ -12,12 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { NavController } from '@ionic/angular'; +import { Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core'; +import { IonRefresher, NavController } from '@ionic/angular'; import { CoreCourses, CoreCoursesProvider } from '../../services/courses'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; +import { CoreCoursesDashboard } from '@features/courses/services/dashboard'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { CoreBlockComponent } from '@features/block/components/block/block'; /** * Page that displays the dashboard page. @@ -29,16 +33,20 @@ import { CoreSites } from '@services/sites'; }) export class CoreCoursesDashboardPage implements OnInit, OnDestroy { + @ViewChildren(CoreBlockComponent) blocksComponents?: QueryList; + + searchEnabled = false; downloadEnabled = false; downloadCourseEnabled = false; downloadCoursesEnabled = false; downloadEnabledIcon = 'far-square'; + userId?: number; + blocks: Partial[] = []; + loaded = false; protected updateSiteObserver?: CoreEventObserver; - siteName = 'Hello world'; - constructor( protected navCtrl: NavController, ) { } @@ -59,8 +67,82 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { this.switchDownload(this.downloadEnabled && this.downloadCourseEnabled && this.downloadCoursesEnabled); }, CoreSites.instance.getCurrentSiteId()); + + this.loadContent(); } + /** + * Convenience function to fetch the dashboard data. + * + * @return Promise resolved when done. + */ + protected async loadContent(): Promise { + const available = await CoreCoursesDashboard.instance.isAvailable(); + + if (available) { + this.userId = CoreSites.instance.getCurrentSiteUserId(); + + try { + this.blocks = await CoreCoursesDashboard.instance.getDashboardBlocks(); + } catch (error) { + CoreDomUtils.instance.showErrorModal(error); + + // Cannot get the blocks, just show dashboard if needed. + this.loadFallbackBlocks(); + } + } else if (!CoreCoursesDashboard.instance.isDisabledInSite()) { + // Not available, but not disabled either. Use fallback. + this.loadFallbackBlocks(); + } else { + // Disabled. + this.blocks = []; + } + + // this.dashboardEnabled = this.blockDelegate.hasSupportedBlock(this.blocks); + this.loaded = true; + } + + /** + * Load fallback blocks to shown before 3.6 when dashboard blocks are not supported. + */ + protected loadFallbackBlocks(): void { + this.blocks = [ + { + name: 'myoverview', + visible: true, + }, + { + name: 'timeline', + visible: true, + }, + ]; + } + + /** + * Refresh the dashboard data. + * + * @param refresher Refresher. + */ + refreshDashboard(refresher: CustomEvent): void { + const promises: Promise[] = []; + + promises.push(CoreCoursesDashboard.instance.invalidateDashboardBlocks()); + + // Invalidate the blocks. + this.blocksComponents?.forEach((blockComponent) => { + promises.push(blockComponent.invalidate().catch(() => { + // Ignore errors. + })); + }); + + Promise.all(promises).finally(() => { + this.loadContent().finally(() => { + refresher?.detail.complete(); + }); + }); + } + + /** * Toggle download enabled. */ diff --git a/src/core/features/courses/services/dashboard.ts b/src/core/features/courses/services/dashboard.ts new file mode 100644 index 000000000..fbf19c3d7 --- /dev/null +++ b/src/core/features/courses/services/dashboard.ts @@ -0,0 +1,140 @@ +// (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 { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { CoreStatusWithWarningsWSResponse } from '@services/ws'; +import { makeSingleton } from '@singletons'; + +const ROOT_CACHE_KEY = 'CoreCoursesDashboard:'; + +/** + * Service that provides some features regarding course overview. + */ +@Injectable({ providedIn: 'root' }) +export class CoreCoursesDashboardProvider { + + /** + * Get cache key for dashboard blocks WS calls. + * + * @param userId User ID. Default, 0 means current user. + * @return Cache key. + */ + protected getDashboardBlocksCacheKey(userId: number = 0): string { + return ROOT_CACHE_KEY + 'blocks:' + userId; + } + + /** + * Get dashboard blocks. + * + * @param userId User ID. Default, current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the list of blocks. + * @since 3.6 + */ + async getDashboardBlocks(userId?: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + const params: CoreBlockGetDashboardBlocksWSParams = { + returncontents: true, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getDashboardBlocksCacheKey(userId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; + if (userId) { + params.userid = userId; + } + const result = await site.read('core_block_get_dashboard_blocks', params, preSets); + + return result.blocks || []; + } + + /** + * Invalidates dashboard blocks WS call. + * + * @param userId User ID. Default, current user. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateDashboardBlocks(userId?: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return await site.invalidateWsCacheForKey(this.getDashboardBlocksCacheKey(userId)); + } + + /** + * Returns whether or not block based Dashboard 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. + * @since 3.6 + */ + async isAvailable(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + // First check if it's disabled. + if (this.isDisabledInSite(site)) { + return false; + } + + return site.wsAvailable('core_block_get_dashboard_blocks'); + } + + /** + * Check if Site Home is disabled in a certain site. + * + * @param siteId Site Id. If not defined, use current site. + * @return Promise resolved with true if disabled, rejected or resolved with false otherwise. + */ + async isDisabled(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return this.isDisabledInSite(site); + } + + /** + * Check if Site Home is disabled in a certain site. + * + * @param site Site. If not defined, use current site. + * @return Whether it's disabled. + */ + isDisabledInSite(site?: CoreSite): boolean { + site = site || CoreSites.instance.getCurrentSite(); + + return !!site?.isFeatureDisabled('CoreMainMenuDelegate_CoreCoursesDashboard'); + } + +} + +export class CoreCoursesDashboard extends makeSingleton(CoreCoursesDashboardProvider) {} + + +/** + * Params of core_block_get_dashboard_blocks WS. + */ +type CoreBlockGetDashboardBlocksWSParams = { + userid?: number; // User id (optional), default is current user. + returncontents?: boolean; // Whether to return the block contents. +}; + +/** + * Data returned by core_block_get_dashboard_blocks WS. + */ +type CoreBlockGetDashboardBlocksWSResponse = { + blocks: CoreCourseBlock[]; // List of blocks in the course. + warnings?: CoreStatusWithWarningsWSResponse[]; +}; diff --git a/src/core/features/courses/services/handlers/dashboard-home.ts b/src/core/features/courses/services/handlers/dashboard-home.ts index 0f2ce7e4e..e3a3edb1c 100644 --- a/src/core/features/courses/services/handlers/dashboard-home.ts +++ b/src/core/features/courses/services/handlers/dashboard-home.ts @@ -13,8 +13,10 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; import { CoreMainMenuHomeHandler, CoreMainMenuHomeHandlerToDisplay } from '@features/mainmenu/services/home-delegate'; import { makeSingleton } from '@singletons'; +import { CoreCoursesDashboard } from '../dashboard'; /** * Handler to add dashboard into home page. @@ -42,10 +44,10 @@ export class CoreDashboardHomeHandlerService implements CoreMainMenuHomeHandler * @param siteId Site ID. If not defined, current site. * @return Whether or not the handler is enabled on a site level. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars async isEnabledForSite(siteId?: string): Promise { - // @todo return this.blockDelegate.hasSupportedBlock(this.blocks); - return true; + const blocks = await CoreCoursesDashboard.instance.getDashboardBlocks(undefined, siteId); + + return CoreBlockDelegate.instance.hasSupportedBlock(blocks); } /** diff --git a/src/core/features/courses/services/handlers/my-courses.home.ts b/src/core/features/courses/services/handlers/my-courses-home.ts similarity index 84% rename from src/core/features/courses/services/handlers/my-courses.home.ts rename to src/core/features/courses/services/handlers/my-courses-home.ts index 49477e125..43796c803 100644 --- a/src/core/features/courses/services/handlers/my-courses.home.ts +++ b/src/core/features/courses/services/handlers/my-courses-home.ts @@ -13,8 +13,11 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; import { CoreMainMenuHomeHandler, CoreMainMenuHomeHandlerToDisplay } from '@features/mainmenu/services/home-delegate'; +import { CoreSiteHome } from '@features/sitehome/services/sitehome'; import { makeSingleton } from '@singletons'; +import { CoreCoursesDashboard } from '../dashboard'; /** * Handler to add my courses into home page. @@ -42,10 +45,10 @@ export class CoreCoursesMyCoursesHomeHandlerService implements CoreMainMenuHomeH * @param siteId Site ID. If not defined, current site. * @return Whether or not the handler is enabled on a site level. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars async isEnabledForSite(siteId?: string): Promise { - // @todo return !this.blockDelegate.hasSupportedBlock(this.blocks) && !CoreSiteHome.instance.isAvailable(siteId); - return true; + const blocks = await CoreCoursesDashboard.instance.getDashboardBlocks(undefined, siteId); + + return !CoreBlockDelegate.instance.hasSupportedBlock(blocks)&& !CoreSiteHome.instance.isAvailable(siteId); } /** diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index bf62e8373..6ad4cc726 100644 --- a/src/core/features/sitehome/pages/index/index.ts +++ b/src/core/features/sitehome/pages/index/index.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { IonRefresher, NavController } from '@ionic/angular'; @@ -24,6 +24,7 @@ import { CoreSiteHome } from '@features/sitehome/services/sitehome'; import { CoreCourses, CoreCoursesProvider } from '@features//courses/services/courses'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks'; /** * Page that displays site home index. @@ -34,7 +35,7 @@ import { CoreCourseHelper } from '@features/course/services/course-helper'; }) export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { - // @todo @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent: CoreBlockCourseBlocksComponent; + @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent?: CoreBlockCourseBlocksComponent; dataLoaded = false; section?: CoreCourseSection & { @@ -158,13 +159,17 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { // @todo promises.push(this.prefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId)); } - // @todo promises.push(this.courseBlocksComponent.invalidateBlocks()); + if (this.courseBlocksComponent) { + promises.push(this.courseBlocksComponent.invalidateBlocks()); + } Promise.all(promises).finally(async () => { const p2: Promise[] = []; p2.push(this.loadContent()); - // @todo p2.push(this.courseBlocksComponent.loadContent()); + if (this.courseBlocksComponent) { + p2.push(this.courseBlocksComponent.loadContent()); + } await Promise.all(p2).finally(() => { refresher?.detail.complete(); From 6035865bc030ac2a62edd12c83626a001b2fc84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 7 Dec 2020 17:04:41 +0100 Subject: [PATCH 04/11] MOBILE-3608 blocks: Add all prerendered blocks --- src/addons/addons.module.ts | 22 ++++ .../activityresults/activityresults.module.ts | 38 ++++++ .../activityresults/activityresults.scss | 29 +++++ .../activityresults/activityresults.ts | 26 ++++ .../components/components.module.ts | 45 +++++++ src/addons/block/activityresults/lang.json | 3 + .../activityresults/services/block-handler.ts | 47 +++++++ src/addons/block/badges/badges.module.ts | 38 ++++++ .../badges/components/badges/badges.scss | 22 ++++ .../block/badges/components/badges/badges.ts | 26 ++++ .../badges/components/components.module.ts | 45 +++++++ src/addons/block/badges/lang.json | 3 + .../block/badges/services/block-handler.ts | 47 +++++++ src/addons/block/blogmenu/blogmenu.module.ts | 38 ++++++ .../components/blogmenu/blogmenu.scss | 17 +++ .../blogmenu/components/blogmenu/blogmenu.ts | 26 ++++ .../blogmenu/components/components.module.ts | 45 +++++++ src/addons/block/blogmenu/lang.json | 3 + .../block/blogmenu/services/block-handler.ts | 47 +++++++ .../block/blogrecent/blogrecent.module.ts | 38 ++++++ .../components/blogrecent/blogrecent.scss | 12 ++ .../components/blogrecent/blogrecent.ts | 26 ++++ .../components/components.module.ts | 45 +++++++ src/addons/block/blogrecent/lang.json | 3 + .../blogrecent/services/block-handler.ts | 47 +++++++ src/addons/block/blogtags/blogtags.module.ts | 38 ++++++ .../components/blogtags/blogtags.scss | 99 +++++++++++++++ .../blogtags/components/blogtags/blogtags.ts | 26 ++++ .../blogtags/components/components.module.ts | 45 +++++++ src/addons/block/blogtags/lang.json | 3 + .../block/blogtags/services/block-handler.ts | 47 +++++++ .../glossaryrandom/glossaryrandom.module.ts | 36 ++++++ src/addons/block/glossaryrandom/lang.json | 3 + .../glossaryrandom/services/block-handler.ts | 48 +++++++ src/addons/block/html/html.module.ts | 30 +++++ .../block/html/services/block-handler.ts | 49 ++++++++ .../newsitems/components/components.module.ts | 45 +++++++ .../components/newsitems/newsitems.scss | 27 ++++ .../components/newsitems/newsitems.ts | 26 ++++ src/addons/block/newsitems/lang.json | 3 + .../block/newsitems/newsitems.module.ts | 38 ++++++ .../block/newsitems/services/block-handler.ts | 47 +++++++ .../components/components.module.ts | 45 +++++++ .../components/onlineusers/onlineusers.scss | 67 ++++++++++ .../components/onlineusers/onlineusers.ts | 26 ++++ src/addons/block/onlineusers/lang.json | 3 + .../block/onlineusers/onlineusers.module.ts | 38 ++++++ .../onlineusers/services/block-handler.ts | 46 +++++++ .../components/components.module.ts | 45 +++++++ .../recentactivity/recentactivity.scss | 25 ++++ .../recentactivity/recentactivity.ts | 26 ++++ src/addons/block/recentactivity/lang.json | 3 + .../recentactivity/recentactivity.module.ts | 38 ++++++ .../recentactivity/services/block-handler.ts | 47 +++++++ .../rssclient/components/components.module.ts | 45 +++++++ .../components/rssclient/rssclient.scss | 18 +++ .../components/rssclient/rssclient.ts | 26 ++++ src/addons/block/rssclient/lang.json | 3 + .../block/rssclient/rssclient.module.ts | 38 ++++++ .../block/rssclient/services/block-handler.ts | 49 ++++++++ .../tags/components/components.module.ts | 45 +++++++ .../block/tags/components/tags/tags.scss | 117 ++++++++++++++++++ src/addons/block/tags/components/tags/tags.ts | 26 ++++ src/addons/block/tags/lang.json | 3 + .../block/tags/services/block-handler.ts | 47 +++++++ src/addons/block/tags/tags.module.ts | 38 ++++++ 66 files changed, 2212 insertions(+) create mode 100644 src/addons/block/activityresults/activityresults.module.ts create mode 100644 src/addons/block/activityresults/components/activityresults/activityresults.scss create mode 100644 src/addons/block/activityresults/components/activityresults/activityresults.ts create mode 100644 src/addons/block/activityresults/components/components.module.ts create mode 100644 src/addons/block/activityresults/lang.json create mode 100644 src/addons/block/activityresults/services/block-handler.ts create mode 100644 src/addons/block/badges/badges.module.ts create mode 100644 src/addons/block/badges/components/badges/badges.scss create mode 100644 src/addons/block/badges/components/badges/badges.ts create mode 100644 src/addons/block/badges/components/components.module.ts create mode 100644 src/addons/block/badges/lang.json create mode 100644 src/addons/block/badges/services/block-handler.ts create mode 100644 src/addons/block/blogmenu/blogmenu.module.ts create mode 100644 src/addons/block/blogmenu/components/blogmenu/blogmenu.scss create mode 100644 src/addons/block/blogmenu/components/blogmenu/blogmenu.ts create mode 100644 src/addons/block/blogmenu/components/components.module.ts create mode 100644 src/addons/block/blogmenu/lang.json create mode 100644 src/addons/block/blogmenu/services/block-handler.ts create mode 100644 src/addons/block/blogrecent/blogrecent.module.ts create mode 100644 src/addons/block/blogrecent/components/blogrecent/blogrecent.scss create mode 100644 src/addons/block/blogrecent/components/blogrecent/blogrecent.ts create mode 100644 src/addons/block/blogrecent/components/components.module.ts create mode 100644 src/addons/block/blogrecent/lang.json create mode 100644 src/addons/block/blogrecent/services/block-handler.ts create mode 100644 src/addons/block/blogtags/blogtags.module.ts create mode 100644 src/addons/block/blogtags/components/blogtags/blogtags.scss create mode 100644 src/addons/block/blogtags/components/blogtags/blogtags.ts create mode 100644 src/addons/block/blogtags/components/components.module.ts create mode 100644 src/addons/block/blogtags/lang.json create mode 100644 src/addons/block/blogtags/services/block-handler.ts create mode 100644 src/addons/block/glossaryrandom/glossaryrandom.module.ts create mode 100644 src/addons/block/glossaryrandom/lang.json create mode 100644 src/addons/block/glossaryrandom/services/block-handler.ts create mode 100644 src/addons/block/html/html.module.ts create mode 100644 src/addons/block/html/services/block-handler.ts create mode 100644 src/addons/block/newsitems/components/components.module.ts create mode 100644 src/addons/block/newsitems/components/newsitems/newsitems.scss create mode 100644 src/addons/block/newsitems/components/newsitems/newsitems.ts create mode 100644 src/addons/block/newsitems/lang.json create mode 100644 src/addons/block/newsitems/newsitems.module.ts create mode 100644 src/addons/block/newsitems/services/block-handler.ts create mode 100644 src/addons/block/onlineusers/components/components.module.ts create mode 100644 src/addons/block/onlineusers/components/onlineusers/onlineusers.scss create mode 100644 src/addons/block/onlineusers/components/onlineusers/onlineusers.ts create mode 100644 src/addons/block/onlineusers/lang.json create mode 100644 src/addons/block/onlineusers/onlineusers.module.ts create mode 100644 src/addons/block/onlineusers/services/block-handler.ts create mode 100644 src/addons/block/recentactivity/components/components.module.ts create mode 100644 src/addons/block/recentactivity/components/recentactivity/recentactivity.scss create mode 100644 src/addons/block/recentactivity/components/recentactivity/recentactivity.ts create mode 100644 src/addons/block/recentactivity/lang.json create mode 100644 src/addons/block/recentactivity/recentactivity.module.ts create mode 100644 src/addons/block/recentactivity/services/block-handler.ts create mode 100644 src/addons/block/rssclient/components/components.module.ts create mode 100644 src/addons/block/rssclient/components/rssclient/rssclient.scss create mode 100644 src/addons/block/rssclient/components/rssclient/rssclient.ts create mode 100644 src/addons/block/rssclient/lang.json create mode 100644 src/addons/block/rssclient/rssclient.module.ts create mode 100644 src/addons/block/rssclient/services/block-handler.ts create mode 100644 src/addons/block/tags/components/components.module.ts create mode 100644 src/addons/block/tags/components/tags/tags.scss create mode 100644 src/addons/block/tags/components/tags/tags.ts create mode 100644 src/addons/block/tags/lang.json create mode 100644 src/addons/block/tags/services/block-handler.ts create mode 100644 src/addons/block/tags/tags.module.ts diff --git a/src/addons/addons.module.ts b/src/addons/addons.module.ts index 98845ab4a..9379c35c7 100644 --- a/src/addons/addons.module.ts +++ b/src/addons/addons.module.ts @@ -14,6 +14,17 @@ import { NgModule } from '@angular/core'; +import { AddonBlockActivityResultsModule } from './block/activityresults/activityresults.module'; +import { AddonBlockBadgesModule } from './block/badges/badges.module'; +import { AddonBlockBlogMenuModule } from './block/blogmenu/blogmenu.module'; +import { AddonBlockBlogRecentModule } from './block/blogrecent/blogrecent.module'; +import { AddonBlockBlogTagsModule } from './block/blogtags/blogtags.module'; +import { AddonBlockGlossaryRandomModule } from './block/glossaryrandom/glossaryrandom.module'; +import { AddonBlockHtmlModule } from './block/html/html.module'; +import { AddonBlockNewsItemsModule } from './block/newsitems/newsitems.module'; +import { AddonBlockOnlineUsersModule } from './block/onlineusers/onlineusers.module'; +import { AddonBlockRssClientModule } from './block/rssclient/rssclient.module'; +import { AddonBlockTagsModule } from './block/tags/tags.module'; import { AddonPrivateFilesModule } from './privatefiles/privatefiles.module'; import { AddonFilterModule } from './filter/filter.module'; import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield.module'; @@ -22,6 +33,17 @@ import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield imports: [ AddonPrivateFilesModule, AddonFilterModule, + AddonBlockActivityResultsModule, + AddonBlockBadgesModule, + AddonBlockBlogMenuModule, + AddonBlockBlogRecentModule, + AddonBlockBlogTagsModule, + AddonBlockGlossaryRandomModule, + AddonBlockHtmlModule, + AddonBlockNewsItemsModule, + AddonBlockOnlineUsersModule, + AddonBlockRssClientModule, + AddonBlockTagsModule, AddonUserProfileFieldModule, ], }) diff --git a/src/addons/block/activityresults/activityresults.module.ts b/src/addons/block/activityresults/activityresults.module.ts new file mode 100644 index 000000000..0d3341953 --- /dev/null +++ b/src/addons/block/activityresults/activityresults.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockActivityResultsHandler } from './services/block-handler'; +import { AddonBlockActivityResultsComponentsModule } from './components/components.module'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockActivityResultsComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockActivityResultsHandler.instance); + }, + }, + ], +}) +export class AddonBlockActivityResultsModule {} diff --git a/src/addons/block/activityresults/components/activityresults/activityresults.scss b/src/addons/block/activityresults/components/activityresults/activityresults.scss new file mode 100644 index 000000000..40e29efef --- /dev/null +++ b/src/addons/block/activityresults/components/activityresults/activityresults.scss @@ -0,0 +1,29 @@ +:host .core-block-content ::ng-deep { + table.grades { + text-align: start; + width: 100%; + + .number { + text-align: start; + width: 10%; + } + + .name { + text-align: start; + width: 77%; + } + + .grade { + text-align: end; + } + + caption { + text-align: start; + padding-top: .75rem; + padding-bottom: .75rem; + color: var(--gray-darker); + font-weight: bold; + font-size: 18px; + } + } +} diff --git a/src/addons/block/activityresults/components/activityresults/activityresults.ts b/src/addons/block/activityresults/components/activityresults/activityresults.ts new file mode 100644 index 000000000..aaa609e3f --- /dev/null +++ b/src/addons/block/activityresults/components/activityresults/activityresults.ts @@ -0,0 +1,26 @@ +// (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 } from '@angular/core'; +import { CoreBlockPreRenderedComponent } from '@features/block/components/pre-rendered-block/pre-rendered-block'; + +/** + * Component to render a activity results block. + */ +@Component({ + selector: 'addon-block-activity-results', + templateUrl: '../../../../../core/features/block/components/pre-rendered-block/core-block-pre-rendered.html', + styleUrls: ['activityresults.scss'], +}) +export class AddonBlockActivityResultsComponent extends CoreBlockPreRenderedComponent {} diff --git a/src/addons/block/activityresults/components/components.module.ts b/src/addons/block/activityresults/components/components.module.ts new file mode 100644 index 000000000..c7910cee2 --- /dev/null +++ b/src/addons/block/activityresults/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { AddonBlockActivityResultsComponent } from './activityresults/activityresults'; + +@NgModule({ + declarations: [ + AddonBlockActivityResultsComponent, + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + exports: [ + AddonBlockActivityResultsComponent, + ], + entryComponents: [ + AddonBlockActivityResultsComponent, + ], +}) +export class AddonBlockActivityResultsComponentsModule {} diff --git a/src/addons/block/activityresults/lang.json b/src/addons/block/activityresults/lang.json new file mode 100644 index 000000000..4dd6abbb7 --- /dev/null +++ b/src/addons/block/activityresults/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Activity results" +} \ No newline at end of file diff --git a/src/addons/block/activityresults/services/block-handler.ts b/src/addons/block/activityresults/services/block-handler.ts new file mode 100644 index 000000000..cdcb3cb1e --- /dev/null +++ b/src/addons/block/activityresults/services/block-handler.ts @@ -0,0 +1,47 @@ +// (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 { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { AddonBlockActivityResultsComponent } from '../components/activityresults/activityresults'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockActivityResultsHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockActivityResults'; + blockName = 'activity_results'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_activityresults.pluginname', + class: 'addon-block-activity-results', + component: AddonBlockActivityResultsComponent, + }; + } + +} + +export class AddonBlockActivityResultsHandler extends makeSingleton(AddonBlockActivityResultsHandlerService) {} diff --git a/src/addons/block/badges/badges.module.ts b/src/addons/block/badges/badges.module.ts new file mode 100644 index 000000000..0fcd2dbe7 --- /dev/null +++ b/src/addons/block/badges/badges.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockBadgesHandler } from './services/block-handler'; +import { AddonBlockBadgesComponentsModule } from './components/components.module'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockBadgesComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockBadgesHandler.instance); + }, + }, + ], +}) +export class AddonBlockBadgesModule {} diff --git a/src/addons/block/badges/components/badges/badges.scss b/src/addons/block/badges/components/badges/badges.scss new file mode 100644 index 000000000..5ca9725d5 --- /dev/null +++ b/src/addons/block/badges/components/badges/badges.scss @@ -0,0 +1,22 @@ +:host .core-block-content ::ng-deep { + ul.badges { + list-style: none; + margin-left: 0; + margin-right: 0; + -webkit-padding-start: 0; + + li { + position: relative; + display: inline-block; + padding-top: 1em; + text-align: center; + vertical-align: top; + width: 150px; + + .badge-name { + display: block; + padding: 5px; + } + } + } +} diff --git a/src/addons/block/badges/components/badges/badges.ts b/src/addons/block/badges/components/badges/badges.ts new file mode 100644 index 000000000..5f7f64a30 --- /dev/null +++ b/src/addons/block/badges/components/badges/badges.ts @@ -0,0 +1,26 @@ +// (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 } from '@angular/core'; +import { CoreBlockPreRenderedComponent } from '@features/block/components/pre-rendered-block/pre-rendered-block'; + +/** + * Component to render a badges block. + */ +@Component({ + selector: 'addon-block-badges', + templateUrl: '../../../../../core/features/block/components/pre-rendered-block/core-block-pre-rendered.html', + styleUrls: ['badges.scss'], +}) +export class AddonBlockBadgesComponent extends CoreBlockPreRenderedComponent {} diff --git a/src/addons/block/badges/components/components.module.ts b/src/addons/block/badges/components/components.module.ts new file mode 100644 index 000000000..b4e67d7c8 --- /dev/null +++ b/src/addons/block/badges/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { AddonBlockBadgesComponent } from './badges/badges'; + +@NgModule({ + declarations: [ + AddonBlockBadgesComponent, + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + exports: [ + AddonBlockBadgesComponent, + ], + entryComponents: [ + AddonBlockBadgesComponent, + ], +}) +export class AddonBlockBadgesComponentsModule {} diff --git a/src/addons/block/badges/lang.json b/src/addons/block/badges/lang.json new file mode 100644 index 000000000..dd957321f --- /dev/null +++ b/src/addons/block/badges/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Latest badges" +} \ No newline at end of file diff --git a/src/addons/block/badges/services/block-handler.ts b/src/addons/block/badges/services/block-handler.ts new file mode 100644 index 000000000..85f7bc760 --- /dev/null +++ b/src/addons/block/badges/services/block-handler.ts @@ -0,0 +1,47 @@ +// (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 { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { AddonBlockBadgesComponent } from '../components/badges/badges'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockBadgesHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockBadges'; + blockName = 'badges'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_badges.pluginname', + class: 'addon-block-badges', + component: AddonBlockBadgesComponent, + }; + } + +} + +export class AddonBlockBadgesHandler extends makeSingleton(AddonBlockBadgesHandlerService) {} diff --git a/src/addons/block/blogmenu/blogmenu.module.ts b/src/addons/block/blogmenu/blogmenu.module.ts new file mode 100644 index 000000000..3d5a1af95 --- /dev/null +++ b/src/addons/block/blogmenu/blogmenu.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockBlogMenuHandler } from './services/block-handler'; +import { AddonBlockBlogMenuComponentsModule } from './components/components.module'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockBlogMenuComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockBlogMenuHandler.instance); + }, + }, + ], +}) +export class AddonBlockBlogMenuModule {} diff --git a/src/addons/block/blogmenu/components/blogmenu/blogmenu.scss b/src/addons/block/blogmenu/components/blogmenu/blogmenu.scss new file mode 100644 index 000000000..9cad821b4 --- /dev/null +++ b/src/addons/block/blogmenu/components/blogmenu/blogmenu.scss @@ -0,0 +1,17 @@ +:host { + .core-block-content ::ng-deep { + ul.list { + list-style: none; + margin-left: 0; + margin-right: 0; + -webkit-padding-start: 0; + + li { + padding-bottom: 8px; + } + } + } + .core-block-footer ::ng-deep { + display: none; + } +} diff --git a/src/addons/block/blogmenu/components/blogmenu/blogmenu.ts b/src/addons/block/blogmenu/components/blogmenu/blogmenu.ts new file mode 100644 index 000000000..415c09e87 --- /dev/null +++ b/src/addons/block/blogmenu/components/blogmenu/blogmenu.ts @@ -0,0 +1,26 @@ +// (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 } from '@angular/core'; +import { CoreBlockPreRenderedComponent } from '@features/block/components/pre-rendered-block/pre-rendered-block'; + +/** + * Component to render a blog menu block. + */ +@Component({ + selector: 'addon-block-blog-menu', + templateUrl: '../../../../../core/features/block/components/pre-rendered-block/core-block-pre-rendered.html', + styleUrls: ['blogmenu.scss'], +}) +export class AddonBlockBlogMenuComponent extends CoreBlockPreRenderedComponent {} diff --git a/src/addons/block/blogmenu/components/components.module.ts b/src/addons/block/blogmenu/components/components.module.ts new file mode 100644 index 000000000..ac8ea52ad --- /dev/null +++ b/src/addons/block/blogmenu/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { AddonBlockBlogMenuComponent } from './blogmenu/blogmenu'; + +@NgModule({ + declarations: [ + AddonBlockBlogMenuComponent, + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + exports: [ + AddonBlockBlogMenuComponent, + ], + entryComponents: [ + AddonBlockBlogMenuComponent, + ], +}) +export class AddonBlockBlogMenuComponentsModule {} diff --git a/src/addons/block/blogmenu/lang.json b/src/addons/block/blogmenu/lang.json new file mode 100644 index 000000000..23541f7a0 --- /dev/null +++ b/src/addons/block/blogmenu/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Blog menu" +} \ No newline at end of file diff --git a/src/addons/block/blogmenu/services/block-handler.ts b/src/addons/block/blogmenu/services/block-handler.ts new file mode 100644 index 000000000..718a29245 --- /dev/null +++ b/src/addons/block/blogmenu/services/block-handler.ts @@ -0,0 +1,47 @@ +// (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 { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { AddonBlockBlogMenuComponent } from '../components/blogmenu/blogmenu'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockBlogMenuHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockBlogMenu'; + blockName = 'blog_menu'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_blogmenu.pluginname', + class: 'addon-block-blog-menu', + component: AddonBlockBlogMenuComponent, + }; + } + +} + +export class AddonBlockBlogMenuHandler extends makeSingleton(AddonBlockBlogMenuHandlerService) {} diff --git a/src/addons/block/blogrecent/blogrecent.module.ts b/src/addons/block/blogrecent/blogrecent.module.ts new file mode 100644 index 000000000..5048ade0f --- /dev/null +++ b/src/addons/block/blogrecent/blogrecent.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockBlogRecentHandler } from './services/block-handler'; +import { AddonBlockBlogRecentComponentsModule } from './components/components.module'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockBlogRecentComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockBlogRecentHandler.instance); + }, + }, + ], +}) +export class AddonBlockBlogRecentModule {} diff --git a/src/addons/block/blogrecent/components/blogrecent/blogrecent.scss b/src/addons/block/blogrecent/components/blogrecent/blogrecent.scss new file mode 100644 index 000000000..cd28d8abd --- /dev/null +++ b/src/addons/block/blogrecent/components/blogrecent/blogrecent.scss @@ -0,0 +1,12 @@ +:host .core-block-content ::ng-deep { + ul.list { + list-style: none; + margin-left: 0; + margin-right: 0; + -webkit-padding-start: 0; + + li { + padding-bottom: 8px; + } + } +} diff --git a/src/addons/block/blogrecent/components/blogrecent/blogrecent.ts b/src/addons/block/blogrecent/components/blogrecent/blogrecent.ts new file mode 100644 index 000000000..94715c86a --- /dev/null +++ b/src/addons/block/blogrecent/components/blogrecent/blogrecent.ts @@ -0,0 +1,26 @@ +// (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 } from '@angular/core'; +import { CoreBlockPreRenderedComponent } from '@features/block/components/pre-rendered-block/pre-rendered-block'; + +/** + * Component to render a blog recent block. + */ +@Component({ + selector: 'addon-block-blog-recent', + templateUrl: '../../../../../core/features/block/components/pre-rendered-block/core-block-pre-rendered.html', + styleUrls: ['blogrecent.scss'], +}) +export class AddonBlockBlogRecentComponent extends CoreBlockPreRenderedComponent {} diff --git a/src/addons/block/blogrecent/components/components.module.ts b/src/addons/block/blogrecent/components/components.module.ts new file mode 100644 index 000000000..552884705 --- /dev/null +++ b/src/addons/block/blogrecent/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { AddonBlockBlogRecentComponent } from './blogrecent/blogrecent'; + +@NgModule({ + declarations: [ + AddonBlockBlogRecentComponent, + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + exports: [ + AddonBlockBlogRecentComponent, + ], + entryComponents: [ + AddonBlockBlogRecentComponent, + ], +}) +export class AddonBlockBlogRecentComponentsModule {} diff --git a/src/addons/block/blogrecent/lang.json b/src/addons/block/blogrecent/lang.json new file mode 100644 index 000000000..a92c0cce5 --- /dev/null +++ b/src/addons/block/blogrecent/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Recent blog entries" +} \ No newline at end of file diff --git a/src/addons/block/blogrecent/services/block-handler.ts b/src/addons/block/blogrecent/services/block-handler.ts new file mode 100644 index 000000000..816ec71bf --- /dev/null +++ b/src/addons/block/blogrecent/services/block-handler.ts @@ -0,0 +1,47 @@ +// (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 { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { AddonBlockBlogRecentComponent } from '../components/blogrecent/blogrecent'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockBlogRecentHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockBlogRecent'; + blockName = 'blog_recent'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_blogrecent.pluginname', + class: 'addon-block-blog-recent', + component: AddonBlockBlogRecentComponent, + }; + } + +} + +export class AddonBlockBlogRecentHandler extends makeSingleton(AddonBlockBlogRecentHandlerService) {} diff --git a/src/addons/block/blogtags/blogtags.module.ts b/src/addons/block/blogtags/blogtags.module.ts new file mode 100644 index 000000000..08103a679 --- /dev/null +++ b/src/addons/block/blogtags/blogtags.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockBlogTagsHandler } from './services/block-handler'; +import { AddonBlockBlogTagsComponentsModule } from './components/components.module'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockBlogTagsComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockBlogTagsHandler.instance); + }, + }, + ], +}) +export class AddonBlockBlogTagsModule {} diff --git a/src/addons/block/blogtags/components/blogtags/blogtags.scss b/src/addons/block/blogtags/components/blogtags/blogtags.scss new file mode 100644 index 000000000..75f3917dd --- /dev/null +++ b/src/addons/block/blogtags/components/blogtags/blogtags.scss @@ -0,0 +1,99 @@ +:host .core-block-content ::ng-deep { + ul.inline-list { + font-size: 80%; + list-style: none; + margin-left: 0; + margin-right: 0; + -webkit-padding-start: 0; + + li { + padding: .2em; + display: inline-block; + + a { + background: var(--ion-color-primary); + color: var(--ion-color-primary-contrast); + padding: 3px 8px; + -webkit-font-smoothing: antialiased; + display: inline-block; + min-width: 10px; + font-weight: bold; + line-height: 1; + text-align: center; + white-space: nowrap; + contain: content; + vertical-align: baseline; + text-decoration: none; + border-radius: 4px; + } + .s20 { + font-size: 1.5em; + font-weight: bold; + } + + .s19 { + font-size: 1.5em; + } + + .s18 { + font-size: 1.4em; + font-weight: bold; + } + + .s17 { + font-size: 1.4em; + } + + .s16 { + font-size: 1.3em; + font-weight: bold; + } + + .s15 { + font-size: 1.3em; + } + + .s14 { + font-size: 1.2em; + font-weight: bold; + } + + .s13 { + font-size: 1.2em; + } + + .s12, + .s11 { + font-size: 1.1em; + font-weight: bold; + } + + .s10, + .s9 { + font-size: 1.1em; + } + + .s8, + .s7 { + font-size: 1em; + font-weight: bold; + } + + .s6, + .s5 { + font-size: 1em; + } + + .s4, + .s3 { + font-size: 0.9em; + font-weight: bold; + } + + .s2, + .s1 { + font-size: 0.9em; + } + } + } +} diff --git a/src/addons/block/blogtags/components/blogtags/blogtags.ts b/src/addons/block/blogtags/components/blogtags/blogtags.ts new file mode 100644 index 000000000..df38a4007 --- /dev/null +++ b/src/addons/block/blogtags/components/blogtags/blogtags.ts @@ -0,0 +1,26 @@ +// (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 } from '@angular/core'; +import { CoreBlockPreRenderedComponent } from '@features/block/components/pre-rendered-block/pre-rendered-block'; + +/** + * Component to render a blo gtags block. + */ +@Component({ + selector: 'addon-block-blog-tags', + templateUrl: '../../../../../core/features/block/components/pre-rendered-block/core-block-pre-rendered.html', + styleUrls: ['blogtags.scss'], +}) +export class AddonBlockBlogTagsComponent extends CoreBlockPreRenderedComponent {} diff --git a/src/addons/block/blogtags/components/components.module.ts b/src/addons/block/blogtags/components/components.module.ts new file mode 100644 index 000000000..8e07f0c0c --- /dev/null +++ b/src/addons/block/blogtags/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { AddonBlockBlogTagsComponent } from './blogtags/blogtags'; + +@NgModule({ + declarations: [ + AddonBlockBlogTagsComponent, + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + exports: [ + AddonBlockBlogTagsComponent, + ], + entryComponents: [ + AddonBlockBlogTagsComponent, + ], +}) +export class AddonBlockBlogTagsComponentsModule {} diff --git a/src/addons/block/blogtags/lang.json b/src/addons/block/blogtags/lang.json new file mode 100644 index 000000000..683c3aa90 --- /dev/null +++ b/src/addons/block/blogtags/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Blog tags" +} \ No newline at end of file diff --git a/src/addons/block/blogtags/services/block-handler.ts b/src/addons/block/blogtags/services/block-handler.ts new file mode 100644 index 000000000..41bb68f27 --- /dev/null +++ b/src/addons/block/blogtags/services/block-handler.ts @@ -0,0 +1,47 @@ +// (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 { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { AddonBlockBlogTagsComponent } from '../components/blogtags/blogtags'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockBlogTagsHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockBlogTags'; + blockName = 'blog_tags'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_blogtags.pluginname', + class: 'addon-block-blog-tags', + component: AddonBlockBlogTagsComponent, + }; + } + +} + +export class AddonBlockBlogTagsHandler extends makeSingleton(AddonBlockBlogTagsHandlerService) {} diff --git a/src/addons/block/glossaryrandom/glossaryrandom.module.ts b/src/addons/block/glossaryrandom/glossaryrandom.module.ts new file mode 100644 index 000000000..75723df8a --- /dev/null +++ b/src/addons/block/glossaryrandom/glossaryrandom.module.ts @@ -0,0 +1,36 @@ +// (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 { AddonBlockGlossaryRandomHandler } from './services/block-handler'; + +@NgModule({ + imports: [ + IonicModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockGlossaryRandomHandler.instance); + }, + }, + ], +}) +export class AddonBlockGlossaryRandomModule {} diff --git a/src/addons/block/glossaryrandom/lang.json b/src/addons/block/glossaryrandom/lang.json new file mode 100644 index 000000000..1ae4de38c --- /dev/null +++ b/src/addons/block/glossaryrandom/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Random glossary entry" +} \ No newline at end of file diff --git a/src/addons/block/glossaryrandom/services/block-handler.ts b/src/addons/block/glossaryrandom/services/block-handler.ts new file mode 100644 index 000000000..94096e488 --- /dev/null +++ b/src/addons/block/glossaryrandom/services/block-handler.ts @@ -0,0 +1,48 @@ +// (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 { CoreBlockPreRenderedComponent } from '@features/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockGlossaryRandomHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockGlossaryRandom'; + blockName = 'glossary_random'; + + /** + * Returns the data needed to render the block. + * + * @param block The block to render. + * @return Data or promise resolved with the data. + */ + getDisplayData(block: CoreCourseBlock): CoreBlockHandlerData { + return { + title: block.contents?.title || 'addon.block_glossaryrandom.pluginname', + class: 'addon-block-glossary-random', + component: CoreBlockPreRenderedComponent, + }; + } + +} + +export class AddonBlockGlossaryRandomHandler extends makeSingleton(AddonBlockGlossaryRandomHandlerService) {} diff --git a/src/addons/block/html/html.module.ts b/src/addons/block/html/html.module.ts new file mode 100644 index 000000000..418b406df --- /dev/null +++ b/src/addons/block/html/html.module.ts @@ -0,0 +1,30 @@ +// (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 { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockHtmlHandler } from './services/block-handler'; + +@NgModule({ + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockHtmlHandler.instance); + }, + }, + ], +}) +export class AddonBlockHtmlModule {} diff --git a/src/addons/block/html/services/block-handler.ts b/src/addons/block/html/services/block-handler.ts new file mode 100644 index 000000000..d77fd2deb --- /dev/null +++ b/src/addons/block/html/services/block-handler.ts @@ -0,0 +1,49 @@ +// (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 { CoreBlockPreRenderedComponent } from '@features/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockHtmlHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockHtml'; + blockName = 'html'; + + /** + * Returns the data needed to render the block. + * + * @param block The block to render. + * @return Data or promise resolved with the data. + */ + getDisplayData(block: CoreCourseBlock): CoreBlockHandlerData { + + return { + title: block.contents?.title || '', + class: 'addon-block-html', + component: CoreBlockPreRenderedComponent, + }; + } + +} + +export class AddonBlockHtmlHandler extends makeSingleton(AddonBlockHtmlHandlerService) {} diff --git a/src/addons/block/newsitems/components/components.module.ts b/src/addons/block/newsitems/components/components.module.ts new file mode 100644 index 000000000..55c178d71 --- /dev/null +++ b/src/addons/block/newsitems/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { AddonBlockNewsItemsComponent } from './newsitems/newsitems'; + +@NgModule({ + declarations: [ + AddonBlockNewsItemsComponent, + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + exports: [ + AddonBlockNewsItemsComponent, + ], + entryComponents: [ + AddonBlockNewsItemsComponent, + ], +}) +export class AddonBlockNewsItemsComponentsModule {} diff --git a/src/addons/block/newsitems/components/newsitems/newsitems.scss b/src/addons/block/newsitems/components/newsitems/newsitems.scss new file mode 100644 index 000000000..9ca636473 --- /dev/null +++ b/src/addons/block/newsitems/components/newsitems/newsitems.scss @@ -0,0 +1,27 @@ +:host { + .core-block-content ::ng-deep { + .unlist { + list-style-type: none; + margin-left: 0; + margin-right: 0; + -webkit-padding-start: 0; + + li.post { + padding-bottom: 16px; + } + li.post:last-child { + padding-bottom: 0; + } + } + } + + // Hide RSS link. + .core-block-footer ::ng-deep { + a { + display: none; + } + a:first-child { + display: inline; + } + } +} diff --git a/src/addons/block/newsitems/components/newsitems/newsitems.ts b/src/addons/block/newsitems/components/newsitems/newsitems.ts new file mode 100644 index 000000000..749868338 --- /dev/null +++ b/src/addons/block/newsitems/components/newsitems/newsitems.ts @@ -0,0 +1,26 @@ +// (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 } from '@angular/core'; +import { CoreBlockPreRenderedComponent } from '@features/block/components/pre-rendered-block/pre-rendered-block'; + +/** + * Component to render a news items block. + */ +@Component({ + selector: 'addon-block-news-items', + templateUrl: '../../../../../core/features/block/components/pre-rendered-block/core-block-pre-rendered.html', + styleUrls: ['newsitems.scss'], +}) +export class AddonBlockNewsItemsComponent extends CoreBlockPreRenderedComponent {} diff --git a/src/addons/block/newsitems/lang.json b/src/addons/block/newsitems/lang.json new file mode 100644 index 000000000..83b981297 --- /dev/null +++ b/src/addons/block/newsitems/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Latest announcements" +} \ No newline at end of file diff --git a/src/addons/block/newsitems/newsitems.module.ts b/src/addons/block/newsitems/newsitems.module.ts new file mode 100644 index 000000000..b328ad276 --- /dev/null +++ b/src/addons/block/newsitems/newsitems.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockNewsItemsHandler } from './services/block-handler'; +import { AddonBlockNewsItemsComponentsModule } from './components/components.module'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockNewsItemsComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockNewsItemsHandler.instance); + }, + }, + ], +}) +export class AddonBlockNewsItemsModule {} diff --git a/src/addons/block/newsitems/services/block-handler.ts b/src/addons/block/newsitems/services/block-handler.ts new file mode 100644 index 000000000..04f1ceac5 --- /dev/null +++ b/src/addons/block/newsitems/services/block-handler.ts @@ -0,0 +1,47 @@ +// (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 { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { AddonBlockNewsItemsComponent } from '../components/newsitems/newsitems'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockNewsItemsHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockNewsItems'; + blockName = 'news_items'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_newsitems.pluginname', + class: 'addon-block-news-items', + component: AddonBlockNewsItemsComponent, + }; + } + +} + +export class AddonBlockNewsItemsHandler extends makeSingleton(AddonBlockNewsItemsHandlerService) {} diff --git a/src/addons/block/onlineusers/components/components.module.ts b/src/addons/block/onlineusers/components/components.module.ts new file mode 100644 index 000000000..384ffd1d2 --- /dev/null +++ b/src/addons/block/onlineusers/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { AddonBlockOnlineUsersComponent } from './onlineusers/onlineusers'; + +@NgModule({ + declarations: [ + AddonBlockOnlineUsersComponent, + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + exports: [ + AddonBlockOnlineUsersComponent, + ], + entryComponents: [ + AddonBlockOnlineUsersComponent, + ], +}) +export class AddonBlockOnlineUsersComponentsModule {} diff --git a/src/addons/block/onlineusers/components/onlineusers/onlineusers.scss b/src/addons/block/onlineusers/components/onlineusers/onlineusers.scss new file mode 100644 index 000000000..c3d4b4c40 --- /dev/null +++ b/src/addons/block/onlineusers/components/onlineusers/onlineusers.scss @@ -0,0 +1,67 @@ +:host .core-block-content ::ng-deep { + max-height: 200px; + overflow-y: auto; + .item-inner, + .input-wrapper { + overflow-y: visible; + align-self: start; + } + + .list { + margin-left: 0; + margin-right: 0; + -webkit-padding-start: 0; + + li.listentry { + clear: both; + list-style-type: none; + + .user { + float: left; + position: relative; + padding-bottom: 16px; + + .core-adapted-img-container { + display: inline; + margin-left: 0; + margin-right: 8px; + } + + .userpicture { + vertical-align: text-bottom; + } + } + + .message { + float: right; + margin-top: 3px; + } + + .uservisibility { // No support on the app. + display: none; + } + } + } + + .info { + text-align: center; + } + +} + +:host-context([dir=rtl]) .core-block-content ::ng-deep { + .list li.listentry { + .user { + float: right; + + .core-adapted-img-container { + margin-left: 8px; + margin-right: 0; + } + } + + .message { + float: left; + } + } +} diff --git a/src/addons/block/onlineusers/components/onlineusers/onlineusers.ts b/src/addons/block/onlineusers/components/onlineusers/onlineusers.ts new file mode 100644 index 000000000..38880fbfd --- /dev/null +++ b/src/addons/block/onlineusers/components/onlineusers/onlineusers.ts @@ -0,0 +1,26 @@ +// (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 } from '@angular/core'; +import { CoreBlockPreRenderedComponent } from '@features/block/components/pre-rendered-block/pre-rendered-block'; + +/** + * Component to render a online users block. + */ +@Component({ + selector: 'addon-block-online-users', + templateUrl: '../../../../../core/features/block/components/pre-rendered-block/core-block-pre-rendered.html', + styleUrls: ['onlineusers.scss'], +}) +export class AddonBlockOnlineUsersComponent extends CoreBlockPreRenderedComponent {} diff --git a/src/addons/block/onlineusers/lang.json b/src/addons/block/onlineusers/lang.json new file mode 100644 index 000000000..4bc6cd412 --- /dev/null +++ b/src/addons/block/onlineusers/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Online users" +} \ No newline at end of file diff --git a/src/addons/block/onlineusers/onlineusers.module.ts b/src/addons/block/onlineusers/onlineusers.module.ts new file mode 100644 index 000000000..6eef61477 --- /dev/null +++ b/src/addons/block/onlineusers/onlineusers.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockOnlineUsersHandler } from './services/block-handler'; +import { AddonBlockOnlineUsersComponentsModule } from './components/components.module'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockOnlineUsersComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockOnlineUsersHandler.instance); + }, + }, + ], +}) +export class AddonBlockOnlineUsersModule {} diff --git a/src/addons/block/onlineusers/services/block-handler.ts b/src/addons/block/onlineusers/services/block-handler.ts new file mode 100644 index 000000000..813dfaae0 --- /dev/null +++ b/src/addons/block/onlineusers/services/block-handler.ts @@ -0,0 +1,46 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreBlockHandlerData } from '@features/block/services/block-delegate'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { AddonBlockOnlineUsersComponent } from '../components/onlineusers/onlineusers'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockOnlineUsersHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockOnlineUsers'; + blockName = 'online_users'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_onlineusers.pluginname', + class: 'addon-block-online-users', + component: AddonBlockOnlineUsersComponent, + }; + } + +} + +export class AddonBlockOnlineUsersHandler extends makeSingleton(AddonBlockOnlineUsersHandlerService) {} diff --git a/src/addons/block/recentactivity/components/components.module.ts b/src/addons/block/recentactivity/components/components.module.ts new file mode 100644 index 000000000..ce4fe2ba0 --- /dev/null +++ b/src/addons/block/recentactivity/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { AddonBlockRecentActivityComponent } from './recentactivity/recentactivity'; + +@NgModule({ + declarations: [ + AddonBlockRecentActivityComponent, + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + exports: [ + AddonBlockRecentActivityComponent, + ], + entryComponents: [ + AddonBlockRecentActivityComponent, + ], +}) +export class AddonBlockRecentActivityComponentsModule {} diff --git a/src/addons/block/recentactivity/components/recentactivity/recentactivity.scss b/src/addons/block/recentactivity/components/recentactivity/recentactivity.scss new file mode 100644 index 000000000..a2e9d77a2 --- /dev/null +++ b/src/addons/block/recentactivity/components/recentactivity/recentactivity.scss @@ -0,0 +1,25 @@ +:host .core-block-content ::ng-deep { + .activitydate, .activityhead { + text-align: center; + } + + .unlist { + list-style: none; + margin-left: 0; + margin-right: 0; + -webkit-padding-start: 0; + li { + margin-bottom: 1em; + + .head .date { + float: right; + } + } + } +} + +:host-context([dir=rtl]) .core-block-content ::ng-deep { + .unlist li .head .date { + float: left; + } +} diff --git a/src/addons/block/recentactivity/components/recentactivity/recentactivity.ts b/src/addons/block/recentactivity/components/recentactivity/recentactivity.ts new file mode 100644 index 000000000..e13e693dc --- /dev/null +++ b/src/addons/block/recentactivity/components/recentactivity/recentactivity.ts @@ -0,0 +1,26 @@ +// (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 } from '@angular/core'; +import { CoreBlockPreRenderedComponent } from '@features/block/components/pre-rendered-block/pre-rendered-block'; + +/** + * Component to render a recent activity block. + */ +@Component({ + selector: 'addon-block-recent-activity', + templateUrl: '../../../../../core/features/block/components/pre-rendered-block/core-block-pre-rendered.html', + styleUrls: ['recentactivity.scss'], +}) +export class AddonBlockRecentActivityComponent extends CoreBlockPreRenderedComponent {} diff --git a/src/addons/block/recentactivity/lang.json b/src/addons/block/recentactivity/lang.json new file mode 100644 index 000000000..29f7996e2 --- /dev/null +++ b/src/addons/block/recentactivity/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Recent activity" +} \ No newline at end of file diff --git a/src/addons/block/recentactivity/recentactivity.module.ts b/src/addons/block/recentactivity/recentactivity.module.ts new file mode 100644 index 000000000..97ae712df --- /dev/null +++ b/src/addons/block/recentactivity/recentactivity.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockRecentActivityHandler } from './services/block-handler'; +import { AddonBlockRecentActivityComponentsModule } from './components/components.module'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockRecentActivityComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockRecentActivityHandler.instance); + }, + }, + ], +}) +export class AddonBlockRecentActivityModule {} diff --git a/src/addons/block/recentactivity/services/block-handler.ts b/src/addons/block/recentactivity/services/block-handler.ts new file mode 100644 index 000000000..a2e35f24b --- /dev/null +++ b/src/addons/block/recentactivity/services/block-handler.ts @@ -0,0 +1,47 @@ +// (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 { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { AddonBlockRecentActivityComponent } from '../components/recentactivity/recentactivity'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockRecentActivityHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockRecentActivity'; + blockName = 'recent_activity'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_recentactivity.pluginname', + class: 'addon-block-recent-activity', + component: AddonBlockRecentActivityComponent, + }; + } + +} + +export class AddonBlockRecentActivityHandler extends makeSingleton(AddonBlockRecentActivityHandlerService) {} diff --git a/src/addons/block/rssclient/components/components.module.ts b/src/addons/block/rssclient/components/components.module.ts new file mode 100644 index 000000000..b94be30d3 --- /dev/null +++ b/src/addons/block/rssclient/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { AddonBlockRssClientComponent } from './rssclient/rssclient'; + +@NgModule({ + declarations: [ + AddonBlockRssClientComponent, + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + exports: [ + AddonBlockRssClientComponent, + ], + entryComponents: [ + AddonBlockRssClientComponent, + ], +}) +export class AddonBlockRssClientComponentsModule {} diff --git a/src/addons/block/rssclient/components/rssclient/rssclient.scss b/src/addons/block/rssclient/components/rssclient/rssclient.scss new file mode 100644 index 000000000..bd23c6bc0 --- /dev/null +++ b/src/addons/block/rssclient/components/rssclient/rssclient.scss @@ -0,0 +1,18 @@ +:host .core-block-content ::ng-deep { + .list { + list-style: none; + margin-left: 0; + margin-right: 0; + -webkit-padding-start: 0; + + li { + border-top: 1px solid var(--gray); + padding: 5px; + padding-bottom: 8px; + } + + li:first-child { + border-top-width: 0; + } + } +} diff --git a/src/addons/block/rssclient/components/rssclient/rssclient.ts b/src/addons/block/rssclient/components/rssclient/rssclient.ts new file mode 100644 index 000000000..c80610358 --- /dev/null +++ b/src/addons/block/rssclient/components/rssclient/rssclient.ts @@ -0,0 +1,26 @@ +// (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 } from '@angular/core'; +import { CoreBlockPreRenderedComponent } from '@features/block/components/pre-rendered-block/pre-rendered-block'; + +/** + * Component to render a rss client block. + */ +@Component({ + selector: 'addon-block-rss-client', + templateUrl: '../../../../../core/features/block/components/pre-rendered-block/core-block-pre-rendered.html', + styleUrls: ['rssclient.scss'], +}) +export class AddonBlockRssClientComponent extends CoreBlockPreRenderedComponent {} diff --git a/src/addons/block/rssclient/lang.json b/src/addons/block/rssclient/lang.json new file mode 100644 index 000000000..18282971b --- /dev/null +++ b/src/addons/block/rssclient/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Remote RSS feeds" +} \ No newline at end of file diff --git a/src/addons/block/rssclient/rssclient.module.ts b/src/addons/block/rssclient/rssclient.module.ts new file mode 100644 index 000000000..c572d8925 --- /dev/null +++ b/src/addons/block/rssclient/rssclient.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockRssClientHandler } from './services/block-handler'; +import { AddonBlockRssClientComponentsModule } from './components/components.module'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockRssClientComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockRssClientHandler.instance); + }, + }, + ], +}) +export class AddonBlockRssClientModule {} diff --git a/src/addons/block/rssclient/services/block-handler.ts b/src/addons/block/rssclient/services/block-handler.ts new file mode 100644 index 000000000..15368c908 --- /dev/null +++ b/src/addons/block/rssclient/services/block-handler.ts @@ -0,0 +1,49 @@ +// (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 { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { AddonBlockRssClientComponent } from '../components/rssclient/rssclient'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockRssClientHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockRssClient'; + blockName = 'rss_client'; + + /** + * Returns the data needed to render the block. + * + * @param block The block to render. + * @return Data or promise resolved with the data. + */ + getDisplayData(block: CoreCourseBlock): CoreBlockHandlerData { + + return { + title: block.contents?.title || 'addon.block_rssclient.pluginname', + class: 'addon-block-rss-client', + component: AddonBlockRssClientComponent, + }; + } + +} + +export class AddonBlockRssClientHandler extends makeSingleton(AddonBlockRssClientHandlerService) {} diff --git a/src/addons/block/tags/components/components.module.ts b/src/addons/block/tags/components/components.module.ts new file mode 100644 index 000000000..e26cc198b --- /dev/null +++ b/src/addons/block/tags/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { AddonBlockTagsComponent } from './tags/tags'; + +@NgModule({ + declarations: [ + AddonBlockTagsComponent, + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + exports: [ + AddonBlockTagsComponent, + ], + entryComponents: [ + AddonBlockTagsComponent, + ], +}) +export class AddonBlockTagsComponentsModule {} diff --git a/src/addons/block/tags/components/tags/tags.scss b/src/addons/block/tags/components/tags/tags.scss new file mode 100644 index 000000000..b1144d4e0 --- /dev/null +++ b/src/addons/block/tags/components/tags/tags.scss @@ -0,0 +1,117 @@ +:host .core-block-content ::ng-deep { + .tag_cloud { + font-size: 80%; + text-align: center; + ul.inline-list { + list-style: none; + margin-left: 0; + margin-right: 0; + -webkit-padding-start: 0; + + li { + padding: .2em; + display: inline-block; + + a { + background: var(--ion-color-primary); + color: var(--ion-color-primary-contrast); + padding: 3px 8px; + -webkit-font-smoothing: antialiased; + display: inline-block; + min-width: 10px; + font-weight: bold; + line-height: 1; + text-align: center; + white-space: nowrap; + contain: content; + vertical-align: baseline; + text-decoration: none; + border-radius: 4px; + } + .s20 { + font-size: 2.7em; + } + + .s19 { + font-size: 2.6em; + } + + .s18 { + font-size: 2.5em; + } + + .s17 { + font-size: 2.4em; + } + + .s16 { + font-size: 2.3em; + } + + .s15 { + font-size: 2.2em; + } + + .s14 { + font-size: 2.1em; + } + + .s13 { + font-size: 2em; + } + + .s12 { + font-size: 1.9em; + } + + .s11 { + font-size: 1.8em; + } + + .s10 { + font-size: 1.7em; + } + + .s9 { + font-size: 1.6em; + } + + .s8 { + font-size: 1.5em; + } + + .s7 { + font-size: 1.4em; + } + + .s6 { + font-size: 1.3em; + } + + .s5 { + font-size: 1.2em; + } + + .s4 { + font-size: 1.1em; + } + + .s3 { + font-size: 1em; + } + + .s2 { + font-size: 0.9em; + } + + .s1 { + font-size: 0.8em; + } + + .s0 { + font-size: 0.7em; + } + } + } + } +} diff --git a/src/addons/block/tags/components/tags/tags.ts b/src/addons/block/tags/components/tags/tags.ts new file mode 100644 index 000000000..9ae0e6c2c --- /dev/null +++ b/src/addons/block/tags/components/tags/tags.ts @@ -0,0 +1,26 @@ +// (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 } from '@angular/core'; +import { CoreBlockPreRenderedComponent } from '@features/block/components/pre-rendered-block/pre-rendered-block'; + +/** + * Component to render a tags block. + */ +@Component({ + selector: 'addon-block-tags', + templateUrl: '../../../../../core/features/block/components/pre-rendered-block/core-block-pre-rendered.html', + styleUrls: ['tags.scss'], +}) +export class AddonBlockTagsComponent extends CoreBlockPreRenderedComponent {} diff --git a/src/addons/block/tags/lang.json b/src/addons/block/tags/lang.json new file mode 100644 index 000000000..a4080dd78 --- /dev/null +++ b/src/addons/block/tags/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Tags" +} \ No newline at end of file diff --git a/src/addons/block/tags/services/block-handler.ts b/src/addons/block/tags/services/block-handler.ts new file mode 100644 index 000000000..381151d24 --- /dev/null +++ b/src/addons/block/tags/services/block-handler.ts @@ -0,0 +1,47 @@ +// (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 { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { AddonBlockTagsComponent } from '../components/tags/tags'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockTagsHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockTags'; + blockName = 'tags'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_tags.pluginname', + class: 'addon-block-tags', + component: AddonBlockTagsComponent, + }; + } + +} + +export class AddonBlockTagsHandler extends makeSingleton(AddonBlockTagsHandlerService) {} diff --git a/src/addons/block/tags/tags.module.ts b/src/addons/block/tags/tags.module.ts new file mode 100644 index 000000000..e20b990b6 --- /dev/null +++ b/src/addons/block/tags/tags.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockTagsHandler } from './services/block-handler'; +import { AddonBlockTagsComponentsModule } from './components/components.module'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockTagsComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockTagsHandler.instance); + }, + }, + ], +}) +export class AddonBlockTagsModule {} From da286a80e7cd772d0a4320eaa502ea64e64cc5e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 7 Dec 2020 17:02:30 +0100 Subject: [PATCH 05/11] MOBILE-3608 blocks: Add all only title blocks --- src/addons/addons.module.ts | 14 +++++ .../calendarmonth/calendarmonth.module.ts | 36 +++++++++++ src/addons/block/calendarmonth/lang.json | 3 + .../calendarmonth/services/block-handler.ts | 61 ++++++++++++++++++ .../calendarupcoming.module.ts | 36 +++++++++++ src/addons/block/calendarupcoming/lang.json | 3 + .../services/block-handler.ts | 62 +++++++++++++++++++ src/addons/block/comments/comments.module.ts | 36 +++++++++++ src/addons/block/comments/lang.json | 3 + .../block/comments/services/block-handler.ts | 59 ++++++++++++++++++ .../completionstatus.module.ts | 36 +++++++++++ src/addons/block/completionstatus/lang.json | 3 + .../services/block-handler.ts | 55 ++++++++++++++++ src/addons/block/learningplans/lang.json | 3 + .../learningplans/learningplans.module.ts | 38 ++++++++++++ .../learningplans/services/block-handler.ts | 48 ++++++++++++++ src/addons/block/privatefiles/lang.json | 3 + .../block/privatefiles/privatefiles.module.ts | 36 +++++++++++ .../privatefiles/services/block-handler.ts | 49 +++++++++++++++ src/addons/block/selfcompletion/lang.json | 3 + .../selfcompletion/selfcompletion.module.ts | 36 +++++++++++ .../selfcompletion/services/block-handler.ts | 53 ++++++++++++++++ 22 files changed, 676 insertions(+) create mode 100644 src/addons/block/calendarmonth/calendarmonth.module.ts create mode 100644 src/addons/block/calendarmonth/lang.json create mode 100644 src/addons/block/calendarmonth/services/block-handler.ts create mode 100644 src/addons/block/calendarupcoming/calendarupcoming.module.ts create mode 100644 src/addons/block/calendarupcoming/lang.json create mode 100644 src/addons/block/calendarupcoming/services/block-handler.ts create mode 100644 src/addons/block/comments/comments.module.ts create mode 100644 src/addons/block/comments/lang.json create mode 100644 src/addons/block/comments/services/block-handler.ts create mode 100644 src/addons/block/completionstatus/completionstatus.module.ts create mode 100644 src/addons/block/completionstatus/lang.json create mode 100644 src/addons/block/completionstatus/services/block-handler.ts create mode 100644 src/addons/block/learningplans/lang.json create mode 100644 src/addons/block/learningplans/learningplans.module.ts create mode 100644 src/addons/block/learningplans/services/block-handler.ts create mode 100644 src/addons/block/privatefiles/lang.json create mode 100644 src/addons/block/privatefiles/privatefiles.module.ts create mode 100644 src/addons/block/privatefiles/services/block-handler.ts create mode 100644 src/addons/block/selfcompletion/lang.json create mode 100644 src/addons/block/selfcompletion/selfcompletion.module.ts create mode 100644 src/addons/block/selfcompletion/services/block-handler.ts diff --git a/src/addons/addons.module.ts b/src/addons/addons.module.ts index 9379c35c7..680edb1dd 100644 --- a/src/addons/addons.module.ts +++ b/src/addons/addons.module.ts @@ -19,11 +19,18 @@ import { AddonBlockBadgesModule } from './block/badges/badges.module'; import { AddonBlockBlogMenuModule } from './block/blogmenu/blogmenu.module'; import { AddonBlockBlogRecentModule } from './block/blogrecent/blogrecent.module'; import { AddonBlockBlogTagsModule } from './block/blogtags/blogtags.module'; +import { AddonBlockCalendarMonthModule } from './block/calendarmonth/calendarmonth.module'; +import { AddonBlockCalendarUpcomingModule } from './block/calendarupcoming/calendarupcoming.module'; +import { AddonBlockCommentsModule } from './block/comments/comments.module'; +import { AddonBlockCompletionStatusModule } from './block/completionstatus/completionstatus.module'; import { AddonBlockGlossaryRandomModule } from './block/glossaryrandom/glossaryrandom.module'; import { AddonBlockHtmlModule } from './block/html/html.module'; +import { AddonBlockLearningPlansModule } from './block/learningplans/learningplans.module'; import { AddonBlockNewsItemsModule } from './block/newsitems/newsitems.module'; import { AddonBlockOnlineUsersModule } from './block/onlineusers/onlineusers.module'; +import { AddonBlockPrivateFilesModule } from './block/privatefiles/privatefiles.module'; import { AddonBlockRssClientModule } from './block/rssclient/rssclient.module'; +import { AddonBlockSelfCompletionModule } from './block/selfcompletion/selfcompletion.module'; import { AddonBlockTagsModule } from './block/tags/tags.module'; import { AddonPrivateFilesModule } from './privatefiles/privatefiles.module'; import { AddonFilterModule } from './filter/filter.module'; @@ -38,11 +45,18 @@ import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield AddonBlockBlogMenuModule, AddonBlockBlogRecentModule, AddonBlockBlogTagsModule, + AddonBlockCalendarMonthModule, + AddonBlockCalendarUpcomingModule, + AddonBlockCommentsModule, + AddonBlockCompletionStatusModule, AddonBlockGlossaryRandomModule, AddonBlockHtmlModule, + AddonBlockLearningPlansModule, AddonBlockNewsItemsModule, AddonBlockOnlineUsersModule, + AddonBlockPrivateFilesModule, AddonBlockRssClientModule, + AddonBlockSelfCompletionModule, AddonBlockTagsModule, AddonUserProfileFieldModule, ], diff --git a/src/addons/block/calendarmonth/calendarmonth.module.ts b/src/addons/block/calendarmonth/calendarmonth.module.ts new file mode 100644 index 000000000..c432e4d0e --- /dev/null +++ b/src/addons/block/calendarmonth/calendarmonth.module.ts @@ -0,0 +1,36 @@ +// (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 { AddonBlockCalendarMonthHandler } from './services/block-handler'; + +@NgModule({ + imports: [ + IonicModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockCalendarMonthHandler.instance); + }, + }, + ], +}) +export class AddonBlockCalendarMonthModule {} diff --git a/src/addons/block/calendarmonth/lang.json b/src/addons/block/calendarmonth/lang.json new file mode 100644 index 000000000..86a476c29 --- /dev/null +++ b/src/addons/block/calendarmonth/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Calendar" +} \ No newline at end of file diff --git a/src/addons/block/calendarmonth/services/block-handler.ts b/src/addons/block/calendarmonth/services/block-handler.ts new file mode 100644 index 000000000..ed08c724d --- /dev/null +++ b/src/addons/block/calendarmonth/services/block-handler.ts @@ -0,0 +1,61 @@ +// (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 { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +// import { AddonCalendar } from '@addon/calendar/services/calendar'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { Params } from '@angular/router'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockCalendarMonthHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockCalendarMonth'; + blockName = 'calendar_month'; + + /** + * Returns the data needed to render the block. + * + * @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(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData { + // @todo + const link = 'AddonCalendarListPage'; + const linkParams: Params = contextLevel == 'course' ? { courseId: instanceId } : {}; + + /* if (AddonCalendar.instance.canViewMonthInSite()) { + link = 'AddonCalendarIndexPage'; + }*/ + + return { + title: 'addon.block_calendarmonth.pluginname', + class: 'addon-block-calendar-month', + component: CoreBlockOnlyTitleComponent, + link: link, + linkParams: linkParams, + }; + } + +} + +export class AddonBlockCalendarMonthHandler extends makeSingleton(AddonBlockCalendarMonthHandlerService) {} diff --git a/src/addons/block/calendarupcoming/calendarupcoming.module.ts b/src/addons/block/calendarupcoming/calendarupcoming.module.ts new file mode 100644 index 000000000..08884ab7a --- /dev/null +++ b/src/addons/block/calendarupcoming/calendarupcoming.module.ts @@ -0,0 +1,36 @@ +// (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 { AddonBlockCalendarUpcomingHandler } from './services/block-handler'; + +@NgModule({ + imports: [ + IonicModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockCalendarUpcomingHandler.instance); + }, + }, + ], +}) +export class AddonBlockCalendarUpcomingModule {} diff --git a/src/addons/block/calendarupcoming/lang.json b/src/addons/block/calendarupcoming/lang.json new file mode 100644 index 000000000..4d73d8f67 --- /dev/null +++ b/src/addons/block/calendarupcoming/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Upcoming events" +} \ No newline at end of file diff --git a/src/addons/block/calendarupcoming/services/block-handler.ts b/src/addons/block/calendarupcoming/services/block-handler.ts new file mode 100644 index 000000000..6a5d0bda6 --- /dev/null +++ b/src/addons/block/calendarupcoming/services/block-handler.ts @@ -0,0 +1,62 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreBlockHandlerData } from '@features/block/services/block-delegate'; +import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +// import { AddonCalendar } from '@addon/calendar/services/calendar'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { Params } from '@angular/router'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockCalendarUpcomingHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockCalendarUpcoming'; + blockName = 'calendar_upcoming'; + + /** + * Returns the data needed to render the block. + * + * @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(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData { + // @todo + const link = 'AddonCalendarListPage'; + const linkParams: Params = contextLevel == 'course' ? { courseId: instanceId } : {}; + + /* if (AddonCalendar.instance.canViewMonthInSite()) { + link = 'AddonCalendarIndexPage'; + linkParams.upcoming = true; + }*/ + + return { + title: 'addon.block_calendarupcoming.pluginname', + class: 'addon-block-calendar-upcoming', + component: CoreBlockOnlyTitleComponent, + link: link, + linkParams: linkParams, + }; + } + +} + +export class AddonBlockCalendarUpcomingHandler extends makeSingleton(AddonBlockCalendarUpcomingHandlerService) {} diff --git a/src/addons/block/comments/comments.module.ts b/src/addons/block/comments/comments.module.ts new file mode 100644 index 000000000..678abdbbb --- /dev/null +++ b/src/addons/block/comments/comments.module.ts @@ -0,0 +1,36 @@ +// (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 { AddonBlockCommentsHandler } from './services/block-handler'; + +@NgModule({ + imports: [ + IonicModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockCommentsHandler.instance); + }, + }, + ], +}) +export class AddonBlockCommentsModule {} diff --git a/src/addons/block/comments/lang.json b/src/addons/block/comments/lang.json new file mode 100644 index 000000000..adcbcabae --- /dev/null +++ b/src/addons/block/comments/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Comments" +} \ No newline at end of file diff --git a/src/addons/block/comments/services/block-handler.ts b/src/addons/block/comments/services/block-handler.ts new file mode 100644 index 000000000..0a72b3360 --- /dev/null +++ b/src/addons/block/comments/services/block-handler.ts @@ -0,0 +1,59 @@ +// (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 { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockCommentsHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockComments'; + blockName = 'comments'; + + /** + * Returns the data needed to render the block. + * + * @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(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData { + // @todo + + return { + title: 'addon.block_comments.pluginname', + class: 'addon-block-comments', + component: CoreBlockOnlyTitleComponent, + link: 'CoreCommentsViewerPage', + linkParams: { + contextLevel: contextLevel, + instanceId: instanceId, + componentName: 'block_comments', + area: 'page_comments', + itemId: 0, + }, + }; + } + +} + +export class AddonBlockCommentsHandler extends makeSingleton(AddonBlockCommentsHandlerService) {} diff --git a/src/addons/block/completionstatus/completionstatus.module.ts b/src/addons/block/completionstatus/completionstatus.module.ts new file mode 100644 index 000000000..a57bc8fcc --- /dev/null +++ b/src/addons/block/completionstatus/completionstatus.module.ts @@ -0,0 +1,36 @@ +// (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 { AddonBlockCompletionStatusHandler } from './services/block-handler'; + +@NgModule({ + imports: [ + IonicModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockCompletionStatusHandler.instance); + }, + }, + ], +}) +export class AddonBlockCompletionStatusModule {} diff --git a/src/addons/block/completionstatus/lang.json b/src/addons/block/completionstatus/lang.json new file mode 100644 index 000000000..fe57356da --- /dev/null +++ b/src/addons/block/completionstatus/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Course completion status" +} \ No newline at end of file diff --git a/src/addons/block/completionstatus/services/block-handler.ts b/src/addons/block/completionstatus/services/block-handler.ts new file mode 100644 index 000000000..00458928d --- /dev/null +++ b/src/addons/block/completionstatus/services/block-handler.ts @@ -0,0 +1,55 @@ +// (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 { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockCompletionStatusHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockCompletionStatus'; + blockName = 'completionstatus'; + + /** + * Returns the data needed to render the block. + * + * @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(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData { + // @todo + + return { + title: 'addon.block_completionstatus.pluginname', + class: 'addon-block-completion-status', + component: CoreBlockOnlyTitleComponent, + link: 'AddonCourseCompletionReportPage', + linkParams: { + courseId: instanceId, + }, + }; + } + +} + +export class AddonBlockCompletionStatusHandler extends makeSingleton(AddonBlockCompletionStatusHandlerService) {} diff --git a/src/addons/block/learningplans/lang.json b/src/addons/block/learningplans/lang.json new file mode 100644 index 000000000..0a7f81e22 --- /dev/null +++ b/src/addons/block/learningplans/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Learning plans" +} \ No newline at end of file diff --git a/src/addons/block/learningplans/learningplans.module.ts b/src/addons/block/learningplans/learningplans.module.ts new file mode 100644 index 000000000..acf5d32e7 --- /dev/null +++ b/src/addons/block/learningplans/learningplans.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockLearningPlansHandler } from './services/block-handler'; +import { CoreBlockComponentsModule } from '@features/block/components/components.module'; + +@NgModule({ + imports: [ + IonicModule, + CoreBlockComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockLearningPlansHandler.instance); + }, + }, + ], +}) +export class AddonBlockLearningPlansModule {} diff --git a/src/addons/block/learningplans/services/block-handler.ts b/src/addons/block/learningplans/services/block-handler.ts new file mode 100644 index 000000000..3340be569 --- /dev/null +++ b/src/addons/block/learningplans/services/block-handler.ts @@ -0,0 +1,48 @@ +// (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 { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockLearningPlansHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockLearningPlans'; + blockName = 'lp'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + // @todo + + return { + title: 'addon.block_learningplans.pluginname', + class: 'addon-block-learning-plans', + component: CoreBlockOnlyTitleComponent, + link: 'AddonCompetencyPlanListPage', + }; + } + +} + +export class AddonBlockLearningPlansHandler extends makeSingleton(AddonBlockLearningPlansHandlerService) {} diff --git a/src/addons/block/privatefiles/lang.json b/src/addons/block/privatefiles/lang.json new file mode 100644 index 000000000..bba9d4bc0 --- /dev/null +++ b/src/addons/block/privatefiles/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Private files" +} \ No newline at end of file diff --git a/src/addons/block/privatefiles/privatefiles.module.ts b/src/addons/block/privatefiles/privatefiles.module.ts new file mode 100644 index 000000000..a19075e8f --- /dev/null +++ b/src/addons/block/privatefiles/privatefiles.module.ts @@ -0,0 +1,36 @@ +// (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 { AddonBlockPrivateFilesHandler } from './services/block-handler'; + +@NgModule({ + imports: [ + IonicModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockPrivateFilesHandler.instance); + }, + }, + ], +}) +export class AddonBlockPrivateFilesModule {} diff --git a/src/addons/block/privatefiles/services/block-handler.ts b/src/addons/block/privatefiles/services/block-handler.ts new file mode 100644 index 000000000..8b2071b7b --- /dev/null +++ b/src/addons/block/privatefiles/services/block-handler.ts @@ -0,0 +1,49 @@ +// (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 { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { AddonPrivateFilesMainMenuHandlerService } from '@/addons/privatefiles/services/handlers/mainmenu'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockPrivateFilesHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockPrivateFiles'; + blockName = 'private_files'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_privatefiles.pluginname', + class: 'addon-block-private-files', + component: CoreBlockOnlyTitleComponent, + link: '/main/' + AddonPrivateFilesMainMenuHandlerService.PAGE_NAME, + linkParams: { root: 'my' }, + }; + } + +} + +export class AddonBlockPrivateFilesHandler extends makeSingleton(AddonBlockPrivateFilesHandlerService) {} diff --git a/src/addons/block/selfcompletion/lang.json b/src/addons/block/selfcompletion/lang.json new file mode 100644 index 000000000..32521695a --- /dev/null +++ b/src/addons/block/selfcompletion/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Self completion" +} \ No newline at end of file diff --git a/src/addons/block/selfcompletion/selfcompletion.module.ts b/src/addons/block/selfcompletion/selfcompletion.module.ts new file mode 100644 index 000000000..13d8e0712 --- /dev/null +++ b/src/addons/block/selfcompletion/selfcompletion.module.ts @@ -0,0 +1,36 @@ +// (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 { AddonBlockSelfCompletionHandler } from './services/block-handler'; + +@NgModule({ + imports: [ + IonicModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockSelfCompletionHandler.instance); + }, + }, + ], +}) +export class AddonBlockSelfCompletionModule {} diff --git a/src/addons/block/selfcompletion/services/block-handler.ts b/src/addons/block/selfcompletion/services/block-handler.ts new file mode 100644 index 000000000..ff046df7b --- /dev/null +++ b/src/addons/block/selfcompletion/services/block-handler.ts @@ -0,0 +1,53 @@ +// (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 { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockSelfCompletionHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockSelfCompletion'; + blockName = 'selfcompletion'; + + /** + * Returns the data needed to render the block. + * + * @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(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData { + // @todo + + return { + title: 'addon.block_selfcompletion.pluginname', + class: 'addon-block-self-completion', + component: CoreBlockOnlyTitleComponent, + link: 'AddonCourseCompletionReportPage', + linkParams: { courseId: instanceId }, + }; + } + +} + +export class AddonBlockSelfCompletionHandler extends makeSingleton(AddonBlockSelfCompletionHandlerService) {} From c12133eea3d00bf97ad1c14b90c3d573c3a41f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 10 Dec 2020 11:45:27 +0100 Subject: [PATCH 06/11] MOBILE-3608 blocks: Course overview block --- src/addons/addons.module.ts | 2 + .../components/components.module.ts | 47 ++ .../myoverview/addon-block-myoverview.html | 93 +++ .../components/myoverview/myoverview.ts | 586 +++++++++++++++ src/addons/block/myoverview/lang.json | 14 + .../block/myoverview/myoverview.module.ts | 38 + .../myoverview/services/block-handler.ts | 58 ++ .../coursecompletion.module.ts | 44 ++ src/addons/coursecompletion/lang.json | 23 + .../services/coursecompletion.ts | 313 ++++++++ src/assets/img/icons/courses.svg | 257 +++++++ src/core/components/empty-box/empty-box.scss | 6 + src/core/components/loading/loading.scss | 9 + .../block/components/block/block.scss | 31 +- .../features/course/services/course-helper.ts | 44 +- .../services/course-options-delegate.ts | 674 ++++++++++++++++++ .../pages/course-preview/course-preview.ts | 5 +- .../courses/pages/my-courses/my-courses.ts | 4 +- .../courses/services/courses-helper.ts | 95 ++- src/core/services/utils/text.ts | 8 +- src/theme/app.scss | 9 +- 21 files changed, 2304 insertions(+), 56 deletions(-) create mode 100644 src/addons/block/myoverview/components/components.module.ts create mode 100644 src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html create mode 100644 src/addons/block/myoverview/components/myoverview/myoverview.ts create mode 100644 src/addons/block/myoverview/lang.json create mode 100644 src/addons/block/myoverview/myoverview.module.ts create mode 100644 src/addons/block/myoverview/services/block-handler.ts create mode 100644 src/addons/coursecompletion/coursecompletion.module.ts create mode 100644 src/addons/coursecompletion/lang.json create mode 100644 src/addons/coursecompletion/services/coursecompletion.ts create mode 100644 src/assets/img/icons/courses.svg create mode 100644 src/core/features/course/services/course-options-delegate.ts diff --git a/src/addons/addons.module.ts b/src/addons/addons.module.ts index 680edb1dd..e03732e71 100644 --- a/src/addons/addons.module.ts +++ b/src/addons/addons.module.ts @@ -26,6 +26,7 @@ import { AddonBlockCompletionStatusModule } from './block/completionstatus/compl import { AddonBlockGlossaryRandomModule } from './block/glossaryrandom/glossaryrandom.module'; import { AddonBlockHtmlModule } from './block/html/html.module'; import { AddonBlockLearningPlansModule } from './block/learningplans/learningplans.module'; +import { AddonBlockMyOverviewModule } from './block/myoverview/myoverview.module'; import { AddonBlockNewsItemsModule } from './block/newsitems/newsitems.module'; import { AddonBlockOnlineUsersModule } from './block/onlineusers/onlineusers.module'; import { AddonBlockPrivateFilesModule } from './block/privatefiles/privatefiles.module'; @@ -51,6 +52,7 @@ import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield AddonBlockCompletionStatusModule, AddonBlockGlossaryRandomModule, AddonBlockHtmlModule, + AddonBlockMyOverviewModule, AddonBlockLearningPlansModule, AddonBlockNewsItemsModule, AddonBlockOnlineUsersModule, diff --git a/src/addons/block/myoverview/components/components.module.ts b/src/addons/block/myoverview/components/components.module.ts new file mode 100644 index 000000000..29199d738 --- /dev/null +++ b/src/addons/block/myoverview/components/components.module.ts @@ -0,0 +1,47 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreCoursesComponentsModule } from '@features/courses/components/components.module'; + +import { AddonBlockMyOverviewComponent } from './myoverview/myoverview'; + +@NgModule({ + declarations: [ + AddonBlockMyOverviewComponent, + ], + imports: [ + CommonModule, + IonicModule, + FormsModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCoursesComponentsModule, + ], + exports: [ + AddonBlockMyOverviewComponent, + ], + entryComponents: [ + AddonBlockMyOverviewComponent, + ], +}) +export class AddonBlockMyOverviewComponentsModule {} diff --git a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html new file mode 100644 index 000000000..1532ecda2 --- /dev/null +++ b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html @@ -0,0 +1,93 @@ + + +

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

+
+ +
+ + + + + + {{prefetchCoursesData[selectedFilter].badge}} + + +
+ + + + + + + + + +
+ +
+ + + + {{ 'addon.block_myoverview.allincludinghidden' | translate }} + + + {{ 'addon.block_myoverview.all' | translate }} + + + {{ 'addon.block_myoverview.inprogress' | translate }} + + + {{ 'addon.block_myoverview.future' | translate }} + + + {{ 'addon.block_myoverview.past' | translate }} + + + + {{ customOption.name }} + + + + {{ 'addon.block_myoverview.favourites' | translate }} + + + {{ 'addon.block_myoverview.hiddencourses' | translate }} + + +
+ + + + + + + + + +
+ + + + + + + + +
+
diff --git a/src/addons/block/myoverview/components/myoverview/myoverview.ts b/src/addons/block/myoverview/components/myoverview/myoverview.ts new file mode 100644 index 000000000..63a6956c8 --- /dev/null +++ b/src/addons/block/myoverview/components/myoverview/myoverview.ts @@ -0,0 +1,586 @@ +// (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, Input, OnDestroy, ViewChild, OnChanges, SimpleChange } from '@angular/core'; +import { IonSearchbar } from '@ionic/angular'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreTimeUtils } from '@services/utils/time'; +import { CoreSites } from '@services/sites'; +import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData, CoreCourses } from '@features/courses/services/courses'; +import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper'; +import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; +import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; +import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; +import { CoreSite } from '@classes/site'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreTextUtils } from '@services/utils/text'; +import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; + +const FILTER_PRIORITY = ['all', 'allincludinghidden', 'inprogress', 'future', 'past', 'favourite', 'hidden', 'custom']; + +/** + * Component to render a my overview block. + */ +@Component({ + selector: 'addon-block-myoverview', + templateUrl: 'addon-block-myoverview.html', +}) +export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy { + + @ViewChild('searchbar') searchbar?: IonSearchbar; + @Input() downloadEnabled = false; + + courses = { + filter: '', + all: [], + allincludinghidden: [], + past: [], + inprogress: [], + future: [], + favourite: [], + hidden: [], + custom: [], // Leave it empty to avoid download all those courses. + }; + + customFilter: { + name: string; + value: string; + }[] = []; + + selectedFilter = 'inprogress'; + sort = 'fullname'; + currentSite?: CoreSite; + filteredCourses: CoreEnrolledCourseDataWithOptions[] = []; + prefetchCoursesData = { + all: { + icon: '', + statusTranslatable: 'core.loading', + status: '', + loading: true, + }, + allincludinghidden: { + icon: '', + statusTranslatable: 'core.loading', + status: '', + loading: true, + }, + inprogress: { + icon: '', + statusTranslatable: 'core.loading', + status: '', + loading: true, + }, + past: { + icon: '', + statusTranslatable: 'core.loading', + status: '', + loading: true, + }, + future: { + icon: '', + statusTranslatable: 'core.loading', + status: '', + loading: true, + }, + favourite: { + icon: '', + statusTranslatable: 'core.loading', + status: '', + loading: true, + }, + hidden: { + icon: '', + statusTranslatable: 'core.loading', + status: '', + loading: true, + }, + custom: { + icon: '', + statusTranslatable: '', + status: '', + loading: false, + }, // Leave it empty to avoid download all those courses. + }; + + showFilters = { // Options are show, disabled, hidden. + all: 'show', + allincludinghidden: 'show', + past: 'show', + inprogress: 'show', + future: 'show', + favourite: 'show', + hidden: 'show', + custom: 'hidden', + }; + + showFilter = false; + showSelectorFilter = false; + showSortFilter = false; + downloadCourseEnabled = false; + downloadCoursesEnabled = false; + showSortByShortName = false; + + protected prefetchIconsInitialized = false; + protected isDestroyed = false; + protected coursesObserver?: CoreEventObserver; + protected updateSiteObserver?: CoreEventObserver; + protected courseIds: number[] = []; + protected fetchContentDefaultError = 'Error getting my overview data.'; + + constructor() { + super('AddonBlockMyOverviewComponent'); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + // Refresh the enabled flags if enabled. + this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); + this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); + + // Refresh the enabled flags if site is updated. + this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { + this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); + this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); + + }, CoreSites.instance.getCurrentSiteId()); + + this.coursesObserver = CoreEvents.on( + CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, + (data: CoreCoursesMyCoursesUpdatedEventData) => { + + if (data.action == CoreCoursesProvider.ACTION_ENROL || data.action == CoreCoursesProvider.ACTION_STATE_CHANGED) { + this.refreshCourseList(); + } + }, + CoreSites.instance.getCurrentSiteId(), + ); + + this.currentSite = CoreSites.instance.getCurrentSite(); + + const promises: Promise[] = []; + if (this.currentSite) { + promises.push(this.currentSite.getLocalSiteConfig('AddonBlockMyOverviewSort', this.sort).then((value) => { + this.sort = value; + + return; + })); + promises.push(this.currentSite.getLocalSiteConfig('AddonBlockMyOverviewFilter', this.selectedFilter).then((value) => { + this.selectedFilter = value; + + return; + })); + } + + Promise.all(promises).finally(() => { + super.ngOnInit(); + }); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) { + // Download all courses is enabled now, initialize it. + this.initPrefetchCoursesIcons(); + } + } + + /** + * Perform the invalidate content function. + * + * @return Resolved when done. + */ + protected async invalidateContent(): Promise { + const promises: Promise[] = []; + + // Invalidate course completion data. + promises.push(CoreCourses.instance.invalidateUserCourses().finally(() => + CoreUtils.instance.allPromises(this.courseIds.map((courseId) => + AddonCourseCompletion.instance.invalidateCourseCompletion(courseId))))); + + promises.push(CoreCourseOptionsDelegate.instance.clearAndInvalidateCoursesOptions()); + if (this.courseIds.length > 0) { + promises.push(CoreCourses.instance.invalidateCoursesByField('ids', this.courseIds.join(','))); + } + + await CoreUtils.instance.allPromises(promises).finally(() => { + this.prefetchIconsInitialized = false; + }); + } + + /** + * Fetch the courses for my overview. + * + * @return Promise resolved when done. + */ + protected async fetchContent(): Promise { + const config = this.block.configsRecord || {}; + + const showCategories = config && config.displaycategories && config.displaycategories.value == '1'; + + const courses = await CoreCoursesHelper.instance.getUserCoursesWithOptions(this.sort, undefined, undefined, showCategories); + + // Check to show sort by short name only if the text is visible. + if (courses.length > 0) { + const sampleCourse = courses[0]; + this.showSortByShortName = !!sampleCourse.displayname && !!sampleCourse.shortname && + sampleCourse.fullname != sampleCourse.displayname; + } + + // Rollback to sort by full name if user is sorting by short name then Moodle web change the config. + if (!this.showSortByShortName && this.sort === 'shortname') { + this.switchSort('fullname'); + } + + this.courseIds = courses.map((course) => course.id); + + this.showSortFilter = courses.length > 0 && typeof courses[0].lastaccess != 'undefined'; + + this.initCourseFilters(courses); + + this.courses.filter = ''; + this.showFilter = false; + + this.showFilters.all = this.getShowFilterValue( + !config || config.displaygroupingall.value == '1', + this.courses.all.length === 0, + ); + // Do not show allincludinghiddenif config it's not present (before 3.8). + this.showFilters.allincludinghidden = + this.getShowFilterValue( + config.displaygroupingallincludinghidden.value == '1', + this.courses.allincludinghidden.length === 0, + ); + + this.showFilters.inprogress = this.getShowFilterValue( + !config || config.displaygroupinginprogress.value == '1', + this.courses.inprogress.length === 0, + ); + this.showFilters.past = this.getShowFilterValue( + !config || config.displaygroupingpast.value == '1', + this.courses.past.length === 0, + ); + this.showFilters.future = this.getShowFilterValue( + !config || config.displaygroupingfuture.value == '1', + this.courses.future.length === 0, + ); + + this.showSelectorFilter = courses.length > 0 && (this.courses.past.length > 0 || this.courses.future.length > 0 || + typeof courses[0].enddate != 'undefined'); + + this.showFilters.hidden = this.getShowFilterValue( + this.showSelectorFilter && typeof courses[0].hidden != 'undefined' && + (!config || config.displaygroupinghidden.value == '1'), + this.courses.hidden.length === 0, + ); + + this.showFilters.favourite = this.getShowFilterValue( + this.showSelectorFilter && typeof courses[0].isfavourite != 'undefined' && + (!config || (config.displaygroupingstarred && config.displaygroupingstarred.value == '1') || + (config.displaygroupingfavourites && config.displaygroupingfavourites.value == '1')), + this.courses.favourite.length === 0, + ); + + this.showFilters.custom = this.getShowFilterValue( + this.showSelectorFilter && config?.displaygroupingcustomfield.value == '1' && + !!config?.customfieldsexport && !!config?.customfieldsexport.value, + false, + ); + if (this.showFilters.custom == 'show') { + this.customFilter = CoreTextUtils.instance.parseJSON(config.customfieldsexport.value, []); + } else { + this.customFilter = []; + } + + if (this.showSelectorFilter) { + // Check if any selector is shown and not disabled. + this.showSelectorFilter = Object.keys(this.showFilters).some((key) => this.showFilters[key] == 'show'); + + if (!this.showSelectorFilter) { + // All filters disabled, display all the courses. + this.showFilters.all = 'show'; + } + } + + if (!this.showSelectorFilter) { + // No selector, display all the courses. + this.selectedFilter = 'all'; + } + this.setCourseFilter(this.selectedFilter); + + this.initPrefetchCoursesIcons(); + } + + /** + * Helper function to help with filter values. + * + * @param showCondition If true, filter will be shown. + * @param disabledCondition If true, and showCondition is also met, it will be shown as disabled. + * @return show / disabled / hidden value. + */ + protected getShowFilterValue(showCondition: boolean, disabledCondition: boolean): string { + return showCondition ? (disabledCondition ? 'disabled' : 'show') : 'hidden'; + } + + /** + * The filter has changed. + * + * @param Received Event. + */ + filterChanged(event: Event): void { + const target = event?.target || null; + + const newValue = target?.value.trim().toLowerCase(); + if (!newValue || this.courses.allincludinghidden.length <= 0) { + this.filteredCourses = this.courses.allincludinghidden; + } else { + // Use displayname if avalaible, or fullname if not. + if (this.courses.allincludinghidden.length > 0 && + typeof this.courses.allincludinghidden[0].displayname != 'undefined') { + this.filteredCourses = this.courses.allincludinghidden.filter((course) => + course.displayname && course.displayname.toLowerCase().indexOf(newValue) > -1); + } else { + this.filteredCourses = this.courses.allincludinghidden.filter((course) => + course.fullname.toLowerCase().indexOf(newValue) > -1); + } + } + } + + /** + * Initialize the prefetch icon for selected courses. + */ + protected initPrefetchCoursesIcons(): void { + if (this.prefetchIconsInitialized || !this.downloadEnabled) { + // Already initialized. + return; + } + + this.prefetchIconsInitialized = true; + + Object.keys(this.prefetchCoursesData).forEach(async (filter) => { + this.prefetchCoursesData[filter] = + await CoreCourseHelper.instance.initPrefetchCoursesIcons(this.courses[filter], this.prefetchCoursesData[filter]); + }); + } + + /** + * Prefetch all the shown courses. + * + * @return Promise resolved when done. + */ + async prefetchCourses(): Promise { + const selected = this.selectedFilter; + const initialIcon = this.prefetchCoursesData[selected].icon; + + try { + await CoreCourseHelper.instance.prefetchCourses(this.courses[selected], this.prefetchCoursesData[selected]); + } catch (error) { + if (!this.isDestroyed) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); + this.prefetchCoursesData[selected].icon = initialIcon; + } + } + } + + /** + * Refresh the list of courses. + * + * @return Promise resolved when done. + */ + protected async refreshCourseList(): Promise { + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED); + + try { + await CoreCourses.instance.invalidateUserCourses(); + } catch (error) { + // Ignore errors. + } + + await this.loadContent(true); + } + + /** + * The selected courses filter have changed. + */ + selectedChanged(): void { + this.setCourseFilter(this.selectedFilter); + } + + /** + * Set selected courses filter. + * + * @param filter Filter name to set. + */ + protected async setCourseFilter(filter: string): Promise { + this.selectedFilter = filter; + + if (this.showFilters.custom == 'show' && filter.startsWith('custom-') && + typeof this.customFilter[filter.substr(7)] != 'undefined') { + + const filterName = this.block.configsRecord!.customfiltergrouping.value; + const filterValue = this.customFilter[filter.substr(7)].value; + + this.loaded = false; + try { + const courses = await CoreCourses.instance.getEnrolledCoursesByCustomField(filterName, filterValue); + + // Get the courses information from allincludinghidden to get the max info about the course. + const courseIds = courses.map((course) => course.id); + + this.filteredCourses = this.courses.allincludinghidden.filter((allCourse) => + courseIds.indexOf(allCourse.id) !== -1); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError); + } finally { + this.loaded = true; + } + + return; + } + + // Only save the filter if not a custom one. + this.currentSite?.setLocalSiteConfig('AddonBlockMyOverviewFilter', filter); + + if (this.showFilters[filter] == 'show') { + this.filteredCourses = this.courses[filter]; + } else { + const activeFilter = FILTER_PRIORITY.find((name) => this.showFilters[name] == 'show'); + + if (activeFilter) { + this.setCourseFilter(activeFilter); + } + } + } + + /** + * Init courses filters. + * + * @param courses Courses to filter. + */ + initCourseFilters(courses: CoreEnrolledCourseDataWithOptions[]): void { + this.courses.allincludinghidden = courses; + + if (this.showSortFilter) { + if (this.sort == 'lastaccess') { + courses.sort((a, b) => (b.lastaccess || 0) - (a.lastaccess || 0)); + } else if (this.sort == 'fullname') { + courses.sort((a, b) => { + const compareA = a.fullname.toLowerCase(); + const compareB = b.fullname.toLowerCase(); + + return compareA.localeCompare(compareB); + }); + } else if (this.sort == 'shortname') { + courses.sort((a, b) => { + const compareA = a.shortname.toLowerCase(); + const compareB = b.shortname.toLowerCase(); + + return compareA.localeCompare(compareB); + }); + } + } + + this.courses.all = []; + this.courses.past = []; + this.courses.inprogress = []; + this.courses.future = []; + this.courses.favourite = []; + this.courses.hidden = []; + + const today = CoreTimeUtils.instance.timestamp(); + courses.forEach((course) => { + if (course.hidden) { + this.courses.hidden.push(course); + } else { + this.courses.all.push(course); + + if ((course.enddate && course.enddate < today) || course.completed) { + // Courses that have already ended. + this.courses.past.push(course); + } else if (course.startdate && course.startdate > today) { + // Courses that have not started yet. + this.courses.future.push(course); + } else { + // Courses still in progress. + this.courses.inprogress.push(course); + } + + if (course.isfavourite) { + this.courses.favourite.push(course); + } + } + }); + + this.setCourseFilter(this.selectedFilter); + } + + /** + * The selected courses sort filter have changed. + * + * @param sort New sorting. + */ + switchSort(sort: string): void { + this.sort = sort; + this.currentSite?.setLocalSiteConfig('AddonBlockMyOverviewSort', this.sort); + this.initCourseFilters(this.courses.allincludinghidden); + } + + /** + * Show or hide the filter. + */ + switchFilter(): void { + this.showFilter = !this.showFilter; + this.courses.filter = ''; + + if (this.showFilter) { + this.filteredCourses = this.courses.allincludinghidden; + } else { + this.setCourseFilter(this.selectedFilter); + } + } + + /** + * Popover closed after clicking switch filter. + */ + switchFilterClosed(): void { + if (this.showFilter) { + setTimeout(() => { + this.searchbar?.setFocus(); + }); + } + } + + /** + * If switch button that enables the filter input is shown or not. + * + * @return If switch button that enables the filter input is shown or not. + */ + showFilterSwitchButton(): boolean { + return this.loaded && this.courses.allincludinghidden && this.courses.allincludinghidden.length > 5; + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + this.coursesObserver?.off(); + this.updateSiteObserver?.off(); + } + +} diff --git a/src/addons/block/myoverview/lang.json b/src/addons/block/myoverview/lang.json new file mode 100644 index 000000000..7bca82636 --- /dev/null +++ b/src/addons/block/myoverview/lang.json @@ -0,0 +1,14 @@ +{ + "all": "All (except removed from view)", + "allincludinghidden": "All", + "favourites": "Starred", + "future": "Future", + "hiddencourses": "Removed from view", + "inprogress": "In progress", + "lastaccessed": "Last accessed", + "nocourses": "No courses", + "past": "Past", + "pluginname": "Course overview", + "shortname": "Short name", + "title": "Course name" +} diff --git a/src/addons/block/myoverview/myoverview.module.ts b/src/addons/block/myoverview/myoverview.module.ts new file mode 100644 index 000000000..483e1e36e --- /dev/null +++ b/src/addons/block/myoverview/myoverview.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockMyOverviewComponentsModule } from './components/components.module'; +import { AddonBlockMyOverviewHandler } from './services/block-handler'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockMyOverviewComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockMyOverviewHandler.instance); + }, + }, + ], +}) +export class AddonBlockMyOverviewModule {} diff --git a/src/addons/block/myoverview/services/block-handler.ts b/src/addons/block/myoverview/services/block-handler.ts new file mode 100644 index 000000000..e95e322f9 --- /dev/null +++ b/src/addons/block/myoverview/services/block-handler.ts @@ -0,0 +1,58 @@ +// (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 { AddonBlockMyOverviewComponent } from '../components/myoverview/myoverview'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockMyOverviewHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockMyOverview'; + blockName = 'myoverview'; + + /** + * 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 { + return (CoreSites.instance.getCurrentSite()?.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_myoverview.pluginname', + class: 'addon-block-myoverview', + component: AddonBlockMyOverviewComponent, + }; + } + +} + +export class AddonBlockMyOverviewHandler extends makeSingleton(AddonBlockMyOverviewHandlerService) {} diff --git a/src/addons/coursecompletion/coursecompletion.module.ts b/src/addons/coursecompletion/coursecompletion.module.ts new file mode 100644 index 000000000..ccc8af630 --- /dev/null +++ b/src/addons/coursecompletion/coursecompletion.module.ts @@ -0,0 +1,44 @@ +// (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'; +// @todo import { AddonCourseCompletionCourseOptionHandler } from './services/course-option-handler'; +// @todo import { AddonCourseCompletionUserHandler } from './services/user-handler'; +// @todo import { AddonCourseCompletionComponentsModule } from './components/components.module'; +// @todo import { CoreCourseOptionsDelegate } from '@features/course/services/options-delegate'; +// @todo import { CoreUserDelegate } from '@features/user/services/user-delegate'; + +@NgModule({ + imports: [ + // AddonCourseCompletionComponentsModule, + ], + providers: [ + // AddonCourseCompletionCourseOptionHandler, + // AddonCourseCompletionUserHandler, + ], +}) +export class AddonCourseCompletionModule { + + /* @todo constructor( + courseOptionsDelegate: CoreCourseOptionsDelegate, + courseOptionHandler: AddonCourseCompletionCourseOptionHandler, + userDelegate: CoreUserDelegate, + userHandler: AddonCourseCompletionUserHandler, + ) { + // Register handlers. + courseOptionsDelegate.registerHandler(courseOptionHandler); + userDelegate.registerHandler(userHandler); + }*/ + +} diff --git a/src/addons/coursecompletion/lang.json b/src/addons/coursecompletion/lang.json new file mode 100644 index 000000000..81ef0272e --- /dev/null +++ b/src/addons/coursecompletion/lang.json @@ -0,0 +1,23 @@ +{ + "complete": "Complete", + "completecourse": "Complete course", + "completed": "Completed", + "completiondate": "Completion date", + "completionmenuitem": "Completion", + "couldnotloadreport": "Could not load the course completion report. Please try again later.", + "coursecompletion": "Course completion", + "criteria": "Criteria", + "criteriagroup": "Criteria group", + "criteriarequiredall": "All criteria below are required", + "criteriarequiredany": "Any criteria below are required", + "inprogress": "In progress", + "manualselfcompletion": "Manual self completion", + "nottracked": "You are currently not being tracked by completion in this course", + "notyetstarted": "Not yet started", + "pending": "Pending", + "required": "Required", + "requiredcriteria": "Required criteria", + "requirement": "Requirement", + "status": "Status", + "viewcoursereport": "View course report" +} \ No newline at end of file diff --git a/src/addons/coursecompletion/services/coursecompletion.ts b/src/addons/coursecompletion/services/coursecompletion.ts new file mode 100644 index 000000000..6dc4e6811 --- /dev/null +++ b/src/addons/coursecompletion/services/coursecompletion.ts @@ -0,0 +1,313 @@ +// (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 { CoreLogger } from '@singletons/logger'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreCourses } from '@features/courses/services/courses'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton } from '@singletons'; + +const ROOT_CACHE_KEY = 'mmaCourseCompletion:'; + +/** + * Service to handle course completion. + */ +@Injectable({ providedIn: 'root' }) +export class AddonCourseCompletionProvider { + + protected logger: CoreLogger; + + constructor() { + this.logger = CoreLogger.getInstance('AddonCourseCompletion'); + } + + /** + * Returns whether or not the user can mark a course as self completed. + * It can if it's configured in the course and it hasn't been completed yet. + * + * @param userId User ID. + * @param completion Course completion. + * @return True if user can mark course as self completed, false otherwise. + */ + canMarkSelfCompleted(userId: number, completion: AddonCourseCompletionCourseCompletionStatus): boolean { + if (CoreSites.instance.getCurrentSiteUserId() != userId) { + return false; + } + + let selfCompletionActive = false; + let alreadyMarked = false; + + completion.completions.forEach((criteria) => { + if (criteria.type === 1) { + // Self completion criteria found. + selfCompletionActive = true; + alreadyMarked = criteria.complete; + } + }); + + return selfCompletionActive && !alreadyMarked; + } + + /** + * Get completed status text. The language code returned is meant to be translated. + * + * @param completion Course completion. + * @return Language code of the text to show. + */ + getCompletedStatusText(completion: AddonCourseCompletionCourseCompletionStatus): string { + if (completion.completed) { + return 'addon.coursecompletion.completed'; + } + + // Let's calculate status. + const hasStarted = completion.completions.some((criteria) => criteria.timecompleted || criteria.complete); + + if (hasStarted) { + return 'addon.coursecompletion.inprogress'; + } + + return 'addon.coursecompletion.notyetstarted'; + } + + /** + * Get course completion status for a certain course and user. + * + * @param courseId Course ID. + * @param userId User ID. If not defined, use current user. + * @param preSets Presets to use when calling the WebService. + * @param siteId Site ID. If not defined, use current site. + * @return Promise to be resolved when the completion is retrieved. + */ + async getCompletion( + courseId: number, + userId?: number, + preSets: CoreSiteWSPreSets = {}, + siteId?: string, + ): Promise { + + const site = await CoreSites.instance.getSite(siteId); + userId = userId || site.getUserId(); + this.logger.debug('Get completion for course ' + courseId + ' and user ' + userId); + + const data: CoreCompletionGetCourseCompletionStatusWSParams = { + courseid: courseId, + userid: userId, + }; + + preSets.cacheKey = this.getCompletionCacheKey(courseId, userId); + preSets.updateFrequency = preSets.updateFrequency || CoreSite.FREQUENCY_SOMETIMES; + preSets.cacheErrors = ['notenroled']; + + const result: CoreCompletionGetCourseCompletionStatusWSResponse = + await site.read('core_completion_get_course_completion_status', data, preSets); + if (result.completionstatus) { + return result.completionstatus; + } + + throw null; + } + + /** + * Get cache key for get completion WS calls. + * + * @param courseId Course ID. + * @param useIid User ID. + * @return Cache key. + */ + protected getCompletionCacheKey(courseId: number, userId: number): string { + return ROOT_CACHE_KEY + 'view:' + courseId + ':' + userId; + } + + /** + * Invalidates view course completion WS call. + * + * @param courseId Course ID. + * @param userId User ID. If not defined, use current user. + * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved when the list is invalidated. + */ + async invalidateCourseCompletion(courseId: number, userId?: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + userId = userId || site.getUserId(); + + await site.invalidateWsCacheForKey(this.getCompletionCacheKey(courseId, userId)); + } + + /** + * Returns whether or not the view course completion plugin is enabled for the current site. + * + * @return True if plugin enabled, false otherwise. + */ + isPluginViewEnabled(): boolean { + return CoreSites.instance.isLoggedIn(); + } + + /** + * Returns whether or not the view course completion plugin is enabled for a certain course. + * + * @param courseId Course ID. + * @param preferCache True if shouldn't call WS if data is cached, false otherwise. + * @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. + */ + async isPluginViewEnabledForCourse(courseId: number, preferCache: boolean = true): Promise { + if (!courseId) { + throw null; + } + + const course = await CoreCourses.instance.getUserCourse(courseId, preferCache); + + if (course) { + if (typeof course.enablecompletion != 'undefined' && !course.enablecompletion) { + // Completion not enabled for the course. + return false; + } + + if (typeof course.completionhascriteria != 'undefined' && !course.completionhascriteria) { + // No criteria, cannot view completion. + return false; + } + } + + return true; + } + + /** + * Returns whether or not the view course completion plugin is enabled for a certain user. + * + * @param courseId Course ID. + * @param userId User ID. If not defined, use current user. + * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. + */ + async isPluginViewEnabledForUser(courseId: number, userId?: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + const currentUserId = site.getUserId(); + + // Check if user wants to view his own completion. + try { + if (!userId || userId == currentUserId) { + // Viewing own completion. Get the course to check if it has completion criteria. + const course = await CoreCourses.instance.getUserCourse(courseId, true); + + // If the site is returning the completionhascriteria then the user can view his own completion. + // We already checked the value in isPluginViewEnabledForCourse. + if (course && typeof course.completionhascriteria != 'undefined') { + return true; + } + } + } catch { + // Ignore errors. + } + + // User not viewing own completion or the site doesn't tell us if the course has criteria. + // The only way to know if completion can be viewed is to call the WS. + // Disable emergency cache to be able to detect that the plugin has been disabled (WS will fail). + const preSets: CoreSiteWSPreSets = { + emergencyCache: false, + }; + + try { + await this.getCompletion(courseId, userId, preSets); + + return true; + } catch (error) { + if (CoreUtils.instance.isWebServiceError(error)) { + // The WS returned an error, plugin is not enabled. + return false; + } + } + + try { + // Not a WS error. Check if we have a cached value. + preSets.omitExpires = true; + + await this.getCompletion(courseId, userId, preSets); + + return true; + } catch { + return false; + } + } + + /** + * Mark a course as self completed. + * + * @param courseId Course ID. + * @param siteId Site ID. If not defined, use current site. + * @return Promise resolved on success. + */ + async markCourseAsSelfCompleted(courseId: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + const params: CoreCompletionMarkCourseSelfCompletedWSParams = { + courseid: courseId, + }; + + const response = await site.write('core_completion_mark_course_self_completed', params); + + if (!response.status) { + throw null; + } + } + +} + +export class AddonCourseCompletion extends makeSingleton(AddonCourseCompletionProvider) {} + +/** + * Completion status returned by core_completion_get_course_completion_status. + */ +export type AddonCourseCompletionCourseCompletionStatus = { + completed: boolean; // True if the course is complete, false otherwise. + aggregation: number; // Aggregation method 1 means all, 2 means any. + completions: { + type: number; // Completion criteria type. + title: string; // Completion criteria Title. + status: string; // Completion status (Yes/No) a % or number. + complete: boolean; // Completion status (true/false). + timecompleted: number; // Timestamp for criteria completetion. + details: { + type: string; // Type description. + criteria: string; // Criteria description. + requirement: string; // Requirement description. + status: string; // Status description, can be anything. + }; // Details. + }[]; +}; + +/** + * Params of core_completion_get_course_completion_status WS. + */ +export type CoreCompletionGetCourseCompletionStatusWSParams = { + courseid: number; // Course ID. + userid: number; // User ID. +}; + +/** + * Data returned by core_completion_get_course_completion_status WS. + */ +export type CoreCompletionGetCourseCompletionStatusWSResponse = { + completionstatus: AddonCourseCompletionCourseCompletionStatus; // Course status. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of core_completion_mark_course_self_completed WS. + */ +export type CoreCompletionMarkCourseSelfCompletedWSParams = { + courseid: number; // Course ID. +}; diff --git a/src/assets/img/icons/courses.svg b/src/assets/img/icons/courses.svg new file mode 100644 index 000000000..7bd9cb672 --- /dev/null +++ b/src/assets/img/icons/courses.svg @@ -0,0 +1,257 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/core/components/empty-box/empty-box.scss b/src/core/components/empty-box/empty-box.scss index 9fea08738..b81dc6ae7 100644 --- a/src/core/components/empty-box/empty-box.scss +++ b/src/core/components/empty-box/empty-box.scss @@ -62,6 +62,12 @@ } } } + + &.core-empty-inline .core-empty-box { + position: relative; + z-index: initial; + height: auto; + } } :host-context(core-block-course-blocks) .core-empty-box { diff --git a/src/core/components/loading/loading.scss b/src/core/components/loading/loading.scss index 2a054b7a3..53746da0e 100644 --- a/src/core/components/loading/loading.scss +++ b/src/core/components/loading/loading.scss @@ -40,4 +40,13 @@ &.core-loading-loaded { position: unset; } + + &.core-loading-center { + display: block; + + .core-loading-container { + margin-top: 10px; + position: relative; + } + } } diff --git a/src/core/features/block/components/block/block.scss b/src/core/features/block/components/block/block.scss index 3acf40fdd..43be2ef93 100644 --- a/src/core/features/block/components/block/block.scss +++ b/src/core/features/block/components/block/block.scss @@ -1,30 +1,11 @@ :host { - // @todo - position: relative; - display: block; - - core-loading.core-loading-center { + position: relative; display: block; - .core-loading-container { - margin-top: 10px; - position: relative; + ion-item-divider { + min-height: 60px; + .core-button-spinner { + margin: 0; + } } - } - - core-empty-box .core-empty-box { - position: relative; - z-index: initial; - //@include position(initial, initial, null, initial); - height: auto; - } - - ion-item-divider { - //@include padding-horizontal(null, 0px); - min-height: 60px; - } - - ion-item-divider .core-button-spinner { - margin: 0; - } } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 81f85e2cb..6b31a738f 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -35,6 +35,11 @@ import { CoreArray } from '@singletons/array'; import { CoreIonLoadingElement } from '@classes/ion-loading'; import { CoreCourseOffline } from './course-offline'; import { CoreNavHelper, CoreNavHelperService } from '@services/nav-helper'; +import { + CoreCourseOptionsDelegate, + CoreCourseOptionsHandlerToDisplay, + CoreCourseOptionsMenuHandlerToDisplay, +} from './course-options-delegate'; /** * Prefetch info of a module. @@ -197,8 +202,8 @@ export class CoreCourseHelperProvider { const promises = courses.map((course) => { const subPromises: Promise[] = []; let sections: CoreCourseSection[]; - let handlers: any; - let menuHandlers: any; + let handlers: CoreCourseOptionsHandlerToDisplay[] = []; + let menuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = []; let success = true; // Get the sections and the handlers. @@ -208,15 +213,16 @@ export class CoreCourseHelperProvider { return; })); - /** - * @todo - subPromises.push(this.courseOptionsDelegate.getHandlersToDisplay(this.injector, course).then((cHandlers: any) => { + subPromises.push(CoreCourseOptionsDelegate.instance.getHandlersToDisplay(course).then((cHandlers) => { handlers = cHandlers; + + return; })); - subPromises.push(this.courseOptionsDelegate.getMenuHandlersToDisplay(this.injector, course).then((mHandlers: any) => { + subPromises.push(CoreCourseOptionsDelegate.instance.getMenuHandlersToDisplay(course).then((mHandlers) => { menuHandlers = mHandlers; + + return; })); - */ return Promise.all(subPromises).then(() => this.prefetchCourse(course, sections, handlers, menuHandlers, siteId)) .catch((error) => { @@ -777,8 +783,8 @@ export class CoreCourseHelperProvider { async prefetchCourse( course: CoreEnrolledCourseDataWithExtraInfoAndOptions, sections: CoreCourseSection[], - courseHandlers: any[], // @todo CoreCourseOptionsHandlerToDisplay[], - courseMenuHandlers: any[], // @todo CoreCourseOptionsMenuHandlerToDisplay[], + courseHandlers: CoreCourseOptionsHandlerToDisplay[], + courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[], siteId?: string, ): Promise { siteId = siteId || CoreSites.instance.getCurrentSiteId(); @@ -797,16 +803,15 @@ export class CoreCourseHelperProvider { siteId, ).then(async () => { - const promises: Promise[] = []; + const promises: Promise[] = []; + /* @todo // Prefetch all the sections. If the first section is "All sections", use it. Otherwise, use a fake "All sections". - /* - * @todo - let allSectionsSection = sections[0]; - if (sections[0].id != CoreCourseProvider.ALL_SECTIONS_ID) { + let allSectionsSection: Partial = sections[0]; + if (sections[0].id != CoreCourseProvider.ALL_SECTIONS_ID) { allSectionsSection = { id: CoreCourseProvider.ALL_SECTIONS_ID }; } - promises.push(this.prefetchSection(allSectionsSection, course.id, sections)); + promises.push(this.prefetchSection(allSectionsSection, course.id, sections));*/ // Prefetch course options. courseHandlers.forEach((handler) => { @@ -818,7 +823,7 @@ export class CoreCourseHelperProvider { if (handler.prefetch) { promises.push(handler.prefetch(course)); } - });*/ + }); // Prefetch other data needed to render the course. if (CoreCourses.instance.isGetCoursesByFieldAvailable()) { @@ -832,10 +837,11 @@ export class CoreCourseHelperProvider { // @todo promises.push(this.filterHelper.getFilters('course', course.id)); - return CoreUtils.instance.allPromises(promises); - }).then(() => + await CoreUtils.instance.allPromises(promises); + // Download success, mark the course as downloaded. - CoreCourse.instance.setCourseStatus(course.id, CoreConstants.DOWNLOADED, siteId)).catch(async (error) => { + return CoreCourse.instance.setCourseStatus(course.id, CoreConstants.DOWNLOADED, siteId); + }).catch(async (error) => { // Error, restore previous status. await CoreCourse.instance.setCoursePreviousStatus(course.id, siteId); diff --git a/src/core/features/course/services/course-options-delegate.ts b/src/core/features/course/services/course-options-delegate.ts new file mode 100644 index 000000000..9c28d0071 --- /dev/null +++ b/src/core/features/course/services/course-options-delegate.ts @@ -0,0 +1,674 @@ +// (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. +// @todo test delegate + +import { Injectable, Type } from '@angular/core'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CoreEvents } from '@singletons/events'; +import { CoreSites } from '@services/sites'; +import { CoreUtils, PromiseDefer } from '@services/utils/utils'; +import { CoreCourses, CoreCoursesProvider, CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; +import { CoreCourseProvider } from './course'; +import { Params } from '@angular/router'; +import { makeSingleton } from '@singletons'; +import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; + +/** + * Interface that all course options handlers must implement. + */ +export interface CoreCourseOptionsHandler extends CoreDelegateHandler { + /** + * The highest priority is displayed first. + */ + priority: number; + + /** + * True if this handler should appear in menu rather than as a tab. + */ + isMenuHandler?: boolean; + + /** + * Whether or not the handler is enabled for a certain course. + * + * @param courseId The course ID. + * @param accessData Access type and data. Default, guest, ... + * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return True or promise resolved with true if enabled. + */ + isEnabledForCourse(courseId: number, + accessData: any, // @todo: define type. + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + admOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): boolean | Promise; + + /** + * Returns the data needed to render the handler. + * + * @param course The course. // @todo: define type in the whole file. + * @return Data or promise resolved with the data. + */ + getDisplayData?( + course: CoreEnrolledCourseDataWithExtraInfoAndOptions, + ): CoreCourseOptionsHandlerData | Promise; + + /** + * Should invalidate the data to determine if the handler is enabled for a certain course. + * + * @param courseId The course ID. + * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return Promise resolved when done. + */ + invalidateEnabledForCourse?( + courseId: number, + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + admOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): Promise; + + /** + * Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline. + * + * @param course The course. + * @return Promise resolved when done. + */ + prefetch?(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise; +} + +/** + * Interface that course options handlers implement if they appear in the menu rather than as a tab. + */ +export interface CoreCourseOptionsMenuHandler extends CoreCourseOptionsHandler { + /** + * Returns the data needed to render the handler. + * + * @param course The course. + * @return Data or promise resolved with data. + */ + getMenuDisplayData( + course: CoreEnrolledCourseDataWithExtraInfoAndOptions, + ): CoreCourseOptionsMenuHandlerData | Promise; +} + +/** + * Data needed to render a course handler. It's returned by the handler. + */ +export interface CoreCourseOptionsHandlerData { + /** + * Title to display for the handler. + */ + title: string; + + /** + * Class to add to the displayed handler. + */ + class?: string; + + /** + * The component to render the handler. It must be the component class, not the name or an instance. + * When the component is created, it will receive the courseId as input. + */ + component: Type; + + /** + * Data to pass to the component. All the properties in this object will be passed to the component as inputs. + */ + componentData?: Record; +} + +/** + * Data needed to render a course menu handler. It's returned by the handler. + */ +export interface CoreCourseOptionsMenuHandlerData { + /** + * Title to display for the handler. + */ + title: string; + + /** + * Class to add to the displayed handler. + */ + class?: string; + + /** + * Name of the page to load for the handler. + */ + page: string; + + /** + * Params to pass to the page (other than 'course' which is always sent). + */ + pageParams?: Params; + + /** + * Name of the icon to display for the handler. + */ + icon: string; // Name of the icon to display in the tab. +} + +/** + * Data returned by the delegate for each handler. + */ +export interface CoreCourseOptionsHandlerToDisplay { + /** + * Data to display. + */ + data: CoreCourseOptionsHandlerData; + + /** + * Name of the handler, or name and sub context (AddonMessages, AddonMessages:blockContact, ...). + */ + name: string; + + /** + * The highest priority is displayed first. + */ + priority?: number; + + /** + * Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline. + * + * @param course The course. + * @return Promise resolved when done. + */ + prefetch?(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise; +} + +/** + * Additional data returned if it is a menu item. + */ +export interface CoreCourseOptionsMenuHandlerToDisplay { + /** + * Data to display. + */ + data: CoreCourseOptionsMenuHandlerData; + + /** + * Name of the handler, or name and sub context (AddonMessages, AddonMessages:blockContact, ...). + */ + name: string; + + /** + * The highest priority is displayed first. + */ + priority?: number; + + /** + * Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline. + * + * @param course The course. + * @return Promise resolved when done. + */ + prefetch?(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise; +} + +/** + * Service to interact with plugins to be shown in each course (participants, learning plans, ...). + */ +@Injectable( { providedIn: 'root' }) +export class CoreCourseOptionsDelegateService extends CoreDelegate { + + protected loaded: { [courseId: number]: boolean } = {}; + protected lastUpdateHandlersForCoursesStart: { + [courseId: number]: number; + } = {}; + + protected coursesHandlers: { + [courseId: number]: { + access: any; + navOptions?: CoreCourseUserAdminOrNavOptionIndexed; + admOptions?: CoreCourseUserAdminOrNavOptionIndexed; + deferred: PromiseDefer; + enabledHandlers: CoreCourseOptionsHandler[]; + enabledMenuHandlers: CoreCourseOptionsMenuHandler[]; + }; + } = {}; + + protected featurePrefix = 'CoreCourseOptionsDelegate_'; + + constructor() { + super('CoreCourseOptionsDelegate'); + + CoreEvents.on(CoreEvents.LOGOUT, () => { + this.clearCoursesHandlers(); + }); + } + + /** + * Check if handlers are loaded for a certain course. + * + * @param courseId The course ID to check. + * @return True if handlers are loaded, false otherwise. + */ + areHandlersLoaded(courseId: number): boolean { + return !!this.loaded[courseId]; + } + + /** + * Clear all course options handlers. + * + * @param courseId The course ID. If not defined, all handlers will be cleared. + */ + protected clearCoursesHandlers(courseId?: number): void { + if (courseId) { + if (!this.loaded[courseId]) { + // Don't clear if not loaded, it's probably an ongoing load and it could cause JS errors. + return; + } + + this.loaded[courseId] = false; + delete this.coursesHandlers[courseId]; + } else { + for (const courseId in this.coursesHandlers) { + this.clearCoursesHandlers(Number(courseId)); + } + } + } + + /** + * Clear all courses handlers and invalidate its options. + * + * @param courseId The course ID. If not defined, all handlers will be cleared. + * @return Promise resolved when done. + */ + async clearAndInvalidateCoursesOptions(courseId?: number): Promise { + const promises: Promise[] = []; + + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED); + + // Invalidate course enabled data for the handlers that are enabled at site level. + if (courseId) { + // Invalidate only options for this course. + promises.push(CoreCourses.instance.invalidateCoursesAdminAndNavOptions([courseId])); + promises.push(this.invalidateCourseHandlers(courseId)); + } else { + // Invalidate all options. + promises.push(CoreCourses.instance.invalidateUserNavigationOptions()); + promises.push(CoreCourses.instance.invalidateUserAdministrationOptions()); + + for (const cId in this.coursesHandlers) { + promises.push(this.invalidateCourseHandlers(parseInt(cId, 10))); + } + } + + this.clearCoursesHandlers(courseId); + + await Promise.all(promises); + } + + /** + * Get the handlers for a course using a certain access type. + * + * @param courseId The course ID. + * @param refresh True if it should refresh the list. + * @param accessData Access type and data. Default, guest, ... + * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return Promise resolved with array of handlers. + */ + protected async getHandlersForAccess( + courseId: number, + refresh: boolean, + accessData: any, + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + admOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): Promise { + + // If the handlers aren't loaded, do not refresh. + if (!this.loaded[courseId]) { + refresh = false; + } + + if (refresh || !this.coursesHandlers[courseId] || this.coursesHandlers[courseId].access.type != accessData.type) { + if (!this.coursesHandlers[courseId]) { + this.coursesHandlers[courseId] = { + access: accessData, + navOptions, + admOptions, + deferred: CoreUtils.instance.promiseDefer(), + enabledHandlers: [], + enabledMenuHandlers: [], + }; + } else { + this.coursesHandlers[courseId].access = accessData; + this.coursesHandlers[courseId].navOptions = navOptions; + this.coursesHandlers[courseId].admOptions = admOptions; + this.coursesHandlers[courseId].deferred = CoreUtils.instance.promiseDefer(); + } + + this.updateHandlersForCourse(courseId, accessData, navOptions, admOptions); + } + + await this.coursesHandlers[courseId].deferred.promise; + + return this.coursesHandlers[courseId].enabledHandlers; + } + + /** + * Get the list of handlers that should be displayed for a course. + * This function should be called only when the handlers need to be displayed, since it can call several WebServices. + * + * @param course The course object. + * @param refresh True if it should refresh the list. + * @param isGuest Whether it's guest. + * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return Promise resolved with array of handlers. + */ + getHandlersToDisplay( + course: CoreEnrolledCourseDataWithExtraInfoAndOptions, + refresh = false, + isGuest = false, + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + admOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): Promise { + return this.getHandlersToDisplayInternal(false, course, refresh, isGuest, navOptions, admOptions) as + Promise; + } + + /** + * Get the list of menu handlers that should be displayed for a course. + * This function should be called only when the handlers need to be displayed, since it can call several WebServices. + * + * @param course The course object. + * @param refresh True if it should refresh the list. + * @param isGuest Whether it's guest. + * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return Promise resolved with array of handlers. + */ + getMenuHandlersToDisplay( + course: CoreEnrolledCourseDataWithExtraInfoAndOptions, + refresh = false, + isGuest = false, + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + admOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): Promise { + return this.getHandlersToDisplayInternal(true, course, refresh, isGuest, navOptions, admOptions) as + Promise; + } + + /** + * Get the list of menu handlers that should be displayed for a course. + * This function should be called only when the handlers need to be displayed, since it can call several WebServices. + * + * @param menu If true, gets menu handlers; false, gets tab handlers + * @param course The course object. + * @param refresh True if it should refresh the list. + * @param isGuest Whether it's guest. + * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return Promise resolved with array of handlers. + */ + protected async getHandlersToDisplayInternal( + menu: boolean, + course: CoreEnrolledCourseDataWithExtraInfoAndOptions, + refresh = false, + isGuest = false, + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + admOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): Promise { + + const accessData = { + type: isGuest ? CoreCourseProvider.ACCESS_GUEST : CoreCourseProvider.ACCESS_DEFAULT, + }; + const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[] = []; + + if (navOptions) { + course.navOptions = navOptions; + } + if (admOptions) { + course.admOptions = admOptions; + } + + await this.loadCourseOptions(course, refresh); + // Call getHandlersForAccess to make sure the handlers have been loaded. + await this.getHandlersForAccess(course.id, refresh, accessData, course.navOptions, course.admOptions); + const promises: Promise[] = []; + + let handlerList: CoreCourseOptionsMenuHandler[] | CoreCourseOptionsHandler[]; + if (menu) { + handlerList = this.coursesHandlers[course.id].enabledMenuHandlers; + } else { + handlerList = this.coursesHandlers[course.id].enabledHandlers; + } + + handlerList.forEach((handler: CoreCourseOptionsMenuHandler | CoreCourseOptionsHandler) => { + const getFunction = menu + ? (handler as CoreCourseOptionsMenuHandler).getMenuDisplayData + : (handler as CoreCourseOptionsHandler).getDisplayData; + + promises.push(Promise.resolve(getFunction!.call(handler, course)).then((data) => { + handlersToDisplay.push({ + data: data, + priority: handler.priority, + prefetch: handler.prefetch && handler.prefetch.bind(handler), + name: handler.name, + }); + + return; + }).catch((err) => { + this.logger.error('Error getting data for handler', handler.name, err); + })); + }); + + await Promise.all(promises); + + // Sort them by priority. + handlersToDisplay.sort(( + a: CoreCourseOptionsHandlerToDisplay | CoreCourseOptionsMenuHandlerToDisplay, + b: CoreCourseOptionsHandlerToDisplay | CoreCourseOptionsMenuHandlerToDisplay, + ) => (b.priority || 0) - (a.priority || 0)); + + return handlersToDisplay; + } + + /** + * Check if a course has any handler enabled for default access, using course object. + * + * @param course The course object. + * @param refresh True if it should refresh the list. + * @return Promise resolved with boolean: true if it has handlers, false otherwise. + */ + async hasHandlersForCourse(course: CoreEnrolledCourseDataWithExtraInfoAndOptions, refresh = false): Promise { + // Load course options if missing. + await this.loadCourseOptions(course, refresh); + + return await this.hasHandlersForDefault(course.id, refresh, course.navOptions, course.admOptions); + } + + /** + * Check if a course has any handler enabled for default access. + * + * @param courseId The course ID. + * @param refresh True if it should refresh the list. + * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return Promise resolved with boolean: true if it has handlers, false otherwise. + */ + async hasHandlersForDefault( + courseId: number, + refresh = false, + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + admOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): Promise { + // Default access. + const accessData = { + type: CoreCourseProvider.ACCESS_DEFAULT, + }; + + const handlers = await this.getHandlersForAccess(courseId, refresh, accessData, navOptions, admOptions); + + return !!(handlers && handlers.length); + } + + /** + * Check if a course has any handler enabled for guest access. + * + * @param courseId The course ID. + * @param refresh True if it should refresh the list. + * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return Promise resolved with boolean: true if it has handlers, false otherwise. + */ + async hasHandlersForGuest( + courseId: number, + refresh = false, + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + admOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): Promise { + // Guest access. + const accessData = { + type: CoreCourseProvider.ACCESS_GUEST, + }; + + const handlers = await this.getHandlersForAccess(courseId, refresh, accessData, navOptions, admOptions); + + return !!(handlers && handlers.length); + } + + /** + * Invalidate the data to be able to determine if handlers are enabled for a certain course. + * + * @param courseId Course ID. + * @return Promise resolved when done. + */ + async invalidateCourseHandlers(courseId: number): Promise { + const promises: Promise[] = []; + const courseData = this.coursesHandlers[courseId]; + + if (!courseData || !courseData.enabledHandlers) { + return; + } + + courseData.enabledHandlers.forEach((handler) => { + if (handler?.invalidateEnabledForCourse) { + promises.push( + handler.invalidateEnabledForCourse(courseId, courseData.navOptions, courseData.admOptions), + ); + } + }); + + await CoreUtils.instance.allPromises(promises); + } + + /** + * Check if a time belongs to the last update handlers for course call. + * This is to handle the cases where updateHandlersForCourse don't finish in the same order as they're called. + * + * @param courseId Course ID. + * @param time Time to check. + * @return Whether it's the last call. + */ + isLastUpdateCourseCall(courseId: number, time: number): boolean { + if (!this.lastUpdateHandlersForCoursesStart[courseId]) { + return true; + } + + return time == this.lastUpdateHandlersForCoursesStart[courseId]; + } + + /** + * Load course options if missing. + * + * @param course The course object. + * @param refresh True if it should refresh the list. + * @return Promise resolved when done. + */ + protected async loadCourseOptions(course: CoreEnrolledCourseDataWithExtraInfoAndOptions, refresh = false): Promise { + if (CoreCourses.instance.canGetAdminAndNavOptions() && + (typeof course.navOptions == 'undefined' || typeof course.admOptions == 'undefined' || refresh)) { + + const options = await CoreCourses.instance.getCoursesAdminAndNavOptions([course.id]); + course.navOptions = options.navOptions[course.id]; + course.admOptions = options.admOptions[course.id]; + } + } + + /** + * Update handlers for each course. + */ + updateData(): void { + // Update handlers for all courses. + for (const courseId in this.coursesHandlers) { + const handler = this.coursesHandlers[courseId]; + this.updateHandlersForCourse(parseInt(courseId, 10), handler.access, handler.navOptions, handler.admOptions); + } + } + + /** + * Update the handlers for a certain course. + * + * @param courseId The course ID. + * @param accessData Access type and data. Default, guest, ... + * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. + * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. + * @return Resolved when updated. + */ + async updateHandlersForCourse( + courseId: number, + accessData: any, + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + admOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): Promise { + const promises: Promise[] = []; + const enabledForCourse: CoreCourseOptionsHandler[] = []; + const enabledForCourseMenu: CoreCourseOptionsMenuHandler[] = []; + const siteId = CoreSites.instance.getCurrentSiteId(); + const now = Date.now(); + + this.lastUpdateHandlersForCoursesStart[courseId] = now; + + for (const name in this.enabledHandlers) { + const handler = this.enabledHandlers[name]; + + // Checks if the handler is enabled for the user. + promises.push(Promise.resolve(handler.isEnabledForCourse(courseId, accessData, navOptions, admOptions)) + .then((enabled) => { + if (enabled) { + if (handler.isMenuHandler) { + enabledForCourseMenu.push( handler); + } else { + enabledForCourse.push(handler); + } + } + + return; + }).catch(() => { + // Nothing to do here, it is not enabled for this user. + })); + } + + try { + await Promise.all(promises); + } catch { + // Never fails. + } + + // Verify that this call is the last one that was started. + // Check that site hasn't changed since the check started. + if (this.isLastUpdateCourseCall(courseId, now) && CoreSites.instance.getCurrentSiteId() === siteId) { + // Update the coursesHandlers array with the new enabled addons. + this.coursesHandlers[courseId].enabledHandlers = enabledForCourse; + this.coursesHandlers[courseId].enabledMenuHandlers = enabledForCourseMenu; + this.loaded[courseId] = true; + + // Resolve the promise. + this.coursesHandlers[courseId].deferred.resolve(); + } + } + +} + +export class CoreCourseOptionsDelegate extends makeSingleton(CoreCourseOptionsDelegateService) {} diff --git a/src/core/features/courses/pages/course-preview/course-preview.ts b/src/core/features/courses/pages/course-preview/course-preview.ts index 57da1fc46..2bde270cd 100644 --- a/src/core/features/courses/pages/course-preview/course-preview.ts +++ b/src/core/features/courses/pages/course-preview/course-preview.ts @@ -27,7 +27,7 @@ import { CoreCoursesProvider, CoreEnrolledCourseData, } from '@features/courses/services/courses'; -// import { CoreCourseOptionsDelegate } from '@features/course/services/options-delegate'; +import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; import { CoreCourse, CoreCourseProvider } from '@features/course/services/course'; import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; import { Translate } from '@singletons'; @@ -77,7 +77,6 @@ export class CoreCoursesCoursePreviewPage implements OnInit, OnDestroy { constructor( protected modalCtrl: ModalController, - // protected courseOptionsDelegate: CoreCourseOptionsDelegate, protected zone: NgZone, protected route: ActivatedRoute, protected navCtrl: NavController, @@ -389,7 +388,7 @@ export class CoreCoursesCoursePreviewPage implements OnInit, OnDestroy { promises.push(CoreCourses.instance.invalidateUserCourses()); promises.push(CoreCourses.instance.invalidateCourse(this.course!.id)); promises.push(CoreCourses.instance.invalidateCourseEnrolmentMethods(this.course!.id)); - // @todo promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions(this.course!.id)); + promises.push(CoreCourseOptionsDelegate.instance.clearAndInvalidateCoursesOptions(this.course!.id)); if (CoreSites.instance.getCurrentSite() && !CoreSites.instance.getCurrentSite()!.isVersionGreaterEqualThan('3.7')) { promises.push(CoreCourses.instance.invalidateCoursesByField('id', this.course!.id)); } diff --git a/src/core/features/courses/pages/my-courses/my-courses.ts b/src/core/features/courses/pages/my-courses/my-courses.ts index 0799240c8..015dc4212 100644 --- a/src/core/features/courses/pages/my-courses/my-courses.ts +++ b/src/core/features/courses/pages/my-courses/my-courses.ts @@ -25,7 +25,7 @@ import { import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper'; import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CoreConstants } from '@/core/constants'; -// import { CoreCourseOptionsDelegate } from '@core/course/services/options-delegate'; +import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; /** * Page that displays the list of courses the user is enrolled in. @@ -128,7 +128,7 @@ export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy { const promises: Promise[] = []; promises.push(CoreCourses.instance.invalidateUserCourses()); - // @todo promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions()); + promises.push(CoreCourseOptionsDelegate.instance.clearAndInvalidateCoursesOptions()); if (this.courseIds) { promises.push(CoreCourses.instance.invalidateCoursesByField('ids', this.courseIds)); } diff --git a/src/core/features/courses/services/courses-helper.ts b/src/core/features/courses/services/courses-helper.ts index b1e7ef96f..d06f7b814 100644 --- a/src/core/features/courses/services/courses-helper.ts +++ b/src/core/features/courses/services/courses-helper.ts @@ -19,7 +19,7 @@ import { CoreSites } from '@services/sites'; import { CoreCourses, CoreCourseSearchedData, CoreCourseUserAdminOrNavOptionIndexed, CoreEnrolledCourseData } from './courses'; import { makeSingleton } from '@singletons'; import { CoreWSExternalFile } from '@services/ws'; -// import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers/coursecompletion'; +import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; // import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; /** @@ -161,8 +161,97 @@ export class CoreCoursesHelperProvider { * @param loadCategoryNames Whether load category names or not. * @return Courses filled with options. */ - async getUserCoursesWithOptions(): Promise { - // @todo params and logic + async getUserCoursesWithOptions( + sort: string = 'fullname', + slice: number = 0, + filter?: string, + loadCategoryNames: boolean = false, + ): Promise { + + let courses: CoreEnrolledCourseDataWithOptions[] = await CoreCourses.instance.getUserCourses(); + if (courses.length <= 0) { + return []; + } + + const promises: Promise[] = []; + const courseIds = courses.map((course) => course.id); + + if (CoreCourses.instance.canGetAdminAndNavOptions()) { + // Load course options of the course. + promises.push(CoreCourses.instance.getCoursesAdminAndNavOptions(courseIds).then((options) => { + courses.forEach((course) => { + course.navOptions = options.navOptions[course.id]; + course.admOptions = options.admOptions[course.id]; + }); + + return; + })); + } + + promises.push(this.loadCoursesExtraInfo(courses, loadCategoryNames)); + + await Promise.all(promises); + + switch (filter) { + case 'isfavourite': + courses = courses.filter((course) => !!course.isfavourite); + break; + default: + // Filter not implemented. + } + + switch (sort) { + case 'fullname': + courses.sort((a, b) => { + const compareA = a.fullname.toLowerCase(); + const compareB = b.fullname.toLowerCase(); + + return compareA.localeCompare(compareB); + }); + break; + case 'lastaccess': + courses.sort((a, b) => (b.lastaccess || 0) - (a.lastaccess || 0)); + break; + // @todo Time modified property is not defined in CoreEnrolledCourseDataWithOptions, so it won't do nothing. + // case 'timemodified': + // courses.sort((a, b) => b.timemodified - a.timemodified); + // break; + case 'shortname': + courses.sort((a, b) => { + const compareA = a.shortname.toLowerCase(); + const compareB = b.shortname.toLowerCase(); + + return compareA.localeCompare(compareB); + }); + break; + default: + // Sort not implemented. Do not sort. + } + + courses = slice > 0 ? courses.slice(0, slice) : courses; + + return Promise.all(courses.map(async (course) => { + if (typeof course.completed != 'undefined') { + // The WebService already returns the completed status, no need to fetch it. + return course; + } + + if (typeof course.enablecompletion != 'undefined' && !course.enablecompletion) { + // Completion is disabled for this course, there is no need to fetch the completion status. + return course; + } + + try { + const completion = await AddonCourseCompletion.instance.getCompletion(course.id); + + course.completed = completion?.completed; + } catch { + // Ignore error, maybe course completion is disabled or user has no permission. + course.completed = false; + } + + return course; + })); } /** diff --git a/src/core/services/utils/text.ts b/src/core/services/utils/text.ts index 4530ff07a..facc5b870 100644 --- a/src/core/services/utils/text.ts +++ b/src/core/services/utils/text.ts @@ -650,7 +650,7 @@ export class CoreTextUtilsProvider { * @param logErrorFn An error to call with the exception to log the error. If not supplied, no error. * @return JSON parsed as object or what it gets. */ - parseJSON(json: string, defaultValue?: T, logErrorFn?: (error?: Error) => void): T | string { + parseJSON(json: string, defaultValue?: T, logErrorFn?: (error?: Error) => void): T { try { return JSON.parse(json); } catch (error) { @@ -661,7 +661,11 @@ export class CoreTextUtilsProvider { } // Error parsing, return the default value or the original value. - return typeof defaultValue != 'undefined' ? defaultValue : json; + if (typeof defaultValue != 'undefined') { + return defaultValue; + } + + throw new CoreError('JSON cannot be parsed and not default value has been provided') ; } /** diff --git a/src/theme/app.scss b/src/theme/app.scss index 07a599401..a3f55ee7a 100644 --- a/src/theme/app.scss +++ b/src/theme/app.scss @@ -81,7 +81,7 @@ ion-item-divider { // Ionic list. ion-list.list-md { - padding-bottom: 0; + padding: 0; } // Header. @@ -248,10 +248,15 @@ ion-avatar ion-img, ion-avatar img { // Select. ion-select.core-button-select, .core-button-select { - background-color: var(--ion-color-primary-contrast); + background: var(--ion-color-primary-contrast); color: var(--ion-color-primary); white-space: normal; min-height: 45px; + margin: 8px; + box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12); + &::part(icon) { + margin: 0 8px; + } } // File uploader. From b2ecdf5224429793c011843e3557d17e08f27576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 10 Dec 2020 14:09:03 +0100 Subject: [PATCH 07/11] MOBILE-3608 core: Handle content links --- src/core/directives/link.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/directives/link.ts b/src/core/directives/link.ts index 35d491e9b..40efb60b9 100644 --- a/src/core/directives/link.ts +++ b/src/core/directives/link.ts @@ -22,6 +22,7 @@ import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreTextUtils } from '@services/utils/text'; import { CoreConstants } from '@/core/constants'; +import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; /** * Directive to open a link in external browser or in the app. @@ -76,8 +77,10 @@ export class CoreLinkDirective implements OnInit { if (CoreUtils.instance.isTrueOrOne(this.capture)) { href = CoreTextUtils.instance.decodeURI(href); - // @todo: Handle link. - this.navigate(href, openIn); + const treated = CoreContentLinksHelper.instance.handleLink(href, undefined, true, true); + if (!treated) { + this.navigate(href, openIn); + } } else { this.navigate(href, openIn); } From d0af70be5af6a414f5a2c5d92b876ea96df9e4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 10 Dec 2020 14:43:15 +0100 Subject: [PATCH 08/11] MOBILE-3608 blocks: Starred and recent courses block --- src/addons/addons.module.ts | 4 + .../components/components.module.ts | 45 ++++ .../addon-block-recentlyaccessedcourses.html | 22 ++ .../recentlyaccessedcourses.ts | 237 ++++++++++++++++++ .../block/recentlyaccessedcourses/lang.json | 4 + .../recentlyaccessedcourses.module.ts | 40 +++ .../services/block-handler.ts | 46 ++++ .../components/components.module.ts | 45 ++++ .../addon-block-starredcourses.html | 22 ++ .../starredcourses/starredcourses.ts | 217 ++++++++++++++++ src/addons/block/starredcourses/lang.json | 4 + .../starredcourses/services/block-handler.ts | 46 ++++ .../starredcourses/starredcourses.module.ts | 38 +++ .../course-progress/course-progress.scss | 30 ++- src/theme/app.scss | 8 + 15 files changed, 796 insertions(+), 12 deletions(-) create mode 100644 src/addons/block/recentlyaccessedcourses/components/components.module.ts create mode 100644 src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html create mode 100644 src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts create mode 100644 src/addons/block/recentlyaccessedcourses/lang.json create mode 100644 src/addons/block/recentlyaccessedcourses/recentlyaccessedcourses.module.ts create mode 100644 src/addons/block/recentlyaccessedcourses/services/block-handler.ts create mode 100644 src/addons/block/starredcourses/components/components.module.ts create mode 100644 src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html create mode 100644 src/addons/block/starredcourses/components/starredcourses/starredcourses.ts create mode 100644 src/addons/block/starredcourses/lang.json create mode 100644 src/addons/block/starredcourses/services/block-handler.ts create mode 100644 src/addons/block/starredcourses/starredcourses.module.ts diff --git a/src/addons/addons.module.ts b/src/addons/addons.module.ts index e03732e71..fe7e431d5 100644 --- a/src/addons/addons.module.ts +++ b/src/addons/addons.module.ts @@ -30,8 +30,10 @@ import { AddonBlockMyOverviewModule } from './block/myoverview/myoverview.module import { AddonBlockNewsItemsModule } from './block/newsitems/newsitems.module'; import { AddonBlockOnlineUsersModule } from './block/onlineusers/onlineusers.module'; import { AddonBlockPrivateFilesModule } from './block/privatefiles/privatefiles.module'; +import { AddonBlockRecentlyAccessedCoursesModule } from './block/recentlyaccessedcourses/recentlyaccessedcourses.module'; import { AddonBlockRssClientModule } from './block/rssclient/rssclient.module'; import { AddonBlockSelfCompletionModule } from './block/selfcompletion/selfcompletion.module'; +import { AddonBlockStarredCoursesModule } from './block/starredcourses/starredcourses.module'; import { AddonBlockTagsModule } from './block/tags/tags.module'; import { AddonPrivateFilesModule } from './privatefiles/privatefiles.module'; import { AddonFilterModule } from './filter/filter.module'; @@ -57,8 +59,10 @@ import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield AddonBlockNewsItemsModule, AddonBlockOnlineUsersModule, AddonBlockPrivateFilesModule, + AddonBlockRecentlyAccessedCoursesModule, AddonBlockRssClientModule, AddonBlockSelfCompletionModule, + AddonBlockStarredCoursesModule, AddonBlockTagsModule, AddonUserProfileFieldModule, ], diff --git a/src/addons/block/recentlyaccessedcourses/components/components.module.ts b/src/addons/block/recentlyaccessedcourses/components/components.module.ts new file mode 100644 index 000000000..560df1e3e --- /dev/null +++ b/src/addons/block/recentlyaccessedcourses/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreCoursesComponentsModule } from '@features/courses/components/components.module'; + +import { AddonBlockRecentlyAccessedCoursesComponent } from './recentlyaccessedcourses/recentlyaccessedcourses'; + +@NgModule({ + declarations: [ + AddonBlockRecentlyAccessedCoursesComponent, + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCoursesComponentsModule, + ], + exports: [ + AddonBlockRecentlyAccessedCoursesComponent, + ], + entryComponents: [ + AddonBlockRecentlyAccessedCoursesComponent, + ], +}) +export class AddonBlockRecentlyAccessedCoursesComponentsModule {} diff --git a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html new file mode 100644 index 000000000..b4fe5b2e1 --- /dev/null +++ b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html @@ -0,0 +1,22 @@ + + +

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

+
+
+ + + + + {{prefetchCoursesData.badge}} + +
+
+ + + +
+ + + +
+
diff --git a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts new file mode 100644 index 000000000..ffb1f3f22 --- /dev/null +++ b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts @@ -0,0 +1,237 @@ +// (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, OnDestroy, Input, OnChanges, SimpleChange } from '@angular/core'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreSites } from '@services/sites'; +import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData, CoreCourses } from '@features/courses/services/courses'; +import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper'; +import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; +import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; +import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; +import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreDomUtils } from '@services/utils/dom'; + +/** + * Component to render a recent courses block. + */ +@Component({ + selector: 'addon-block-recentlyaccessedcourses', + templateUrl: 'addon-block-recentlyaccessedcourses.html', +}) +export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy { + + @Input() downloadEnabled = false; + + courses: CoreEnrolledCourseDataWithOptions [] = []; + prefetchCoursesData: CorePrefetchStatusInfo = { + icon: '', + statusTranslatable: 'core.loading', + status: '', + loading: true, + badge: '', + }; + + downloadCourseEnabled = false; + downloadCoursesEnabled = false; + + protected prefetchIconsInitialized = false; + protected isDestroyed = false; + protected coursesObserver?: CoreEventObserver; + protected updateSiteObserver?: CoreEventObserver; + protected courseIds = []; + protected fetchContentDefaultError = 'Error getting recent courses data.'; + + constructor() { + super('AddonBlockRecentlyAccessedCoursesComponent'); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + + // Refresh the enabled flags if enabled. + this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); + this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); + + // Refresh the enabled flags if site is updated. + this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { + this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); + this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); + + }, CoreSites.instance.getCurrentSiteId()); + + this.coursesObserver = CoreEvents.on( + CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, + (data: CoreCoursesMyCoursesUpdatedEventData) => { + + if (this.shouldRefreshOnUpdatedEvent(data)) { + this.refreshCourseList(); + } + }, + + CoreSites.instance.getCurrentSiteId(), + ); + + super.ngOnInit(); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) { + // Download all courses is enabled now, initialize it. + this.initPrefetchCoursesIcons(); + } + } + + /** + * Perform the invalidate content function. + * + * @return Resolved when done. + */ + protected async invalidateContent(): Promise { + const promises: Promise[] = []; + + promises.push(CoreCourses.instance.invalidateUserCourses().finally(() => + // Invalidate course completion data. + CoreUtils.instance.allPromises(this.courseIds.map((courseId) => + AddonCourseCompletion.instance.invalidateCourseCompletion(courseId))))); + + promises.push(CoreCourseOptionsDelegate.instance.clearAndInvalidateCoursesOptions()); + if (this.courseIds.length > 0) { + promises.push(CoreCourses.instance.invalidateCoursesByField('ids', this.courseIds.join(','))); + } + + await CoreUtils.instance.allPromises(promises).finally(() => { + this.prefetchIconsInitialized = false; + }); + } + + /** + * Fetch the courses for recent courses. + * + * @return Promise resolved when done. + */ + protected async fetchContent(): Promise { + const showCategories = this.block.configsRecord && this.block.configsRecord.displaycategories && + this.block.configsRecord.displaycategories.value == '1'; + + this.courses = await CoreCoursesHelper.instance.getUserCoursesWithOptions('lastaccess', 10, undefined, showCategories); + this.initPrefetchCoursesIcons(); + } + + /** + * Refresh the list of courses. + * + * @return Promise resolved when done. + */ + protected async refreshCourseList(): Promise { + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED); + + try { + await CoreCourses.instance.invalidateUserCourses(); + } catch (error) { + // Ignore errors. + } + + await this.loadContent(true); + } + + /** + * Initialize the prefetch icon for selected courses. + */ + protected async initPrefetchCoursesIcons(): Promise { + if (this.prefetchIconsInitialized || !this.downloadEnabled) { + // Already initialized. + return; + } + + this.prefetchIconsInitialized = true; + + this.prefetchCoursesData = await CoreCourseHelper.instance.initPrefetchCoursesIcons(this.courses, this.prefetchCoursesData); + } + + /** + * Whether list should be refreshed based on a EVENT_MY_COURSES_UPDATED event. + * + * @param data Event data. + * @return Whether to refresh. + */ + protected shouldRefreshOnUpdatedEvent(data: CoreCoursesMyCoursesUpdatedEventData): boolean { + if (data.action == CoreCoursesProvider.ACTION_ENROL) { + // Always update if user enrolled in a course. + return true; + } + + if (data.action == CoreCoursesProvider.ACTION_VIEW && data.courseId != CoreSites.instance.getCurrentSiteHomeId() && + this.courses[0] && data.courseId != this.courses[0].id) { + // Update list if user viewed a course that isn't the most recent one and isn't site home. + return true; + } + + if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED && data.state == CoreCoursesProvider.STATE_FAVOURITE && + data.courseId && this.hasCourse(data.courseId)) { + // Update list if a visible course is now favourite or unfavourite. + return true; + } + + return false; + } + + /** + * Check if a certain course is in the list of courses. + * + * @param courseId Course ID to search. + * @return Whether it's in the list. + */ + protected hasCourse(courseId: number): boolean { + if (!this.courses) { + return false; + } + + return !!this.courses.find((course) => course.id == courseId); + } + + /** + * Prefetch all the shown courses. + * + * @return Promise resolved when done. + */ + async prefetchCourses(): Promise { + const initialIcon = this.prefetchCoursesData.icon; + + try { + return CoreCourseHelper.instance.prefetchCourses(this.courses, this.prefetchCoursesData); + } catch (error) { + if (!this.isDestroyed) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); + this.prefetchCoursesData.icon = initialIcon; + } + } + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + this.coursesObserver?.off(); + this.updateSiteObserver?.off(); + } + +} diff --git a/src/addons/block/recentlyaccessedcourses/lang.json b/src/addons/block/recentlyaccessedcourses/lang.json new file mode 100644 index 000000000..e87006df7 --- /dev/null +++ b/src/addons/block/recentlyaccessedcourses/lang.json @@ -0,0 +1,4 @@ +{ + "nocourses": "No recent courses", + "pluginname": "Recently accessed courses" +} diff --git a/src/addons/block/recentlyaccessedcourses/recentlyaccessedcourses.module.ts b/src/addons/block/recentlyaccessedcourses/recentlyaccessedcourses.module.ts new file mode 100644 index 000000000..aeac5a5df --- /dev/null +++ b/src/addons/block/recentlyaccessedcourses/recentlyaccessedcourses.module.ts @@ -0,0 +1,40 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockRecentlyAccessedCoursesComponentsModule } from './components/components.module'; +import { AddonBlockRecentlyAccessedCoursesHandler } from './services/block-handler'; + +@NgModule({ + imports: [ + IonicModule, + CoreComponentsModule, + AddonBlockRecentlyAccessedCoursesComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockRecentlyAccessedCoursesHandler.instance); + }, + }, + ], +}) +export class AddonBlockRecentlyAccessedCoursesModule {} diff --git a/src/addons/block/recentlyaccessedcourses/services/block-handler.ts b/src/addons/block/recentlyaccessedcourses/services/block-handler.ts new file mode 100644 index 000000000..a104adf90 --- /dev/null +++ b/src/addons/block/recentlyaccessedcourses/services/block-handler.ts @@ -0,0 +1,46 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreBlockHandlerData } from '@features/block/services/block-delegate'; +import { AddonBlockRecentlyAccessedCoursesComponent } from '../components/recentlyaccessedcourses/recentlyaccessedcourses'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockRecentlyAccessedCoursesHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockRecentlyAccessedCourses'; + blockName = 'recentlyaccessedcourses'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_recentlyaccessedcourses.pluginname', + class: 'addon-block-recentlyaccessedcourses', + component: AddonBlockRecentlyAccessedCoursesComponent, + }; + } + +} + +export class AddonBlockRecentlyAccessedCoursesHandler extends makeSingleton(AddonBlockRecentlyAccessedCoursesHandlerService) {} diff --git a/src/addons/block/starredcourses/components/components.module.ts b/src/addons/block/starredcourses/components/components.module.ts new file mode 100644 index 000000000..8dc0291d0 --- /dev/null +++ b/src/addons/block/starredcourses/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreCoursesComponentsModule } from '@features/courses/components/components.module'; + +import { AddonBlockStarredCoursesComponent } from './starredcourses/starredcourses'; + +@NgModule({ + declarations: [ + AddonBlockStarredCoursesComponent, + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCoursesComponentsModule, + ], + exports: [ + AddonBlockStarredCoursesComponent, + ], + entryComponents: [ + AddonBlockStarredCoursesComponent, + ], +}) +export class AddonBlockStarredCoursesComponentsModule {} diff --git a/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html b/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html new file mode 100644 index 000000000..09db419a6 --- /dev/null +++ b/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html @@ -0,0 +1,22 @@ + + +

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

+
+
+ + + + + {{prefetchCoursesData.badge}} + +
+
+ + + +
+ + + +
+
diff --git a/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts b/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts new file mode 100644 index 000000000..d7a848b96 --- /dev/null +++ b/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts @@ -0,0 +1,217 @@ +// (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, OnDestroy, Input, OnChanges, SimpleChange } from '@angular/core'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreSites } from '@services/sites'; +import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData, CoreCourses } from '@features/courses/services/courses'; +import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper'; +import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; +import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; +import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; +import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreDomUtils } from '@services/utils/dom'; + +/** + * Component to render a starred courses block. + */ +@Component({ + selector: 'addon-block-starredcourses', + templateUrl: 'addon-block-starredcourses.html', +}) +export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy { + + @Input() downloadEnabled = false; + + courses: CoreEnrolledCourseDataWithOptions [] = []; + prefetchCoursesData: CorePrefetchStatusInfo = { + icon: '', + statusTranslatable: 'core.loading', + status: '', + loading: true, + badge: '', + }; + + downloadCourseEnabled = false; + downloadCoursesEnabled = false; + + protected prefetchIconsInitialized = false; + protected isDestroyed = false; + protected coursesObserver?: CoreEventObserver; + protected updateSiteObserver?: CoreEventObserver; + protected courseIds: number[] = []; + protected fetchContentDefaultError = 'Error getting starred courses data.'; + + constructor() { + super('AddonBlockStarredCoursesComponent'); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + // Refresh the enabled flags if enabled. + this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); + this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); + + // Refresh the enabled flags if site is updated. + this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { + this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); + this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); + + }, CoreSites.instance.getCurrentSiteId()); + + this.coursesObserver = CoreEvents.on( + CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, + (data: CoreCoursesMyCoursesUpdatedEventData) => { + + if (this.shouldRefreshOnUpdatedEvent(data)) { + this.refreshCourseList(); + } + this.refreshContent(); + }, + + CoreSites.instance.getCurrentSiteId(), + ); + + super.ngOnInit(); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(changes: {[name: string]: SimpleChange}): void { + if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) { + // Download all courses is enabled now, initialize it. + this.initPrefetchCoursesIcons(); + } + } + + /** + * Perform the invalidate content function. + * + * @return Resolved when done. + */ + protected async invalidateContent(): Promise { + const promises: Promise[] = []; + + promises.push(CoreCourses.instance.invalidateUserCourses().finally(() => + // Invalidate course completion data. + CoreUtils.instance.allPromises(this.courseIds.map((courseId) => + AddonCourseCompletion.instance.invalidateCourseCompletion(courseId))))); + + promises.push(CoreCourseOptionsDelegate.instance.clearAndInvalidateCoursesOptions()); + if (this.courseIds.length > 0) { + promises.push(CoreCourses.instance.invalidateCoursesByField('ids', this.courseIds.join(','))); + } + + await CoreUtils.instance.allPromises(promises).finally(() => { + this.prefetchIconsInitialized = false; + }); + } + + /** + * Fetch the courses. + * + * @return Promise resolved when done. + */ + protected async fetchContent(): Promise { + const showCategories = this.block.configsRecord && this.block.configsRecord.displaycategories && + this.block.configsRecord.displaycategories.value == '1'; + + this.courses = await CoreCoursesHelper.instance.getUserCoursesWithOptions('timemodified', 0, 'isfavourite', showCategories); + this.initPrefetchCoursesIcons(); + } + + /** + * Refresh the list of courses. + * + * @return Promise resolved when done. + */ + protected async refreshCourseList(): Promise { + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED); + + try { + await CoreCourses.instance.invalidateUserCourses(); + } catch (error) { + // Ignore errors. + } + + await this.loadContent(true); + } + + /** + * Whether list should be refreshed based on a EVENT_MY_COURSES_UPDATED event. + * + * @param data Event data. + * @return Whether to refresh. + */ + protected shouldRefreshOnUpdatedEvent(data: CoreCoursesMyCoursesUpdatedEventData): boolean { + if (data.action == CoreCoursesProvider.ACTION_ENROL) { + // Always update if user enrolled in a course. + // New courses shouldn't be favourite by default, but just in case. + return true; + } + + if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED && data.state == CoreCoursesProvider.STATE_FAVOURITE) { + // Update list when making a course favourite or not. + return true; + } + + return false; + } + + /** + * Initialize the prefetch icon for selected courses. + */ + protected async initPrefetchCoursesIcons(): Promise { + if (this.prefetchIconsInitialized || !this.downloadEnabled) { + // Already initialized. + return; + } + + this.prefetchIconsInitialized = true; + + this.prefetchCoursesData = await CoreCourseHelper.instance.initPrefetchCoursesIcons(this.courses, this.prefetchCoursesData); + } + + /** + * Prefetch all the shown courses. + * + * @return Promise resolved when done. + */ + async prefetchCourses(): Promise { + const initialIcon = this.prefetchCoursesData.icon; + + try { + return CoreCourseHelper.instance.prefetchCourses(this.courses, this.prefetchCoursesData); + } catch (error) { + if (!this.isDestroyed) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); + this.prefetchCoursesData.icon = initialIcon; + } + } + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + this.coursesObserver?.off(); + this.updateSiteObserver?.off(); + } + +} diff --git a/src/addons/block/starredcourses/lang.json b/src/addons/block/starredcourses/lang.json new file mode 100644 index 000000000..cc3dd03c7 --- /dev/null +++ b/src/addons/block/starredcourses/lang.json @@ -0,0 +1,4 @@ +{ + "nocourses": "No starred courses", + "pluginname": "Starred courses" +} diff --git a/src/addons/block/starredcourses/services/block-handler.ts b/src/addons/block/starredcourses/services/block-handler.ts new file mode 100644 index 000000000..b220d9be3 --- /dev/null +++ b/src/addons/block/starredcourses/services/block-handler.ts @@ -0,0 +1,46 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreBlockHandlerData } from '@features/block/services/block-delegate'; +import { AddonBlockStarredCoursesComponent } from '../components/starredcourses/starredcourses'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockStarredCoursesHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockStarredCourses'; + blockName = 'starredcourses'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.starredcourses.pluginname', + class: 'addon-block-starredcourses', + component: AddonBlockStarredCoursesComponent, + }; + } + +} + +export class AddonBlockStarredCoursesHandler extends makeSingleton(AddonBlockStarredCoursesHandlerService) {} diff --git a/src/addons/block/starredcourses/starredcourses.module.ts b/src/addons/block/starredcourses/starredcourses.module.ts new file mode 100644 index 000000000..20dfba809 --- /dev/null +++ b/src/addons/block/starredcourses/starredcourses.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockStarredCoursesComponentsModule } from './components/components.module'; +import { AddonBlockStarredCoursesHandler } from './services/block-handler'; + +@NgModule({ + imports: [ + IonicModule, + AddonBlockStarredCoursesComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockStarredCoursesHandler.instance); + }, + }, + ], +}) +export class AddonBlockStarredCoursesModule {} diff --git a/src/core/features/courses/components/course-progress/course-progress.scss b/src/core/features/courses/components/course-progress/course-progress.scss index 67b5b9470..50dc956eb 100644 --- a/src/core/features/courses/components/course-progress/course-progress.scss +++ b/src/core/features/courses/components/course-progress/course-progress.scss @@ -109,27 +109,34 @@ // @todo :host-context(.core-horizontal-scroll) { - /*@include horizontal_scroll_item(80%, 250px, 300px);*/ + 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; + } + } ion-card { .core-course-thumb { padding-top: 30%; } - .core-course-link { - /*@include padding(4px, 0px, 4px, 8px);*/ - .core-course-additional-info { - font-size: 1.2rem; - } + .core-course-header { + padding-top: 4px; + padding-bottom: 4px; .core-course-title { margin: 3px 0; - h2 { - font-size: 1.5rem; - ion-icon { - margin-right: 2px; - } + h2 ion-icon { + margin-right: 2px; } &.core-course-with-buttons { @@ -148,7 +155,6 @@ .item-button[icon-only] { min-width: 40px; width: 40px; - font-size: 1.5rem; padding: 8px; } diff --git a/src/theme/app.scss b/src/theme/app.scss index a3f55ee7a..e88fd2d8c 100644 --- a/src/theme/app.scss +++ b/src/theme/app.scss @@ -277,3 +277,11 @@ ion-select.core-button-select, cursor: pointer; text-decoration: underline; } + +// Horizontal scrolling elements +.core-horizontal-scroll { + display: flex; + flex-flow: nowrap; + overflow-x: scroll; + flex-direction: row; +} From f8320f6b58e39667b8beb8454828a7018287b7b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 10 Dec 2020 16:16:55 +0100 Subject: [PATCH 09/11] MOBILE-3608 core: Implement missing parts of Course and filters --- .../classes/module-grade-handler.ts | 4 +- .../course-progress/course-progress.ts | 6 +- .../features/filter/services/filter-helper.ts | 82 ++++++++----------- src/core/features/settings/pages/site/site.ts | 2 +- .../settings/services/settings-helper.ts | 17 ++-- 5 files changed, 49 insertions(+), 62 deletions(-) diff --git a/src/core/features/contentlinks/classes/module-grade-handler.ts b/src/core/features/contentlinks/classes/module-grade-handler.ts index e083d5781..704d152c0 100644 --- a/src/core/features/contentlinks/classes/module-grade-handler.ts +++ b/src/core/features/contentlinks/classes/module-grade-handler.ts @@ -16,7 +16,7 @@ import { CoreContentLinksAction } from '../services/contentlinks-delegate'; import { CoreContentLinksHandlerBase } from './base-handler'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -// import { CoreCourseHelperProvider } from '@features/course/providers/helper'; +// import { CoreCourseHelper } from '@features/course/services/helper'; import { Params } from '@angular/router'; /** @@ -77,7 +77,7 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB const site = await CoreSites.instance.getSite(siteId); if (!params.userid || params.userid == site.getUserId()) { // No user specified or current user. Navigate to module. - // @todo this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId, undefined, + // @todo CoreCourseHelper.instance.navigateToModule(parseInt(params.id, 10), siteId, courseId, undefined, // this.useModNameToGetModule ? this.modName : undefined, undefined, navCtrl); } else if (this.canReview) { // Use the goToReview function. diff --git a/src/core/features/courses/components/course-progress/course-progress.ts b/src/core/features/courses/components/course-progress/course-progress.ts index 2f70fad9f..057156451 100644 --- a/src/core/features/courses/components/course-progress/course-progress.ts +++ b/src/core/features/courses/components/course-progress/course-progress.ts @@ -17,7 +17,7 @@ import { PopoverController } from '@ionic/angular'; import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -// import { CoreUserProvider } from '@core/user/providers/user'; +// import { CoreUser } from '@core/user/services/user'; import { CoreCourses } from '@features/courses/services/courses'; import { CoreCourse, CoreCourseProvider } from '@features/course/services/course'; import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; @@ -252,7 +252,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { * Hide/Unhide the course from the course list. * * @param hide True to hide and false to show. - * @todo + * @todo CoreUser */ // eslint-disable-next-line @typescript-eslint/no-unused-vars protected setCourseHidden(hide: boolean): void { @@ -263,7 +263,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { * Favourite/Unfavourite the course from the course list. * * @param favourite True to favourite and false to unfavourite. - * @todo + * @todo CoreUser */ // eslint-disable-next-line @typescript-eslint/no-unused-vars protected setCourseFavourite(favourite: boolean): void { diff --git a/src/core/features/filter/services/filter-helper.ts b/src/core/features/filter/services/filter-helper.ts index ec273ccc5..588be18f7 100644 --- a/src/core/features/filter/services/filter-helper.ts +++ b/src/core/features/filter/services/filter-helper.ts @@ -24,8 +24,8 @@ import { CoreFilterClassifiedFilters, CoreFiltersGetAvailableInContextWSParamContext, } from './filter'; -// import { CoreCourseProvider } from '@features/course/providers/course'; -// import { CoreCoursesProvider } from '@features/courses/providers/courses'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreCourses } from '@features/courses/services/courses'; import { makeSingleton } from '@singletons'; import { CoreEvents, CoreEventSiteData } from '@singletons/events'; import { CoreLogger } from '@singletons/logger'; @@ -73,22 +73,19 @@ export class CoreFilterHelperProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the contexts. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars async getBlocksContexts(courseId: number, siteId?: string): Promise { - return []; - // @todo - // const blocks = await this.courseProvider.getCourseBlocks(courseId, siteId); + const blocks = await CoreCourse.instance.getCourseBlocks(courseId, siteId); - // const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = []; + const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = []; - // blocks.forEach((block) => { - // contexts.push({ - // contextlevel: 'block', - // instanceid: block.instanceid, - // }); - // }); + blocks.forEach((block) => { + contexts.push({ + contextlevel: 'block', + instanceid: block.instanceid, + }); + }); - // return contexts; + return contexts; } /** @@ -132,22 +129,19 @@ export class CoreFilterHelperProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the contexts. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars async getCourseContexts(courseId: number, siteId?: string): Promise { - // @todo - return []; - // const courseIds = await this.coursesProvider.getCourseIdsIfEnrolled(courseId, siteId); + const courseIds = await CoreCourses.instance.getCourseIdsIfEnrolled(courseId, siteId); - // const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = []; + const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = []; - // courseIds.forEach((courseId) => { - // contexts.push({ - // contextlevel: 'course', - // instanceid: courseId - // }); - // }); + courseIds.forEach((courseId) => { + contexts.push({ + contextlevel: 'course', + instanceid: courseId, + }); + }); - // return contexts; + return contexts; } /** @@ -157,29 +151,25 @@ export class CoreFilterHelperProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the contexts. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars async getCourseModulesContexts(courseId: number, siteId?: string): Promise { - // @todo - return []; + const sections = await CoreCourse.instance.getSections(courseId, false, true, undefined, siteId); - // const sections = await this.courseProvider.getSections(courseId, false, true, undefined, siteId); + const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = []; - // const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = []; + sections.forEach((section) => { + if (section.modules) { + section.modules.forEach((module) => { + if (module.uservisible) { + contexts.push({ + contextlevel: 'module', + instanceid: module.id, + }); + } + }); + } + }); - // sections.forEach((section) => { - // if (section.modules) { - // section.modules.forEach((module) => { - // if (module.uservisible) { - // contexts.push({ - // contextlevel: 'module', - // instanceid: module.id - // }); - // } - // }); - // } - // }); - - // return contexts; + return contexts; } /** @@ -243,7 +233,7 @@ export class CoreFilterHelperProvider { const getFilters = this.getCourseContexts.bind(this, instanceId, siteId); return this.getCacheableFilters(contextLevel, instanceId, getFilters, options, site); - } else if (contextLevel == 'block' && options.courseId) { // @todo && this.courseProvider.canGetCourseBlocks(site) + } else if (contextLevel == 'block' && options.courseId && CoreCourse.instance.canGetCourseBlocks(site)) { // Get all the course blocks filters with a single call to decrease number of WS calls. const getFilters = this.getBlocksContexts.bind(this, options.courseId, siteId); diff --git a/src/core/features/settings/pages/site/site.ts b/src/core/features/settings/pages/site/site.ts index e3f1ad2bb..ca5bc7b65 100644 --- a/src/core/features/settings/pages/site/site.ts +++ b/src/core/features/settings/pages/site/site.ts @@ -21,7 +21,7 @@ import { CoreEventObserver, CoreEvents, CoreEventSiteUpdatedData } from '@single import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; // import { CoreSplitViewComponent } from '@components/split-view/split-view'; -// import { CoreSharedFiles } from '@features/sharedfiles/providers/sharedfiles'; +// import { CoreSharedFiles } from '@features/sharedfiles/services/sharedfiles'; import { CoreSettingsHelper, CoreSiteSpaceUsage } from '../../services/settings-helper'; import { CoreApp } from '@services/app'; import { CoreSiteInfo } from '@classes/site'; diff --git a/src/core/features/settings/services/settings-helper.ts b/src/core/features/settings/services/settings-helper.ts index 3b253a3d5..3e442ed16 100644 --- a/src/core/features/settings/services/settings-helper.ts +++ b/src/core/features/settings/services/settings-helper.ts @@ -22,7 +22,7 @@ import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { CoreConfig } from '@services/config'; -// import { CoreFilterProvider } from '@features/filter/providers/filter'; +import { CoreFilter } from '@features/filter/services/filter'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreCourse } from '@features/course/services/course'; import { makeSingleton, Translate } from '@singletons'; @@ -55,8 +55,6 @@ export class CoreSettingsHelperProvider { protected prefersDark?: MediaQueryList; constructor() { - // protected filterProvider: CoreFilterProvider, - if (!CoreConstants.CONFIG.forceColorScheme) { // Update color scheme when a user enters or leaves a site, or when the site info is updated. const applySiteScheme = (): void => { @@ -87,7 +85,6 @@ export class CoreSettingsHelperProvider { * @param siteName Site Name. * @param siteId: Site ID. * @return Resolved with detailed new info when done. - * @todo filterProvider and courseProviderpart. */ async deleteSiteStorage(siteName: string, siteId: string): Promise { const siteInfo: CoreSiteSpaceUsage = { @@ -95,7 +92,7 @@ export class CoreSettingsHelperProvider { spaceUsage: 0, }; - // siteName = await this.filterProvider.formatText(siteName, { clean: true, singleLine: true, filter: false }, [], siteId); + siteName = await CoreFilter.instance.formatText(siteName, { clean: true, singleLine: true, filter: false }, [], siteId); const title = Translate.instance.instant('core.settings.deletesitefilestitle'); const message = Translate.instance.instant('core.settings.deletesitefiles', { sitename: siteName }); @@ -187,16 +184,16 @@ export class CoreSettingsHelperProvider { * @param name Name of the processor to get. * @param fallback True to return first processor if not found, false to not return any. Defaults to true. * @return Processor. - * @todo + * @todo typings */ getProcessor(processors: any[], name: string, fallback: boolean = true): any { if (!processors || !processors.length) { return; } - for (let i = 0; i < processors.length; i++) { - if (processors[i].name == name) { - return processors[i]; - } + + const processor = processors.find((processor) => processor.name == name); + if (processor) { + return processor; } // Processor not found, return first if requested. From 253d6829a9db967f954d9d337847cffe5e737131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 10 Dec 2020 16:23:46 +0100 Subject: [PATCH 10/11] MOBILE-3608 block: Implement site main menu block --- src/addons/addons.module.ts | 2 + .../components/components.module.ts | 46 ++++++ .../addon-block-sitemainmenu.html | 14 ++ .../components/sitemainmenu/sitemainmenu.ts | 116 +++++++++++++++ src/addons/block/sitemainmenu/lang.json | 3 + .../sitemainmenu/services/block-handler.ts | 46 ++++++ .../block/sitemainmenu/sitemainmenu.module.ts | 42 ++++++ src/addons/mod/forum/services/forum.ts | 132 ++++++++++++++++++ .../features/course/services/course-helper.ts | 50 ++++++- .../features/sitehome/pages/index/index.html | 2 +- .../features/sitehome/pages/index/index.ts | 28 +++- .../features/sitehome/services/sitehome.ts | 24 +++- src/core/services/sites.ts | 2 +- 13 files changed, 491 insertions(+), 16 deletions(-) create mode 100644 src/addons/block/sitemainmenu/components/components.module.ts create mode 100644 src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html create mode 100644 src/addons/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts create mode 100644 src/addons/block/sitemainmenu/lang.json create mode 100644 src/addons/block/sitemainmenu/services/block-handler.ts create mode 100644 src/addons/block/sitemainmenu/sitemainmenu.module.ts create mode 100644 src/addons/mod/forum/services/forum.ts diff --git a/src/addons/addons.module.ts b/src/addons/addons.module.ts index fe7e431d5..6686059a2 100644 --- a/src/addons/addons.module.ts +++ b/src/addons/addons.module.ts @@ -33,6 +33,7 @@ import { AddonBlockPrivateFilesModule } from './block/privatefiles/privatefiles. import { AddonBlockRecentlyAccessedCoursesModule } from './block/recentlyaccessedcourses/recentlyaccessedcourses.module'; import { AddonBlockRssClientModule } from './block/rssclient/rssclient.module'; import { AddonBlockSelfCompletionModule } from './block/selfcompletion/selfcompletion.module'; +import { AddonBlockSiteMainMenuModule } from './block/sitemainmenu/sitemainmenu.module'; import { AddonBlockStarredCoursesModule } from './block/starredcourses/starredcourses.module'; import { AddonBlockTagsModule } from './block/tags/tags.module'; import { AddonPrivateFilesModule } from './privatefiles/privatefiles.module'; @@ -62,6 +63,7 @@ import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield AddonBlockRecentlyAccessedCoursesModule, AddonBlockRssClientModule, AddonBlockSelfCompletionModule, + AddonBlockSiteMainMenuModule, AddonBlockStarredCoursesModule, AddonBlockTagsModule, AddonUserProfileFieldModule, diff --git a/src/addons/block/sitemainmenu/components/components.module.ts b/src/addons/block/sitemainmenu/components/components.module.ts new file mode 100644 index 000000000..726a43dbb --- /dev/null +++ b/src/addons/block/sitemainmenu/components/components.module.ts @@ -0,0 +1,46 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +// import { CoreCourseComponentsModule } from '@features/course/components/components.module'; + +import { AddonBlockSiteMainMenuComponent } from './sitemainmenu/sitemainmenu'; + + +@NgModule({ + declarations: [ + AddonBlockSiteMainMenuComponent, + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + // CoreCourseComponentsModule, + ], + exports: [ + AddonBlockSiteMainMenuComponent, + ], + entryComponents: [ + AddonBlockSiteMainMenuComponent, + ], +}) +export class AddonBlockSiteMainMenuComponentsModule {} diff --git a/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html b/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html new file mode 100644 index 000000000..eceb7a099 --- /dev/null +++ b/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html @@ -0,0 +1,14 @@ + + +

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

+
+
+ + + + + + + + + diff --git a/src/addons/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts b/src/addons/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts new file mode 100644 index 000000000..4df2d9b89 --- /dev/null +++ b/src/addons/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts @@ -0,0 +1,116 @@ +// (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, Input } from '@angular/core'; +import { CoreSites } from '@services/sites'; +import { CoreCourse, CoreCourseSection } from '@features/course/services/course'; +import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { CoreSiteHome, FrontPageItemNames } from '@features/sitehome/services/sitehome'; +// @todo import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; + +/** + * Component to render a site main menu block. + */ +@Component({ + selector: 'addon-block-sitemainmenu', + templateUrl: 'addon-block-sitemainmenu.html', +}) +export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent implements OnInit { + + @Input() downloadEnabled = false; + + component = 'AddonBlockSiteMainMenu'; + mainMenuBlock?: CoreCourseSection; + siteHomeId = 1; + + protected fetchContentDefaultError = 'Error getting main menu data.'; + + constructor() { + super('AddonBlockSiteMainMenuComponent'); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + this.siteHomeId = CoreSites.instance.getCurrentSite()?.getSiteHomeId() || 1; + + super.ngOnInit(); + } + + /** + * Perform the invalidate content function. + * + * @return Resolved when done. + */ + protected async invalidateContent(): Promise { + const promises: Promise[] = []; + + promises.push(CoreCourse.instance.invalidateSections(this.siteHomeId)); + promises.push(CoreSiteHome.instance.invalidateNewsForum(this.siteHomeId)); + + if (this.mainMenuBlock && this.mainMenuBlock.modules) { + // Invalidate modules prefetch data. + // @todo promises.push(this.prefetchDelegate.invalidateModules(this.mainMenuBlock.modules, this.siteHomeId)); + } + + await Promise.all(promises); + } + + /** + * Fetch the data to render the block. + * + * @return Promise resolved when done. + */ + protected async fetchContent(): Promise { + const sections = await CoreCourse.instance.getSections(this.siteHomeId, false, true); + + this.mainMenuBlock = sections.find((section) => section.section == 0); + if (!this.mainMenuBlock) { + return; + } + + const currentSite = CoreSites.instance.getCurrentSite(); + const config = currentSite ? currentSite.getStoredConfig() || {} : {}; + if (!config.frontpageloggedin) { + return; + } + // Check if Site Home displays announcements. If so, remove it from the main menu block. + const items = config.frontpageloggedin.split(','); + const hasNewsItem = items.find((item) => parseInt(item, 10) == FrontPageItemNames['NEWS_ITEMS']); + + const hasContent = CoreCourseHelper.instance.sectionHasContent(this.mainMenuBlock); + CoreCourseHelper.instance.addHandlerDataForModules([this.mainMenuBlock], this.siteHomeId, undefined, undefined, true); + + if (!hasNewsItem || !hasContent) { + return; + } + + // Remove forum activity (news one only) from the main menu block to prevent duplicates. + try { + const forum = await CoreSiteHome.instance.getNewsForum(this.siteHomeId); + // Search the module that belongs to site news. + const forumIndex = + this.mainMenuBlock.modules.findIndex((mod) => mod.modname == 'forum' && mod.instance == forum.id); + + if (forumIndex >= 0) { + this.mainMenuBlock.modules.splice(forumIndex, 1); + } + } catch { + // Ignore errors. + } + } + +} diff --git a/src/addons/block/sitemainmenu/lang.json b/src/addons/block/sitemainmenu/lang.json new file mode 100644 index 000000000..0f3aca2ff --- /dev/null +++ b/src/addons/block/sitemainmenu/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Main menu" +} diff --git a/src/addons/block/sitemainmenu/services/block-handler.ts b/src/addons/block/sitemainmenu/services/block-handler.ts new file mode 100644 index 000000000..876822ec5 --- /dev/null +++ b/src/addons/block/sitemainmenu/services/block-handler.ts @@ -0,0 +1,46 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreBlockHandlerData } from '@features/block/services/block-delegate'; +import { AddonBlockSiteMainMenuComponent } from '../components/sitemainmenu/sitemainmenu'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockSiteMainMenuHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockSiteMainMenu'; + blockName = 'site_main_menu'; + + /** + * Returns the data needed to render the block. + * + * @return Data or promise resolved with the data. + */ + getDisplayData(): CoreBlockHandlerData { + + return { + title: 'addon.block_sitemainmenu.pluginname', + class: 'addon-block-sitemainmenu', + component: AddonBlockSiteMainMenuComponent, + }; + } + +} + +export class AddonBlockSiteMainMenuHandler extends makeSingleton(AddonBlockSiteMainMenuHandlerService) {} diff --git a/src/addons/block/sitemainmenu/sitemainmenu.module.ts b/src/addons/block/sitemainmenu/sitemainmenu.module.ts new file mode 100644 index 000000000..bc8310b50 --- /dev/null +++ b/src/addons/block/sitemainmenu/sitemainmenu.module.ts @@ -0,0 +1,42 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonBlockSiteMainMenuComponentsModule } from './components/components.module'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockSiteMainMenuHandler } from './services/block-handler'; + +@NgModule({ + imports: [ + IonicModule, + CoreComponentsModule, + CoreDirectivesModule, + AddonBlockSiteMainMenuComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.instance.registerHandler(AddonBlockSiteMainMenuHandler.instance); + }, + }, + ], +}) +export class AddonBlockSiteMainMenuModule {} diff --git a/src/addons/mod/forum/services/forum.ts b/src/addons/mod/forum/services/forum.ts new file mode 100644 index 000000000..9a30af0cb --- /dev/null +++ b/src/addons/mod/forum/services/forum.ts @@ -0,0 +1,132 @@ +// (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 { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreSitesCommonWSOptions, CoreSites } from '@services/sites'; +import { CoreWSExternalFile } from '@services/ws'; +import { makeSingleton } from '@singletons'; + +const ROOT_CACHE_KEY = 'mmaModForum:'; + +/** + * Service that provides some features for forums. + * + * @todo Add all content. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModForumProvider { + + static readonly COMPONENT = 'mmaModForum'; + + /** + * Get cache key for forum data WS calls. + * + * @param courseId Course ID. + * @return Cache key. + */ + protected getForumDataCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'forum:' + courseId; + } + + /** + * Get all course forums. + * + * @param courseId Course ID. + * @param options Other options. + * @return Promise resolved when the forums are retrieved. + */ + async getCourseForums(courseId: number, options: CoreSitesCommonWSOptions = {}): Promise { + const site = await CoreSites.instance.getSite(options.siteId); + + const params: AddonModForumGetForumsByCoursesWSParams = { + courseids: [courseId], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getForumDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModForumProvider.COMPONENT, + ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), + }; + + return site.read('mod_forum_get_forums_by_courses', params, preSets); + } + + /** + * Invalidates forum data. + * + * @param courseId Course ID. + * @return Promise resolved when the data is invalidated. + */ + async invalidateForumData(courseId: number): Promise { + await CoreSites.instance.getCurrentSite()?.invalidateWsCacheForKey(this.getForumDataCacheKey(courseId)); + } + +} + +export class AddonModForum extends makeSingleton(AddonModForumProvider) {} + +/** + * Params of mod_forum_get_forums_by_courses WS. + */ +type AddonModForumGetForumsByCoursesWSParams = { + courseids?: number[]; // Array of Course IDs. +}; + +/** + * General forum activity data. + */ +export type AddonModForumData = { + id: number; // Forum id. + course: number; // Course id. + type: string; // The forum type. + name: string; // Forum name. + intro: string; // The forum intro. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles?: CoreWSExternalFile[]; + duedate?: number; // Duedate for the user. + cutoffdate?: number; // Cutoffdate for the user. + assessed: number; // Aggregate type. + assesstimestart: number; // Assess start time. + assesstimefinish: number; // Assess finish time. + scale: number; // Scale. + // eslint-disable-next-line @typescript-eslint/naming-convention + grade_forum: number; // Whole forum grade. + // eslint-disable-next-line @typescript-eslint/naming-convention + grade_forum_notify: number; // Whether to send notifications to students upon grading by default. + maxbytes: number; // Maximum attachment size. + maxattachments: number; // Maximum number of attachments. + forcesubscribe: number; // Force users to subscribe. + trackingtype: number; // Subscription mode. + rsstype: number; // RSS feed for this activity. + rssarticles: number; // Number of RSS recent articles. + timemodified: number; // Time modified. + warnafter: number; // Post threshold for warning. + blockafter: number; // Post threshold for blocking. + blockperiod: number; // Time period for blocking. + completiondiscussions: number; // Student must create discussions. + completionreplies: number; // Student must post replies. + completionposts: number; // Student must post discussions or replies. + cmid: number; // Course module id. + numdiscussions?: number; // Number of discussions in the forum. + cancreatediscussions?: boolean; // If the user can create discussions. + lockdiscussionafter?: number; // After what period a discussion is locked. + istracked?: boolean; // If the user is tracking the forum. + unreadpostscount?: number; // The number of unread posts for tracked forums. +}; + +/** + * Data returned by mod_forum_get_forums_by_courses WS. + */ +export type AddonModForumGetForumsByCoursesWSResponse = AddonModForumData[]; diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 6b31a738f..0df7ad379 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -134,8 +134,52 @@ export class CoreCourseHelperProvider { * @param forCoursePage Whether the data will be used to render the course page. * @return Whether the sections have content. */ - addHandlerDataForModules(): void { - // @todo params and logic + addHandlerDataForModules( + sections: CoreCourseSection[], + courseId: number, + completionStatus?: any, // eslint-disable-line @typescript-eslint/no-unused-vars + courseName?: string, // eslint-disable-line @typescript-eslint/no-unused-vars + forCoursePage = false, // eslint-disable-line @typescript-eslint/no-unused-vars + ): boolean { + + let hasContent = false; + + sections.forEach((section) => { + if (!section || !this.sectionHasContent(section) || !section.modules) { + return; + } + + hasContent = true; + + /* @todo + section.modules.forEach((module) => { + module.handlerData = this.moduleDelegate.getModuleDataFor(module.modname, module, courseId, section.id, + forCoursePage); + + if (module.completiondata && module.completion > 0) { + module.completiondata.courseId = courseId; + module.completiondata.courseName = courseName; + module.completiondata.tracking = module.completion; + module.completiondata.cmid = module.id; + + // Use of completionstatus is deprecated, use completiondata instead. + module.completionstatus = module.completiondata; + } else if (completionStatus && typeof completionStatus[module.id] != 'undefined') { + // Should not happen on > 3.6. Check if activity has completions and if it's marked. + module.completiondata = completionStatus[module.id]; + module.completiondata.courseId = courseId; + module.completiondata.courseName = courseName; + + // Use of completionstatus is deprecated, use completiondata instead. + module.completionstatus = module.completiondata; + } + + // Check if the module is stealth. + module.isStealth = module.visibleoncoursepage === 0 || (module.visible && !section.visible); + });*/ + }); + + return hasContent; } /** @@ -913,7 +957,7 @@ export class CoreCourseHelperProvider { * @return Whether the section has content. * @todo section type. */ - sectionHasContent(section: any): boolean { + sectionHasContent(section: CoreCourseSection): boolean { if (section.hiddenbynumsections) { return false; } diff --git a/src/core/features/sitehome/pages/index/index.html b/src/core/features/sitehome/pages/index/index.html index f89fa3a0b..06f865f6e 100644 --- a/src/core/features/sitehome/pages/index/index.html +++ b/src/core/features/sitehome/pages/index/index.html @@ -72,7 +72,7 @@ News (TODO) - +
diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index 6ad4cc726..e4beca68f 100644 --- a/src/core/features/sitehome/pages/index/index.ts +++ b/src/core/features/sitehome/pages/index/index.ts @@ -17,7 +17,7 @@ import { ActivatedRoute } from '@angular/router'; import { IonRefresher, NavController } from '@ionic/angular'; import { CoreSite, CoreSiteConfig } from '@classes/site'; -import { CoreCourse, CoreCourseSection } from '@features/course/services/course'; +import { CoreCourse, CoreCourseModuleBasicInfo, CoreCourseSection } from '@features/course/services/course'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreSites } from '@services/sites'; import { CoreSiteHome } from '@features/sitehome/services/sitehome'; @@ -44,13 +44,14 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { hasContent = false; items: string[] = []; - siteHomeId?: number; + siteHomeId = 1; currentSite?: CoreSite; searchEnabled = false; downloadEnabled = false; downloadCourseEnabled = false; downloadCoursesEnabled = false; downloadEnabledIcon = 'far-square'; + newsForumModule?: CoreCourseModuleBasicInfo; protected updateSiteObserver?: CoreEventObserver; @@ -80,7 +81,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { }, CoreSites.instance.getCurrentSiteId()); this.currentSite = CoreSites.instance.getCurrentSite()!; - this.siteHomeId = this.currentSite.getSiteHomeId(); + this.siteHomeId = this.currentSite?.getSiteHomeId() || 1; const module = navParams['module']; if (module) { @@ -106,6 +107,23 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { this.items = await CoreSiteHome.instance.getFrontPageItems(config.frontpageloggedin); this.hasContent = this.items.length > 0; + if (this.items.some((item) => item == 'NEWS_ITEMS')) { + // Get the news forum. + try { + const forum = await CoreSiteHome.instance.getNewsForum(); + this.newsForumModule = await CoreCourse.instance.getModuleBasicInfo(forum.cmid); + /* @todo this.newsForumModule.handlerData = this.moduleDelegate.getModuleDataFor( + this.newsForumModule.modname, + this.newsForumModule, + this.siteHomeId, + this.newsForumModule.section, + true, + );*/ + } catch { + // Ignore errors. + } + } + try { const sections = await CoreCourse.instance.getSections(this.siteHomeId!, false, true); @@ -114,13 +132,13 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { if (this.section) { this.section.hasContent = false; this.section.hasContent = CoreCourseHelper.instance.sectionHasContent(this.section); - /* @todo this.hasContent = CoreCourseHelper.instance.addHandlerDataForModules( + this.hasContent = CoreCourseHelper.instance.addHandlerDataForModules( [this.section], this.siteHomeId, undefined, undefined, true, - ) || this.hasContent;*/ + ) || this.hasContent; } // Add log in Moodle. diff --git a/src/core/features/sitehome/services/sitehome.ts b/src/core/features/sitehome/services/sitehome.ts index 98ddf2a8d..9fd56c6a5 100644 --- a/src/core/features/sitehome/services/sitehome.ts +++ b/src/core/features/sitehome/services/sitehome.ts @@ -19,6 +19,7 @@ import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { makeSingleton } from '@singletons'; import { CoreCourse, CoreCourseSection } from '../../course/services/course'; import { CoreCourses } from '../../courses/services/courses'; +import { AddonModForum, AddonModForumData } from '@/addons/mod/forum/services/forum'; /** * Items with index 1 and 3 were removed on 2.5 and not being supported in the app. @@ -44,8 +45,19 @@ export class CoreSiteHomeProvider { * @param siteHomeId Site Home ID. * @return Promise resolved with the forum if found, rejected otherwise. */ - getNewsForum(): void { - // @todo params and logic. + async getNewsForum(siteHomeId?: number): Promise { + if (!siteHomeId) { + siteHomeId = CoreSites.instance.getCurrentSite()?.getSiteHomeId() || 1; + } + + const forums = await AddonModForum.instance.getCourseForums(siteHomeId); + const forum = forums.find((forum) => forum.type == 'news'); + + if (forum) { + return forum; + } + + throw null; } /** @@ -54,8 +66,8 @@ export class CoreSiteHomeProvider { * @param siteHomeId Site Home ID. * @return Promise resolved when invalidated. */ - invalidateNewsForum(): void { - // @todo params and logic. + async invalidateNewsForum(siteHomeId: number): Promise { + await AddonModForum.instance.invalidateForumData(siteHomeId); } /** @@ -154,8 +166,8 @@ export class CoreSiteHomeProvider { let add = false; switch (itemNumber) { case FrontPageItemNames['NEWS_ITEMS']: - // @todo - add = true; + // Get number of news items to show. + add = !!CoreSites.instance.getCurrentSite()?.getStoredConfig('newsitems'); break; case FrontPageItemNames['LIST_OF_CATEGORIES']: case FrontPageItemNames['COMBO_LIST']: diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 513c11f9c..b2f8ecb08 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -1618,7 +1618,7 @@ export class CoreSitesProvider { * @param strategy Reading strategy. * @return PreSets options object. */ - getReadingStrategyPreSets(strategy: CoreSitesReadingStrategy): CoreSiteWSPreSets { + getReadingStrategyPreSets(strategy?: CoreSitesReadingStrategy): CoreSiteWSPreSets { switch (strategy) { case CoreSitesReadingStrategy.PreferCache: return { From 361edb125236aefec83a8847f8ef22a18b9e665a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 11 Dec 2020 15:40:34 +0100 Subject: [PATCH 11/11] MOBILE-3608 block: Add CoreCache and some types --- src/core/classes/cache.ts | 112 ++++++++++++++++++++ src/core/features/course/services/course.ts | 78 +++++++------- src/core/services/filepool.ts | 8 +- src/core/services/plugin-file-delegate.ts | 12 ++- src/core/services/sites.ts | 3 + src/core/services/utils/dom.ts | 3 +- src/core/services/utils/utils.ts | 3 +- src/core/singletons/events.ts | 9 ++ 8 files changed, 182 insertions(+), 46 deletions(-) create mode 100644 src/core/classes/cache.ts diff --git a/src/core/classes/cache.ts b/src/core/classes/cache.ts new file mode 100644 index 000000000..833ecac56 --- /dev/null +++ b/src/core/classes/cache.ts @@ -0,0 +1,112 @@ +// (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. + +/** + * A cache to store values in memory to speed up processes. + * + * The data is organized by "entries" that are identified by an ID. Each entry can have multiple values stored, + * and each value has its own timemodified. + * + * Values expire after a certain time. + */ +export class CoreCache { + + protected cacheStore: { + [key: string]: CoreCacheEntry; + } = {}; + + /** + * Clear the cache. + */ + clear(): void { + this.cacheStore = {}; + } + + /** + * Get all the data stored in the cache for a certain id. + * + * @param id The ID to identify the entry. + * @return The data from the cache. Undefined if not found. + */ + getEntry(id: string): CoreCacheEntry { + if (!this.cacheStore[id]) { + this.cacheStore[id] = {}; + } + + return this.cacheStore[id]; + } + + /** + * Get the status of a module from the "cache". + * + * @param id The ID to identify the entry. + * @param name Name of the value to get. + * @param ignoreInvalidate Whether it should always return the cached data, even if it's expired. + * @return Cached value. Undefined if not cached or expired. + */ + getValue(id: string, name: string, ignoreInvalidate = false): T | undefined { + const entry = this.getEntry(id); + + if (entry[name] && typeof entry[name].value != 'undefined') { + const now = Date.now(); + // Invalidate after 5 minutes. + if (ignoreInvalidate || entry[name].timemodified + 300000 >= now) { + return entry[name].value; + } + } + + return undefined; + } + + /** + * Invalidate all the cached data for a certain entry. + * + * @param id The ID to identify the entry. + */ + invalidate(id: string): void { + const entry = this.getEntry(id); + for (const name in entry) { + entry[name].timemodified = 0; + } + } + + /** + * Update the status of a module in the "cache". + * + * @param id The ID to identify the entry. + * @param name Name of the value to set. + * @param value Value to set. + * @return The set value. + */ + setValue(id: string, name: string, value: T): T { + const entry = this.getEntry(id); + entry[name] = { + value: value, + timemodified: Date.now(), + }; + + return value; + } + +} + +/** + * Cache entry + */ +export type CoreCacheEntry = { + [name: string]: { + value?: any; // eslint-disable-line @typescript-eslint/no-explicit-any + timemodified: number; + }; +}; diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 537f2d6aa..3086104af 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -345,7 +345,7 @@ export class CoreCourseProvider { ignoreCache: boolean = false, siteId?: string, modName?: string, - ): Promise { + ): Promise { siteId = siteId || CoreSites.instance.getCurrentSiteId(); // Helper function to do the WS request without processing the result. @@ -439,7 +439,7 @@ export class CoreCourseProvider { sections = await this.getSections(courseId, false, false, preSets, siteId); } - let foundModule: CoreCourseModule | undefined; + let foundModule: CoreCourseModuleData | undefined; const foundSection = sections.some((section) => { if (sectionId != null && @@ -739,12 +739,12 @@ export class CoreCourseProvider { * @param sections Sections. * @return Modules. */ - getSectionsModules(sections: CoreCourseSection[]): CoreCourseModule[] { + getSectionsModules(sections: CoreCourseSection[]): CoreCourseModuleData[] { if (!sections || !sections.length) { return []; } - return sections.reduce((previous: CoreCourseModule[], section) => previous.concat(section.modules || []), []); + return sections.reduce((previous: CoreCourseModuleData[], section) => previous.concat(section.modules || []), []); } /** @@ -829,7 +829,7 @@ export class CoreCourseProvider { * @return Promise resolved when loaded. */ async loadModuleContents( - module: CoreCourseModule & CoreCourseModuleBasicInfo, + module: CoreCourseModuleData & CoreCourseModuleBasicInfo, courseId?: number, sectionId?: number, preferCache?: boolean, @@ -964,7 +964,7 @@ export class CoreCourseProvider { * @param module The module object. * @return Whether the module has a view page. */ - moduleHasView(module: CoreCourseModuleSummary | CoreCourseModule): boolean { + moduleHasView(module: CoreCourseModuleSummary | CoreCourseModuleData): boolean { return !!module.url; } @@ -1345,7 +1345,7 @@ export type CoreCourseSection = { hiddenbynumsections?: number; // Whether is a section hidden in the course format. uservisible?: boolean; // Is the section visible for the user?. availabilityinfo?: string; // Availability information. - modules: CoreCourseModule[]; + modules: CoreCourseModuleData[]; }; /** @@ -1371,11 +1371,10 @@ export type CoreCourseGetCourseModuleWSResponse = { warnings?: CoreStatusWithWarningsWSResponse[]; }; - /** * Course module type. */ -export type CoreCourseModule = { // List of module. +export type CoreCourseModuleData = { // List of module. id: number; // Activity id. course?: number; // The course id. url?: string; // Activity url. @@ -1403,35 +1402,7 @@ export type CoreCourseModule = { // List of module. overrideby: number; // The user id who has overriden the status. valueused?: boolean; // Whether the completion status affects the availability of another activity. }; - contents: { - type: string; // A file or a folder or external link. - filename: string; // Filename. - filepath: string; // Filepath. - filesize: number; // Filesize. - fileurl?: string; // Downloadable file url. - content?: string; // Raw content, will be used when type is content. - timecreated: number; // Time created. - timemodified: number; // Time modified. - sortorder: number; // Content sort order. - mimetype?: string; // File mime type. - isexternalfile?: boolean; // Whether is an external file. - repositorytype?: string; // The repository type for external files. - userid: number; // User who added this content to moodle. - author: string; // Content owner. - license: string; // Content license. - tags?: { // Tags. - id: number; // Tag id. - name: string; // Tag name. - rawname: string; // The raw, unnormalised name for the tag as entered by users. - isstandard: boolean; // Whether this tag is standard. - tagcollid: number; // Tag collection id. - taginstanceid: number; // Tag instance id. - taginstancecontextid: number; // Context the tag instance belongs to. - itemid: number; // Id of the record tagged. - ordering: number; // Tag ordering. - flag: number; // Whether the tag is flagged as inappropriate. - }[]; - }[]; + contents: CoreCourseModuleContentFile[]; contentsinfo?: { // Contents summary information. filescount: number; // Total number of files. filessize: number; // Total files size. @@ -1441,6 +1412,37 @@ export type CoreCourseModule = { // List of module. }; }; +export type CoreCourseModuleContentFile = { + type: string; // A file or a folder or external link. + filename: string; // Filename. + filepath: string; // Filepath. + filesize: number; // Filesize. + fileurl?: string; // Downloadable file url. + url?: string; // @deprecated. Use fileurl instead. + content?: string; // Raw content, will be used when type is content. + timecreated: number; // Time created. + timemodified: number; // Time modified. + sortorder: number; // Content sort order. + mimetype?: string; // File mime type. + isexternalfile?: boolean; // Whether is an external file. + repositorytype?: string; // The repository type for external files. + userid: number; // User who added this content to moodle. + author: string; // Content owner. + license: string; // Content license. + tags?: { // Tags. + id: number; // Tag id. + name: string; // Tag name. + rawname: string; // The raw, unnormalised name for the tag as entered by users. + isstandard: boolean; // Whether this tag is standard. + tagcollid: number; // Tag collection id. + taginstanceid: number; // Tag instance id. + taginstancecontextid: number; // Context the tag instance belongs to. + itemid: number; // Id of the record tagged. + ordering: number; // Tag ordering. + flag: number; // Whether the tag is flagged as inappropriate. + }[]; +}; + /** * Course module basic info type. */ diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 52b84b08a..a82ae02d0 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { Md5 } from 'ts-md5/dist/md5'; import { CoreApp } from '@services/app'; -import { CoreEvents } from '@singletons/events'; +import { CoreEventPackageStatusChanged, CoreEvents } from '@singletons/events'; import { CoreFile } from '@services/file'; import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; import { CoreSites } from '@services/sites'; @@ -544,7 +544,7 @@ export class CoreFilepoolProvider { entries.forEach((entry) => { // Trigger module status changed, setting it as not downloaded. - this.triggerPackageStatusChanged(siteId, CoreConstants.NOT_DOWNLOADED, entry.component, entry.componentId); + this.triggerPackageStatusChanged(siteId, CoreConstants.NOT_DOWNLOADED, entry.component!, entry.componentId); }); } @@ -2949,8 +2949,8 @@ export class CoreFilepoolProvider { * @param component Package's component. * @param componentId An ID to use in conjunction with the component. */ - protected triggerPackageStatusChanged(siteId: string, status: string, component?: string, componentId?: string | number): void { - const data = { + protected triggerPackageStatusChanged(siteId: string, status: string, component: string, componentId?: string | number): void { + const data: CoreEventPackageStatusChanged = { component, componentId: this.fixComponentId(componentId), status, diff --git a/src/core/services/plugin-file-delegate.ts b/src/core/services/plugin-file-delegate.ts index 67c377e33..93cd9340b 100644 --- a/src/core/services/plugin-file-delegate.ts +++ b/src/core/services/plugin-file-delegate.ts @@ -133,7 +133,7 @@ export class CorePluginFileDelegateService extends CoreDelegate { + async getFilesDownloadSize(files: CoreWSExternalFile[], siteId: string): Promise { const filteredFiles = []; await Promise.all(files.map(async (file) => { @@ -154,7 +154,7 @@ export class CorePluginFileDelegateService extends CoreDelegate { + async getFilesSize(files: CoreWSExternalFile[], siteId?: string): Promise { const result = { size: 0, total: true, @@ -402,3 +402,11 @@ export type CorePluginFileDownloadableResult = { */ reason?: string; }; + +/** + * Sum of file sizes. + */ +export type CoreFileSizeSum = { + size: number; // Sum of file sizes. + total: boolean; // False if any file size is not avalaible. +}; diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index b2f8ecb08..7c6c6ac16 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -304,6 +304,9 @@ export class CoreSitesProvider { async siteExists(siteUrl: string): Promise { let data: CoreSitesLoginTokenResponse; + // Use a valid path first. + siteUrl = CoreUrlUtils.instance.removeUrlParams(siteUrl); + try { data = await Http.instance.post(siteUrl + '/login/token.php', {}).pipe(timeout(CoreWS.instance.getRequestTimeout())) .toPromise(); diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index fa85d4366..cff84e7d1 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -34,6 +34,7 @@ import { CoreSilentError } from '@classes/errors/silenterror'; import { makeSingleton, Translate, AlertController, LoadingController, ToastController } from '@singletons'; import { CoreLogger } from '@singletons/logger'; +import { CoreFileSizeSum } from '@services/plugin-file-delegate'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -134,7 +135,7 @@ export class CoreDomUtilsProvider { * @return Promise resolved when the user confirms or if no confirm needed. */ async confirmDownloadSize( - size: {size: number; total: boolean}, + size: CoreFileSizeSum, message?: string, unknownMessage?: string, wifiThreshold?: number, diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index b554e0999..ba4861dad 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -28,6 +28,7 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreWSError } from '@classes/errors/wserror'; import { makeSingleton, Clipboard, InAppBrowser, FileOpener, WebIntent, QRScanner, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; +import { CoreFileSizeSum } from '@services/plugin-file-delegate'; type TreeNode = T & { children: TreeNode[] }; @@ -1327,7 +1328,7 @@ export class CoreUtilsProvider { * @return File size and a boolean to indicate if it is the total size or only partial. * @deprecated since 3.8.0. Use CorePluginFileDelegate.getFilesSize instead. */ - sumFileSizes(files: CoreWSExternalFile[]): { size: number; total: boolean } { + sumFileSizes(files: CoreWSExternalFile[]): CoreFileSizeSum { const result = { size: 0, total: true, diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index eae75d94f..65b738a3a 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -242,6 +242,15 @@ export type CoreEventCourseStatusChanged = { status: string; }; +/** + * Data passed to PACKAGE_STATUS_CHANGED event. + */ +export type CoreEventPackageStatusChanged = { + component: string; + componentId: string | number; + status: string; +}; + /** * Data passed to USER_DELETED event. */