From 9d0a937f9dd399aeaa6145a7cdf2e1f35aca0085 Mon Sep 17 00:00:00 2001 From: sam marshall Date: Fri, 15 Mar 2019 16:38:44 +0000 Subject: [PATCH] MOBILE-2935 Support site plugins for blocks on dashboard page --- src/core/block/components/block/block.ts | 42 ++++++++++-- src/core/block/providers/delegate.ts | 29 +++++++- src/core/compile/providers/compile.ts | 2 + .../classes/handlers/block-handler.ts | 60 ++++++++++++++++ .../siteplugins/components/block/block.ts | 68 +++++++++++++++++++ .../block/core-siteplugins-block.html | 1 + .../components/components.module.ts | 4 ++ src/core/siteplugins/providers/helper.ts | 29 +++++++- 8 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 src/core/siteplugins/classes/handlers/block-handler.ts create mode 100644 src/core/siteplugins/components/block/block.ts create mode 100644 src/core/siteplugins/components/block/core-siteplugins-block.html diff --git a/src/core/block/components/block/block.ts b/src/core/block/components/block/block.ts index 4d6b67dc0..8f239a981 100644 --- a/src/core/block/components/block/block.ts +++ b/src/core/block/components/block/block.ts @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit, Injector, ViewChild } from '@angular/core'; +import { Component, Input, OnInit, Injector, ViewChild, OnDestroy } from '@angular/core'; import { CoreBlockDelegate } from '../../providers/delegate'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; +import { Subscription } from 'rxjs'; +import { CoreEventsProvider } from '@providers/events'; /** * Component to render a block. @@ -23,7 +25,7 @@ import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-comp selector: 'core-block', templateUrl: 'core-block.html' }) -export class CoreBlockComponent implements OnInit { +export class CoreBlockComponent implements OnInit, OnDestroy { @ViewChild(CoreDynamicComponent) dynamicComponent: CoreDynamicComponent; @Input() block: any; // The block to render. @@ -37,7 +39,10 @@ export class CoreBlockComponent implements OnInit { class: string; // CSS class to apply to the block. loaded = false; - constructor(protected injector: Injector, protected blockDelegate: CoreBlockDelegate) { } + blockSubscription: Subscription; + + constructor(protected injector: Injector, protected blockDelegate: CoreBlockDelegate, + protected eventsProvider: CoreEventsProvider) { } /** * Component being initialized. @@ -50,9 +55,28 @@ export class CoreBlockComponent implements OnInit { } // Get the data to render the block. + this.initBlock(); + } + + /** + * 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). + */ + initBlock(): void { this.blockDelegate.getBlockDisplayData(this.injector, this.block, this.contextLevel, this.instanceId).then((data) => { if (!data) { - // Block not supported, don't render it. + // 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 = this.blockDelegate.blocksUpdateObservable.subscribe({ + next: (): void => { + this.blockSubscription.unsubscribe(); + delete this.blockSubscription; + this.initBlock(); + } + }); + return; } @@ -73,6 +97,16 @@ export class CoreBlockComponent implements OnInit { }); } + /** + * On destroy of the component, clear up any subscriptions. + */ + ngOnDestroy(): void { + if (this.blockSubscription) { + this.blockSubscription.unsubscribe(); + delete this.blockSubscription; + } + } + /** * Refresh the data. * diff --git a/src/core/block/providers/delegate.ts b/src/core/block/providers/delegate.ts index 0275350be..a319d9b06 100644 --- a/src/core/block/providers/delegate.ts +++ b/src/core/block/providers/delegate.ts @@ -19,6 +19,8 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreBlockDefaultHandler } from './default-block-handler'; import { CoreSite } from '@classes/site'; +import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins'; +import { Subject } from 'rxjs'; /** * Interface that all blocks must implement. @@ -83,9 +85,12 @@ export class CoreBlockDelegate extends CoreDelegate { protected featurePrefix = 'CoreBlockDelegate_'; + blocksUpdateObservable: Subject; + constructor(logger: CoreLoggerProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, - protected defaultHandler: CoreBlockDefaultHandler) { + protected defaultHandler: CoreBlockDefaultHandler, protected sitePluginsProvider: CoreSitePluginsProvider) { super('CoreBlockDelegate', logger, sitesProvider, eventsProvider); + this.blocksUpdateObservable = new Subject(); } /** @@ -157,4 +162,26 @@ export class CoreBlockDelegate extends CoreDelegate { protected isFeatureDisabled(handler: CoreDelegateHandler, site: CoreSite): boolean { return this.areBlocksDisabledInSite(site) || super.isFeatureDisabled(handler, site); } + + /** + * Gets the handler name for a given block name. + * + * @param {string} name Block name e.g. 'activity_modules' + * @return {string} Full name of corresponding handler + */ + getHandlerName(name: string): string { + if (!this.isBlockSupported(name)) { + return ''; + } + + return this.getHandler(name, true).name; + } + + /** + * Called when there are new block handlers available. Informs anyone who subscribed to the + * observable. + */ + updateData(): void { + this.blocksUpdateObservable.next(); + } } diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index 111db31e7..7b76555ce 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -78,6 +78,7 @@ import { CoreBlockComponentsModule } from '@core/block/components/components.mod import { CoreCourseUnsupportedModuleComponent } from '@core/course/components/unsupported-module/unsupported-module'; import { CoreCourseFormatSingleActivityComponent } from '@core/course/formats/singleactivity/components/singleactivity'; import { CoreSitePluginsModuleIndexComponent } from '@core/siteplugins/components/module-index/module-index'; +import { CoreSitePluginsBlockComponent } from '@core/siteplugins/components/block/block'; import { CoreSitePluginsCourseOptionComponent } from '@core/siteplugins/components/course-option/course-option'; import { CoreSitePluginsCourseFormatComponent } from '@core/siteplugins/components/course-format/course-format'; import { CoreSitePluginsQuestionComponent } from '@core/siteplugins/components/question/question'; @@ -269,6 +270,7 @@ export class CoreCompileProvider { instance['CoreCourseUnsupportedModuleComponent'] = CoreCourseUnsupportedModuleComponent; instance['CoreCourseFormatSingleActivityComponent'] = CoreCourseFormatSingleActivityComponent; instance['CoreSitePluginsModuleIndexComponent'] = CoreSitePluginsModuleIndexComponent; + instance['CoreSitePluginsBlockComponent'] = CoreSitePluginsBlockComponent; instance['CoreSitePluginsCourseOptionComponent'] = CoreSitePluginsCourseOptionComponent; instance['CoreSitePluginsCourseFormatComponent'] = CoreSitePluginsCourseFormatComponent; instance['CoreSitePluginsQuestionComponent'] = CoreSitePluginsQuestionComponent; diff --git a/src/core/siteplugins/classes/handlers/block-handler.ts b/src/core/siteplugins/classes/handlers/block-handler.ts new file mode 100644 index 000000000..ef16a70e2 --- /dev/null +++ b/src/core/siteplugins/classes/handlers/block-handler.ts @@ -0,0 +1,60 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { Injector } from '@angular/core'; +import { CoreSitePluginsBaseHandler } from './base-handler'; +import { CoreBlockHandler, CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreSitePluginsBlockComponent } from '@core/siteplugins/components/block/block'; + +/** + * Handler to support a block using a site plugin. + */ +export class CoreSitePluginsBlockHandler extends CoreSitePluginsBaseHandler implements CoreBlockHandler { + + constructor(name: string, public blockName: string, protected handlerSchema: any, protected initResult: any) { + super(name); + } + + /** + * Gets display data for this block. The class and title can be provided either by data from + * the handler schema (mobile.php) or using default values. + * + * @param {Injector} injector Injector + * @param {any} block Block data + * @param {string} contextLevel Context level (not used) + * @param {number} instanceId Instance if (not used) + * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number): + CoreBlockHandlerData | Promise { + let title, + className; + if (this.handlerSchema.displaydata && this.handlerSchema.displaydata.title) { + title = this.handlerSchema.displaydata.title; + } else { + title = 'plugins.block_' + block.name + '.pluginname'; + } + if (this.handlerSchema.displaydata && this.handlerSchema.displaydata.class) { + className = this.handlerSchema.displaydata.class; + } else { + className = 'block_' + block.name; + } + + return { + title: title, + class: className, + component: CoreSitePluginsBlockComponent + }; + } +} diff --git a/src/core/siteplugins/components/block/block.ts b/src/core/siteplugins/components/block/block.ts new file mode 100644 index 000000000..d1e927add --- /dev/null +++ b/src/core/siteplugins/components/block/block.ts @@ -0,0 +1,68 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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, OnChanges, Input, ViewChild, Injector } from '@angular/core'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content'; +import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; + +/** + * Component that displays the index of a course format site plugin. + */ +@Component({ + selector: 'core-site-plugins-block', + templateUrl: 'core-siteplugins-block.html', +}) +export class CoreSitePluginsBlockComponent extends CoreBlockBaseComponent implements OnChanges { + @Input() block: any; + @Input() contextLevel: number; + @Input() instanceId: number; + + @ViewChild(CoreSitePluginsPluginContentComponent) content: CoreSitePluginsPluginContentComponent; + + component: string; + method: string; + args: any; + initResult: any; + + constructor(protected injector: Injector, protected sitePluginsProvider: CoreSitePluginsProvider, + protected blockDelegate: CoreBlockDelegate) { + super(injector, 'CoreSitePluginsBlockComponent'); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(): void { + if (!this.component) { + // Initialize the data. + const handlerName = this.blockDelegate.getHandlerName(this.block.name); + const handler = this.sitePluginsProvider.getSitePluginHandler(handlerName); + if (handler) { + this.component = handler.plugin.component; + this.method = handler.handlerSchema.method; + this.args = { }; + this.initResult = handler.initResult; + } + } + } + + /** + * Pass on content invalidation by refreshing content in the plugin content component. + */ + protected invalidateContent(): Promise { + return Promise.resolve(this.content.refreshContent()); + } +} diff --git a/src/core/siteplugins/components/block/core-siteplugins-block.html b/src/core/siteplugins/components/block/core-siteplugins-block.html new file mode 100644 index 000000000..94fd9c04f --- /dev/null +++ b/src/core/siteplugins/components/block/core-siteplugins-block.html @@ -0,0 +1 @@ + diff --git a/src/core/siteplugins/components/components.module.ts b/src/core/siteplugins/components/components.module.ts index 9619ee02e..ebc7d03b9 100644 --- a/src/core/siteplugins/components/components.module.ts +++ b/src/core/siteplugins/components/components.module.ts @@ -29,11 +29,13 @@ import { CoreSitePluginsQuizAccessRuleComponent } from './quiz-access-rule/quiz- import { CoreSitePluginsAssignFeedbackComponent } from './assign-feedback/assign-feedback'; import { CoreSitePluginsAssignSubmissionComponent } from './assign-submission/assign-submission'; import { CoreSitePluginsWorkshopAssessmentStrategyComponent } from './workshop-assessment-strategy/workshop-assessment-strategy'; +import { CoreSitePluginsBlockComponent } from '@core/siteplugins/components/block/block'; @NgModule({ declarations: [ CoreSitePluginsPluginContentComponent, CoreSitePluginsModuleIndexComponent, + CoreSitePluginsBlockComponent, CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, @@ -56,6 +58,7 @@ import { CoreSitePluginsWorkshopAssessmentStrategyComponent } from './workshop-a exports: [ CoreSitePluginsPluginContentComponent, CoreSitePluginsModuleIndexComponent, + CoreSitePluginsBlockComponent, CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, @@ -68,6 +71,7 @@ import { CoreSitePluginsWorkshopAssessmentStrategyComponent } from './workshop-a ], entryComponents: [ CoreSitePluginsModuleIndexComponent, + CoreSitePluginsBlockComponent, CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index 395b777d5..ef7b93a60 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -64,6 +64,8 @@ import { CoreSitePluginsQuizAccessRuleHandler } from '../classes/handlers/quiz-a import { CoreSitePluginsAssignFeedbackHandler } from '../classes/handlers/assign-feedback-handler'; import { CoreSitePluginsAssignSubmissionHandler } from '../classes/handlers/assign-submission-handler'; import { CoreSitePluginsWorkshopAssessmentStrategyHandler } from '../classes/handlers/workshop-assessment-strategy-handler'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { CoreSitePluginsBlockHandler } from '@core/siteplugins/classes/handlers/block-handler'; /** * Helper service to provide functionalities regarding site plugins. It basically has the features to load and register site @@ -92,7 +94,7 @@ export class CoreSitePluginsHelperProvider { private assignSubmissionDelegate: AddonModAssignSubmissionDelegate, private translate: TranslateService, private assignFeedbackDelegate: AddonModAssignFeedbackDelegate, private appProvider: CoreAppProvider, private workshopAssessmentStrategyDelegate: AddonWorkshopAssessmentStrategyDelegate, - private courseProvider: CoreCourseProvider) { + private courseProvider: CoreCourseProvider, private blockDelegate: CoreBlockDelegate) { this.logger = logger.getInstance('CoreSitePluginsHelperProvider'); @@ -478,6 +480,10 @@ export class CoreSitePluginsHelperProvider { promise = Promise.resolve(this.registerQuestionBehaviourHandler(plugin, handlerName, handlerSchema)); break; + case 'CoreBlockDelegate': + promise = Promise.resolve(this.registerBlockHandler(plugin, handlerName, handlerSchema, result)); + break; + case 'AddonMessageOutputDelegate': promise = Promise.resolve(this.registerMessageOutputHandler(plugin, handlerName, handlerSchema, result)); break; @@ -611,6 +617,27 @@ export class CoreSitePluginsHelperProvider { }); } + /** + * Given a handler in a plugin, register it in the block delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} initResult Result of init function. + * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. + */ + protected registerBlockHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any): + string | Promise { + + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), + blockName = (handlerSchema.moodlecomponent || plugin.component).replace('block_', ''); + + this.blockDelegate.registerHandler( + new CoreSitePluginsBlockHandler(uniqueName, blockName, handlerSchema, initResult)); + + return uniqueName; + } + /** * Given a handler in a plugin, register it in the course format delegate. *