diff --git a/src/addons/qbehaviour/deferredcbm/component/deferredcbm.ts b/src/addons/qbehaviour/deferredcbm/component/deferredcbm.ts index 88b47fcf2..4656beb62 100644 --- a/src/addons/qbehaviour/deferredcbm/component/deferredcbm.ts +++ b/src/addons/qbehaviour/deferredcbm/component/deferredcbm.ts @@ -32,6 +32,9 @@ export class AddonQbehaviourDeferredCBMComponent { @Input() offlineEnabled?: boolean | string; // Whether the question can be answered in offline. @Input() contextLevel?: string; // The context level. @Input() contextInstanceId?: number; // The instance ID related to the context. + @Input() courseId?: number; // Course ID the question belongs to (if any). It can be used to improve performance with filters. + @Input() review?: boolean; // Whether the user is in review mode. + @Input() preferredBehaviour?: string; // Preferred behaviour. @Output() buttonClicked = new EventEmitter(); // Will emit when a behaviour button is clicked. @Output() onAbort = new EventEmitter(); // Should emit an event if the question should be aborted. diff --git a/src/addons/qbehaviour/informationitem/component/informationitem.ts b/src/addons/qbehaviour/informationitem/component/informationitem.ts index bbc0a243f..995bbe28c 100644 --- a/src/addons/qbehaviour/informationitem/component/informationitem.ts +++ b/src/addons/qbehaviour/informationitem/component/informationitem.ts @@ -32,6 +32,9 @@ export class AddonQbehaviourInformationItemComponent { @Input() offlineEnabled?: boolean | string; // Whether the question can be answered in offline. @Input() contextLevel?: string; // The context level. @Input() contextInstanceId?: number; // The instance ID related to the context. + @Input() courseId?: number; // Course ID the question belongs to (if any). It can be used to improve performance with filters. + @Input() review?: boolean; // Whether the user is in review mode. + @Input() preferredBehaviour?: string; // Preferred behaviour. @Output() buttonClicked = new EventEmitter(); // Will emit when a behaviour button is clicked. @Output() onAbort = new EventEmitter(); // Should emit an event if the question should be aborted. diff --git a/src/core/features/course/components/format/format.ts b/src/core/features/course/components/format/format.ts index 07414a062..301d34cc2 100644 --- a/src/core/features/course/components/format/format.ts +++ b/src/core/features/course/components/format/format.ts @@ -507,6 +507,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { } await Promise.all(promises); + + refresher?.detail.complete(); + done?.(); } /** diff --git a/src/core/features/question/classes/base-question-component.ts b/src/core/features/question/classes/base-question-component.ts index 5c41e753c..f22757bb5 100644 --- a/src/core/features/question/classes/base-question-component.ts +++ b/src/core/features/question/classes/base-question-component.ts @@ -39,6 +39,7 @@ export class CoreQuestionBaseComponent { @Input() contextInstanceId?: number; // The instance ID related to the context. @Input() courseId?: number; // The course the question belongs to (if any). @Input() review?: boolean; // Whether the user is in review mode. + @Input() preferredBehaviour?: string; // Preferred behaviour. @Output() buttonClicked = new EventEmitter(); // Will emit when a behaviour button is clicked. @Output() onAbort = new EventEmitter(); // Should emit an event if the question should be aborted. diff --git a/src/core/features/question/components/question/question.ts b/src/core/features/question/components/question/question.ts index 21afeda9f..5617e21bf 100644 --- a/src/core/features/question/components/question/question.ts +++ b/src/core/features/question/components/question/question.ts @@ -95,6 +95,7 @@ export class CoreQuestionComponent implements OnInit { contextInstanceId: this.contextInstanceId, courseId: this.courseId, review: this.review, + preferredBehaviour: this.preferredBehaviour, buttonClicked: this.buttonClicked, onAbort: this.onAbort, }; diff --git a/src/core/features/siteplugins/components/assign-feedback/assign-feedback.ts b/src/core/features/siteplugins/components/assign-feedback/assign-feedback.ts new file mode 100644 index 000000000..09478e69d --- /dev/null +++ b/src/core/features/siteplugins/components/assign-feedback/assign-feedback.ts @@ -0,0 +1,63 @@ +// (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 { AddonModAssignAssign, AddonModAssignPlugin, AddonModAssignSubmission } from '@addons/mod/assign/services/assign'; +import { AddonModAssignFeedbackDelegate } from '@addons/mod/assign/services/feedback-delegate'; +import { CoreSitePluginsCompileInitComponent } from '@features/siteplugins/classes/compile-init-component'; + +/** + * Component that displays an assign feedback plugin created using a site plugin. + */ +@Component({ + selector: 'core-site-plugins-assign-feedback', + templateUrl: 'core-siteplugins-assign-feedback.html', +}) +export class CoreSitePluginsAssignFeedbackComponent extends CoreSitePluginsCompileInitComponent implements OnInit { + + @Input() assign!: AddonModAssignAssign; // The assignment. + @Input() submission!: AddonModAssignSubmission; // The submission. + @Input() plugin!: AddonModAssignPlugin; // The plugin object. + @Input() userId!: number; // The user ID of the submission. + @Input() canEdit = false; // Whether the user can edit. + @Input() edit = false; // Whether the user is editing. + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Pass the input and output data to the component. + this.jsData.assign = this.assign; + this.jsData.submission = this.submission; + this.jsData.plugin = this.plugin; + this.jsData.userId = this.userId; + this.jsData.edit = this.edit; + this.jsData.canEdit = this.canEdit; + + if (this.plugin) { + this.getHandlerData(AddonModAssignFeedbackDelegate.getHandlerName(this.plugin.type)); + } + } + + /** + * Invalidate the data. + * + * @return Promise resolved when done. + */ + invalidate(): Promise { + return Promise.resolve(); + } + +} diff --git a/src/core/features/siteplugins/components/assign-feedback/core-siteplugins-assign-feedback.html b/src/core/features/siteplugins/components/assign-feedback/core-siteplugins-assign-feedback.html new file mode 100644 index 000000000..f58bcd913 --- /dev/null +++ b/src/core/features/siteplugins/components/assign-feedback/core-siteplugins-assign-feedback.html @@ -0,0 +1 @@ + diff --git a/src/core/features/siteplugins/components/assign-submission/assign-submission.ts b/src/core/features/siteplugins/components/assign-submission/assign-submission.ts new file mode 100644 index 000000000..d45e62922 --- /dev/null +++ b/src/core/features/siteplugins/components/assign-submission/assign-submission.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 { Component, OnInit, Input } from '@angular/core'; + +import { AddonModAssignAssign, AddonModAssignPlugin, AddonModAssignSubmission } from '@addons/mod/assign/services/assign'; +import { AddonModAssignSubmissionDelegate } from '@addons/mod/assign/services/submission-delegate'; +import { CoreSitePluginsCompileInitComponent } from '@features/siteplugins/classes/compile-init-component'; + +/** + * Component that displays an assign submission plugin created using a site plugin. + */ +@Component({ + selector: 'core-site-plugins-assign-submission', + templateUrl: 'core-siteplugins-assign-submission.html', +}) +export class CoreSitePluginsAssignSubmissionComponent extends CoreSitePluginsCompileInitComponent implements OnInit { + + @Input() assign!: AddonModAssignAssign; // The assignment. + @Input() submission!: AddonModAssignSubmission; // The submission. + @Input() plugin!: AddonModAssignPlugin; // The plugin object. + @Input() edit = false; // Whether the user is editing. + @Input() allowOffline = false; // Whether to allow offline. + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Pass the input and output data to the component. + this.jsData.assign = this.assign; + this.jsData.submission = this.submission; + this.jsData.plugin = this.plugin; + this.jsData.edit = this.edit; + this.jsData.allowOffline = this.allowOffline; + + if (this.plugin) { + this.getHandlerData(AddonModAssignSubmissionDelegate.getHandlerName(this.plugin.type)); + } + } + + /** + * Invalidate the data. + * + * @return Promise resolved when done. + */ + invalidate(): Promise { + return Promise.resolve(); + } + +} diff --git a/src/core/features/siteplugins/components/assign-submission/core-siteplugins-assign-submission.html b/src/core/features/siteplugins/components/assign-submission/core-siteplugins-assign-submission.html new file mode 100644 index 000000000..f58bcd913 --- /dev/null +++ b/src/core/features/siteplugins/components/assign-submission/core-siteplugins-assign-submission.html @@ -0,0 +1 @@ + diff --git a/src/core/features/siteplugins/components/block/block.ts b/src/core/features/siteplugins/components/block/block.ts new file mode 100644 index 000000000..297a5d89b --- /dev/null +++ b/src/core/features/siteplugins/components/block/block.ts @@ -0,0 +1,80 @@ +// (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, OnChanges, ViewChild } from '@angular/core'; + +import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { CoreSitePlugins, CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins'; +import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content'; + +/** + * 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 { + + @ViewChild(CoreSitePluginsPluginContentComponent) content?: CoreSitePluginsPluginContentComponent; + + component?: string; + method?: string; + args?: Record; + initResult?: CoreSitePluginsContent | null; + + constructor() { + super('CoreSitePluginsBlockComponent'); + } + + /** + * Detect changes on input properties. + */ + ngOnChanges(): void { + if (this.component) { + return; + } + + // Initialize the data. + const handlerName = CoreBlockDelegate.getHandlerName(this.block.name); + const handler = CoreSitePlugins.getSitePluginHandler(handlerName); + if (!handler) { + return; + } + + this.component = handler.plugin.component; + this.method = handler.handlerSchema.method; + this.args = { + contextlevel: this.contextLevel, + instanceid: this.instanceId, + blockid: this.block.instanceid, + }; + this.initResult = handler.initResult; + } + + /** + * Invalidate block data. + * + * @return Promise resolved when done. + */ + protected async invalidateContent(): Promise { + if (!this.component || !this.method) { + return; + } + + return CoreSitePlugins.invalidateContent(this.component, this.method, this.args); + } + +} diff --git a/src/core/features/siteplugins/components/block/core-siteplugins-block.html b/src/core/features/siteplugins/components/block/core-siteplugins-block.html new file mode 100644 index 000000000..4198fb8c6 --- /dev/null +++ b/src/core/features/siteplugins/components/block/core-siteplugins-block.html @@ -0,0 +1,3 @@ + + diff --git a/src/core/features/siteplugins/components/components.module.ts b/src/core/features/siteplugins/components/components.module.ts new file mode 100644 index 000000000..1145557fb --- /dev/null +++ b/src/core/features/siteplugins/components/components.module.ts @@ -0,0 +1,71 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { CoreCompileHtmlComponentModule } from '@features/compile/components/compile-html/compile-html.module'; +import { CoreSitePluginsPluginContentComponent } from './plugin-content/plugin-content'; +import { CoreSitePluginsModuleIndexComponent } from './module-index/module-index'; +import { CoreSitePluginsCourseOptionComponent } from './course-option/course-option'; +import { CoreSitePluginsCourseFormatComponent } from './course-format/course-format'; +import { CoreSitePluginsUserProfileFieldComponent } from './user-profile-field/user-profile-field'; +import { CoreSitePluginsQuestionComponent } from './question/question'; +import { CoreSitePluginsQuestionBehaviourComponent } from './question-behaviour/question-behaviour'; +import { CoreSitePluginsQuizAccessRuleComponent } from './quiz-access-rule/quiz-access-rule'; +import { CoreSitePluginsAssignFeedbackComponent } from './assign-feedback/assign-feedback'; +import { CoreSitePluginsAssignSubmissionComponent } from './assign-submission/assign-submission'; +// @todo +// import { CoreSitePluginsWorkshopAssessmentStrategyComponent } from './workshop-assessment-strategy/workshop-assessment-strategy'; +import { CoreSitePluginsBlockComponent } from './block/block'; +import { CoreSitePluginsOnlyTitleBlockComponent } from './only-title-block/only-title-block'; + +@NgModule({ + declarations: [ + CoreSitePluginsPluginContentComponent, + CoreSitePluginsModuleIndexComponent, + CoreSitePluginsBlockComponent, + CoreSitePluginsOnlyTitleBlockComponent, + CoreSitePluginsCourseOptionComponent, + CoreSitePluginsCourseFormatComponent, + CoreSitePluginsUserProfileFieldComponent, + CoreSitePluginsQuestionComponent, + CoreSitePluginsQuestionBehaviourComponent, + CoreSitePluginsQuizAccessRuleComponent, + CoreSitePluginsAssignFeedbackComponent, + CoreSitePluginsAssignSubmissionComponent, + // @todo CoreSitePluginsWorkshopAssessmentStrategyComponent, + ], + imports: [ + CoreSharedModule, + CoreCompileHtmlComponentModule, + ], + providers: [], + exports: [ + CoreSitePluginsPluginContentComponent, + CoreSitePluginsModuleIndexComponent, + CoreSitePluginsBlockComponent, + CoreSitePluginsOnlyTitleBlockComponent, + CoreSitePluginsCourseOptionComponent, + CoreSitePluginsCourseFormatComponent, + CoreSitePluginsUserProfileFieldComponent, + CoreSitePluginsQuestionComponent, + CoreSitePluginsQuestionBehaviourComponent, + CoreSitePluginsQuizAccessRuleComponent, + CoreSitePluginsAssignFeedbackComponent, + CoreSitePluginsAssignSubmissionComponent, + // @todo CoreSitePluginsWorkshopAssessmentStrategyComponent, + ], +}) +export class CoreSitePluginsComponentsModule {} diff --git a/src/core/features/siteplugins/components/course-format/core-siteplugins-course-format.html b/src/core/features/siteplugins/components/course-format/core-siteplugins-course-format.html new file mode 100644 index 000000000..875413506 --- /dev/null +++ b/src/core/features/siteplugins/components/course-format/core-siteplugins-course-format.html @@ -0,0 +1,3 @@ + + diff --git a/src/core/features/siteplugins/components/course-format/course-format.ts b/src/core/features/siteplugins/components/course-format/course-format.ts new file mode 100644 index 000000000..8c8bb7251 --- /dev/null +++ b/src/core/features/siteplugins/components/course-format/course-format.ts @@ -0,0 +1,104 @@ +// (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, OnChanges, Input, ViewChild, Output, EventEmitter } from '@angular/core'; +import { IonRefresher } from '@ionic/angular'; + +import { CoreCourseFormatComponent } from '@features/course/components/format/format'; +import { CoreCourseModuleCompletionData, CoreCourseSectionWithStatus } from '@features/course/services/course-helper'; +import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; +import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; +import { CoreSitePlugins, CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins'; +import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content'; + +/** + * Component that displays the index of a course format site plugin. + */ +@Component({ + selector: 'core-site-plugins-course-format', + templateUrl: 'core-siteplugins-course-format.html', +}) +export class CoreSitePluginsCourseFormatComponent implements OnChanges { + + @Input() course?: CoreCourseAnyCourseData; // The course to render. + @Input() sections?: CoreCourseSectionWithStatus[]; // List of course sections. The status will be calculated in this component. + @Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled. + @Input() initialSectionId?: number; // The section to load first (by ID). + @Input() initialSectionNumber?: number; // The section to load first (by number). + @Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section. + @Output() completionChanged = new EventEmitter(); // Notify when any module completion changes. + + // Special input, allows access to the parent instance properties and methods. + // Please notice that all the other inputs/outputs are also accessible through this instance, so they could be removed. + // However, we decided to keep them to support ngOnChanges and to make templates easier to read. + @Input() coreCourseFormatComponent?: CoreCourseFormatComponent; + + @ViewChild(CoreSitePluginsPluginContentComponent) content?: CoreSitePluginsPluginContentComponent; + + component?: string; + method?: string; + args?: Record; + initResult?: CoreSitePluginsContent | null; + data?: Record; + + /** + * Detect changes on input properties. + */ + ngOnChanges(): void { + if (!this.course || !this.course.format) { + return; + } + + if (!this.component) { + // Initialize the data. + const handlerName = CoreCourseFormatDelegate.getHandlerName(this.course.format); + const handler = CoreSitePlugins.getSitePluginHandler(handlerName); + + if (handler) { + this.component = handler.plugin.component; + this.method = handler.handlerSchema.method; + this.args = { + courseid: this.course.id, + downloadenabled: this.downloadEnabled, + }; + this.initResult = handler.initResult; + } + } + + // Pass input data to the component. + this.data = { + course: this.course, + sections: this.sections, + downloadEnabled: this.downloadEnabled, + initialSectionId: this.initialSectionId, + initialSectionNumber: this.initialSectionNumber, + moduleId: this.moduleId, + completionChanged: this.completionChanged, + coreCourseFormatComponent: this.coreCourseFormatComponent, + }; + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + * @param done Function to call when done. + * @param afterCompletionChange Whether the refresh is due to a completion change. + * @return Promise resolved when done. + */ + async doRefresh(refresher?: CustomEvent, done?: () => void, afterCompletionChange?: boolean): Promise { + await this.content?.refreshContent(afterCompletionChange); + } + +} diff --git a/src/core/features/siteplugins/components/course-option/core-siteplugins-course-option.html b/src/core/features/siteplugins/components/course-option/core-siteplugins-course-option.html new file mode 100644 index 000000000..e63a4ec08 --- /dev/null +++ b/src/core/features/siteplugins/components/course-option/core-siteplugins-course-option.html @@ -0,0 +1,8 @@ + + + + + + + diff --git a/src/core/features/siteplugins/components/course-option/course-option.ts b/src/core/features/siteplugins/components/course-option/course-option.ts new file mode 100644 index 000000000..8d97833a6 --- /dev/null +++ b/src/core/features/siteplugins/components/course-option/course-option.ts @@ -0,0 +1,78 @@ +// (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, ViewChild } from '@angular/core'; + +import { CoreSitePlugins, CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins'; +import { IonRefresher } from '@ionic/angular'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content'; + +/** + * Component that displays the index of a course option site plugin. + */ +@Component({ + selector: 'core-site-plugins-course-option', + templateUrl: 'core-siteplugins-course-option.html', +}) +export class CoreSitePluginsCourseOptionComponent implements OnInit { + + @Input() courseId?: number; + @Input() handlerUniqueName?: string; + + @ViewChild(CoreSitePluginsPluginContentComponent) content?: CoreSitePluginsPluginContentComponent; + + component?: string; + method?: string; + args?: Record; + initResult?: CoreSitePluginsContent | null; + ptrEnabled = true; + + /** + * Component being initialized. + */ + ngOnInit(): void { + if (!this.handlerUniqueName) { + return; + } + + const handler = CoreSitePlugins.getSitePluginHandler(this.handlerUniqueName); + if (!handler) { + return; + } + + this.component = handler.plugin.component; + this.method = handler.handlerSchema.method; + this.args = { + courseid: this.courseId, + }; + this.initResult = handler.initResult; + this.ptrEnabled = !('ptrenabled' in handler.handlerSchema) || + !CoreUtils.isFalseOrZero(handler.handlerSchema.ptrenabled); + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + */ + async refreshData(refresher: IonRefresher): Promise { + try { + await this.content?.refreshContent(false); + } finally { + refresher.complete(); + } + } + +} diff --git a/src/core/features/siteplugins/components/module-index/core-siteplugins-module-index.html b/src/core/features/siteplugins/components/module-index/core-siteplugins-module-index.html new file mode 100644 index 000000000..03f022807 --- /dev/null +++ b/src/core/features/siteplugins/components/module-index/core-siteplugins-module-index.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/core/features/siteplugins/components/module-index/module-index.ts b/src/core/features/siteplugins/components/module-index/module-index.ts new file mode 100644 index 000000000..9f54212df --- /dev/null +++ b/src/core/features/siteplugins/components/module-index/module-index.ts @@ -0,0 +1,205 @@ +// (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 { CoreConstants } from '@/core/constants'; +import { Component, OnInit, OnDestroy, Input, ViewChild } from '@angular/core'; + +import { CoreSiteWSPreSets } from '@classes/site'; +import { CoreCourseHelper, CoreCourseModule } from '@features/course/services/course-helper'; +import { + CoreCourseModuleDelegate, + CoreCourseModuleMainComponent, +} from '@features/course/services/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { + CoreSitePlugins, + CoreSitePluginsContent, + CoreSitePluginsCourseModuleHandlerData, +} from '@features/siteplugins/services/siteplugins'; +import { IonRefresher } from '@ionic/angular'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; +import { CoreEventObserver } from '@singletons/events'; +import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content'; + +/** + * Component that displays the index of a module site plugin. + */ +@Component({ + selector: 'core-site-plugins-module-index', + templateUrl: 'core-siteplugins-module-index.html', +}) +export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, CoreCourseModuleMainComponent { + + @Input() module!: CoreCourseModule; // The module. + @Input() courseId!: number; // Course ID the module belongs to. + @Input() pageTitle?: string; // Current page title. It can be used by the "new-content" directives. + + @ViewChild(CoreSitePluginsPluginContentComponent) content?: CoreSitePluginsPluginContentComponent; + + component?: string; + method?: string; + args?: Record; + initResult?: CoreSitePluginsContent | null; + preSets?: CoreSiteWSPreSets; + + // Data for context menu. + externalUrl?: string; + description?: string; + refreshIcon?: string; + prefetchStatus?: string; + prefetchStatusIcon?: string; + prefetchText?: string; + size?: string; + contextMenuStatusObserver?: CoreEventObserver; + contextFileStatusObserver?: CoreEventObserver; + displayOpenInBrowser = true; + displayDescription = true; + displayRefresh = true; + displayPrefetch = true; + displaySize = true; + ptrEnabled = true; + isDestroyed = false; + + jsData?: Record; // Data to pass to the component. + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.refreshIcon = 'spinner'; + + if (!this.module) { + return; + } + + const handlerName = CoreCourseModuleDelegate.getHandlerName(this.module.modname); + const handler = CoreSitePlugins.getSitePluginHandler(handlerName); + + if (handler) { + this.component = handler.plugin.component; + this.preSets = { componentId: this.module.id }; + this.method = handler.handlerSchema.method; + this.args = { + courseid: this.courseId, + cmid: this.module.id, + }; + this.initResult = handler.initResult; + this.jsData = { + module: this.module, + courseId: this.courseId, + }; + + const handlerSchema = handler.handlerSchema; + + this.displayOpenInBrowser = !CoreUtils.isFalseOrZero(handlerSchema.displayopeninbrowser); + this.displayDescription = !CoreUtils.isFalseOrZero(handlerSchema.displaydescription); + this.displayRefresh = !CoreUtils.isFalseOrZero(handlerSchema.displayrefresh); + this.displayPrefetch = !CoreUtils.isFalseOrZero(handlerSchema.displayprefetch); + this.displaySize = !CoreUtils.isFalseOrZero(handlerSchema.displaysize); + this.ptrEnabled = !CoreUtils.isFalseOrZero(handlerSchema.ptrenabled); + } + + // Get the data for the context menu. + this.description = this.module.description; + this.externalUrl = this.module.url; + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + * @param done Function to call when done. + * @return Promise resolved when done. + */ + async doRefresh(refresher?: CustomEvent | null, done?: () => void): Promise { + if (this.content) { + this.refreshIcon = CoreConstants.ICON_LOADING; + } + + try { + await this.content?.refreshContent(false); + } finally { + refresher?.detail.complete(); + done && done(); + } + } + + /** + * Function called when the data of the site plugin content is loaded. + */ + contentLoaded(refresh: boolean): void { + this.refreshIcon = CoreConstants.ICON_REFRESH; + + // Check if there is a prefetch handler for this type of module. + if (CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(this.module)) { + CoreCourseHelper.fillContextMenu(this, this.module, this.courseId, refresh, this.component); + } + } + + /** + * Function called when starting to load the data of the site plugin content. + */ + contentLoading(): void { + this.refreshIcon = CoreConstants.ICON_LOADING; + } + + /** + * Expand the description. + */ + expandDescription(): void { + CoreTextUtils.viewText(Translate.instant('core.description'), this.description!, { + component: this.component, + componentId: this.module.id, + filter: true, + contextLevel: 'module', + instanceId: this.module.id, + courseId: this.courseId, + }); + } + + /** + * Prefetch the module. + */ + prefetch(): void { + CoreCourseHelper.contextMenuPrefetch(this, this.module, this.courseId); + } + + /** + * Confirm and remove downloaded files. + */ + removeFiles(): void { + CoreCourseHelper.confirmAndRemoveFiles(this.module, this.courseId); + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + } + + /** + * Call a certain function on the component instance. + * + * @param name Name of the function to call. + * @param params List of params to send to the function. + * @return Result of the call. Undefined if no component instance or the function doesn't exist. + */ + callComponentFunction(name: string, params?: unknown[]): unknown | undefined { + return this.content?.callComponentFunction(name, params); + } + +} diff --git a/src/core/features/siteplugins/components/only-title-block/core-siteplugins-only-title-block.html b/src/core/features/siteplugins/components/only-title-block/core-siteplugins-only-title-block.html new file mode 100644 index 000000000..b61dc38bf --- /dev/null +++ b/src/core/features/siteplugins/components/only-title-block/core-siteplugins-only-title-block.html @@ -0,0 +1,5 @@ + + +

{{ title | translate }}

+
+
diff --git a/src/core/features/siteplugins/components/only-title-block/only-title-block.ts b/src/core/features/siteplugins/components/only-title-block/only-title-block.ts new file mode 100644 index 000000000..a492d2166 --- /dev/null +++ b/src/core/features/siteplugins/components/only-title-block/only-title-block.ts @@ -0,0 +1,68 @@ +// (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 '@features/block/classes/base-block-component'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; + +/** + * Component to render blocks with only a title and link. + */ +@Component({ + selector: 'core-siteplugins-only-title-block', + templateUrl: 'core-siteplugins-only-title-block.html', +}) +export class CoreSitePluginsOnlyTitleBlockComponent extends CoreBlockBaseComponent implements OnInit { + + constructor() { + super('CoreSitePluginsOnlyTitleBlockComponent'); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + super.ngOnInit(); + + this.fetchContentDefaultError = 'Error getting ' + (this.block.contents?.title || 'block') + ' data.'; + } + + /** + * Go to the block page. + */ + gotoBlock(): void { + const handlerName = CoreBlockDelegate.getHandlerName(this.block.name); + const handler = CoreSitePlugins.getSitePluginHandler(handlerName); + + if (!handler) { + return; + } + + // @todo + // navCtrl.push('CoreSitePluginsPluginPage', { + // title: this.title, + // component: handler.plugin.component, + // method: handler.handlerSchema.method, + // initResult: handler.initResult, + // args: { + // contextlevel: this.contextLevel, + // instanceid: this.instanceId, + // }, + // ptrEnabled: handler.handlerSchema.ptrenabled, + // }); + } + +} diff --git a/src/core/features/siteplugins/components/plugin-content/core-siteplugins-plugin-content.html b/src/core/features/siteplugins/components/plugin-content/core-siteplugins-plugin-content.html new file mode 100644 index 000000000..b8ff6b1bd --- /dev/null +++ b/src/core/features/siteplugins/components/plugin-content/core-siteplugins-plugin-content.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/core/features/siteplugins/components/plugin-content/plugin-content.ts b/src/core/features/siteplugins/components/plugin-content/plugin-content.ts new file mode 100644 index 000000000..48b4cb749 --- /dev/null +++ b/src/core/features/siteplugins/components/plugin-content/plugin-content.ts @@ -0,0 +1,219 @@ +// (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, Output, EventEmitter, DoCheck, KeyValueDiffers, ViewChild, KeyValueDiffer } from '@angular/core'; +import { CoreSiteWSPreSets } from '@classes/site'; +import { CoreCompileHtmlComponent } from '@features/compile/components/compile-html/compile-html'; +import { CoreSitePlugins, CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins'; +import { CoreDomUtils } from '@services/utils/dom'; +import { Subject } from 'rxjs'; + +/** + * Component to render a site plugin content. + */ +@Component({ + selector: 'core-site-plugins-plugin-content', + templateUrl: 'core-siteplugins-plugin-content.html', +}) +export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck { + + // Get the compile element. Don't set the right type to prevent circular dependencies. + @ViewChild('compile') compileComponent?: CoreCompileHtmlComponent; + + @Input() component!: string; + @Input() method!: string; + @Input() args?: Record; + @Input() initResult?: CoreSitePluginsContent | null; // Result of the init WS call of the handler. + @Input() data?: Record; // Data to pass to the component. + @Input() preSets?: CoreSiteWSPreSets; // The preSets for the WS call. + @Input() pageTitle?: string; // Current page title. It can be used by the "new-content" directives. + @Output() onContentLoaded = new EventEmitter(); // Emits an event when the content is loaded. + @Output() onLoadingContent = new EventEmitter(); // Emits an event when starts to load the content. + + content?: string; // Content. + javascript?: string; // Javascript to execute. + otherData?: Record; // Other data of the content. + dataLoaded = false; + invalidateObservable = new Subject(); // An observable to notify observers when to invalidate data. + jsData?: Record; // Data to pass to the component. + forceCompile?: boolean; // Force compilation on PTR. + + protected differ: KeyValueDiffer; // To detect changes in the data input. + + constructor(differs: KeyValueDiffers) { + this.differ = differs.find([]).create(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.fetchContent(); + } + + /** + * Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays). + */ + ngDoCheck(): void { + if (!this.data || !this.jsData) { + return; + } + + // Check if there's any change in the data object. + const changes = this.differ.diff(this.data); + if (changes) { + this.jsData = Object.assign(this.jsData, this.data); + } + } + + /** + * Fetches the content to render. + * + * @param refresh Whether the user is refreshing. + * @return Promise resolved when done. + */ + async fetchContent(refresh?: boolean): Promise { + this.onLoadingContent.emit(refresh); + + this.forceCompile = false; + + const preSets = Object.assign({}, this.preSets); + preSets.component = preSets.component || this.component; + + try { + const result = await CoreSitePlugins.getContent(this.component, this.method, this.args, preSets); + + this.content = result.templates.length ? result.templates[0].html : ''; // Load first template. + this.javascript = result.javascript; + this.otherData = result.otherdata; + this.data = this.data || {}; + this.forceCompile = true; + + this.jsData = Object.assign(this.data, CoreSitePlugins.createDataForJS(this.initResult, result)); + + // Pass some methods as jsData so they can be called from the template too. + this.jsData.fetchContent = this.fetchContent.bind(this); + this.jsData.openContent = this.openContent.bind(this); + this.jsData.refreshContent = this.refreshContent.bind(this); + this.jsData.updateContent = this.updateContent.bind(this); + + this.onContentLoaded.emit(refresh); + } catch (error) { + // Make it think it's loaded - otherwise it sticks on 'loading' and stops navigation working. + this.content = '
'; + this.onContentLoaded.emit(refresh); + + CoreDomUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true); + } finally { + this.dataLoaded = true; + } + } + + /** + * Open a new page with a new content. + * + * @param title The title to display with the new content. + * @param args New params. + * @param component New component. If not provided, current component + * @param method New method. If not provided, current method + * @param jsData JS variables to pass to the new view so they can be used in the template or JS. + * If true is supplied instead of an object, all initial variables from current page will be copied. + * @param preSets The preSets for the WS call of the new content. + * @param ptrEnabled Whether PTR should be enabled in the new page. Defaults to true. + */ + openContent( + title: string, + args?: Record, + component?: string, + method?: string, + jsData?: Record | boolean, + preSets?: CoreSiteWSPreSets, + ptrEnabled?: boolean, + ): void { + if (jsData === true) { + jsData = this.data; + } + + // @todo + // this.navCtrl.push('CoreSitePluginsPluginPage', { + // title: title, + // component: component || this.component, + // method: method || this.method, + // args: args, + // initResult: this.initResult, + // jsData: jsData, + // preSets: preSets, + // ptrEnabled: ptrEnabled, + // }); + } + + /** + * Refresh the data. + * + * @param showSpinner Whether to show spinner while refreshing. + */ + async refreshContent(showSpinner: boolean = true): Promise { + if (showSpinner) { + this.dataLoaded = false; + } + + this.invalidateObservable.next(); // Notify observers. + + try { + await CoreSitePlugins.invalidateContent(this.component, this.method, this.args); + } finally { + await this.fetchContent(true); + } + } + + /** + * Update the content, usually with a different method or params. + * + * @param args New params. + * @param component New component. If not provided, current component + * @param method New method. If not provided, current method + * @param jsData JS variables to pass to the new view so they can be used in the template or JS. + * @param preSets New preSets to use. If not provided, use current preSets. + */ + updateContent( + args?: Record, + component?: string, + method?: string, + jsData?: Record, + preSets?: CoreSiteWSPreSets, + ): void { + this.component = component || this.component; + this.method = method || this.method; + this.args = args; + this.dataLoaded = false; + this.preSets = preSets || this.preSets; + if (jsData) { + Object.assign(this.data, jsData); + } + + this.fetchContent(); + } + + /** + * Call a certain function on the component instance. + * + * @param name Name of the function to call. + * @param params List of params to send to the function. + * @return Result of the call. Undefined if no component instance or the function doesn't exist. + */ + callComponentFunction(name: string, params?: unknown[]): unknown | undefined { + return this.compileComponent?.callComponentFunction(name, params); + } + +} diff --git a/src/core/features/siteplugins/components/question-behaviour/core-siteplugins-question-behaviour.html b/src/core/features/siteplugins/components/question-behaviour/core-siteplugins-question-behaviour.html new file mode 100644 index 000000000..f58bcd913 --- /dev/null +++ b/src/core/features/siteplugins/components/question-behaviour/core-siteplugins-question-behaviour.html @@ -0,0 +1 @@ + diff --git a/src/core/features/siteplugins/components/question-behaviour/question-behaviour.ts b/src/core/features/siteplugins/components/question-behaviour/question-behaviour.ts new file mode 100644 index 000000000..e6e9fe380 --- /dev/null +++ b/src/core/features/siteplugins/components/question-behaviour/question-behaviour.ts @@ -0,0 +1,68 @@ +// (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, Output, EventEmitter } from '@angular/core'; + +import { CoreQuestionBehaviourDelegate } from '@features/question/services/behaviour-delegate'; +import { CoreQuestionBehaviourButton, CoreQuestionQuestion } from '@features/question/services/question-helper'; +import { CoreSitePluginsCompileInitComponent } from '@features/siteplugins/classes/compile-init-component'; + + +/** + * Component that displays a question behaviour created using a site plugin. + */ +@Component({ + selector: 'core-site-plugins-question-behaviour', + templateUrl: 'core-siteplugins-question-behaviour.html', +}) +export class CoreSitePluginsQuestionBehaviourComponent extends CoreSitePluginsCompileInitComponent implements OnInit { + + @Input() question?: CoreQuestionQuestion; // The question. + @Input() component?: string; // The component the question belongs to. + @Input() componentId?: number; // ID of the component the question belongs to. + @Input() attemptId?: number; // Attempt ID. + @Input() offlineEnabled?: boolean | string; // Whether the question can be answered in offline. + @Input() contextLevel?: string; // The context level. + @Input() contextInstanceId?: number; // The instance ID related to the context. + @Input() courseId?: number; // Course ID the question belongs to (if any). It can be used to improve performance with filters. + @Input() review?: boolean; // Whether the user is in review mode. + @Input() preferredBehaviour?: string; // Preferred behaviour. + @Output() buttonClicked = new EventEmitter(); // Will emit when a behaviour button is clicked. + @Output() onAbort = new EventEmitter(); // Should emit an event if the question should be aborted. + + constructor() { + super(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Pass the input and output data to the component. + this.jsData.question = this.question; + this.jsData.component = this.component; + this.jsData.componentId = this.componentId; + this.jsData.attemptId = this.attemptId; + this.jsData.offlineEnabled = this.offlineEnabled; + this.jsData.contextLevel = this.contextLevel; + this.jsData.contextInstanceId = this.contextInstanceId; + this.jsData.buttonClicked = this.buttonClicked; + this.jsData.onAbort = this.onAbort; + + if (this.question) { + this.getHandlerData(CoreQuestionBehaviourDelegate.getHandlerName(this.preferredBehaviour || '')); + } + } + +} diff --git a/src/core/features/siteplugins/components/question/core-siteplugins-question.html b/src/core/features/siteplugins/components/question/core-siteplugins-question.html new file mode 100644 index 000000000..f58bcd913 --- /dev/null +++ b/src/core/features/siteplugins/components/question/core-siteplugins-question.html @@ -0,0 +1 @@ + diff --git a/src/core/features/siteplugins/components/question/question.ts b/src/core/features/siteplugins/components/question/question.ts new file mode 100644 index 000000000..f071a0afc --- /dev/null +++ b/src/core/features/siteplugins/components/question/question.ts @@ -0,0 +1,67 @@ +// (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, Output, EventEmitter } from '@angular/core'; + +import { AddonModQuizQuestion } from '@features/question/classes/base-question-component'; +import { CoreQuestionDelegate } from '@features/question/services/question-delegate'; +import { CoreQuestionBehaviourButton } from '@features/question/services/question-helper'; +import { CoreSitePluginsCompileInitComponent } from '@features/siteplugins/classes/compile-init-component'; + +/** + * Component that displays a question created using a site plugin. + */ +@Component({ + selector: 'core-site-plugins-question', + templateUrl: 'core-siteplugins-question.html', +}) +export class CoreSitePluginsQuestionComponent extends CoreSitePluginsCompileInitComponent implements OnInit { + + @Input() question?: AddonModQuizQuestion; // The question to render. + @Input() component?: string; // The component the question belongs to. + @Input() componentId?: number; // ID of the component the question belongs to. + @Input() attemptId?: number; // Attempt ID. + @Input() offlineEnabled?: boolean | string; // Whether the question can be answered in offline. + @Input() contextLevel?: string; // The context level. + @Input() contextInstanceId?: number; // The instance ID related to the context. + @Input() courseId?: number; // Course ID the question belongs to (if any). It can be used to improve performance with filters. + @Input() review?: boolean; // Whether the user is in review mode. + @Input() preferredBehaviour?: string; // Preferred behaviour. + @Output() buttonClicked = new EventEmitter(); // Will emit when a behaviour button is clicked. + @Output() onAbort = new EventEmitter(); // Should emit an event if the question should be aborted. + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Pass the input and output data to the component. + this.jsData.question = this.question; + this.jsData.component = this.component; + this.jsData.componentId = this.componentId; + this.jsData.attemptId = this.attemptId; + this.jsData.offlineEnabled = this.offlineEnabled; + this.jsData.contextLevel = this.contextLevel; + this.jsData.contextInstanceId = this.contextInstanceId; + this.jsData.courseId = this.courseId; + this.jsData.review = this.review; + this.jsData.preferredBehaviour = this.preferredBehaviour; + this.jsData.buttonClicked = this.buttonClicked; + this.jsData.onAbort = this.onAbort; + + if (this.question) { + this.getHandlerData(CoreQuestionDelegate.getHandlerName('qtype_' + this.question.type)); + } + } + +} diff --git a/src/core/features/siteplugins/components/quiz-access-rule/core-siteplugins-quiz-access-rule.html b/src/core/features/siteplugins/components/quiz-access-rule/core-siteplugins-quiz-access-rule.html new file mode 100644 index 000000000..f58bcd913 --- /dev/null +++ b/src/core/features/siteplugins/components/quiz-access-rule/core-siteplugins-quiz-access-rule.html @@ -0,0 +1 @@ + diff --git a/src/core/features/siteplugins/components/quiz-access-rule/quiz-access-rule.ts b/src/core/features/siteplugins/components/quiz-access-rule/quiz-access-rule.ts new file mode 100644 index 000000000..874d6fbf6 --- /dev/null +++ b/src/core/features/siteplugins/components/quiz-access-rule/quiz-access-rule.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 { Component, OnInit, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { AddonModQuizAccessRuleDelegate } from '@addons/mod/quiz/services/access-rules-delegate'; +import { AddonModQuizAttemptWSData, AddonModQuizQuizWSData } from '@addons/mod/quiz/services/quiz'; +import { CoreSitePluginsCompileInitComponent } from '@features/siteplugins/classes/compile-init-component'; + +/** + * Component that displays a quiz access rule created using a site plugin. + */ +@Component({ + selector: 'core-site-plugins-quiz-access-rule', + templateUrl: 'core-siteplugins-quiz-access-rule.html', +}) +export class CoreSitePluginsQuizAccessRuleComponent extends CoreSitePluginsCompileInitComponent implements OnInit { + + @Input() rule?: string; // The name of the rule. + @Input() quiz?: AddonModQuizQuizWSData; // The quiz the rule belongs to. + @Input() attempt?: AddonModQuizAttemptWSData; // The attempt being started/continued. + @Input() prefetch?: boolean; // Whether the user is prefetching the quiz. + @Input() siteId?: string; // Site ID. + @Input() form?: FormGroup; // Form where to add the form control. + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Pass the input and output data to the component. + this.jsData.rule = this.rule; + this.jsData.quiz = this.quiz; + this.jsData.attempt = this.attempt; + this.jsData.prefetch = this.prefetch; + this.jsData.siteId = this.siteId; + this.jsData.form = this.form; + + if (this.rule) { + this.getHandlerData(AddonModQuizAccessRuleDelegate.getHandlerName(this.rule)); + } + } + +} diff --git a/src/core/features/siteplugins/components/user-profile-field/core-siteplugins-user-profile-field.html b/src/core/features/siteplugins/components/user-profile-field/core-siteplugins-user-profile-field.html new file mode 100644 index 000000000..f58bcd913 --- /dev/null +++ b/src/core/features/siteplugins/components/user-profile-field/core-siteplugins-user-profile-field.html @@ -0,0 +1 @@ + diff --git a/src/core/features/siteplugins/components/user-profile-field/user-profile-field.ts b/src/core/features/siteplugins/components/user-profile-field/user-profile-field.ts new file mode 100644 index 000000000..6b2240675 --- /dev/null +++ b/src/core/features/siteplugins/components/user-profile-field/user-profile-field.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 { Component, OnInit, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { AuthEmailSignupProfileField } from '@features/login/services/login-helper'; +import { CoreSitePluginsCompileInitComponent } from '@features/siteplugins/classes/compile-init-component'; +import { CoreUserProfileField } from '@features/user/services/user'; +import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate'; + + +/** + * Component that displays a user profile field created using a site plugin. + */ +@Component({ + selector: 'core-site-plugins-user-profile-field', + templateUrl: 'core-siteplugins-user-profile-field.html', +}) +export class CoreSitePluginsUserProfileFieldComponent extends CoreSitePluginsCompileInitComponent implements OnInit { + + @Input() field?: AuthEmailSignupProfileField | CoreUserProfileField; // The profile field to be rendered. + @Input() signup = false; // True if editing the field in signup. Defaults to false. + @Input() edit = false; // True if editing the field. Defaults to false. + @Input() disabled = false; // True if disabled. Defaults to false. + @Input() form?: FormGroup; // Form where to add the form control. Required if edit=true or signup=true. + @Input() registerAuth?: string; // Register auth method. E.g. 'email'. + @Input() contextLevel?: string; // The context level. + @Input() contextInstanceId?: number; // The instance ID related to the context. + @Input() courseId?: number; // Course ID the field belongs to (if any). It can be used to improve performance with filters. + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Pass the input data to the component. + this.jsData.field = this.field; + this.jsData.signup = this.signup; + this.jsData.edit = this.edit; + this.jsData.disabled = this.disabled; + this.jsData.form = this.form; + this.jsData.registerAuth = this.registerAuth; + + if (this.field) { + const type = 'type' in this.field ? this.field.type : this.field.datatype; + this.getHandlerData(CoreUserProfileFieldDelegate.getHandlerName(type || '')); + } + } + +} diff --git a/src/core/features/user/classes/base-profilefield-component.ts b/src/core/features/user/classes/base-profilefield-component.ts index 88f390862..f99fc225f 100644 --- a/src/core/features/user/classes/base-profilefield-component.ts +++ b/src/core/features/user/classes/base-profilefield-component.ts @@ -27,12 +27,15 @@ import { CoreUserProfileField } from '@features/user/services/user'; export abstract class CoreUserProfileFieldBaseComponent implements OnInit { @Input() field?: AuthEmailSignupProfileField | CoreUserProfileField; // The profile field to be rendered. + @Input() signup = false; // True if editing the field in signup. Defaults to false. @Input() edit = false; // True if editing the field. Defaults to false. @Input() disabled = false; // True if disabled. Defaults to false. - @Input() form?: FormGroup; // Form where to add the form control. + @Input() form?: FormGroup; // Form where to add the form control. Required if edit=true or signup=true. + @Input() registerAuth?: string; // Register auth method. E.g. 'email'. @Input() contextLevel?: string; // The context level. @Input() contextInstanceId?: number; // The instance ID related to the context. - @Input() courseId?: number; // The course the field belongs to (if any). + @Input() courseId?: number; // Course ID the field belongs to (if any). It can be used to improve performance with filters. + control?: FormControl; modelName = ''; diff --git a/src/core/features/user/components/user-profile-field/user-profile-field.ts b/src/core/features/user/components/user-profile-field/user-profile-field.ts index 9f32d4591..c63c5dfae 100644 --- a/src/core/features/user/components/user-profile-field/user-profile-field.ts +++ b/src/core/features/user/components/user-profile-field/user-profile-field.ts @@ -16,6 +16,7 @@ import { Component, Input, OnInit, Type } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { AuthEmailSignupProfileField } from '@features/login/services/login-helper'; +import { CoreUserProfileField } from '@features/user/services/user'; import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate'; import { CoreUtils } from '@services/utils/utils'; @@ -28,7 +29,7 @@ import { CoreUtils } from '@services/utils/utils'; }) export class CoreUserProfileFieldComponent implements OnInit { - @Input() field?: AuthEmailSignupProfileField; // The profile field to be rendered. + @Input() field?: AuthEmailSignupProfileField | CoreUserProfileField; // The profile field to be rendered. @Input() signup = false; // True if editing the field in signup. Defaults to false. @Input() edit = false; // True if editing the field. Defaults to false. @Input() form?: FormGroup; // Form where to add the form control. Required if edit=true or signup=true. @@ -54,7 +55,7 @@ export class CoreUserProfileFieldComponent implements OnInit { this.data.edit = CoreUtils.isTrueOrOne(this.edit); if (this.edit) { this.data.signup = CoreUtils.isTrueOrOne(this.signup); - this.data.disabled = CoreUtils.isTrueOrOne(this.field.locked); + this.data.disabled = 'locked' in this.field && CoreUtils.isTrueOrOne(this.field.locked); this.data.form = this.form; this.data.registerAuth = this.registerAuth; this.data.contextLevel = this.contextLevel; @@ -66,7 +67,7 @@ export class CoreUserProfileFieldComponent implements OnInit { } export type CoreUserProfileFieldComponentData = { - field?: AuthEmailSignupProfileField; + field?: AuthEmailSignupProfileField | CoreUserProfileField; edit?: boolean; signup?: boolean; disabled?: boolean;