From 9cc7c73b31336f56696d8a5f9f02009c2b0ab1b1 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 1 Mar 2021 16:12:38 +0100 Subject: [PATCH 01/10] MOBILE-3664 compile: Implement compile --- src/addons/badges/badges.module.ts | 7 +- src/addons/calendar/calendar.module.ts | 14 +- .../messageoutput/messageoutput.module.ts | 7 +- src/addons/messages/messages.module.ts | 12 +- src/addons/mod/assign/assign.module.ts | 17 +- src/addons/mod/book/book.module.ts | 7 +- src/addons/mod/folder/folder.module.ts | 9 +- src/addons/mod/forum/forum.module.ts | 13 +- src/addons/mod/imscp/imscp.module.ts | 7 +- src/addons/mod/lesson/lesson.module.ts | 13 +- src/addons/mod/page/page.module.ts | 9 +- src/addons/mod/quiz/quiz.module.ts | 15 +- src/addons/mod/resource/resource.module.ts | 7 +- src/addons/mod/url/url.module.ts | 9 +- .../notifications/notifications.module.ts | 9 +- .../privatefiles/privatefiles.module.ts | 9 +- src/app/app.module.ts | 5 +- src/core/core.module.ts | 59 ++- src/core/features/block/block.module.ts | 18 +- .../block/services/handlers/default-block.ts | 2 +- src/core/features/comments/comments.module.ts | 12 +- .../compile-html/compile-html.module.ts | 30 ++ .../components/compile-html/compile-html.ts | 291 +++++++++++++ src/core/features/compile/services/compile.ts | 390 ++++++++++++++++++ .../contentlinks/contentlinks.module.ts | 31 ++ src/core/features/course/course.module.ts | 25 +- src/core/features/courses/courses.module.ts | 11 +- src/core/features/editor/editor.module.ts | 7 +- src/core/features/emulator/emulator.module.ts | 27 ++ .../fileuploader/fileuploader.module.ts | 11 +- src/core/features/filter/filter.module.ts | 31 ++ src/core/features/grades/grades.module.ts | 9 +- src/core/features/h5p/h5p.module.ts | 7 +- src/core/features/login/login.module.ts | 5 + src/core/features/mainmenu/mainmenu.module.ts | 10 +- .../pushnotifications.module.ts | 10 +- src/core/features/question/question.module.ts | 34 ++ src/core/features/search/search.module.ts | 7 +- src/core/features/settings/settings.module.ts | 9 +- src/core/features/sitehome/sitehome.module.ts | 7 +- src/core/features/tag/tag.module.ts | 11 +- src/core/features/user/user.module.ts | 18 +- src/core/features/xapi/xapi.module.ts | 9 +- 43 files changed, 1200 insertions(+), 50 deletions(-) create mode 100644 src/core/features/compile/components/compile-html/compile-html.module.ts create mode 100644 src/core/features/compile/components/compile-html/compile-html.ts create mode 100644 src/core/features/compile/services/compile.ts create mode 100644 src/core/features/contentlinks/contentlinks.module.ts create mode 100644 src/core/features/filter/filter.module.ts create mode 100644 src/core/features/question/question.module.ts diff --git a/src/addons/badges/badges.module.ts b/src/addons/badges/badges.module.ts index fecca7062..de0fd76b9 100644 --- a/src/addons/badges/badges.module.ts +++ b/src/addons/badges/badges.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { AddonBadgesMyBadgesLinkHandler } from './services/handlers/mybadges-link'; @@ -23,6 +23,11 @@ import { AddonBadgesUserHandler } from './services/handlers/user'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate'; import { AddonBadgesPushClickHandler } from './services/handlers/push-click'; +import { AddonBadgesProvider } from './services/badges'; + +export const ADDON_BADGES_SERVICES: Type[] = [ + AddonBadgesProvider, +]; const mainMenuRoutes: Routes = [ { diff --git a/src/addons/calendar/calendar.module.ts b/src/addons/calendar/calendar.module.ts index 8cb2dcd45..0017176f0 100644 --- a/src/addons/calendar/calendar.module.ts +++ b/src/addons/calendar/calendar.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module'; @@ -27,8 +27,18 @@ import { CORE_SITE_SCHEMAS } from '@services/sites'; import { CALENDAR_SITE_SCHEMA } from './services/database/calendar'; import { CALENDAR_OFFLINE_SITE_SCHEMA } from './services/database/calendar-offline'; import { AddonCalendarComponentsModule } from './components/components.module'; -import { AddonCalendar } from './services/calendar'; +import { AddonCalendar, AddonCalendarProvider } from './services/calendar'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { AddonCalendarOfflineProvider } from './services/calendar-offline'; +import { AddonCalendarHelperProvider } from './services/calendar-helper'; +import { AddonCalendarSyncProvider } from './services/calendar-sync'; + +export const ADDON_CALENDAR_SERVICES: Type[] = [ + AddonCalendarProvider, + AddonCalendarOfflineProvider, + AddonCalendarHelperProvider, + AddonCalendarSyncProvider, +]; const mainMenuChildrenRoutes: Routes = [ { diff --git a/src/addons/messageoutput/messageoutput.module.ts b/src/addons/messageoutput/messageoutput.module.ts index c12500d44..ac418e76b 100644 --- a/src/addons/messageoutput/messageoutput.module.ts +++ b/src/addons/messageoutput/messageoutput.module.ts @@ -12,9 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NgModule } from '@angular/core'; +import { NgModule, Type } from '@angular/core'; import { AddonMessageOutputAirnotifierModule } from './airnotifier/airnotifier.module'; +import { AddonMessageOutputDelegateService } from './services/messageoutput-delegate'; + +export const ADDON_MESSAGEOUTPUT_SERVICES: Type[] = [ + AddonMessageOutputDelegateService, +]; @NgModule({ declarations: [ diff --git a/src/addons/messages/messages.module.ts b/src/addons/messages/messages.module.ts index d097a4363..251b053e9 100644 --- a/src/addons/messages/messages.module.ts +++ b/src/addons/messages/messages.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module'; @@ -33,9 +33,17 @@ import { AddonMessagesPushClickHandler } from './services/handlers/push-click'; import { CoreUserDelegate } from '@features/user/services/user-delegate'; import { AddonMessagesSendMessageUserHandler } from './services/handlers/user-send-message'; import { Network, NgZone } from '@singletons'; -import { AddonMessagesSync } from './services/messages-sync'; +import { AddonMessagesSync, AddonMessagesSyncProvider } from './services/messages-sync'; import { AddonMessagesSyncCronHandler } from './services/handlers/sync-cron'; import { CoreSitePreferencesRoutingModule } from '@features/settings/pages/site/site-routing'; +import { AddonMessagesProvider } from './services/messages'; +import { AddonMessagesOfflineProvider } from './services/messages-offline'; + +export const ADDON_MESSAGES_SERVICES: Type[] = [ + AddonMessagesProvider, + AddonMessagesOfflineProvider, + AddonMessagesSyncProvider, +]; const mainMenuChildrenRoutes: Routes = [ { diff --git a/src/addons/mod/assign/assign.module.ts b/src/addons/mod/assign/assign.module.ts index c7b8f32fb..46bd253e9 100644 --- a/src/addons/mod/assign/assign.module.ts +++ b/src/addons/mod/assign/assign.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; @@ -23,15 +23,30 @@ import { CoreCronDelegate } from '@services/cron'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { AddonModAssignComponentsModule } from './components/components.module'; import { AddonModAssignFeedbackModule } from './feedback/feedback.module'; +import { AddonModAssignProvider } from './services/assign'; +import { AddonModAssignHelperProvider } from './services/assign-helper'; +import { AddonModAssignOfflineProvider } from './services/assign-offline'; +import { AddonModAssignSyncProvider } from './services/assign-sync'; import { OFFLINE_SITE_SCHEMA } from './services/database/assign'; +import { AddonModAssignFeedbackDelegateService } from './services/feedback-delegate'; import { AddonModAssignIndexLinkHandler } from './services/handlers/index-link'; import { AddonModAssignListLinkHandler } from './services/handlers/list-link'; import { AddonModAssignModuleHandler, AddonModAssignModuleHandlerService } from './services/handlers/module'; import { AddonModAssignPrefetchHandler } from './services/handlers/prefetch'; import { AddonModAssignPushClickHandler } from './services/handlers/push-click'; import { AddonModAssignSyncCronHandler } from './services/handlers/sync-cron'; +import { AddonModAssignSubmissionDelegateService } from './services/submission-delegate'; import { AddonModAssignSubmissionModule } from './submission/submission.module'; +export const ADDON_MOD_ASSIGN_SERVICES: Type[] = [ + AddonModAssignProvider, + AddonModAssignOfflineProvider, + AddonModAssignSyncProvider, + AddonModAssignHelperProvider, + AddonModAssignFeedbackDelegateService, + AddonModAssignSubmissionDelegateService, +]; + const routes: Routes = [ { path: AddonModAssignModuleHandlerService.PAGE_NAME, diff --git a/src/addons/mod/book/book.module.ts b/src/addons/mod/book/book.module.ts index d5b2b5f3a..3123cf6b6 100644 --- a/src/addons/mod/book/book.module.ts +++ b/src/addons/mod/book/book.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; @@ -25,6 +25,11 @@ import { AddonModBookIndexLinkHandler } from './services/handlers/index-link'; import { AddonModBookListLinkHandler } from './services/handlers/list-link'; import { AddonModBookPrefetchHandler } from './services/handlers/prefetch'; import { AddonModBookTagAreaHandler } from './services/handlers/tag-area'; +import { AddonModBookProvider } from './services/book'; + +export const ADDON_MOD_BOOK_SERVICES: Type[] = [ + AddonModBookProvider, +]; const routes: Routes = [ { diff --git a/src/addons/mod/folder/folder.module.ts b/src/addons/mod/folder/folder.module.ts index fb5395403..77aa6c9cd 100644 --- a/src/addons/mod/folder/folder.module.ts +++ b/src/addons/mod/folder/folder.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; @@ -20,12 +20,19 @@ import { CoreCourseModulePrefetchDelegate } from '@features/course/services/modu import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; import { AddonModFolderComponentsModule } from './components/components.module'; +import { AddonModFolderProvider } from './services/folder'; +import { AddonModFolderHelperProvider } from './services/folder-helper'; import { AddonModFolderIndexLinkHandler } from './services/handlers/index-link'; import { AddonModFolderListLinkHandler } from './services/handlers/list-link'; import { AddonModFolderModuleHandler, AddonModFolderModuleHandlerService } from './services/handlers/module'; import { AddonModFolderPluginFileHandler } from './services/handlers/pluginfile'; import { AddonModFolderPrefetchHandler } from './services/handlers/prefetch'; +export const ADDON_MOD_FOLDER_SERVICES: Type[] = [ + AddonModFolderProvider, + AddonModFolderHelperProvider, +]; + const routes: Routes = [ { path: AddonModFolderModuleHandlerService.PAGE_NAME, diff --git a/src/addons/mod/forum/forum.module.ts b/src/addons/mod/forum/forum.module.ts index d5ca65ae1..62acfa715 100644 --- a/src/addons/mod/forum/forum.module.ts +++ b/src/addons/mod/forum/forum.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { conditionalRoutes } from '@/app/app-routing.module'; @@ -38,6 +38,17 @@ import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate'; import { AddonModForumTagAreaHandler } from './services/handlers/tag-area'; import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate'; import { AddonModForumPushClickHandler } from './services/handlers/push-click'; +import { AddonModForumProvider } from './services/forum'; +import { AddonModForumOfflineProvider } from './services/offline'; +import { AddonModForumHelperProvider } from './services/helper'; +import { AddonModForumSyncProvider } from './services/sync'; + +export const ADDON_MOD_FORUM_SERVICES: Type[] = [ + AddonModForumProvider, + AddonModForumOfflineProvider, + AddonModForumHelperProvider, + AddonModForumSyncProvider, +]; const mainMenuRoutes: Routes = [ { diff --git a/src/addons/mod/imscp/imscp.module.ts b/src/addons/mod/imscp/imscp.module.ts index 8b3d2ff08..8684244f8 100644 --- a/src/addons/mod/imscp/imscp.module.ts +++ b/src/addons/mod/imscp/imscp.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; @@ -25,6 +25,11 @@ import { AddonModImscpListLinkHandler } from './services/handlers/list-link'; import { AddonModImscpModuleHandler, AddonModImscpModuleHandlerService } from './services/handlers/module'; import { AddonModImscpPluginFileHandler } from './services/handlers/pluginfile'; import { AddonModImscpPrefetchHandler } from './services/handlers/prefetch'; +import { AddonModImscpProvider } from './services/imscp'; + +export const ADDON_MOD_IMSCP_SERVICES: Type[] = [ + AddonModImscpProvider, +]; const routes: Routes = [ { diff --git a/src/addons/mod/lesson/lesson.module.ts b/src/addons/mod/lesson/lesson.module.ts index ac90cef84..6ec9fd6e7 100644 --- a/src/addons/mod/lesson/lesson.module.ts +++ b/src/addons/mod/lesson/lesson.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; @@ -32,6 +32,17 @@ import { AddonModLessonPrefetchHandler } from './services/handlers/prefetch'; import { AddonModLessonPushClickHandler } from './services/handlers/push-click'; import { AddonModLessonReportLinkHandler } from './services/handlers/report-link'; import { AddonModLessonSyncCronHandler } from './services/handlers/sync-cron'; +import { AddonModLessonProvider } from './services/lesson'; +import { AddonModLessonHelperProvider } from './services/lesson-helper'; +import { AddonModLessonOfflineProvider } from './services/lesson-offline'; +import { AddonModLessonSyncProvider } from './services/lesson-sync'; + +export const ADDON_MOD_LESSON_SERVICES: Type[] = [ + AddonModLessonProvider, + AddonModLessonOfflineProvider, + AddonModLessonSyncProvider, + AddonModLessonHelperProvider, +]; const routes: Routes = [ { diff --git a/src/addons/mod/page/page.module.ts b/src/addons/mod/page/page.module.ts index ffeb2558f..10bf39c71 100644 --- a/src/addons/mod/page/page.module.ts +++ b/src/addons/mod/page/page.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; @@ -25,6 +25,13 @@ import { AddonModPageListLinkHandler } from './services/handlers/list-link'; import { AddonModPageModuleHandler, AddonModPageModuleHandlerService } from './services/handlers/module'; import { AddonModPagePluginFileHandler } from './services/handlers/pluginfile'; import { AddonModPagePrefetchHandler } from './services/handlers/prefetch'; +import { AddonModPageProvider } from './services/page'; +import { AddonModPageHelperProvider } from './services/page-helper'; + +export const ADDON_MOD_PAGE_SERVICES: Type[] = [ + AddonModPageProvider, + AddonModPageHelperProvider, +]; const routes: Routes = [ { diff --git a/src/addons/mod/quiz/quiz.module.ts b/src/addons/mod/quiz/quiz.module.ts index b1e7f6401..11ecdf0ec 100644 --- a/src/addons/mod/quiz/quiz.module.ts +++ b/src/addons/mod/quiz/quiz.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; @@ -24,6 +24,7 @@ import { CoreCronDelegate } from '@services/cron'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { AddonModQuizAccessRulesModule } from './accessrules/accessrules.module'; import { AddonModQuizComponentsModule } from './components/components.module'; +import { AddonModQuizAccessRuleDelegateService } from './services/access-rules-delegate'; import { SITE_SCHEMA } from './services/database/quiz'; import { AddonModQuizGradeLinkHandler } from './services/handlers/grade-link'; import { AddonModQuizIndexLinkHandler } from './services/handlers/index-link'; @@ -33,6 +34,18 @@ import { AddonModQuizPrefetchHandler } from './services/handlers/prefetch'; import { AddonModQuizPushClickHandler } from './services/handlers/push-click'; import { AddonModQuizReviewLinkHandler } from './services/handlers/review-link'; import { AddonModQuizSyncCronHandler } from './services/handlers/sync-cron'; +import { AddonModQuizProvider } from './services/quiz'; +import { AddonModQuizHelperProvider } from './services/quiz-helper'; +import { AddonModQuizOfflineProvider } from './services/quiz-offline'; +import { AddonModQuizSyncProvider } from './services/quiz-sync'; + +export const ADDON_MOD_QUIZ_SERVICES: Type[] = [ + AddonModQuizAccessRuleDelegateService, + AddonModQuizProvider, + AddonModQuizOfflineProvider, + AddonModQuizHelperProvider, + AddonModQuizSyncProvider, +]; const routes: Routes = [ { diff --git a/src/addons/mod/resource/resource.module.ts b/src/addons/mod/resource/resource.module.ts index a546a08f6..3cecc5ec1 100644 --- a/src/addons/mod/resource/resource.module.ts +++ b/src/addons/mod/resource/resource.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; @@ -25,6 +25,11 @@ import { AddonModResourceListLinkHandler } from './services/handlers/list-link'; import { AddonModResourceModuleHandlerService, AddonModResourceModuleHandler } from './services/handlers/module'; import { AddonModResourcePluginFileHandler } from './services/handlers/pluginfile'; import { AddonModResourcePrefetchHandler } from './services/handlers/prefetch'; +import { AddonModResourceProvider } from './services/resource'; + +export const ADDON_MOD_RESOURCE_SERVICES: Type[] = [ + AddonModResourceProvider, +]; const routes: Routes = [ { diff --git a/src/addons/mod/url/url.module.ts b/src/addons/mod/url/url.module.ts index 072753931..7a30d7856 100644 --- a/src/addons/mod/url/url.module.ts +++ b/src/addons/mod/url/url.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; @@ -23,6 +23,13 @@ import { AddonModUrlIndexLinkHandler } from './services/handlers/index-link'; import { AddonModUrlListLinkHandler } from './services/handlers/list-link'; import { AddonModUrlModuleHandler, AddonModUrlModuleHandlerService } from './services/handlers/module'; import { AddonModUrlPrefetchHandler } from './services/handlers/prefetch'; +import { AddonModUrlProvider } from './services/url'; +import { AddonModUrlHelperProvider } from './services/url-helper'; + +export const ADDON_MOD_URL_SERVICES: Type[] = [ + AddonModUrlProvider, + AddonModUrlHelperProvider, +]; const routes: Routes = [ { diff --git a/src/addons/notifications/notifications.module.ts b/src/addons/notifications/notifications.module.ts index 01abc8f39..c81f9813b 100644 --- a/src/addons/notifications/notifications.module.ts +++ b/src/addons/notifications/notifications.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreCronDelegate } from '@services/cron'; @@ -26,6 +26,13 @@ import { AddonNotificationsCronHandler } from './services/handlers/cron'; import { AddonNotificationsPushClickHandler } from './services/handlers/push-click'; import { AddonNotificationsSettingsHandler, AddonNotificationsSettingsHandlerService } from './services/handlers/settings'; import { CoreSitePreferencesRoutingModule } from '@features/settings/pages/site/site-routing'; +import { AddonNotificationsProvider } from './services/notifications'; +import { AddonNotificationsHelperProvider } from './services/notifications-helper'; + +export const ADDON_NOTIFICATIONS_SERVICES: Type[] = [ + AddonNotificationsProvider, + AddonNotificationsHelperProvider, +]; const routes: Routes = [ { diff --git a/src/addons/privatefiles/privatefiles.module.ts b/src/addons/privatefiles/privatefiles.module.ts index b329899ea..48806c006 100644 --- a/src/addons/privatefiles/privatefiles.module.ts +++ b/src/addons/privatefiles/privatefiles.module.ts @@ -12,13 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate'; import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module'; import { AddonPrivateFilesMainMenuHandler, AddonPrivateFilesMainMenuHandlerService } from './services/handlers/mainmenu'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { AddonPrivateFilesProvider } from './services/privatefiles'; +import { AddonPrivateFilesHelperProvider } from './services/privatefiles-helper'; + +export const ADDON_PRIVATEFILES_SERVICES: Type[] = [ + AddonPrivateFilesProvider, + AddonPrivateFilesHelperProvider, +]; const routes: Routes = [ { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b21e7da51..6a1b4a0e8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NgModule } from '@angular/core'; +import { COMPILER_OPTIONS, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouteReuseStrategy } from '@angular/router'; @@ -28,6 +28,7 @@ import { AddonsModule } from '@/addons/addons.module'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; +import { JitCompilerFactory } from '@angular/platform-browser-dynamic'; // For translate loader. AoT requires an exported function for factories. export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { @@ -55,6 +56,8 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, + { provide: COMPILER_OPTIONS, useValue: {}, multi: true }, + { provide: JitCompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS] }, ], bootstrap: [AppComponent], }) diff --git a/src/core/core.module.ts b/src/core/core.module.ts index 55dd2b93d..ec8c88235 100644 --- a/src/core/core.module.ts +++ b/src/core/core.module.ts @@ -13,7 +13,7 @@ // limitations under the License. import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import { ApplicationInitStatus, Injector, NgModule } from '@angular/core'; +import { ApplicationInitStatus, Injector, NgModule, Type } from '@angular/core'; import { CoreApplicationInitStatus } from './classes/application-init-status'; import { CoreFeaturesModule } from './features/features.module'; @@ -21,6 +21,63 @@ import { CoreInterceptor } from './classes/interceptor'; import { getDatabaseProviders } from './services/database'; import { getInitializerProviders } from './initializers'; +import { CoreDbProvider } from '@services/db'; +import { CoreAppProvider } from '@services/app'; +import { CoreConfigProvider } from '@services/config'; +import { CoreLangProvider } from '@services/lang'; +import { CoreTextUtilsProvider } from '@services/utils/text'; +import { CoreDomUtilsProvider } from '@services/utils/dom'; +import { CoreIframeUtilsProvider } from '@services/utils/iframe'; +import { CoreTimeUtilsProvider } from '@services/utils/time'; +import { CoreUrlUtilsProvider } from '@services/utils/url'; +import { CoreUtilsProvider } from '@services/utils/utils'; +import { CoreMimetypeUtilsProvider } from '@services/utils/mimetype'; +import { CoreFileProvider } from '@services/file'; +import { CoreWSProvider } from '@services/ws'; +import { CoreSitesProvider } from '@services/sites'; +import { CoreLocalNotificationsProvider } from '@services/local-notifications'; +import { CoreGroupsProvider } from '@services/groups'; +import { CoreCronDelegateService } from '@services/cron'; +import { CoreFileSessionProvider } from '@services/file-session'; +import { CoreFilepoolProvider } from '@services/filepool'; +import { CoreUpdateManagerProvider } from '@services/update-manager'; +import { CorePluginFileDelegateService } from '@services/plugin-file-delegate'; +import { CoreSyncProvider } from '@services/sync'; +import { CoreFileHelperProvider } from '@services/file-helper'; +import { CoreGeolocationProvider } from '@services/geolocation'; +import { CoreNavigatorService } from '@services/navigator'; +import { CoreScreenService } from '@services/screen'; + +export const CORE_SERVICES: Type[] = [ + CoreAppProvider, + CoreConfigProvider, + CoreCronDelegateService, + // @todo CoreCustomURLSchemesProvider, + CoreDbProvider, + CoreFileHelperProvider, + CoreFileSessionProvider, + CoreFileProvider, + CoreFilepoolProvider, + CoreGeolocationProvider, + CoreGroupsProvider, + CoreLangProvider, + CoreLocalNotificationsProvider, + CoreNavigatorService, + CorePluginFileDelegateService, + CoreScreenService, + CoreSitesProvider, + CoreSyncProvider, + CoreUpdateManagerProvider, + CoreDomUtilsProvider, + CoreIframeUtilsProvider, + CoreMimetypeUtilsProvider, + CoreTextUtilsProvider, + CoreTimeUtilsProvider, + CoreUrlUtilsProvider, + CoreUtilsProvider, + CoreWSProvider, +]; + @NgModule({ imports: [ CoreFeaturesModule, diff --git a/src/core/features/block/block.module.ts b/src/core/features/block/block.module.ts index 8092c0dbe..27a447f09 100644 --- a/src/core/features/block/block.module.ts +++ b/src/core/features/block/block.module.ts @@ -12,13 +12,17 @@ // 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'; +import { NgModule, Type } from '@angular/core'; + +import { CoreBlockDelegateService } from './services/block-delegate'; +import { CoreBlockHelperProvider } from './services/block-helper'; + +export const CORE_BLOCK_SERVICES: Type[] = [ + CoreBlockDelegateService, + CoreBlockHelperProvider, +]; @NgModule({ - providers: [ - CoreBlockDefaultHandler, - ], + providers: [], }) -export class CoreBlockModule { -} +export class CoreBlockModule {} diff --git a/src/core/features/block/services/handlers/default-block.ts b/src/core/features/block/services/handlers/default-block.ts index f4cb57d9d..f37059a52 100644 --- a/src/core/features/block/services/handlers/default-block.ts +++ b/src/core/features/block/services/handlers/default-block.ts @@ -18,7 +18,7 @@ import { CoreBlockBaseHandler } from '../../classes/base-block-handler'; /** * Default handler used when a block type doesn't have a specific implementation. */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class CoreBlockDefaultHandler extends CoreBlockBaseHandler { name = 'CoreBlockDefault'; diff --git a/src/core/features/comments/comments.module.ts b/src/core/features/comments/comments.module.ts index 491918ac8..a8b1e754c 100644 --- a/src/core/features/comments/comments.module.ts +++ b/src/core/features/comments/comments.module.ts @@ -12,16 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CoreCronDelegate } from '@services/cron'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { CoreCommentsComponentsModule } from './components/components.module'; -import { CoreComments } from './services/comments'; +import { CoreComments, CoreCommentsProvider } from './services/comments'; +import { CoreCommentsOfflineProvider } from './services/comments-offline'; +import { CoreCommentsSyncProvider } from './services/comments-sync'; import { COMMENTS_OFFLINE_SITE_SCHEMA } from './services/database/comments'; import { CoreCommentsSyncCronHandler } from './services/handlers/sync-cron'; +export const CORE_COMMENTS_SERVICES: Type[] = [ + CoreCommentsOfflineProvider, + CoreCommentsSyncProvider, + CoreCommentsProvider, +]; + const routes: Routes = [ { path: 'comments', diff --git a/src/core/features/compile/components/compile-html/compile-html.module.ts b/src/core/features/compile/components/compile-html/compile-html.module.ts new file mode 100644 index 000000000..be8ae045d --- /dev/null +++ b/src/core/features/compile/components/compile-html/compile-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 { NgModule } from '@angular/core'; +import { CoreCompileHtmlComponent } from './compile-html'; +import { CoreSharedModule } from '@/core/shared.module'; + +@NgModule({ + declarations: [ + CoreCompileHtmlComponent, + ], + imports: [ + CoreSharedModule, + ], + exports: [ + CoreCompileHtmlComponent, + ], +}) +export class CoreCompileHtmlComponentModule {} diff --git a/src/core/features/compile/components/compile-html/compile-html.ts b/src/core/features/compile/components/compile-html/compile-html.ts new file mode 100644 index 000000000..a2e5ba06c --- /dev/null +++ b/src/core/features/compile/components/compile-html/compile-html.ts @@ -0,0 +1,291 @@ +// (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, + OnChanges, + OnDestroy, + ViewContainerRef, + ViewChild, + ComponentRef, + SimpleChange, + ChangeDetectorRef, + ElementRef, + Output, + EventEmitter, + DoCheck, + KeyValueDiffers, + AfterContentInit, + AfterViewInit, + Type, + KeyValueDiffer, +} from '@angular/core'; + +import { CoreCompile } from '@features/compile/services/compile'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; + +/** + * This component has a behaviour similar to $compile for AngularJS. Given an HTML code, it will compile it so all its + * components and directives are instantiated. + * + * IMPORTANT: Use this component only if it is a must. It will create and compile a new component and module everytime this + * component is used, so it can slow down the app. + * + * This component has its own module to prevent circular dependencies. If you want to use it, + * you need to import CoreCompileHtmlComponentModule. + * + * You can provide some Javascript code (as text) to be executed inside the component. The context of the javascript code (this) + * will be the component instance created to compile the template. This means your javascript code can interact with the template. + * The component instance will have most of the providers so you can use them in the javascript code. E.g. if you want to use + * CoreAppProvider, you can do it with "this.CoreAppProvider". + */ +@Component({ + selector: 'core-compile-html', + template: '', +}) +// eslint-disable-next-line @angular-eslint/no-conflicting-lifecycle +export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { + + @Input() text!: string; // The HTML text to display. + @Input() javascript?: string; // The Javascript to execute in the component. + @Input() jsData?: Record; // Data to pass to the fake component. + @Input() extraImports: unknown[] = []; // Extra import modules. + @Input() extraProviders: Type[] = []; // Extra providers. + @Input() forceCompile?: boolean; // Set it to true to force compile even if the text/javascript hasn't changed. + @Output() created = new EventEmitter(); // Will emit an event when the component is instantiated. + @Output() compiling = new EventEmitter(); // Event that indicates whether the template is being compiled. + + // Get the container where to put the content. + @ViewChild('dynamicComponent', { read: ViewContainerRef }) container?: ViewContainerRef; + + loaded?: boolean; + componentInstance?: any; // eslint-disable-line @typescript-eslint/no-explicit-any + + protected componentRef?: ComponentRef; + protected element: HTMLElement; + protected differ: KeyValueDiffer; // To detect changes in the jsData input. + protected creatingComponent = false; + protected pendingCalls = {}; + + constructor( + protected changeDetector: ChangeDetectorRef, + element: ElementRef, + differs: KeyValueDiffers, + ) { + this.element = element.nativeElement; + this.differ = differs.find([]).create(); + } + + /** + * Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays). + */ + // eslint-disable-next-line @angular-eslint/no-conflicting-lifecycle + ngDoCheck(): void { + if (!this.componentInstance || this.creatingComponent) { + return; + } + + // Check if there's any change in the jsData object. + const changes = this.differ.diff(this.jsData || {}); + if (changes) { + this.setInputData(); + + if (this.componentInstance.ngOnChanges) { + this.componentInstance.ngOnChanges(CoreDomUtils.createChangesFromKeyValueDiff(changes)); + } + } + } + + /** + * Detect changes on input properties. + */ + // eslint-disable-next-line @angular-eslint/no-conflicting-lifecycle + async ngOnChanges(changes: Record): Promise { + // Only compile if text/javascript has changed or the forceCompile flag has been set to true. + if (this.text && (changes.text || changes.javascript || + (changes.forceCompile && CoreUtils.isTrueOrOne(this.forceCompile)))) { + + // Create a new component and a new module. + this.creatingComponent = true; + this.compiling.emit(true); + + try { + const factory = await CoreCompile.createAndCompileComponent( + this.text, + this.getComponentClass(), + this.extraImports, + ); + + // Destroy previous components. + this.componentRef?.destroy(); + + if (factory) { + // Create the component. + this.componentRef = this.container?.createComponent(factory); + this.componentRef && this.created.emit(this.componentRef.instance); + } + + this.loaded = true; + } catch (error) { + CoreDomUtils.showErrorModal(error); + + this.loaded = true; + } finally { + this.creatingComponent = false; + this.compiling.emit(false); + } + } + } + + /** + * Component destroyed. + */ + // eslint-disable-next-line @angular-eslint/no-conflicting-lifecycle + ngOnDestroy(): void { + this.componentRef?.destroy(); + } + + /** + * Get a class that defines the dynamic component. + * + * @return The component class. + */ + protected getComponentClass(): Type { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const compileInstance = this; + + // Create the component, using the text as the template. + return class CoreCompileHtmlFakeComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy { + + constructor() { + // Store this instance so it can be accessed by the outer component. + compileInstance.componentInstance = this; + + // Create 2 empty properties that can be used by the template to store data. + this['dataObject'] = {}; + this['dataArray'] = []; + + // Inject the libraries. + CoreCompile.injectLibraries(this, compileInstance.extraProviders); + + // Always add these elements, they could be needed on component init (componentObservable). + this['ChangeDetectorRef'] = compileInstance.changeDetector; + this['componentContainer'] = compileInstance.element; + + // Add the data passed to the component. + compileInstance.setInputData(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // If there is some javascript to run, do it now. + if (compileInstance.javascript) { + CoreCompile.executeJavascript(this, compileInstance.javascript); + } + + // Call the pending functions. + for (const name in compileInstance.pendingCalls) { + const pendingCall = compileInstance.pendingCalls[name]; + + if (typeof this[name] == 'function') { + // Call the function. + Promise.resolve(this[name].apply(this, pendingCall.params)).then(pendingCall.defer.resolve) + .catch(pendingCall.defer.reject); + } else { + // Function not defined, resolve the promise. + pendingCall.defer.resolve(); + } + } + + compileInstance.pendingCalls = {}; + } + + /** + * Content has been initialized. + */ + ngAfterContentInit(): void { + // To be overridden. + } + + /** + * View has been initialized. + */ + ngAfterViewInit(): void { + // To be overridden. + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + // To be overridden. + } + + }; + } + + /** + * Set the JS data as input data of the component instance. + */ + protected setInputData(): void { + if (!this.componentInstance) { + return; + } + + for (const name in this.jsData) { + this.componentInstance[name] = this.jsData[name]; + } + } + + /** + * 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. + * @param callWhenCreated If this param is true and the component hasn't been created yet, call the function + * once the component has been created. + * @return Result of the call. Undefined if no component instance or the function doesn't exist. + */ + callComponentFunction(name: string, params?: unknown[], callWhenCreated: boolean = true): unknown { + if (this.componentInstance) { + if (typeof this.componentInstance[name] == 'function') { + return this.componentInstance[name].apply(this.componentInstance, params); + } + } else if (callWhenCreated) { + // Call it when the component is created. + + if (this.pendingCalls[name]) { + // Call already pending, just update the params (allow only 1 call per function until it's initialized). + this.pendingCalls[name].params = params; + + return this.pendingCalls[name].defer.promise; + } + + const defer = CoreUtils.promiseDefer(); + + this.pendingCalls[name] = { + params, + defer, + }; + + return defer.promise; + } + } + +} diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts new file mode 100644 index 000000000..704968c10 --- /dev/null +++ b/src/core/features/compile/services/compile.ts @@ -0,0 +1,390 @@ +// (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, + Injector, + Component, + NgModule, + Compiler, + ComponentFactory, + ComponentRef, + NgModuleRef, + NO_ERRORS_SCHEMA, + Type, +} from '@angular/core'; +import { JitCompilerFactory } from '@angular/platform-browser-dynamic'; +import { + Platform, + ActionSheetController, + AlertController, + LoadingController, + ModalController, + PopoverController, + ToastController, +} from '@ionic/angular'; +import { TranslateService } from '@ngx-translate/core'; + +import { CoreLogger } from '@singletons/logger'; +import { CoreEvents } from '@singletons/events'; +import { makeSingleton } from '@singletons'; + +// Import core services. +import { CORE_SERVICES } from '@/core/core.module'; +import { CORE_BLOCK_SERVICES } from '@features/block/block.module'; +import { CORE_COMMENTS_SERVICES } from '@features/comments/comments.module'; +import { CORE_CONTENTLINKS_SERVICES } from '@features/contentlinks/contentlinks.module'; +import { CORE_COURSE_SERVICES } from '@features/course/course.module'; +import { CORE_COURSES_SERVICES } from '@features/courses/courses.module'; +import { CORE_EDITOR_SERVICES } from '@features/editor/editor.module'; +import { IONIC_NATIVE_SERVICES } from '@features/emulator/emulator.module'; +import { CORE_FILEUPLOADER_SERVICES } from '@features/fileuploader/fileuploader.module'; +import { CORE_FILTER_SERVICES } from '@features/filter/filter.module'; +import { CORE_GRADES_SERVICES } from '@features/grades/grades.module'; +import { CORE_H5P_SERVICES } from '@features/h5p/h5p.module'; +import { CORE_LOGIN_SERVICES } from '@features/login/login.module'; +import { CORE_MAINMENU_SERVICES } from '@features/mainmenu/mainmenu.module'; +import { CORE_PUSHNOTIFICATIONS_SERVICES } from '@features/pushnotifications/pushnotifications.module'; +import { CORE_QUESTION_SERVICES } from '@features/question/question.module'; +// @todo import { CORE_SHAREDFILES_SERVICES } from '@features/sharedfiles/sharedfiles.module'; +import { CORE_SEARCH_SERVICES } from '@features/search/search.module'; +import { CORE_SETTINGS_SERVICES } from '@features/settings/settings.module'; +import { CORE_SITEHOME_SERVICES } from '@features/sitehome/sitehome.module'; +import { CORE_TAG_SERVICES } from '@features/tag/tag.module'; +import { CORE_USER_SERVICES } from '@features/user/user.module'; +import { CORE_XAPI_SERVICES } from '@features/xapi/xapi.module'; +// @todo import { CoreSitePluginsProvider } from '@features/siteplugins/services/siteplugins'; + +// Import other libraries and providers. +import { DomSanitizer } from '@angular/platform-browser'; +import { FormBuilder, Validators } from '@angular/forms'; +import { HttpClient } from '@angular/common/http'; +import { CoreConstants } from '@/core/constants'; +import moment from 'moment'; +import { Md5 } from 'ts-md5/dist/md5'; + +// Import core classes that can be useful for site plugins. +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreArray } from '@singletons/array'; +import { CoreUrl } from '@singletons/url'; +import { CoreWindow } from '@singletons/window'; +import { CoreCache } from '@classes/cache'; +import { CoreDelegate } from '@classes/delegate'; +import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksModuleGradeHandler } from '@features/contentlinks/classes/module-grade-handler'; +import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; +import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; +import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler'; +import { CoreGeolocationError, CoreGeolocationErrorReason } from '@services/geolocation'; + +// Import all core modules that define components, directives and pipes. +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreCourseComponentsModule } from '@features/course/components/components.module'; +import { CoreCourseDirectivesModule } from '@features/course/directives/directives.module'; +import { CoreCoursesComponentsModule } from '@features/courses/components/components.module'; +// @todo import { CoreSitePluginsDirectivesModule } from '@features/siteplugins/directives/directives.module'; +import { CoreUserComponentsModule } from '@features/user/components/components.module'; +import { CoreQuestionComponentsModule } from '@features/question/components/components.module'; +import { CoreBlockComponentsModule } from '@features/block/components/components.module'; +import { CoreEditorComponentsModule } from '@features/editor/components/components.module'; +import { CoreSearchComponentsModule } from '@features/search/components/components.module'; + +// Import some components so they can be injected dynamically. +import { CoreCourseUnsupportedModuleComponent } from '@features/course/components/unsupported-module/unsupported-module'; +import { CoreCourseFormatSingleActivityComponent } from '@features/course/format/singleactivity/components/singleactivity'; +// @todo +// import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index'; +// import { CoreSitePluginsBlockComponent } from '@features/siteplugins/components/block/block'; +// import { CoreSitePluginsCourseOptionComponent } from '@features/siteplugins/components/course-option/course-option'; +// import { CoreSitePluginsCourseFormatComponent } from '@features/siteplugins/components/course-format/course-format'; +// import { CoreSitePluginsQuestionComponent } from '@features/siteplugins/components/question/question'; +// import { CoreSitePluginsQuestionBehaviourComponent } from '@features/siteplugins/components/question-behaviour/question-behaviour'; +// import { CoreSitePluginsUserProfileFieldComponent } from '@features/siteplugins/components/user-profile-field/user-profile-field'; +// import { CoreSitePluginsQuizAccessRuleComponent } from '@features/siteplugins/components/quiz-access-rule/quiz-access-rule'; +// import { CoreSitePluginsAssignFeedbackComponent } from '@features/siteplugins/components/assign-feedback/assign-feedback'; +// import { CoreSitePluginsAssignSubmissionComponent } from '@features/siteplugins/components/assign-submission/assign-submission'; + +// Import addon providers. Do not import database module because it causes circular dependencies. +import { ADDON_BADGES_SERVICES } from '@addons/badges/badges.module'; +import { ADDON_CALENDAR_SERVICES } from '@addons/calendar/calendar.module'; +// @todo import { ADDON_COMPETENCY_SERVICES } from '@addons/competency/competency.module'; +import { ADDON_MESSAGEOUTPUT_SERVICES } from '@addons/messageoutput/messageoutput.module'; +import { ADDON_MESSAGES_SERVICES } from '@addons/messages/messages.module'; +import { ADDON_MOD_ASSIGN_SERVICES } from '@addons/mod/assign/assign.module'; +import { ADDON_MOD_BOOK_SERVICES } from '@addons/mod/book/book.module'; +// @todo import { ADDON_MOD_CHAT_SERVICES } from '@addons/mod/chat/chat.module'; +// @todo import { ADDON_MOD_CHOICE_SERVICES } from '@addons/mod/choice/choice.module'; +// @todo import { ADDON_MOD_FEEDBACK_SERVICES } from '@addons/mod/feedback/feedback.module'; +import { ADDON_MOD_FOLDER_SERVICES } from '@addons/mod/folder/folder.module'; +import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module'; +// @todo import { ADDON_MOD_GLOSSARY_SERVICES } from '@addons/mod/glossary/glossary.module'; +// @todo import { ADDON_MOD_H5P_ACTIVITY_SERVICES } from '@addons/mod/h5pactivity/h5pactivity.module'; +import { ADDON_MOD_IMSCP_SERVICES } from '@addons/mod/imscp/imscp.module'; +import { ADDON_MOD_LESSON_SERVICES } from '@addons/mod/lesson/lesson.module'; +// @todo import { ADDON_MOD_LTI_SERVICES } from '@addons/mod/lti/lti.module'; +import { ADDON_MOD_PAGE_SERVICES } from '@addons/mod/page/page.module'; +import { ADDON_MOD_QUIZ_SERVICES } from '@addons/mod/quiz/quiz.module'; +import { ADDON_MOD_RESOURCE_SERVICES } from '@addons/mod/resource/resource.module'; +// @todo import { ADDON_MOD_SCORM_SERVICES } from '@addons/mod/scorm/scorm.module'; +// @todo import { ADDON_MOD_SURVEY_SERVICES } from '@addons/mod/survey/survey.module'; +import { ADDON_MOD_URL_SERVICES } from '@addons/mod/url/url.module'; +// @todo import { ADDON_MOD_WIKI_SERVICES } from '@addons/mod/wiki/wiki.module'; +// @todo import { ADDON_MOD_WORKSHOP_SERVICES } from '@addons/mod/workshop/workshop.module'; +// @todo import { ADDON_NOTES_SERVICES } from '@addons/notes/notes.module'; +import { ADDON_NOTIFICATIONS_SERVICES } from '@addons/notifications/notifications.module'; +import { ADDON_PRIVATEFILES_SERVICES } from '@addons/privatefiles/privatefiles.module'; +// @todo import { ADDON_REMOTETHEMES_SERVICES } from '@addons/remotethemes/remotethemes.module'; + +// Import some addon modules that define components, directives and pipes. Only import the important ones. +import { AddonModAssignComponentsModule } from '@addons/mod/assign/components/components.module'; +// @todo import { AddonModWorkshopComponentsModule } from '@addons/mod/workshop/components/components.module'; + +/** + * Service to provide functionalities regarding compiling dynamic HTML and Javascript. + */ +@Injectable({ providedIn: 'root' }) +export class CoreCompileProvider { + + protected logger: CoreLogger; + protected compiler: Compiler; + + // Other Ionic/Angular providers that don't depend on where they are injected. + protected readonly OTHER_SERVICES: unknown[] = [ + TranslateService, HttpClient, Platform, DomSanitizer, ActionSheetController, AlertController, LoadingController, + ModalController, PopoverController, ToastController, FormBuilder, + ]; + + // List of imports for dynamic module. Since the template can have any component we need to import all core components modules. + protected readonly IMPORTS = [ + CoreSharedModule, CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreUserComponentsModule, + CoreCourseDirectivesModule, CoreQuestionComponentsModule, AddonModAssignComponentsModule, + CoreBlockComponentsModule, CoreEditorComponentsModule, CoreSearchComponentsModule, + // @todo AddonModWorkshopComponentsModule, CoreSitePluginsDirectivesModule, + ]; + + constructor(protected injector: Injector, compilerFactory: JitCompilerFactory) { + this.logger = CoreLogger.getInstance('CoreCompileProvider'); + + this.compiler = compilerFactory.createCompiler(); + } + + /** + * Create and compile a dynamic component. + * + * @param template The template of the component. + * @param componentClass The JS class of the component. + * @param extraImports Extra imported modules if needed and not imported by this class. + * @return Promise resolved with the factory to instantiate the component. + */ + async createAndCompileComponent( + template: string, + componentClass: Type, + extraImports: any[] = [], // eslint-disable-line @typescript-eslint/no-explicit-any + ): Promise | undefined> { + // Create the component using the template and the class. + const component = Component({ template })(componentClass); + + const imports = [ + ...this.IMPORTS, + ...extraImports, + ]; + + // Now create the module containing the component. + const module = NgModule({ imports, declarations: [component], schemas: [NO_ERRORS_SCHEMA] })(class {}); + + try { + // Compile the module and the component. + const factories = await this.compiler.compileModuleAndAllComponentsAsync(module); + + // Search and return the factory of the component we just created. + return factories.componentFactories.find(factory => factory.componentType == component); + } catch (error) { + this.logger.error('Error compiling template', template); + this.logger.error(error); + error.message = 'Template has some errors and cannot be displayed.'; + + throw error; + } + } + + /** + * Eval some javascript using the context of the function. + * + * @param javascript The javascript to eval. + * @return Result of the eval. + */ + protected evalInContext(javascript: string): unknown { + // eslint-disable-next-line no-eval + return eval(javascript); + } + + /** + * Execute some javascript code, using a certain instance as the context. + * + * @param instance Instance to use as the context. In the JS code, "this" will be this instance. + * @param javascript The javascript code to eval. + * @return Result of the javascript execution. + */ + executeJavascript(instance: unknown, javascript: string): unknown { + try { + return this.evalInContext.call(instance, javascript); + } catch (ex) { + this.logger.error('Error evaluating javascript', ex); + } + } + + /** + * Inject all the core libraries in a certain object. + * + * @param instance The instance where to inject the libraries. + * @param extraProviders Extra imported providers if needed and not imported by this class. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + injectLibraries(instance: any, extraProviders: Type[] = []): void { + const providers = [ + ...CORE_SERVICES, + ...CORE_BLOCK_SERVICES, + ...CORE_COMMENTS_SERVICES, + ...CORE_CONTENTLINKS_SERVICES, + ...CORE_COURSE_SERVICES, + ...CORE_COURSES_SERVICES, + ...CORE_EDITOR_SERVICES, + ...CORE_FILEUPLOADER_SERVICES, + ...CORE_FILTER_SERVICES, + ...CORE_GRADES_SERVICES, + ...CORE_H5P_SERVICES, + ...CORE_MAINMENU_SERVICES, + ...CORE_LOGIN_SERVICES, + ...CORE_QUESTION_SERVICES, + ...CORE_PUSHNOTIFICATIONS_SERVICES, + ...CORE_SEARCH_SERVICES, + ...CORE_SETTINGS_SERVICES, + // @todo ...CORE_SHAREDFILES_SERVICES, + ...CORE_SITEHOME_SERVICES, + // @todo ...CoreSitePluginsProvider, + ...CORE_TAG_SERVICES, + ...CORE_USER_SERVICES, + ...CORE_XAPI_SERVICES, + ...IONIC_NATIVE_SERVICES, + ...this.OTHER_SERVICES, + ...extraProviders, + ...ADDON_BADGES_SERVICES, + ...ADDON_CALENDAR_SERVICES, + // @todo ...ADDON_COMPETENCY_SERVICES, + ...ADDON_MESSAGEOUTPUT_SERVICES, + ...ADDON_MESSAGES_SERVICES, + ...ADDON_MOD_ASSIGN_SERVICES, + ...ADDON_MOD_BOOK_SERVICES, + // @todo ...ADDON_MOD_CHAT_SERVICES, + // @todo ...ADDON_MOD_CHOICE_SERVICES, + // @todo ...ADDON_MOD_FEEDBACK_SERVICES, + ...ADDON_MOD_FOLDER_SERVICES, + ...ADDON_MOD_FORUM_SERVICES, + // @todo ...ADDON_MOD_GLOSSARY_SERVICES, + // @todo ...ADDON_MOD_H5P_ACTIVITY_SERVICES, + ...ADDON_MOD_IMSCP_SERVICES, + ...ADDON_MOD_LESSON_SERVICES, + // @todo ...ADDON_MOD_LTI_SERVICES, + ...ADDON_MOD_PAGE_SERVICES, + ...ADDON_MOD_QUIZ_SERVICES, + ...ADDON_MOD_RESOURCE_SERVICES, + // @todo ...ADDON_MOD_SCORM_SERVICES, + // @todo ...ADDON_MOD_SURVEY_SERVICES, + ...ADDON_MOD_URL_SERVICES, + // @todo ...ADDON_MOD_WIKI_SERVICES, + // @todo ...ADDON_MOD_WORKSHOP_SERVICES, + // @todo ...ADDON_NOTES_SERVICES, + ...ADDON_NOTIFICATIONS_SERVICES, + ...ADDON_PRIVATEFILES_SERVICES, + // @todo ...ADDON_REMOTETHEMES_SERVICES, + ]; + + // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance. + for (const i in providers) { + const providerDef = providers[i]; + if (typeof providerDef == 'function' && providerDef.name) { + try { + // Inject the provider to the instance. We use the class name as the property name. + instance[providerDef.name.replace(/DelegateService$/, 'Delegate')] = this.injector.get(providerDef); + } catch (ex) { + this.logger.warn('Error injecting provider', providerDef.name, ex); + } + } + } + + // Inject current service. + instance['CoreCompileProvider'] = this; + + // Add some final classes. + instance['injector'] = this.injector; + instance['Validators'] = Validators; + instance['CoreConstants'] = CoreConstants; + instance['CoreConfigConstants'] = CoreConstants.CONFIG; + instance['CoreEventsProvider'] = CoreEvents; + instance['CoreLoggerProvider'] = CoreLogger; + instance['moment'] = moment; + instance['Md5'] = Md5; + instance['CoreSyncBaseProvider'] = CoreSyncBaseProvider; + instance['CoreArray'] = CoreArray; + instance['CoreUrl'] = CoreUrl; + instance['CoreWindow'] = CoreWindow; + instance['CoreCache'] = CoreCache; + instance['CoreDelegate'] = CoreDelegate; + instance['CoreContentLinksHandlerBase'] = CoreContentLinksHandlerBase; + instance['CoreContentLinksModuleGradeHandler'] = CoreContentLinksModuleGradeHandler; + instance['CoreContentLinksModuleIndexHandler'] = CoreContentLinksModuleIndexHandler; + instance['CoreCourseActivityPrefetchHandlerBase'] = CoreCourseActivityPrefetchHandlerBase; + instance['CoreCourseResourcePrefetchHandlerBase'] = CoreCourseResourcePrefetchHandlerBase; + instance['CoreCourseUnsupportedModuleComponent'] = CoreCourseUnsupportedModuleComponent; + instance['CoreCourseFormatSingleActivityComponent'] = CoreCourseFormatSingleActivityComponent; + // @todo instance['CoreSitePluginsModuleIndexComponent'] = CoreSitePluginsModuleIndexComponent; + // instance['CoreSitePluginsBlockComponent'] = CoreSitePluginsBlockComponent; + // instance['CoreSitePluginsCourseOptionComponent'] = CoreSitePluginsCourseOptionComponent; + // instance['CoreSitePluginsCourseFormatComponent'] = CoreSitePluginsCourseFormatComponent; + // instance['CoreSitePluginsQuestionComponent'] = CoreSitePluginsQuestionComponent; + // instance['CoreSitePluginsQuestionBehaviourComponent'] = CoreSitePluginsQuestionBehaviourComponent; + // instance['CoreSitePluginsUserProfileFieldComponent'] = CoreSitePluginsUserProfileFieldComponent; + // instance['CoreSitePluginsQuizAccessRuleComponent'] = CoreSitePluginsQuizAccessRuleComponent; + // instance['CoreSitePluginsAssignFeedbackComponent'] = CoreSitePluginsAssignFeedbackComponent; + // instance['CoreSitePluginsAssignSubmissionComponent'] = CoreSitePluginsAssignSubmissionComponent; + instance['CoreGeolocationError'] = CoreGeolocationError; + instance['CoreGeolocationErrorReason'] = CoreGeolocationErrorReason; + } + + /** + * Instantiate a dynamic component. + * + * @param template The template of the component. + * @param componentClass The JS class of the component. + * @param injector The injector to use. It's recommended to pass it so NavController and similar can be injected. + * @return Promise resolved with the component instance. + */ + async instantiateDynamicComponent( + template: string, + componentClass: Type, + injector?: Injector, + ): Promise | undefined> { + injector = injector || this.injector; + + const factory = await this.createAndCompileComponent(template, componentClass); + + if (factory) { + // Create and return the component. + return factory.create(injector, undefined, undefined, injector.get(NgModuleRef)); + } + } + +} + +export const CoreCompile = makeSingleton(CoreCompileProvider); diff --git a/src/core/features/contentlinks/contentlinks.module.ts b/src/core/features/contentlinks/contentlinks.module.ts new file mode 100644 index 000000000..c7d9d1210 --- /dev/null +++ b/src/core/features/contentlinks/contentlinks.module.ts @@ -0,0 +1,31 @@ +// (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, Type } from '@angular/core'; + +import { CoreContentLinksDelegateService } from './services/contentlinks-delegate'; +import { CoreContentLinksHelperProvider } from './services/contentlinks-helper'; + +export const CORE_CONTENTLINKS_SERVICES: Type[] = [ + CoreContentLinksDelegateService, + CoreContentLinksHelperProvider, +]; + +@NgModule({ + declarations: [], + imports: [], + providers: [], + exports: [], +}) +export class CoreContentLinksModule {} diff --git a/src/core/features/course/course.module.ts b/src/core/features/course/course.module.ts index 8c921270f..b795315c4 100644 --- a/src/core/features/course/course.module.ts +++ b/src/core/features/course/course.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; @@ -24,14 +24,33 @@ import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/course'; import { SITE_SCHEMA as LOG_SITE_SCHEMA } from './services/database/log'; import { SITE_SCHEMA as PREFETCH_SITE_SCHEMA } from './services/database/module-prefetch'; import { CoreCourseIndexRoutingModule } from './pages/index/index-routing.module'; -import { CoreCourseModulePrefetchDelegate } from './services/module-prefetch-delegate'; +import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchDelegateService } from './services/module-prefetch-delegate'; import { CoreCronDelegate } from '@services/cron'; import { CoreCourseLogCronHandler } from './services/handlers/log-cron'; import { CoreCourseSyncCronHandler } from './services/handlers/sync-cron'; import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate'; import { CoreCourseTagAreaHandler } from './services/handlers/course-tag-area'; import { CoreCourseModulesTagAreaHandler } from './services/handlers/modules-tag-area'; -import { CoreCourse } from './services/course'; +import { CoreCourse, CoreCourseProvider } from './services/course'; +import { CoreCourseHelperProvider } from './services/course-helper'; +import { CoreCourseLogHelperProvider } from './services/log-helper'; +import { CoreCourseFormatDelegateService } from './services/format-delegate'; +import { CoreCourseModuleDelegateService } from './services/module-delegate'; +import { CoreCourseOptionsDelegateService } from './services/course-options-delegate'; +import { CoreCourseOfflineProvider } from './services/course-offline'; +import { CoreCourseSyncProvider } from './services/sync'; + +export const CORE_COURSE_SERVICES: Type[] = [ + CoreCourseProvider, + CoreCourseHelperProvider, + CoreCourseLogHelperProvider, + CoreCourseFormatDelegateService, + CoreCourseModuleDelegateService, + CoreCourseModulePrefetchDelegateService, + CoreCourseOptionsDelegateService, + CoreCourseOfflineProvider, + CoreCourseSyncProvider, +]; const routes: Routes = [ { diff --git a/src/core/features/courses/courses.module.ts b/src/core/features/courses/courses.module.ts index 46221b207..611ca2132 100644 --- a/src/core/features/courses/courses.module.ts +++ b/src/core/features/courses/courses.module.ts @@ -12,13 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreMainMenuHomeRoutingModule } from '@features/mainmenu/pages/home/home-routing.module'; import { CoreMainMenuHomeDelegate } from '@features/mainmenu/services/home-delegate'; import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate'; +import { CoreCoursesProvider } from './services/courses'; +import { CoreCoursesHelperProvider } from './services/courses-helper'; +import { CoreCoursesDashboardProvider } from './services/dashboard'; import { CoreCoursesCourseLinkHandler } from './services/handlers/course-link'; import { CoreCoursesIndexLinkHandler } from './services/handlers/courses-index-link'; @@ -28,6 +31,12 @@ import { CoreCoursesEnrolPushClickHandler } from './services/handlers/enrol-push import { CoreCoursesMyCoursesHomeHandler, CoreCoursesMyCoursesHomeHandlerService } from './services/handlers/my-courses-home'; import { CoreCoursesRequestPushClickHandler } from './services/handlers/request-push-click'; +export const CORE_COURSES_SERVICES: Type[] = [ + CoreCoursesProvider, + CoreCoursesDashboardProvider, + CoreCoursesHelperProvider, +]; + const mainMenuHomeChildrenRoutes: Routes = [ { path: '', diff --git a/src/core/features/editor/editor.module.ts b/src/core/features/editor/editor.module.ts index 012860b7c..49cf85887 100644 --- a/src/core/features/editor/editor.module.ts +++ b/src/core/features/editor/editor.module.ts @@ -12,11 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NgModule } from '@angular/core'; +import { NgModule, Type } from '@angular/core'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { CoreEditorComponentsModule } from './components/components.module'; import { SITE_SCHEMA } from './services/database/editor'; +import { CoreEditorOfflineProvider } from './services/editor-offline'; + +export const CORE_EDITOR_SERVICES: Type[] = [ + CoreEditorOfflineProvider, +]; @NgModule({ declarations: [ diff --git a/src/core/features/emulator/emulator.module.ts b/src/core/features/emulator/emulator.module.ts index 9322dd5b8..03d2e211f 100644 --- a/src/core/features/emulator/emulator.module.ts +++ b/src/core/features/emulator/emulator.module.ts @@ -45,6 +45,33 @@ import { StatusBar } from '@ionic-native/status-bar/ngx'; import { WebIntent } from '@ionic-native/web-intent/ngx'; import { Zip } from '@ionic-native/zip/ngx'; +export const IONIC_NATIVE_SERVICES = [ + Badge, + Camera, + Chooser, + Clipboard, + Device, + Diagnostic, + File, + FileOpener, + FileTransfer, + Geolocation, + HTTP, + InAppBrowser, + Keyboard, + LocalNotifications, + Media, + MediaCapture, + Network, + Push, + QRScanner, + SplashScreen, + StatusBar, + SQLite, + WebIntent, + WebView, + Zip, +]; // Mock services. import { CameraMock } from './services/camera'; import { ClipboardMock } from './services/clipboard'; diff --git a/src/core/features/fileuploader/fileuploader.module.ts b/src/core/features/fileuploader/fileuploader.module.ts index 092e8e1a9..52b8d91da 100644 --- a/src/core/features/fileuploader/fileuploader.module.ts +++ b/src/core/features/fileuploader/fileuploader.module.ts @@ -12,15 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; -import { CoreFileUploaderDelegate } from './services/fileuploader-delegate'; +import { CoreFileUploaderProvider } from './services/fileuploader'; +import { CoreFileUploaderDelegate, CoreFileUploaderDelegateService } from './services/fileuploader-delegate'; +import { CoreFileUploaderHelperProvider } from './services/fileuploader-helper'; import { CoreFileUploaderAlbumHandler } from './services/handlers/album'; import { CoreFileUploaderAudioHandler } from './services/handlers/audio'; import { CoreFileUploaderCameraHandler } from './services/handlers/camera'; import { CoreFileUploaderFileHandler } from './services/handlers/file'; import { CoreFileUploaderVideoHandler } from './services/handlers/video'; +export const CORE_FILEUPLOADER_SERVICES: Type[] = [ + CoreFileUploaderProvider, + CoreFileUploaderHelperProvider, + CoreFileUploaderDelegateService, +]; @NgModule({ imports: [], diff --git a/src/core/features/filter/filter.module.ts b/src/core/features/filter/filter.module.ts new file mode 100644 index 000000000..23ddcd933 --- /dev/null +++ b/src/core/features/filter/filter.module.ts @@ -0,0 +1,31 @@ +// (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, Type } from '@angular/core'; +import { CoreFilterProvider } from './services/filter'; +import { CoreFilterDelegateService } from './services/filter-delegate'; +import { CoreFilterHelperProvider } from './services/filter-helper'; + +export const CORE_FILTER_SERVICES: Type[] = [ + CoreFilterProvider, + CoreFilterDelegateService, + CoreFilterHelperProvider, +]; + +@NgModule({ + declarations: [], + imports: [], + providers: [], +}) +export class CoreFilterModule { } diff --git a/src/core/features/grades/grades.module.ts b/src/core/features/grades/grades.module.ts index 05da84659..3a3a3d23f 100644 --- a/src/core/features/grades/grades.module.ts +++ b/src/core/features/grades/grades.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module'; @@ -21,12 +21,19 @@ import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.m import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate'; import { CoreUserDelegate } from '@features/user/services/user-delegate'; +import { CoreGradesProvider } from './services/grades'; +import { CoreGradesHelperProvider } from './services/grades-helper'; import { CoreGradesCourseOptionHandler } from './services/handlers/course-option'; import { CoreGradesMainMenuHandler, CoreGradesMainMenuHandlerService } from './services/handlers/mainmenu'; import { CoreGradesOverviewLinkHandler } from './services/handlers/overview-link'; import { CoreGradesUserHandler } from './services/handlers/user'; import { CoreGradesUserLinkHandler } from './services/handlers/user-link'; +export const CORE_GRADES_SERVICES: Type[] = [ + CoreGradesProvider, + CoreGradesHelperProvider, +]; + const routes: Routes = [ { path: CoreGradesMainMenuHandlerService.PAGE_NAME, diff --git a/src/core/features/h5p/h5p.module.ts b/src/core/features/h5p/h5p.module.ts index cd988c458..e8be04d82 100644 --- a/src/core/features/h5p/h5p.module.ts +++ b/src/core/features/h5p/h5p.module.ts @@ -12,14 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { CoreH5PComponentsModule } from './components/components.module'; import { SITE_SCHEMA } from './services/database/h5p'; +import { CoreH5PProvider } from './services/h5p'; import { CoreH5PPluginFileHandler } from './services/handlers/pluginfile'; +export const CORE_H5P_SERVICES: Type[] = [ + CoreH5PProvider, +]; + @NgModule({ imports: [ CoreH5PComponentsModule, diff --git a/src/core/features/login/login.module.ts b/src/core/features/login/login.module.ts index f17cb2bbe..8525d25b7 100644 --- a/src/core/features/login/login.module.ts +++ b/src/core/features/login/login.module.ts @@ -16,6 +16,11 @@ import { NgModule } from '@angular/core'; import { Routes } from '@angular/router'; import { AppRoutingModule } from '@/app/app-routing.module'; +import { CoreLoginHelperProvider } from './services/login-helper'; + +export const CORE_LOGIN_SERVICES = [ + CoreLoginHelperProvider, +]; const appRoutes: Routes = [ { diff --git a/src/core/features/mainmenu/mainmenu.module.ts b/src/core/features/mainmenu/mainmenu.module.ts index a6d3b9cf8..0c871de7d 100644 --- a/src/core/features/mainmenu/mainmenu.module.ts +++ b/src/core/features/mainmenu/mainmenu.module.ts @@ -18,8 +18,16 @@ import { CoreMainMenuAuthGuard } from '@features/mainmenu/guards/auth'; import { AppRoutingModule } from '@/app/app-routing.module'; -import { CoreMainMenuDelegate } from './services/mainmenu-delegate'; +import { CoreMainMenuDelegate, CoreMainMenuDelegateService } from './services/mainmenu-delegate'; import { CoreMainMenuHomeHandler } from './services/handlers/mainmenu'; +import { CoreMainMenuProvider } from './services/mainmenu'; +import { CoreMainMenuHomeDelegateService } from './services/home-delegate'; + +export const CORE_MAINMENU_SERVICES = [ + CoreMainMenuHomeDelegateService, + CoreMainMenuDelegateService, + CoreMainMenuProvider, +]; const appRoutes: Routes = [ { diff --git a/src/core/features/pushnotifications/pushnotifications.module.ts b/src/core/features/pushnotifications/pushnotifications.module.ts index 797698106..fac638aa2 100644 --- a/src/core/features/pushnotifications/pushnotifications.module.ts +++ b/src/core/features/pushnotifications/pushnotifications.module.ts @@ -12,14 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { CoreCronDelegate } from '@services/cron'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { SITE_SCHEMA } from './services/database/pushnotifications'; import { CorePushNotificationsRegisterCronHandler } from './services/handlers/register-cron'; import { CorePushNotificationsUnregisterCronHandler } from './services/handlers/unregister-cron'; -import { CorePushNotifications } from './services/pushnotifications'; +import { CorePushNotificationsDelegateService } from './services/push-delegate'; +import { CorePushNotifications, CorePushNotificationsProvider } from './services/pushnotifications'; + +export const CORE_PUSHNOTIFICATIONS_SERVICES: Type[] = [ + CorePushNotificationsProvider, + CorePushNotificationsDelegateService, +]; @NgModule({ declarations: [ diff --git a/src/core/features/question/question.module.ts b/src/core/features/question/question.module.ts new file mode 100644 index 000000000..321fe2106 --- /dev/null +++ b/src/core/features/question/question.module.ts @@ -0,0 +1,34 @@ +// (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, Type } from '@angular/core'; +import { CoreQuestionBehaviourDelegateService } from './services/behaviour-delegate'; +import { CoreQuestionProvider } from './services/question'; +import { CoreQuestionDelegateService } from './services/question-delegate'; +import { CoreQuestionHelperProvider } from './services/question-helper'; + +export const CORE_QUESTION_SERVICES: Type[] = [ + CoreQuestionProvider, + CoreQuestionDelegateService, + CoreQuestionBehaviourDelegateService, + CoreQuestionHelperProvider, +]; + +@NgModule({ + declarations: [], + imports: [], + providers: [], + exports: [], +}) +export class CoreQuestionModule {} diff --git a/src/core/features/search/search.module.ts b/src/core/features/search/search.module.ts index 89ba8527e..b60effb9c 100644 --- a/src/core/features/search/search.module.ts +++ b/src/core/features/search/search.module.ts @@ -12,12 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NgModule } from '@angular/core'; +import { NgModule, Type } from '@angular/core'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { CoreSearchComponentsModule } from './components/components.module'; import { SITE_SCHEMA } from './services/search-history-db'; +import { CoreSearchHistoryProvider } from './services/search-history.service'; + +export const CORE_SEARCH_SERVICES: Type[] = [ + CoreSearchHistoryProvider, +]; @NgModule({ imports: [ diff --git a/src/core/features/settings/settings.module.ts b/src/core/features/settings/settings.module.ts index cbafdfd34..28db5b75b 100644 --- a/src/core/features/settings/settings.module.ts +++ b/src/core/features/settings/settings.module.ts @@ -12,13 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { AppRoutingModule } from '@/app/app-routing.module'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; - import { CoreSettingsHelperProvider } from './services/settings-helper'; +import { CoreSettingsDelegateService } from './services/settings-delegate'; + +export const CORE_SETTINGS_SERVICES: Type[] = [ + CoreSettingsDelegateService, + CoreSettingsHelperProvider, +]; const appRoutes: Routes = [ { diff --git a/src/core/features/sitehome/sitehome.module.ts b/src/core/features/sitehome/sitehome.module.ts index 0b80ab422..d77faecd2 100644 --- a/src/core/features/sitehome/sitehome.module.ts +++ b/src/core/features/sitehome/sitehome.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreSiteHomeIndexLinkHandler } from './services/handlers/index-link'; @@ -20,6 +20,11 @@ import { CoreContentLinksDelegate } from '@features/contentlinks/services/conten import { CoreSiteHomeHomeHandler, CoreSiteHomeHomeHandlerService } from './services/handlers/sitehome-home'; import { CoreMainMenuHomeDelegate } from '@features/mainmenu/services/home-delegate'; import { CoreMainMenuHomeRoutingModule } from '@features/mainmenu/pages/home/home-routing.module'; +import { CoreSiteHomeProvider } from './services/sitehome'; + +export const CORE_SITEHOME_SERVICES: Type[] = [ + CoreSiteHomeProvider, +]; const mainMenuHomeRoutes: Routes = [ { diff --git a/src/core/features/tag/tag.module.ts b/src/core/features/tag/tag.module.ts index 13b957682..939c204f8 100644 --- a/src/core/features/tag/tag.module.ts +++ b/src/core/features/tag/tag.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate'; import { CoreMainMenuRoutingModule } from '../mainmenu/mainmenu-routing.module'; @@ -22,6 +22,15 @@ import { CoreTagIndexLinkHandler } from './services/handlers/index-link'; import { CoreTagSearchLinkHandler } from './services/handlers/search-link'; import { CoreTagComponentsModule } from './components/components.module'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CoreTagAreaDelegateService } from './services/tag-area-delegate'; +import { CoreTagHelperProvider } from './services/tag-helper'; +import { CoreTagProvider } from './services/tag'; + +export const CORE_TAG_SERVICES: Type[] = [ + CoreTagAreaDelegateService, + CoreTagHelperProvider, + CoreTagProvider, +]; const routes: Routes = [ { diff --git a/src/core/features/user/user.module.ts b/src/core/features/user/user.module.ts index 7eac3eea3..52c1e87ef 100644 --- a/src/core/features/user/user.module.ts +++ b/src/core/features/user/user.module.ts @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/user'; import { CoreUserComponentsModule } from './components/components.module'; -import { CoreUserDelegate } from './services/user-delegate'; +import { CoreUserDelegate, CoreUserDelegateService } from './services/user-delegate'; import { CoreUserProfileMailHandler } from './services/handlers/profile-mail'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreUserProfileLinkHandler } from './services/handlers/profile-link'; @@ -30,6 +30,20 @@ import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate'; import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module'; import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; import { CoreUserCourseOptionHandler } from './services/handlers/course-option'; +import { CoreUserProfileFieldDelegateService } from './services/user-profile-field-delegate'; +import { CoreUserProvider } from './services/user'; +import { CoreUserHelperProvider } from './services/user-helper'; +import { CoreUserOfflineProvider } from './services/user-offline'; +import { CoreUserSyncProvider } from './services/user-sync'; + +export const CORE_USER_SERVICES: Type[] = [ + CoreUserDelegateService, + CoreUserProfileFieldDelegateService, + CoreUserProvider, + CoreUserHelperProvider, + CoreUserOfflineProvider, + CoreUserSyncProvider, +]; const routes: Routes = [ { diff --git a/src/core/features/xapi/xapi.module.ts b/src/core/features/xapi/xapi.module.ts index 23f76b909..6ee1d9f8a 100644 --- a/src/core/features/xapi/xapi.module.ts +++ b/src/core/features/xapi/xapi.module.ts @@ -12,10 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { NgModule } from '@angular/core'; +import { NgModule, Type } from '@angular/core'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { SITE_SCHEMA } from './services/database/xapi'; +import { CoreXAPIOfflineProvider } from './services/offline'; +import { CoreXAPIProvider } from './services/xapi'; + +export const CORE_XAPI_SERVICES: Type[] = [ + CoreXAPIProvider, + CoreXAPIOfflineProvider, +]; @NgModule({ imports: [], From 99b665915b5e2914784503e98f6dce6e56143c7a Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 2 Mar 2021 16:18:27 +0100 Subject: [PATCH 02/10] MOBILE-3664 siteplugins: Implement services --- .../services/siteplugins-helper.ts | 1135 +++++++++++++++++ .../siteplugins/services/siteplugins.ts | 922 +++++++++++++ 2 files changed, 2057 insertions(+) create mode 100644 src/core/features/siteplugins/services/siteplugins-helper.ts create mode 100644 src/core/features/siteplugins/services/siteplugins.ts diff --git a/src/core/features/siteplugins/services/siteplugins-helper.ts b/src/core/features/siteplugins/services/siteplugins-helper.ts new file mode 100644 index 000000000..3c4c06eeb --- /dev/null +++ b/src/core/features/siteplugins/services/siteplugins-helper.ts @@ -0,0 +1,1135 @@ +// (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 { AddonMessageOutputDelegate } from '@addons/messageoutput/services/messageoutput-delegate'; +import { AddonModAssignFeedbackDelegate } from '@addons/mod/assign/services/feedback-delegate'; +import { AddonModAssignSubmissionDelegate } from '@addons/mod/assign/services/submission-delegate'; +import { AddonModQuizAccessRuleDelegate } from '@addons/mod/quiz/services/access-rules-delegate'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { CoreError } from '@classes/errors/error'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { CoreCompile } from '@features/compile/services/compile'; +import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; +import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreCoursesMyCoursesChangedEventData, CoreCoursesProvider } from '@features/courses/services/courses'; +import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate'; +import { CoreQuestionBehaviourDelegate } from '@features/question/services/behaviour-delegate'; +import { CoreQuestionDelegate } from '@features/question/services/question-delegate'; +import { CoreSettingsDelegate } from '@features/settings/services/settings-delegate'; +import { CoreUserDelegate } from '@features/user/services/user-delegate'; +import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate'; +import { CoreFilepool } from '@services/filepool'; +import { CoreLang } from '@services/lang'; +import { CoreSites } from '@services/sites'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWS } from '@services/ws'; +import { CoreEvents } from '@singletons/events'; +import { CoreLogger } from '@singletons/logger'; +import { CoreSitePluginsAssignFeedbackHandler } from '../classes/handlers/assign-feedback-handler'; +import { CoreSitePluginsAssignSubmissionHandler } from '../classes/handlers/assign-submission-handler'; +import { CoreSitePluginsBlockHandler } from '../classes/handlers/block-handler'; +import { CoreSitePluginsCourseFormatHandler } from '../classes/handlers/course-format-handler'; +import { CoreSitePluginsCourseOptionHandler } from '../classes/handlers/course-option-handler'; +import { CoreSitePluginsMainMenuHandler } from '../classes/handlers/main-menu-handler'; +import { CoreSitePluginsMessageOutputHandler } from '../classes/handlers/message-output-handler'; +import { CoreSitePluginsModuleHandler } from '../classes/handlers/module-handler'; +import { CoreSitePluginsModulePrefetchHandler } from '../classes/handlers/module-prefetch-handler'; +import { CoreSitePluginsQuestionBehaviourHandler } from '../classes/handlers/question-behaviour-handler'; +import { CoreSitePluginsQuestionHandler } from '../classes/handlers/question-handler'; +import { CoreSitePluginsQuizAccessRuleHandler } from '../classes/handlers/quiz-access-rule-handler'; +import { CoreSitePluginsSettingsHandler } from '../classes/handlers/settings-handler'; +import { CoreSitePluginsUserProfileHandler } from '../classes/handlers/user-handler'; +import { CoreSitePluginsUserProfileFieldHandler } from '../classes/handlers/user-profile-field-handler'; +import { + CoreSitePlugins, + CoreSitePluginsContent, + CoreSitePluginsPlugin, + CoreSitePluginsHandlerData, + CoreSitePluginsProvider, + CoreSitePluginsCourseOptionHandlerData, + CoreSitePluginsMainMenuHandlerData, + CoreSitePluginsCourseModuleHandlerData, + CoreSitePluginsCourseFormatHandlerData, + CoreSitePluginsUserHandlerData, + CoreSitePluginsSettingsHandlerData, + CoreSitePluginsMessageOutputHandlerData, + CoreSitePluginsBlockHandlerData, + CoreSitePluginsHandlerCommonData, + CoreSitePluginsInitHandlerData, +} from './siteplugins'; +import { makeSingleton } from '@singletons'; + +const HANDLER_DISABLED = 'core_site_plugins_helper_handler_disabled'; + +/** + * Helper service to provide functionalities regarding site plugins. It basically has the features to load and register site + * plugin. + * + * This code is split from CoreSitePluginsProvider to prevent circular dependencies. + * + * @todo: Support ViewChild and similar in site plugins. Possible solution: make components and directives inject the instance + * inside the host DOM element? + */ +@Injectable({ providedIn: 'root' }) +export class CoreSitePluginsHelperProvider { + + protected logger: CoreLogger; + protected courseRestrictHandlers: Record = {}; + + constructor() { + this.logger = CoreLogger.getInstance('CoreSitePluginsHelperProvider'); + } + + /** + * Initialize. + */ + initialize(): void { + // Fetch the plugins on login. + CoreEvents.on(CoreEvents.LOGIN, async (data) => { + try { + const plugins = await CoreUtils.ignoreErrors(CoreSitePlugins.getPlugins(data.siteId)); + + // Plugins fetched, check that site hasn't changed. + if (data.siteId != CoreSites.getCurrentSiteId() || !plugins?.length) { + return; + } + + // Site is still the same. Load the plugins and trigger the event. + try { + await this.loadSitePlugins(plugins); + } finally { + CoreEvents.trigger(CoreEvents.SITE_PLUGINS_LOADED, {}, data.siteId); + } + } catch (error) { + this.logger.error(error); + } finally { + CoreSitePlugins.setPluginsFetched(); + } + }); + + // Unload plugins on logout if any. + CoreEvents.on(CoreEvents.LOGOUT, () => { + if (CoreSitePlugins.hasSitePluginsLoaded) { + // Temporary fix. Reload the page to unload all plugins. + window.location.reload(); + } + }); + + // Re-load plugins restricted for courses when the list of user courses changes. + CoreEvents.on(CoreCoursesProvider.EVENT_MY_COURSES_CHANGED, (data) => { + if (data && data.siteId && data.siteId == CoreSites.getCurrentSiteId() && data.added && data.added.length) { + this.reloadCourseRestrictHandlers(); + } + }); + } + + /** + * Download the styles for a handler (if any). + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @param siteId Site ID. If not provided, current site. + * @return Promise resolved with the CSS code. + */ + async downloadStyles( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsHandlerData, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + // Get the absolute URL. If it's a relative URL, add the site URL to it. + let url = handlerSchema.styles?.url; + if (url && !CoreUrlUtils.isAbsoluteURL(url)) { + url = CoreTextUtils.concatenatePaths(site.getURL(), url); + } + + if (url && handlerSchema.styles?.version) { + // Add the version to the URL to prevent getting a cached file. + url += (url.indexOf('?') != -1 ? '&' : '?') + 'version=' + handlerSchema.styles.version; + } + + const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); + const componentId = uniqueName + '#main'; + + // Remove the CSS files for this handler that aren't used anymore. Don't block the call for this. + const files = await CoreUtils.ignoreErrors( + CoreFilepool.getFilesByComponent(site.getId(), CoreSitePluginsProvider.COMPONENT, componentId), + ); + + files?.forEach((file) => { + if (file.url != url) { + // It's not the current file, delete it. + CoreUtils.ignoreErrors(CoreFilepool.removeFileByUrl(site.getId(), file.url)); + } + }); + + if (!url) { + // No styles. + return ''; + } + + // Download the file if not downloaded or the version changed. + const path = await CoreFilepool.downloadUrl( + site.getId(), + url, + false, + CoreSitePluginsProvider.COMPONENT, + componentId, + 0, + undefined, + undefined, + undefined, + handlerSchema.styles!.version, + ); + + // File is downloaded, get the contents. + return CoreWS.getText(path); + } + + /** + * Execute a handler's init method if it has any. + * + * @param plugin Data of the plugin. + * @param handlerSchema Data about the handler. + * @return Promise resolved when done. It returns the results of the getContent call and the data returned by + * the init JS (if any). + */ + protected async executeHandlerInit( + plugin: CoreSitePluginsPlugin, + handlerSchema: CoreSitePluginsHandlerData, + ): Promise { + if (!handlerSchema.init) { + return null; + } + + return this.executeMethodAndJS(plugin, handlerSchema.init, true); + } + + /** + * Execute a get_content method and run its javascript (if any). + * + * @param plugin Data of the plugin. + * @param method The method to call. + * @param isInit Whether it's the init method. + * @return Promise resolved with the results of the getContent call and the data returned by the JS (if any). + */ + protected async executeMethodAndJS( + plugin: CoreSitePluginsPlugin, + method: string, + isInit?: boolean, + ): Promise { + const siteId = CoreSites.getCurrentSiteId(); + const preSets: CoreSiteWSPreSets = { + getFromCache: false, // Try to ignore cache. + deleteCacheIfWSError: isInit, // If the init WS call returns an exception we won't use cached data. + }; + + const result = await CoreSitePlugins.getContent(plugin.component, method, {}, preSets); + + if (!result.javascript || CoreSites.getCurrentSiteId() != siteId) { + // No javascript or site has changed, stop. + return result; + } + + // Create a "fake" instance to hold all the libraries. + const instance = { + // eslint-disable-next-line @typescript-eslint/naming-convention + HANDLER_DISABLED: HANDLER_DISABLED, + }; + CoreCompile.injectLibraries(instance); + + // Add some data of the WS call result. + const jsData = CoreSitePlugins.createDataForJS(result); + for (const name in jsData) { + instance[name] = jsData[name]; + } + + // Now execute the javascript using this instance. + result.jsResult = CoreCompile.executeJavascript(instance, result.javascript); + + if (result.jsResult == HANDLER_DISABLED) { + // The "disabled" field was added in 3.8, this is a workaround for previous versions. + result.disabled = true; + } + + return result; + } + + /** + * Fetch site plugins. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. Returns the list of plugins to load. + * @deprecated since 3.9.5. The function was moved to CoreSitePlugins.getPlugins. + */ + async fetchSitePlugins(siteId?: string): Promise { + return CoreSitePlugins.getPlugins(siteId); + } + + /** + * Given an addon name, return the prefix to add to its string keys. + * + * @param addon Name of the addon (plugin.addon). + * @return Prefix. + */ + protected getPrefixForStrings(addon: string): string { + if (addon) { + return 'plugin.' + addon + '.'; + } + + return ''; + } + + /** + * Given an addon name and the key of a string, return the full string key (prefixed). + * + * @param addon Name of the addon (plugin.addon). + * @param key The key of the string. + * @return Full string key. + */ + protected getPrefixedString(addon: string, key: string): string { + return this.getPrefixForStrings(addon) + key; + } + + /** + * Check if a certain plugin is a site plugin and it's enabled in a certain site. + * + * @param plugin Data of the plugin. + * @param site Site affected. + * @return Whether it's a site plugin and it's enabled. + * @deprecated since 3.9.5. The function was moved to CoreSitePlugins.isSitePluginEnabled. + */ + isSitePluginEnabled(plugin: CoreSitePluginsPlugin, site: CoreSite): boolean { + return CoreSitePlugins.isSitePluginEnabled(plugin, site); + } + + /** + * Load the lang strings for a plugin. + * + * @param plugin Data of the plugin. + */ + loadLangStrings(plugin: CoreSitePluginsPlugin): void { + if (!plugin.parsedLang) { + return; + } + + for (const lang in plugin.parsedLang) { + const prefix = this.getPrefixForStrings(plugin.addon); + + CoreLang.addSitePluginsStrings(lang, plugin.parsedLang[lang], prefix); + } + } + + /** + * Load a site plugin. + * + * @param plugin Data of the plugin. + * @return Promise resolved when loaded. + */ + async loadSitePlugin(plugin: CoreSitePluginsPlugin): Promise { + this.logger.debug('Load site plugin:', plugin); + + if (!plugin.parsedHandlers && plugin.handlers) { + plugin.parsedHandlers = CoreTextUtils.parseJSON( + plugin.handlers, + null, + this.logger.error.bind(this.logger, 'Error parsing site plugin handlers'), + ); + } + + if (!plugin.parsedLang && plugin.lang) { + plugin.parsedLang = CoreTextUtils.parseJSON( + plugin.lang, + null, + this.logger.error.bind(this.logger, 'Error parsing site plugin lang'), + ); + } + + CoreSitePlugins.setPluginsLoaded(true); + + // Register lang strings. + this.loadLangStrings(plugin); + + if (plugin.parsedHandlers) { + // Register all the handlers. + await CoreUtils.allPromises(Object.keys(plugin.parsedHandlers).map(async (name) => { + await this.registerHandler(plugin, name, plugin.parsedHandlers![name]); + })); + } + } + + /** + * Load site plugins. + * + * @param plugins The plugins to load. + * @return Promise resolved when loaded. + */ + async loadSitePlugins(plugins: CoreSitePluginsPlugin[]): Promise { + this.courseRestrictHandlers = {}; + + await CoreUtils.allPromises(plugins.map(async (plugin) => { + const pluginPromise = this.loadSitePlugin(plugin); + CoreSitePlugins.registerSitePluginPromise(plugin.component, pluginPromise); + + await pluginPromise; + })); + } + + /** + * Load the styles for a handler. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param fileUrl CSS file URL. + * @param cssCode CSS code. + * @param version Styles version. + * @param siteId Site ID. If not provided, current site. + */ + loadStyles( + plugin: CoreSitePluginsPlugin, + handlerName: string, + fileUrl: string, + cssCode: string, + version?: number, + siteId?: string, + ): void { + siteId = siteId || CoreSites.getCurrentSiteId(); + + // Create the style and add it to the header. + const styleEl = document.createElement('style'); + const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); + + styleEl.setAttribute('id', 'siteplugin-' + uniqueName); + styleEl.innerHTML = cssCode; + + document.head.appendChild(styleEl); + + // Styles have been loaded, now treat the CSS. + CoreUtils.ignoreErrors( + CoreFilepool.treatCSSCode(siteId, fileUrl, cssCode, CoreSitePluginsProvider.COMPONENT, uniqueName, version), + ); + } + + /** + * Register a site plugin handler in the right delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @return Promise resolved when done. + */ + async registerHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsHandlerData, + ): Promise { + // Wait for the init JS to be executed and for the styles to be downloaded. + const siteId = CoreSites.getCurrentSiteId(); + + try { + const [initResult, cssCode] = await Promise.all([ + this.executeHandlerInit(plugin, handlerSchema), + this.downloadStyles(plugin, handlerName, handlerSchema, siteId).catch((error) => { + this.logger.error('Error getting styles for plugin', handlerName, handlerSchema, error); + }), + ]); + + if (initResult?.disabled) { + // This handler is disabled for the current user, stop. + this.logger.warn('Handler disabled by init function', plugin, handlerSchema); + + return; + } + + if (cssCode) { + // Load the styles. + this.loadStyles(plugin, handlerName, handlerSchema.styles!.url!, cssCode, handlerSchema.styles!.version, siteId); + } + + let uniqueName: string | undefined; + + switch (handlerSchema.delegate) { + case 'CoreMainMenuDelegate': + uniqueName = await this.registerMainMenuHandler(plugin, handlerName, handlerSchema, initResult); + break; + + case 'CoreCourseModuleDelegate': + uniqueName = await this.registerModuleHandler(plugin, handlerName, handlerSchema, initResult); + break; + + case 'CoreUserDelegate': + uniqueName = await this.registerUserProfileHandler(plugin, handlerName, handlerSchema, initResult); + break; + + case 'CoreCourseOptionsDelegate': + uniqueName = await this.registerCourseOptionHandler(plugin, handlerName, handlerSchema, initResult); + break; + + case 'CoreCourseFormatDelegate': + uniqueName = await this.registerCourseFormatHandler(plugin, handlerName, handlerSchema); + break; + + case 'CoreUserProfileFieldDelegate': + uniqueName = await this.registerUserProfileFieldHandler(plugin, handlerName, handlerSchema); + break; + + case 'CoreSettingsDelegate': + uniqueName = await this.registerSettingsHandler(plugin, handlerName, handlerSchema, initResult); + break; + + case 'CoreQuestionDelegate': + uniqueName = await this.registerQuestionHandler(plugin, handlerName, handlerSchema); + break; + + case 'CoreQuestionBehaviourDelegate': + uniqueName = await this.registerQuestionBehaviourHandler(plugin, handlerName, handlerSchema); + break; + + case 'CoreBlockDelegate': + uniqueName = await this.registerBlockHandler(plugin, handlerName, handlerSchema, initResult); + break; + + case 'AddonMessageOutputDelegate': + uniqueName = await this.registerMessageOutputHandler(plugin, handlerName, handlerSchema, initResult); + break; + + case 'AddonModQuizAccessRuleDelegate': + uniqueName = await this.registerQuizAccessRuleHandler(plugin, handlerName, handlerSchema); + break; + + case 'AddonModAssignFeedbackDelegate': + uniqueName = await this.registerAssignFeedbackHandler(plugin, handlerName, handlerSchema); + break; + + case 'AddonModAssignSubmissionDelegate': + uniqueName = await this.registerAssignSubmissionHandler(plugin, handlerName, handlerSchema); + break; + + case 'AddonWorkshopAssessmentStrategyDelegate': + uniqueName = await this.registerWorkshopAssessmentStrategyHandler(plugin, handlerName, handlerSchema); + break; + + default: + // Nothing to do. + } + + if (uniqueName) { + // Store the handler data. + CoreSitePlugins.setSitePluginHandler(uniqueName, { + plugin: plugin, + handlerName: handlerName, + handlerSchema: handlerSchema, + initResult, + }); + } + } catch (error) { + throw new CoreError('Error executing init method ' + handlerSchema.init + ': ' + error.message); + } + } + + /** + * Register a handler that relies in a "componentInit" function in a certain delegate. + * These type of handlers will return a generic template and its JS in the main method, so it will be called + * before registering the handler. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @return A promise resolved with a string to identify the handler. + */ + protected async registerComponentInitHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsInitHandlerData, + delegate: CoreDelegate, + createHandlerFn: (uniqueName: string, result: CoreSitePluginsContent) => T, + ): Promise { + + if (!handlerSchema.method) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin', plugin, handlerSchema); + + try { + // Execute the main method and its JS. The template returned will be used in the right component. + const result = await this.executeMethodAndJS(plugin, handlerSchema.method); + + // Create and register the handler. + const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); + const handler = createHandlerFn(uniqueName, result); + + // Store in handlerSchema some data required by the component. + handlerSchema.methodTemplates = result.templates; + handlerSchema.methodJSResult = result.jsResult; + handlerSchema.methodOtherdata = result.otherdata; + + if (result.jsResult) { + // Override default handler functions with the result of the method JS. + const jsResult = > result.jsResult; + for (const property in handler) { + if (property != 'constructor' && typeof handler[property] == 'function' && + typeof jsResult[property] == 'function') { + // eslint-disable-next-line @typescript-eslint/ban-types + handler[property] = ( jsResult[property]).bind(handler); + } + } + } + + delegate.registerHandler(handler); + + return uniqueName; + } catch (error) { + this.logger.error('Error executing main method', plugin.component, handlerSchema.method, error); + } + } + + /** + * Given a handler in a plugin, register it in the assign feedback delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @return Promise resolved with a string to identify the handler. + */ + protected registerAssignFeedbackHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsHandlerCommonData, + ): Promise { + + return this.registerComponentInitHandler( + plugin, + handlerName, + handlerSchema, + AddonModAssignFeedbackDelegate.instance, + (uniqueName) => { + const type = (handlerSchema.moodlecomponent || plugin.component).replace('assignfeedback_', ''); + const prefix = this.getPrefixForStrings(plugin.addon); + + return new CoreSitePluginsAssignFeedbackHandler(uniqueName, type, prefix); + }, + ); + } + + /** + * Given a handler in a plugin, register it in the assign submission delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @return Promise resolved with a string to identify the handler. + */ + protected registerAssignSubmissionHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsHandlerCommonData, + ): Promise { + + return this.registerComponentInitHandler( + plugin, + handlerName, + handlerSchema, + AddonModAssignSubmissionDelegate.instance, + (uniqueName) => { + const type = (handlerSchema.moodlecomponent || plugin.component).replace('assignsubmission_', ''); + const prefix = this.getPrefixForStrings(plugin.addon); + + return new CoreSitePluginsAssignSubmissionHandler(uniqueName, type, prefix); + }, + ); + } + + /** + * Given a handler in a plugin, register it in the block delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @param initResult Result of init function. + * @return A string to identify the handler. + */ + protected registerBlockHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsBlockHandlerData, + initResult: CoreSitePluginsContent | null, + ): string | undefined { + + const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); + const blockName = (handlerSchema.moodlecomponent || plugin.component).replace('block_', ''); + const titleString = handlerSchema.displaydata?.title ?? 'pluginname'; + const prefixedTitle = this.getPrefixedString(plugin.addon, titleString); + + CoreBlockDelegate.registerHandler( + new CoreSitePluginsBlockHandler(uniqueName, prefixedTitle, blockName, handlerSchema, initResult), + ); + + return uniqueName; + } + + /** + * Given a handler in a plugin, register it in the course format delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @return A string to identify the handler. + */ + protected registerCourseFormatHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsCourseFormatHandlerData, + ): string { + this.logger.debug('Register site plugin in course format delegate:', plugin, handlerSchema); + + // Create and register the handler. + const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); + const formatName = (handlerSchema.moodlecomponent || plugin.component).replace('format_', ''); + CoreCourseFormatDelegate.registerHandler( + new CoreSitePluginsCourseFormatHandler(uniqueName, formatName, handlerSchema), + ); + + return uniqueName; + } + + /** + * Given a handler in a plugin, register it in the course options delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @param initResult Result of the init WS call. + * @return A string to identify the handler. + */ + protected registerCourseOptionHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsCourseOptionHandlerData, + initResult: CoreSitePluginsContent | null, + ): string | undefined { + if (!handlerSchema.displaydata) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in course option delegate:', plugin, handlerSchema, initResult); + + // Create and register the handler. + const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); + const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); + const handler = new CoreSitePluginsCourseOptionHandler( + uniqueName, + prefixedTitle, + plugin, + handlerSchema, + initResult, + ); + + CoreCourseOptionsDelegate.registerHandler(handler); + + if (initResult?.restrict?.courses) { + // This handler is restricted to certan courses, store it in the list. + this.courseRestrictHandlers[uniqueName] = { + plugin, + handlerName, + handlerSchema, + handler, + }; + } + + return uniqueName; + } + + /** + * Given a handler in a plugin, register it in the main menu delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @param initResult Result of the init WS call. + * @return A string to identify the handler. + */ + protected registerMainMenuHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsMainMenuHandlerData, + initResult: CoreSitePluginsContent | null, + ): string | undefined { + if (!handlerSchema.displaydata) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in main menu delegate:', plugin, handlerSchema, initResult); + + // Create and register the handler. + const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); + const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); + + CoreMainMenuDelegate.registerHandler( + new CoreSitePluginsMainMenuHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult), + ); + + return uniqueName; + } + + /** + * Given a handler in a plugin, register it in the message output delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @param initResult Result of the init WS call. + * @return A string to identify the handler. + */ + protected registerMessageOutputHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsMessageOutputHandlerData, + initResult: CoreSitePluginsContent | null, + ): string | undefined { + if (!handlerSchema.displaydata) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in message output delegate:', plugin, handlerSchema, initResult); + + // Create and register the handler. + const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); + const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); + const processorName = (handlerSchema.moodlecomponent || plugin.component).replace('message_', ''); + + AddonMessageOutputDelegate.registerHandler( + new CoreSitePluginsMessageOutputHandler(uniqueName, processorName, prefixedTitle, plugin, handlerSchema, initResult), + ); + + return uniqueName; + } + + /** + * Given a handler in a plugin, register it in the module delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @param initResult Result of the init WS call. + * @return A string to identify the handler. + */ + protected registerModuleHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsCourseModuleHandlerData, + initResult: CoreSitePluginsContent | null, + ): string | undefined { + if (!handlerSchema.displaydata) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in module delegate:', plugin, handlerSchema, initResult); + + // Create and register the handler. + const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); + const modName = (handlerSchema.moodlecomponent || plugin.component).replace('mod_', ''); + + CoreCourseModuleDelegate.registerHandler( + new CoreSitePluginsModuleHandler(uniqueName, modName, plugin, handlerSchema, initResult), + ); + + if (handlerSchema.offlinefunctions && Object.keys(handlerSchema.offlinefunctions).length) { + // Register the prefetch handler. + CoreCourseModulePrefetchDelegate.registerHandler( + new CoreSitePluginsModulePrefetchHandler(plugin.component, uniqueName, modName, handlerSchema), + ); + } + + return uniqueName; + } + + /** + * Given a handler in a plugin, register it in the question delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @return Promise resolved with a string to identify the handler. + */ + protected registerQuestionHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsHandlerCommonData, + ): Promise { + const component = handlerSchema.moodlecomponent || plugin.component; + + return this.registerComponentInitHandler( + plugin, + handlerName, + handlerSchema, + CoreQuestionDelegate.instance, + (uniqueName) => new CoreSitePluginsQuestionHandler(uniqueName, component), + ); + } + + /** + * Given a handler in a plugin, register it in the question behaviour delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @return Promise resolved with a string to identify the handler. + */ + protected registerQuestionBehaviourHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsHandlerCommonData, + ): Promise { + + return this.registerComponentInitHandler( + plugin, + handlerName, + handlerSchema, + CoreQuestionBehaviourDelegate.instance, + (uniqueName, result) => { + const type = (handlerSchema.moodlecomponent || plugin.component).replace('qbehaviour_', ''); + + return new CoreSitePluginsQuestionBehaviourHandler(uniqueName, type, !!result.templates.length); + }, + ); + } + + /** + * Given a handler in a plugin, register it in the quiz access rule delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @return Promise resolved with a string to identify the handler. + */ + protected registerQuizAccessRuleHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsHandlerCommonData, + ): Promise { + const component = handlerSchema.moodlecomponent || plugin.component; + + return this.registerComponentInitHandler( + plugin, + handlerName, + handlerSchema, + AddonModQuizAccessRuleDelegate.instance, + (uniqueName, result) => new CoreSitePluginsQuizAccessRuleHandler(uniqueName, component, !!result.templates.length), + ); + } + + /** + * Given a handler in a plugin, register it in the settings delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @param initResult Result of the init WS call. + * @return A string to identify the handler. + */ + protected registerSettingsHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsSettingsHandlerData, + initResult: CoreSitePluginsContent | null, + ): string | undefined { + if (!handlerSchema.displaydata) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in settings delegate:', plugin, handlerSchema, initResult); + + // Create and register the handler. + const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); + const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); + + CoreSettingsDelegate.registerHandler( + new CoreSitePluginsSettingsHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult), + ); + + return uniqueName; + } + + /** + * Given a handler in a plugin, register it in the user profile delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @param initResult Result of the init WS call. + * @return A string to identify the handler. + */ + protected registerUserProfileHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsUserHandlerData, + initResult: CoreSitePluginsContent | null, + ): string | undefined { + if (!handlerSchema.displaydata) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in user profile delegate:', plugin, handlerSchema, initResult); + + // Create and register the handler. + const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); + const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); + const handler = new CoreSitePluginsUserProfileHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult); + + CoreUserDelegate.registerHandler(handler); + + if (initResult && initResult.restrict && initResult.restrict.courses) { + // This handler is restricted to certan courses, store it in the list. + this.courseRestrictHandlers[uniqueName] = { + plugin, + handlerName, + handlerSchema, + handler, + }; + } + + return uniqueName; + } + + /** + * Given a handler in a plugin, register it in the user profile field delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @return Promise resolved with a string to identify the handler. + */ + protected registerUserProfileFieldHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsHandlerCommonData, + ): Promise { + + return this.registerComponentInitHandler( + plugin, + handlerName, + handlerSchema, + CoreUserProfileFieldDelegate.instance, + (uniqueName) => { + const fieldType = (handlerSchema.moodlecomponent || plugin.component).replace('profilefield_', ''); + + return new CoreSitePluginsUserProfileFieldHandler(uniqueName, fieldType); + }, + ); + } + + /** + * Given a handler in a plugin, register it in the workshop assessment strategy delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @return Promise resolved with a string to identify the handler. + * @todo + */ + protected registerWorkshopAssessmentStrategyHandler( + plugin: CoreSitePluginsPlugin, // eslint-disable-line @typescript-eslint/no-unused-vars + handlerName: string, // eslint-disable-line @typescript-eslint/no-unused-vars + handlerSchema: CoreSitePluginsHandlerCommonData, // eslint-disable-line @typescript-eslint/no-unused-vars + ): Promise { + // @todo + return Promise.resolve(''); + + // return this.registerComponentInitHandler( + // plugin, + // handlerName, + // handlerSchema, + // this.workshopAssessmentStrategyDelegate, + // (uniqueName, result) => { + // const strategyName = (handlerSchema.moodlecomponent || plugin.component).replace('workshopform_', ''); + + // return new CoreSitePluginsWorkshopAssessmentStrategyHandler(uniqueName, strategyName); + // }, + // ); + } + + /** + * Reload the handlers that are restricted to certain courses. + * + * @return Promise resolved when done. + */ + protected async reloadCourseRestrictHandlers(): Promise { + if (!Object.keys(this.courseRestrictHandlers).length) { + // No course restrict handlers, nothing to do. + return; + } + + await Promise.all(Object.keys(this.courseRestrictHandlers).map(async (name) => { + const data = this.courseRestrictHandlers[name]; + + if (!data.handler || !data.handler.setInitResult) { + // No handler or it doesn't implement a required function, ignore it. + return; + } + + // Mark the handler as being updated. + data.handler.updatingInit && data.handler.updatingInit(); + + try { + const initResult = await this.executeHandlerInit(data.plugin, data.handlerSchema); + + data.handler.setInitResult(initResult); + } catch (error) { + this.logger.error('Error reloading course restrict handler', error, data.plugin); + } + })); + + CoreEvents.trigger(CoreEvents.SITE_PLUGINS_COURSE_RESTRICT_UPDATED, {}); + } + +} + +export const CoreSitePluginsHelper = makeSingleton(CoreSitePluginsHelperProvider); diff --git a/src/core/features/siteplugins/services/siteplugins.ts b/src/core/features/siteplugins/services/siteplugins.ts new file mode 100644 index 000000000..1110755ef --- /dev/null +++ b/src/core/features/siteplugins/services/siteplugins.ts @@ -0,0 +1,922 @@ +// (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 { CoreConstants } from '@/core/constants'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreCourses } from '@features/courses/services/courses'; +import { CoreApp } from '@services/app'; +import { CoreFilepool } from '@services/filepool'; +import { CoreLang } from '@services/lang'; +import { CoreSites } from '@services/sites'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils, PromiseDefer } from '@services/utils/utils'; +import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { CoreLogger } from '@singletons/logger'; + +const ROOT_CACHE_KEY = 'CoreSitePlugins:'; + +/** + * Service to provide functionalities regarding site plugins. + */ +@Injectable({ providedIn: 'root' }) +export class CoreSitePluginsProvider { + + static readonly COMPONENT = 'CoreSitePlugins'; + + protected logger: CoreLogger; + protected sitePlugins: {[name: string]: CoreSitePluginsHandler} = {}; // Site plugins registered. + protected sitePluginPromises: {[name: string]: Promise} = {}; // Promises of loading plugins. + protected fetchPluginsDeferred: PromiseDefer; + hasSitePluginsLoaded = false; + sitePluginsFinishedLoading = false; + + constructor() { + this.logger = CoreLogger.getInstance('CoreSitePluginsProvider'); + + const observer = CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => { + this.sitePluginsFinishedLoading = true; + observer?.off(); + }); + + // Initialize deferred at start and on logout. + this.fetchPluginsDeferred = CoreUtils.promiseDefer(); + CoreEvents.on(CoreEvents.LOGOUT, () => { + this.fetchPluginsDeferred = CoreUtils.promiseDefer(); + }); + } + + /** + * Add some params that will always be sent for get content. + * + * @param args Original params. + * @param site Site. If not defined, current site. + * @return Promise resolved with the new params. + */ + protected async addDefaultArgs = Record>( + args: T, + site?: CoreSite, + ): Promise { + args = args || {}; + site = site || CoreSites.getCurrentSite(); + + const lang = await CoreLang.getCurrentLanguage(); + + // Clone the object so the original one isn't modified. + // const argsToSend = CoreUtils.clone(args); + + const defaultArgs: CoreSitePluginsDefaultArgs = { + userid: args.userid ?? site?.getUserId(), + appid: CoreConstants.CONFIG.app_id, + appversioncode: CoreConstants.CONFIG.versioncode, + appversionname: CoreConstants.CONFIG.versionname, + applang: lang, + appcustomurlscheme: CoreConstants.CONFIG.customurlscheme, + appisdesktop: false, + appismobile: CoreApp.isMobile(), + appiswide: CoreApp.isWide(), + appplatform: 'browser', + }; + + + if (args.appismobile) { + defaultArgs.appplatform = CoreApp.isIOS() ? 'ios' : 'android'; + } + + return { + ...args, + ...defaultArgs, + }; + } + + /** + * Call a WS for a site plugin. + * + * @param method WS method to use. + * @param data Data to send to the WS. + * @param preSets Extra options. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the response. + */ + async callWS( + method: string, + data: Record, + preSets?: CoreSiteWSPreSets, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + preSets = preSets || {}; + preSets.cacheKey = preSets.cacheKey || this.getCallWSCacheKey(method, data); + + return site.read(method, data, preSets); + } + + /** + * Given the result of a init get_content and, optionally, the result of another get_content, + * build an object with the data to pass to the JS of the get_content. + * + * @param initResult Result of the init WS call. + * @param contentResult Result of the content WS call (if any). + * @return An object with the data to pass to the JS. + */ + createDataForJS( + initResult?: CoreSitePluginsContent | null, + contentResult?: CoreSitePluginsContent | null, + ): Record { + let data: Record = {}; + + if (initResult) { + // First of all, add the data returned by the init JS (if any). + data = Object.assign(data, initResult.jsResult || {}); + + // Now add some data returned by the init WS call. + data.INIT_TEMPLATES = CoreUtils.objectToKeyValueMap(initResult.templates, 'id', 'html'); + data.INIT_OTHERDATA = initResult.otherdata; + } + + if (contentResult) { + // Now add the data returned by the content WS call. + data.CONTENT_TEMPLATES = CoreUtils.objectToKeyValueMap(contentResult.templates, 'id', 'html'); + data.CONTENT_OTHERDATA = contentResult.otherdata; + } + + return data; + } + + /** + * Get cache key for a WS call. + * + * @param method Name of the method. + * @param data Data to identify the WS call. + * @return Cache key. + */ + getCallWSCacheKey(method: string, data: Record): string { + return this.getCallWSCommonCacheKey(method) + ':' + CoreUtils.sortAndStringify(data); + } + + /** + * Get common cache key for a WS call. + * + * @param method Name of the method. + * @return Cache key. + */ + protected getCallWSCommonCacheKey(method: string): string { + return ROOT_CACHE_KEY + 'ws:' + method; + } + + /** + * Get a certain content for a site plugin. + * + * @param component Component where the class is. E.g. mod_assign. + * @param method Method to execute in the class. + * @param args The params for the method. + * @param preSets Extra options. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the result. + */ + async getContent( + component: string, + method: string, + args?: Record, + preSets?: CoreSiteWSPreSets, + siteId?: string, + ): Promise { + this.logger.debug(`Get content for component '${component}' and method '${method}'`); + + const site = await CoreSites.getSite(siteId); + + // Add some params that will always be sent. + args = args || {}; + const argsToSend = await this.addDefaultArgs(args, site); + + // Now call the WS. + const data: CoreSitePluginsGetContentWSParams = { + component: component, + method: method, + args: CoreUtils.objectToArrayOfObjects(argsToSend, 'name', 'value', true), + }; + + preSets = preSets || {}; + preSets.cacheKey = this.getContentCacheKey(component, method, args); + preSets.updateFrequency = preSets.updateFrequency ?? CoreSite.FREQUENCY_OFTEN; + + const result = await site.read('tool_mobile_get_content', data, preSets); + + let otherData: Record = {}; + if (result.otherdata) { + otherData = > CoreUtils.objectToKeyValueMap(result.otherdata, 'name', 'value'); + + // Try to parse all properties that could be JSON encoded strings. + for (const name in otherData) { + const value = otherData[name]; + + if (typeof value == 'string' && (value[0] == '{' || value[0] == '[')) { + otherData[name] = CoreTextUtils.parseJSON(value); + } + } + } + + return Object.assign(result, { otherdata: otherData }); + } + + /** + * Get cache key for get content WS calls. + * + * @param component Component where the class is. E.g. mod_assign. + * @param method Method to execute in the class. + * @param args The params for the method. + * @return Cache key. + */ + protected getContentCacheKey(component: string, method: string, args: Record): string { + return ROOT_CACHE_KEY + 'content:' + component + ':' + method + ':' + CoreUtils.sortAndStringify(args); + } + + /** + * Get the value of a WS param for prefetch. + * + * @param component The component of the handler. + * @param paramName Name of the param as defined by the handler. + * @param courseId Course ID (if prefetching a course). + * @param module The module object returned by WS (if prefetching a module). + * @return The value. + */ + protected getDownloadParam( + component: string, + paramName: string, + courseId?: number, + module?: CoreCourseAnyModuleData, + ): [number] | number | undefined { + switch (paramName) { + case 'courseids': + // The WS needs the list of course IDs. Create the list. + return [courseId!]; + + case component + 'id': + // The WS needs the instance id. + return module && module.instance; + + default: + // No more params supported for now. + } + } + + /** + * Get the unique name of a handler (plugin + handler). + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler inside the plugin. + * @return Unique name. + */ + getHandlerUniqueName(plugin: CoreSitePluginsPlugin, handlerName: string): string { + return plugin.addon + '_' + handlerName; + } + + /** + * Get site plugins for site. + * + * @param siteId Site ID. + * @return Promise resolved with the plugins. + */ + async getPlugins(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + if (!CoreSitePlugins.isGetContentAvailable(site)) { + // Cannot load site plugins, so there's no point to fetch them. + return []; + } + + // Get the list of plugins. Try not to use cache. + const data = await site.read( + 'tool_mobile_get_plugins_supporting_mobile', + {}, + { getFromCache: false }, + ); + + // Return enabled plugins. + return data.plugins.filter((plugin) => this.isSitePluginEnabled(plugin, site)); + } + + /** + * Get a site plugin handler. + * + * @param name Unique name of the handler. + * @return Handler. + */ + getSitePluginHandler(name: string): CoreSitePluginsHandler | undefined { + return this.sitePlugins[name]; + } + + /** + * Invalidate all WS call to a certain method. + * + * @param method WS method to use. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateAllCallWSForMethod(method: string, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getCallWSCommonCacheKey(method)); + } + + /** + * Invalidate a WS call. + * + * @param method WS method to use. + * @param data Data to send to the WS. + * @param preSets Extra options. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateCallWS( + method: string, + data: Record, + preSets?: CoreSiteWSPreSets, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + preSets = preSets || {}; + + await site.invalidateWsCacheForKey(preSets.cacheKey || this.getCallWSCacheKey(method, data)); + } + + /** + * Invalidate a page content. + * + * @param component Component where the class is. E.g. mod_assign. + * @param method Method to execute in the class. + * @param args The params for the method. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateContent(component: string, callback: string, args?: Record, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getContentCacheKey(component, callback, args || {})); + } + + /** + * Check if the get content WS is available. + * + * @param site The site to check. If not defined, current site. + */ + isGetContentAvailable(site?: CoreSite): boolean { + site = site || CoreSites.getCurrentSite(); + + return !!site?.wsAvailable('tool_mobile_get_content'); + } + + /** + * Check if a handler is enabled for a certain course. + * + * @param courseId Course ID to check. + * @param restrictEnrolled If true or undefined, handler is only enabled for courses the user is enrolled in. + * @param restrict Users and courses the handler is restricted to. + * @return Whether the handler is enabled. + */ + async isHandlerEnabledForCourse( + courseId: number, + restrictEnrolled?: boolean, + restrict?: CoreSitePluginsContentRestrict, + ): Promise { + if (restrict?.courses?.indexOf(courseId) == -1) { + // Course is not in the list of restricted courses. + return false; + } + + if (restrictEnrolled || typeof restrictEnrolled == 'undefined') { + // Only enabled for courses the user is enrolled to. Check if the user is enrolled in the course. + try { + await CoreCourses.getUserCourse(courseId, true); + } catch { + return false; + } + } + + return true; + } + + /** + * Check if a handler is enabled for a certain user. + * + * @param userId User ID to check. + * @param restrictCurrent Whether handler is only enabled for current user. + * @param restrict Users and courses the handler is restricted to. + * @return Whether the handler is enabled. + */ + isHandlerEnabledForUser(userId: number, restrictCurrent?: boolean, restrict?: CoreSitePluginsContentRestrict): boolean { + if (restrictCurrent && userId != CoreSites.getCurrentSite()?.getUserId()) { + // Only enabled for current user. + return false; + } + + if (restrict?.users?.indexOf(userId) == -1) { + // User is not in the list of restricted users. + return false; + } + + return true; + } + + /** + * Check if a certain plugin is a site plugin and it's enabled in a certain site. + * + * @param plugin Data of the plugin. + * @param site Site affected. + * @return Whether it's a site plugin and it's enabled. + */ + isSitePluginEnabled(plugin: CoreSitePluginsPlugin, site: CoreSite): boolean { + if (site.isFeatureDisabled('sitePlugin_' + plugin.component + '_' + plugin.addon) || !plugin.handlers) { + return false; + } + + // Site plugin not disabled. Check if it has handlers. + if (!plugin.parsedHandlers) { + plugin.parsedHandlers = CoreTextUtils.parseJSON( + plugin.handlers, + null, + this.logger.error.bind(this.logger, 'Error parsing site plugin handlers'), + ); + } + + return !!(plugin.parsedHandlers && Object.keys(plugin.parsedHandlers).length); + } + + /** + * Load other data into args as determined by useOtherData list. + * If useOtherData is undefined, it won't add any data. + * If useOtherData is an array, it will only copy the properties whose names are in the array. + * If useOtherData is any other value, it will copy all the data from otherData to args. + * + * @param args The current args. + * @param otherData All the other data. + * @param useOtherData Names of the attributes to include. + * @return New args. + */ + loadOtherDataInArgs( + args: Record | undefined, + otherData?: Record, + useOtherData?: string[] | unknown, + ): Record { + if (!args) { + args = {}; + } else { + args = CoreUtils.clone(args); + } + + otherData = otherData || {}; + + if (typeof useOtherData == 'undefined') { + // No need to add other data, return args as they are. + return args; + } else if (Array.isArray(useOtherData)) { + // Include only the properties specified in the array. + for (const i in useOtherData) { + const name = useOtherData[i]; + + if (typeof otherData[name] == 'object' && otherData[name] !== null) { + // Stringify objects. + args[name] = JSON.stringify(otherData[name]); + } else { + args[name] = otherData[name]; + } + } + } else { + // Add all the data to args. + for (const name in otherData) { + if (typeof otherData[name] == 'object' && otherData[name] !== null) { + // Stringify objects. + args[name] = JSON.stringify(otherData[name]); + } else { + args[name] = otherData[name]; + } + } + } + + return args; + } + + /** + * Prefetch offline functions for a site plugin handler. + * + * @param component The component of the handler. + * @param args Params to send to the get_content calls. + * @param handlerSchema The handler schema. + * @param courseId Course ID (if prefetching a course). + * @param module The module object returned by WS (if prefetching a module). + * @param prefetch True to prefetch, false to download right away. + * @param dirPath Path of the directory where to store all the content files. + * @param site Site. If not defined, current site. + * @return Promise resolved when done. + */ + async prefetchFunctions( + component: string, + args: Record, + handlerSchema: CoreSitePluginsCourseModuleHandlerData, + courseId?: number, + module?: CoreCourseAnyModuleData, + prefetch?: boolean, + dirPath?: string, + site?: CoreSite, + ): Promise { + site = site || CoreSites.getCurrentSite(); + if (!site || !handlerSchema.offlinefunctions) { + return; + } + + await Promise.all(Object.keys(handlerSchema.offlinefunctions).map(async(method) => { + if (site!.wsAvailable(method)) { + // The method is a WS. + const paramsList = handlerSchema.offlinefunctions![method]; + const cacheKey = this.getCallWSCacheKey(method, args); + let params: Record = {}; + + if (!paramsList.length) { + // No params defined, send the default ones. + params = args; + } else { + for (const i in paramsList) { + const paramName = paramsList[i]; + + if (typeof args[paramName] != 'undefined') { + params[paramName] = args[paramName]; + } else { + // The param is not one of the default ones. Try to calculate the param to use. + const value = this.getDownloadParam(component, paramName, courseId, module); + if (typeof value != 'undefined') { + params[paramName] = value; + } + } + } + } + + await this.callWS(method, params, { cacheKey }); + + return; + } + + // It's a method to get content. + const preSets: CoreSiteWSPreSets = { + component: component, + }; + if (module) { + preSets.componentId = module.id; + } + + const result = await this.getContent(component, method, args, preSets); + + + // Prefetch the files in the content. + if (result.files.length) { + await CoreFilepool.downloadOrPrefetchFiles( + site!.getId(), + result.files, + !!prefetch, + false, + component, + module?.id, + dirPath, + ); + } + })); + } + + /** + * Store a site plugin handler. + * + * @param name A unique name to identify the handler. + * @param handler Handler to set. + */ + setSitePluginHandler(name: string, handler: CoreSitePluginsHandler): void { + this.sitePlugins[name] = handler; + } + + /** + * Store the promise for a plugin that is being initialised. + * + * @param component + * @param promise + */ + registerSitePluginPromise(component: string, promise: Promise): void { + this.sitePluginPromises[component] = promise; + } + + /** + * Set plugins fetched. + */ + setPluginsFetched(): void { + this.fetchPluginsDeferred.resolve(); + } + + /** + * Set plugins fetched. + */ + setPluginsLoaded(loaded?: boolean): void { + this.hasSitePluginsLoaded = !!loaded; + } + + /** + * Is a plugin being initialised for the specified component? + * + * @param component + */ + sitePluginPromiseExists(component: string): boolean { + return !!this.sitePluginPromises[component]; + } + + /** + * Get the promise for a plugin that is being initialised. + * + * @param component + */ + sitePluginLoaded(component: string): Promise | undefined { + return this.sitePluginPromises[component]; + } + + /** + * Wait for fetch plugins to be done. + * + * @return Promise resolved when site plugins have been fetched. + */ + waitFetchPlugins(): Promise { + return this.fetchPluginsDeferred.promise; + } + +} + +export const CoreSitePlugins = makeSingleton(CoreSitePluginsProvider, ['sitePluginsFinishedLoading', 'hasSitePluginsLoaded']); + +/** + * Handler of a site plugin. + */ +export type CoreSitePluginsHandler = { + plugin: CoreSitePluginsPlugin; // Site plugin data. + handlerName: string; // Name of the handler. + handlerSchema: CoreSitePluginsHandlerData; // Handler's data. + initResult?: CoreSitePluginsContent | null; // Result of the init WS call (if any). +}; + +/** + * Default args added to site plugins calls. + */ +export type CoreSitePluginsDefaultArgs = { + userid?: number; + appid: string; + appversioncode: number; + appversionname: string; + applang: string; + appcustomurlscheme: string; + appisdesktop: boolean; + appismobile: boolean; + appiswide: boolean; + appplatform: string; +}; + +/** + * Params of tool_mobile_get_content WS. + */ +export type CoreSitePluginsGetContentWSParams = { + component: string; // Component where the class is e.g. mod_assign. + method: string; // Method to execute in class \$component\output\mobile. + args?: { // Args for the method are optional. + name: string; // Param name. + value: string; // Param value. + }[]; +}; + +/** + * Data returned by tool_mobile_get_content WS. + */ +export type CoreSitePluginsGetContentWSResponse = { + templates: CoreSitePluginsContentTemplate[]; // Templates required by the generated content. + javascript: string; // JavaScript code. + otherdata: { // Other data that can be used or manipulated by the template via 2-way data-binding. + name: string; // Field name. + value: string; // Field value. + }[]; + files: CoreWSExternalFile[]; + restrict: CoreSitePluginsContentRestrict; // Restrict this content to certain users or courses. + disabled?: boolean; // Whether we consider this disabled or not. +}; + +/** + * Template data returned by tool_mobile_get_content WS. + */ +export type CoreSitePluginsContentTemplate = { + id: string; // ID of the template. + html: string; // HTML code. +}; + +/** + * Template data returned by tool_mobile_get_content WS. + */ +export type CoreSitePluginsContentRestrict = { + users?: number[]; // List of allowed users. + courses?: number[]; // List of allowed courses. +}; + +/** + * Data returned by tool_mobile_get_content WS with calculated data. + */ +export type CoreSitePluginsContentParsed = Omit & { + otherdata: Record; // Other data that can be used or manipulated by the template via 2-way data-binding. +}; + +/** + * Data returned by tool_mobile_get_content WS with calculated data. + */ +export type CoreSitePluginsContent = CoreSitePluginsContentParsed & { + disabled?: boolean; + jsResult?: any; // eslint-disable-line @typescript-eslint/no-explicit-any +}; + +/** + * Data returned by tool_mobile_get_plugins_supporting_mobile WS. + */ +export type CoreSitePluginsGetPluginsSupportingMobileWSResponse = { + plugins: CoreSitePluginsWSPlugin[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Plugin data returned by tool_mobile_get_plugins_supporting_mobile WS. + */ +export type CoreSitePluginsWSPlugin = { + component: string; // The plugin component name. + version: string; // The plugin version number. + addon: string; // The Mobile addon (package) name. + dependencies: string[]; // The list of Mobile addons this addon depends on. + fileurl: string; // The addon package url for download or empty if it doesn't exist. + filehash: string; // The addon package hash or empty if it doesn't exist. + filesize: number; // The addon package size or empty if it doesn't exist. + handlers?: string; // Handlers definition (JSON). + lang?: string; // Language strings used by the handlers (JSON). +}; + +/** + * Plugin data with some calculated data. + */ +export type CoreSitePluginsPlugin = CoreSitePluginsWSPlugin & { + parsedHandlers?: Record | null; + parsedLang?: Record | null; +}; + +/** + * Plugin handler data. + */ +export type CoreSitePluginsHandlerData = CoreSitePluginsInitHandlerData | CoreSitePluginsCourseOptionHandlerData | +CoreSitePluginsMainMenuHandlerData | CoreSitePluginsCourseModuleHandlerData | CoreSitePluginsCourseFormatHandlerData | +CoreSitePluginsUserHandlerData | CoreSitePluginsSettingsHandlerData | CoreSitePluginsMessageOutputHandlerData | +CoreSitePluginsBlockHandlerData; + +/** + * Plugin handler data common to all delegates. + */ +export type CoreSitePluginsHandlerCommonData = { + delegate?: string; + method?: string; + init?: string; + restricttocurrentuser?: boolean; + restricttoenrolledcourses?: boolean; + styles?: { + url?: string; + version?: number; + }; + moodlecomponent?: string; +}; + +/** + * Course option handler specific data. + */ +export type CoreSitePluginsCourseOptionHandlerData = CoreSitePluginsHandlerCommonData & { + displaydata?: { + title?: string; + class?: string; + icon?: string; + }; + priority?: number; + ismenuhandler?: boolean; + ptrenabled?: boolean; +}; + +/** + * Main menu handler specific data. + */ +export type CoreSitePluginsMainMenuHandlerData = CoreSitePluginsHandlerCommonData & { + displaydata?: { + title?: string; + icon?: string; + class?: string; + }; + priority?: number; + ptrenabled?: boolean; +}; + +/** + * Course module handler specific data. + */ +export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommonData & { + displaydata?: { + icon?: string; + class?: string; + }; + method?: string; + offlinefunctions?: Record; + downloadbutton?: boolean; + isresource?: boolean; + updatesnames?: string; + displayopeninbrowser?: boolean; + displaydescription?: boolean; + displayrefresh?: boolean; + displayprefetch?: boolean; + displaysize?: boolean; + coursepagemethod?: string; + ptrenabled?: boolean; + supportedfeatures?: Record; +}; + +/** + * Course format handler specific data. + */ +export type CoreSitePluginsCourseFormatHandlerData = CoreSitePluginsHandlerCommonData & { + canviewallsections?: boolean; + displayenabledownload?: boolean; + displaysectionselector?: boolean; +}; + +/** + * User handler specific data. + */ +export type CoreSitePluginsUserHandlerData = CoreSitePluginsHandlerCommonData & { + displaydata?: { + title?: string; + icon?: string; + class?: string; + }; + type?: string; + priority?: number; + ptrenabled?: boolean; +}; + +/** + * Settings handler specific data. + */ +export type CoreSitePluginsSettingsHandlerData = CoreSitePluginsHandlerCommonData & { + displaydata?: { + title?: string; + icon?: string; + class?: string; + }; + priority?: number; + ptrenabled?: boolean; +}; + +/** + * Message output handler specific data. + */ +export type CoreSitePluginsMessageOutputHandlerData = CoreSitePluginsHandlerCommonData & { + displaydata?: { + title?: string; + icon?: string; + }; + priority?: number; + ptrenabled?: boolean; +}; + +/** + * Block handler specific data. + */ +export type CoreSitePluginsBlockHandlerData = CoreSitePluginsHandlerCommonData & { + displaydata?: { + title?: string; + class?: string; + type?: string; + }; + fallback?: string; +}; + +/** + * Common handler data with some data from the init method. + */ +export type CoreSitePluginsInitHandlerData = CoreSitePluginsHandlerCommonData & { + methodTemplates?: CoreSitePluginsContentTemplate[]; + methodJSResult?: any; // eslint-disable-line @typescript-eslint/no-explicit-any + methodOtherdata?: Record; +}; From 1659df55cb6b83b1839542bb7fc4285343b5db91 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 2 Mar 2021 16:21:01 +0100 Subject: [PATCH 03/10] MOBILE-3664 siteplugins: Implement components --- .../deferredcbm/component/deferredcbm.ts | 3 + .../component/informationitem.ts | 3 + .../course/components/format/format.ts | 3 + .../classes/base-question-component.ts | 1 + .../question/components/question/question.ts | 1 + .../assign-feedback/assign-feedback.ts | 63 +++++ .../core-siteplugins-assign-feedback.html | 1 + .../assign-submission/assign-submission.ts | 61 +++++ .../core-siteplugins-assign-submission.html | 1 + .../siteplugins/components/block/block.ts | 80 +++++++ .../block/core-siteplugins-block.html | 3 + .../components/components.module.ts | 71 ++++++ .../core-siteplugins-course-format.html | 3 + .../components/course-format/course-format.ts | 104 +++++++++ .../core-siteplugins-course-option.html | 8 + .../components/course-option/course-option.ts | 78 +++++++ .../core-siteplugins-module-index.html | 32 +++ .../components/module-index/module-index.ts | 205 ++++++++++++++++ .../core-siteplugins-only-title-block.html | 5 + .../only-title-block/only-title-block.ts | 68 ++++++ .../core-siteplugins-plugin-content.html | 4 + .../plugin-content/plugin-content.ts | 219 ++++++++++++++++++ .../core-siteplugins-question-behaviour.html | 1 + .../question-behaviour/question-behaviour.ts | 68 ++++++ .../question/core-siteplugins-question.html | 1 + .../components/question/question.ts | 67 ++++++ .../core-siteplugins-quiz-access-rule.html | 1 + .../quiz-access-rule/quiz-access-rule.ts | 55 +++++ .../core-siteplugins-user-profile-field.html | 1 + .../user-profile-field/user-profile-field.ts | 61 +++++ .../classes/base-profilefield-component.ts | 7 +- .../user-profile-field/user-profile-field.ts | 7 +- 32 files changed, 1281 insertions(+), 5 deletions(-) create mode 100644 src/core/features/siteplugins/components/assign-feedback/assign-feedback.ts create mode 100644 src/core/features/siteplugins/components/assign-feedback/core-siteplugins-assign-feedback.html create mode 100644 src/core/features/siteplugins/components/assign-submission/assign-submission.ts create mode 100644 src/core/features/siteplugins/components/assign-submission/core-siteplugins-assign-submission.html create mode 100644 src/core/features/siteplugins/components/block/block.ts create mode 100644 src/core/features/siteplugins/components/block/core-siteplugins-block.html create mode 100644 src/core/features/siteplugins/components/components.module.ts create mode 100644 src/core/features/siteplugins/components/course-format/core-siteplugins-course-format.html create mode 100644 src/core/features/siteplugins/components/course-format/course-format.ts create mode 100644 src/core/features/siteplugins/components/course-option/core-siteplugins-course-option.html create mode 100644 src/core/features/siteplugins/components/course-option/course-option.ts create mode 100644 src/core/features/siteplugins/components/module-index/core-siteplugins-module-index.html create mode 100644 src/core/features/siteplugins/components/module-index/module-index.ts create mode 100644 src/core/features/siteplugins/components/only-title-block/core-siteplugins-only-title-block.html create mode 100644 src/core/features/siteplugins/components/only-title-block/only-title-block.ts create mode 100644 src/core/features/siteplugins/components/plugin-content/core-siteplugins-plugin-content.html create mode 100644 src/core/features/siteplugins/components/plugin-content/plugin-content.ts create mode 100644 src/core/features/siteplugins/components/question-behaviour/core-siteplugins-question-behaviour.html create mode 100644 src/core/features/siteplugins/components/question-behaviour/question-behaviour.ts create mode 100644 src/core/features/siteplugins/components/question/core-siteplugins-question.html create mode 100644 src/core/features/siteplugins/components/question/question.ts create mode 100644 src/core/features/siteplugins/components/quiz-access-rule/core-siteplugins-quiz-access-rule.html create mode 100644 src/core/features/siteplugins/components/quiz-access-rule/quiz-access-rule.ts create mode 100644 src/core/features/siteplugins/components/user-profile-field/core-siteplugins-user-profile-field.html create mode 100644 src/core/features/siteplugins/components/user-profile-field/user-profile-field.ts 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; From 2040840d79d08d135a049e0c3d2665464a183f05 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 2 Mar 2021 16:21:15 +0100 Subject: [PATCH 04/10] MOBILE-3664 siteplugins: Implement handlers --- .../activitymodules/activitymodules.ts | 2 +- .../quiz/services/access-rules-delegate.ts | 2 +- .../course/services/module-delegate.ts | 4 +- src/core/features/courses/services/courses.ts | 14 +- .../classes/compile-init-component.ts | 75 ++++++ .../handlers/assign-feedback-handler.ts | 55 ++++ .../handlers/assign-submission-handler.ts | 55 ++++ .../classes/handlers/base-handler.ts | 31 +++ .../classes/handlers/block-handler.ts | 91 +++++++ .../classes/handlers/course-format-handler.ts | 61 +++++ .../classes/handlers/course-option-handler.ts | 137 ++++++++++ .../classes/handlers/main-menu-handler.ts | 62 +++++ .../handlers/message-output-handler.ts | 58 +++++ .../classes/handlers/module-handler.ts | 166 ++++++++++++ .../handlers/module-prefetch-handler.ts | 236 ++++++++++++++++++ .../handlers/question-behaviour-handler.ts | 38 +++ .../classes/handlers/question-handler.ts | 36 +++ .../handlers/quiz-access-rule-handler.ts | 78 ++++++ .../classes/handlers/settings-handler.ts | 63 +++++ .../classes/handlers/user-handler.ts | 127 ++++++++++ .../handlers/user-profile-field-handler.ts | 58 +++++ 21 files changed, 1442 insertions(+), 7 deletions(-) create mode 100644 src/core/features/siteplugins/classes/compile-init-component.ts create mode 100644 src/core/features/siteplugins/classes/handlers/assign-feedback-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/assign-submission-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/base-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/block-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/course-format-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/course-option-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/main-menu-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/message-output-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/module-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/module-prefetch-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/question-behaviour-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/question-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/quiz-access-rule-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/settings-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/user-handler.ts create mode 100644 src/core/features/siteplugins/classes/handlers/user-profile-field-handler.ts diff --git a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts index dc01fdf70..373344396 100644 --- a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts +++ b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts @@ -101,7 +101,7 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i if (modName === 'resources') { icon = CoreCourse.getModuleIconSrc('page', modIcons['page']); } else { - icon = CoreCourseModuleDelegate.getModuleIconSrc(modName, modIcons[modName]); + icon = CoreCourseModuleDelegate.getModuleIconSrc(modName, modIcons[modName]) || ''; } this.entries.push({ diff --git a/src/addons/mod/quiz/services/access-rules-delegate.ts b/src/addons/mod/quiz/services/access-rules-delegate.ts index 0bb48b4e5..0ec872b03 100644 --- a/src/addons/mod/quiz/services/access-rules-delegate.ts +++ b/src/addons/mod/quiz/services/access-rules-delegate.ts @@ -70,7 +70,7 @@ export interface AddonModQuizAccessRuleHandler extends CoreDelegateHandler { * * @return The component (or promise resolved with component) to use, undefined if not found. */ - getPreflightComponent?(): Type | Promise>; + getPreflightComponent?(): undefined | Type | Promise>; /** * Function called when the preflight check has passed. This is a chance to record that fact in some way. diff --git a/src/core/features/course/services/module-delegate.ts b/src/core/features/course/services/module-delegate.ts index 638543dc2..5e2718822 100644 --- a/src/core/features/course/services/module-delegate.ts +++ b/src/core/features/course/services/module-delegate.ts @@ -82,7 +82,7 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler { * * @return The icon src. */ - getIconSrc?(): string; + getIconSrc?(): string | undefined; /** * Check if this type of module supports a certain feature. @@ -336,7 +336,7 @@ export class CoreCourseModuleDelegateService extends CoreDelegate(modname, 'getIconSrc') || CoreCourse.getModuleIconSrc(modname, modicon); } diff --git a/src/core/features/courses/services/courses.ts b/src/core/features/courses/services/courses.ts index f61ba2376..cacb90551 100644 --- a/src/core/features/courses/services/courses.ts +++ b/src/core/features/courses/services/courses.ts @@ -18,7 +18,7 @@ import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { makeSingleton } from '@singletons'; import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; -import { CoreEvents } from '@singletons/events'; +import { CoreEvents, CoreEventSiteData } from '@singletons/events'; import { CoreWSError } from '@classes/errors/wserror'; const ROOT_CACHE_KEY = 'mmCourses:'; @@ -853,7 +853,7 @@ export class CoreCoursesProvider { if (added.length || removed.length) { // At least 1 course was added or removed, trigger the event. - CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_CHANGED, { + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_CHANGED, { added: added, removed: removed, }, site.getId()); @@ -1169,7 +1169,7 @@ export const CoreCourses = makeSingleton(CoreCoursesProvider); /** * Data sent to the EVENT_MY_COURSES_UPDATED. */ -export type CoreCoursesMyCoursesUpdatedEventData = { +export type CoreCoursesMyCoursesUpdatedEventData = CoreEventSiteData & { action: string; // Action performed. courseId?: number; // Course ID affected (if any). course?: CoreCourseAnyCourseData; // Course affected (if any). @@ -1177,6 +1177,14 @@ export type CoreCoursesMyCoursesUpdatedEventData = { value?: boolean; // The new value for the state changed. }; +/** + * Data sent to the EVENT_MY_COURSES_CHANGED. + */ +export type CoreCoursesMyCoursesChangedEventData = CoreEventSiteData & { + added: number[]; + removed: number[]; +}; + /** * Params of core_enrol_get_users_courses WS. */ diff --git a/src/core/features/siteplugins/classes/compile-init-component.ts b/src/core/features/siteplugins/classes/compile-init-component.ts new file mode 100644 index 000000000..6fe45e777 --- /dev/null +++ b/src/core/features/siteplugins/classes/compile-init-component.ts @@ -0,0 +1,75 @@ +// (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 { CoreUtils } from '@services/utils/utils'; +import { CoreSitePlugins, CoreSitePluginsInitHandlerData } from '../services/siteplugins'; + +/** + * Base class for components that will display a component using core-compile-html and want to call a + * componentInit function returned by the handler JS. + */ +export class CoreSitePluginsCompileInitComponent { + + content = ''; // Content. + jsData: Record = {}; // Data to pass to the component. + protected handlerSchema?: CoreSitePluginsInitHandlerData; // The handler data. + + /** + * Function called when the component is created. + * + * @param instance The component instance. + */ + componentCreated(instance: unknown): void { + // Check if the JS defined an init function. + if (instance && this.handlerSchema?.methodJSResult?.componentInit) { + this.handlerSchema.methodJSResult.componentInit.apply(instance); + } + } + + /** + * Get the handler data. + * + * @param name The name of the handler. + */ + getHandlerData(name: string): void { + // Retrieve the handler data. + const handler = CoreSitePlugins.getSitePluginHandler(name); + + this.handlerSchema = handler?.handlerSchema; + + if (!this.handlerSchema) { + return; + } + + // Load first template. + if (this.handlerSchema.methodTemplates?.length) { + this.content = this.handlerSchema.methodTemplates[0].html; + this.jsData.CONTENT_TEMPLATES = CoreUtils.objectToKeyValueMap( + this.handlerSchema.methodTemplates, + 'id', + 'html', + ); + } + + // Pass data from the method result to the component. + if (this.handlerSchema.methodOtherdata) { + this.jsData.CONTENT_OTHERDATA = this.handlerSchema.methodOtherdata; + } + + if (this.handlerSchema.methodJSResult) { + this.jsData.CONTENT_JS_RESULT = this.handlerSchema.methodJSResult; + } + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/assign-feedback-handler.ts b/src/core/features/siteplugins/classes/handlers/assign-feedback-handler.ts new file mode 100644 index 000000000..f68a21f13 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/assign-feedback-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 { Type } from '@angular/core'; + +import { AddonModAssignDefaultFeedbackHandler } from '@addons/mod/assign/services/handlers/default-feedback'; +import { AddonModAssignPlugin } from '@addons/mod/assign/services/assign'; +import { CoreSitePluginsAssignFeedbackComponent } from '@features/siteplugins/components/assign-feedback/assign-feedback'; +import { Translate } from '@singletons'; + +/** + * Handler to display an assign feedback site plugin. + */ +export class CoreSitePluginsAssignFeedbackHandler extends AddonModAssignDefaultFeedbackHandler { + + constructor(public name: string, public type: string, protected prefix: string) { + super(); + } + + /** + * @inheritdoc + */ + getComponent(): Type | undefined { + return CoreSitePluginsAssignFeedbackComponent; + } + + /** + * @inheritdoc + */ + getPluginName(plugin: AddonModAssignPlugin): string { + // Check if there's a translated string for the plugin. + const translationId = this.prefix + 'pluginname'; + const translation = Translate.instant(translationId); + + if (translationId != translation) { + // Translation found, use it. + return translation; + } + + // Fallback to WS string. + return plugin.name; + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/assign-submission-handler.ts b/src/core/features/siteplugins/classes/handlers/assign-submission-handler.ts new file mode 100644 index 000000000..e88ea28f8 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/assign-submission-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 { Type } from '@angular/core'; + +import { AddonModAssignPlugin } from '@addons/mod/assign/services/assign'; +import { AddonModAssignDefaultSubmissionHandler } from '@addons/mod/assign/services/handlers/default-submission'; +import { Translate } from '@singletons'; +import { CoreSitePluginsAssignSubmissionComponent } from '../../components/assign-submission/assign-submission'; + +/** + * Handler to display an assign submission site plugin. + */ +export class CoreSitePluginsAssignSubmissionHandler extends AddonModAssignDefaultSubmissionHandler { + + constructor(public name: string, public type: string, protected prefix: string) { + super(); + } + + /** + * @inheritdoc + */ + getComponent(): Type { + return CoreSitePluginsAssignSubmissionComponent; + } + + /** + * @inheritdoc + */ + getPluginName(plugin: AddonModAssignPlugin): string { + // Check if there's a translated string for the plugin. + const translationId = this.prefix + 'pluginname'; + const translation = Translate.instant(translationId); + + if (translationId != translation) { + // Translation found, use it. + return translation; + } + + // Fallback to WS string. + return plugin.name; + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/base-handler.ts b/src/core/features/siteplugins/classes/handlers/base-handler.ts new file mode 100644 index 000000000..6319177f3 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/base-handler.ts @@ -0,0 +1,31 @@ +// (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 { CoreDelegateHandler } from '@classes/delegate'; + +/** + * Super class for handlers for site plugins. + */ +export class CoreSitePluginsBaseHandler implements CoreDelegateHandler { + + constructor(public name: string) { } + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/block-handler.ts b/src/core/features/siteplugins/classes/handlers/block-handler.ts new file mode 100644 index 000000000..776a273c2 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/block-handler.ts @@ -0,0 +1,91 @@ +// (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 { Type } from '@angular/core'; +import { CoreError } from '@classes/errors/error'; +import { CoreBlockPreRenderedComponent } from '@features/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockDelegate, CoreBlockHandler, CoreBlockHandlerData } from '@features/block/services/block-delegate'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { CoreSitePluginsBlockComponent } from '@features/siteplugins/components/block/block'; +import { CoreSitePluginsOnlyTitleBlockComponent } from '@features/siteplugins/components/only-title-block/only-title-block'; +import { CoreSitePluginsBlockHandlerData, CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins'; +import { CoreLogger } from '@singletons/logger'; +import { CoreSitePluginsBaseHandler } from './base-handler'; + +/** + * Handler to support a block using a site plugin. + */ +export class CoreSitePluginsBlockHandler extends CoreSitePluginsBaseHandler implements CoreBlockHandler { + + protected logger: CoreLogger; + + constructor( + name: string, + public title: string, + public blockName: string, + protected handlerSchema: CoreSitePluginsBlockHandlerData, + protected initResult: CoreSitePluginsContent | null, + ) { + super(name); + + this.logger = CoreLogger.getInstance('CoreSitePluginsBlockHandler'); + } + + /** + * @inheritdoc + */ + async getDisplayData( + block: CoreCourseBlock, + contextLevel: string, + instanceId: number, + ): Promise { + const className = this.handlerSchema.displaydata?.class || 'block_' + block.name; + let component: Type | undefined; + + if (this.handlerSchema.displaydata?.type == 'title') { + component = CoreSitePluginsOnlyTitleBlockComponent; + } else if (this.handlerSchema.displaydata?.type == 'prerendered') { + component = CoreBlockPreRenderedComponent; + } else if (this.handlerSchema.fallback && !this.handlerSchema.method) { + // Try to use the fallback block. + const originalName = block.name; + block.name = this.handlerSchema.fallback; + + try { + const displayData = await CoreBlockDelegate.getBlockDisplayData(block, contextLevel, instanceId); + + if (!displayData) { + throw new CoreError('Cannot get display data for fallback block.'); + } + + this.logger.debug(`Using fallback "${this.handlerSchema.fallback}" for block "${originalName}"`); + component = displayData.component; + } catch (error) { + this.logger.error(`Error using fallback "${this.handlerSchema.fallback}" for block "${originalName}", ` + + 'maybe it doesn\'t exist or isn\'t enabled.', error); + + throw error; + } + } else { + component = CoreSitePluginsBlockComponent; + } + + return { + title: this.title, + class: className, + component, + }; + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/course-format-handler.ts b/src/core/features/siteplugins/classes/handlers/course-format-handler.ts new file mode 100644 index 000000000..1878f6186 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/course-format-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 { Type } from '@angular/core'; + +import { CoreCourseFormatHandler } from '@features/course/services/format-delegate'; +import { CoreSitePluginsCourseFormatHandlerData } from '@features/siteplugins/services/siteplugins'; +import { CoreSitePluginsBaseHandler } from './base-handler'; +import { CoreSitePluginsCourseFormatComponent } from '../../components/course-format/course-format'; + +/** + * Handler to support a course format using a site plugin. + */ +export class CoreSitePluginsCourseFormatHandler extends CoreSitePluginsBaseHandler implements CoreCourseFormatHandler { + + constructor(name: string, public format: string, protected handlerSchema: CoreSitePluginsCourseFormatHandlerData) { + super(name); + } + + /** + * @inheritdoc + */ + canViewAllSections(): boolean { + return this.handlerSchema.canviewallsections ?? true; + } + + /** + * @inheritdoc + */ + displayEnableDownload(): boolean { + return this.handlerSchema.displayenabledownload ?? true; + } + + /** + * @inheritdoc + */ + displaySectionSelector(): boolean { + return this.handlerSchema.displaysectionselector ?? true; + } + + /** + * @inheritdoc + */ + async getCourseFormatComponent(): Promise | undefined> { + if (this.handlerSchema.method) { + return CoreSitePluginsCourseFormatComponent; + } + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/course-option-handler.ts b/src/core/features/siteplugins/classes/handlers/course-option-handler.ts new file mode 100644 index 000000000..6e4cbf464 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/course-option-handler.ts @@ -0,0 +1,137 @@ +// (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 { + CoreCourseOptionsHandler, + CoreCourseOptionsHandlerData, + CoreCourseOptionsMenuHandlerData, +} from '@features/course/services/course-options-delegate'; +import { CoreCourseAnyCourseDataWithOptions } from '@features/courses/services/courses'; +import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; +import { + CoreSitePlugins, + CoreSitePluginsContent, + CoreSitePluginsCourseOptionHandlerData, + CoreSitePluginsPlugin, +} from '@features/siteplugins/services/siteplugins'; +import { CoreUtils, PromiseDefer } from '@services/utils/utils'; +import { CoreSitePluginsBaseHandler } from './base-handler'; + +/** + * Handler to display a site plugin in course options. + */ +export class CoreSitePluginsCourseOptionHandler extends CoreSitePluginsBaseHandler implements CoreCourseOptionsHandler { + + priority: number; + isMenuHandler: boolean; + + protected updatingDefer?: PromiseDefer; + + constructor( + name: string, + protected title: string, + protected plugin: CoreSitePluginsPlugin, + protected handlerSchema: CoreSitePluginsCourseOptionHandlerData, + protected initResult: CoreSitePluginsContent | null, + ) { + super(name); + + this.priority = handlerSchema.priority || 0; + this.isMenuHandler = !!handlerSchema.ismenuhandler; + } + + /** + * @inheritdoc + */ + async isEnabledForCourse(courseId: number): Promise { + // Wait for "init" result to be updated. + if (this.updatingDefer) { + await this.updatingDefer.promise; + } + + return CoreSitePlugins.isHandlerEnabledForCourse( + courseId, + this.handlerSchema.restricttoenrolledcourses, + this.initResult?.restrict, + ); + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreCourseOptionsHandlerData { + return { + title: this.title, + class: this.handlerSchema.displaydata?.class, + page: '@todo CoreSitePluginsCourseOptionComponent', + pageParams: { + handlerUniqueName: this.name, + }, + }; + } + + /** + * @inheritdoc + */ + getMenuDisplayData(course: CoreCourseAnyCourseDataWithOptions): CoreCourseOptionsMenuHandlerData { + return { + title: this.title, + class: this.handlerSchema.displaydata?.class, + icon: this.handlerSchema.displaydata?.icon || '', + page: '@todo CoreSitePluginsPluginPage', + pageParams: { + title: this.title, + component: this.plugin.component, + method: this.handlerSchema.method, + args: { + courseid: course.id, + }, + initResult: this.initResult, + ptrEnabled: this.handlerSchema.ptrenabled, + }, + }; + } + + /** + * @inheritdoc + */ + prefetch(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise { + const args = { + courseid: course.id, + }; + const component = this.plugin.component; + + return CoreSitePlugins.prefetchFunctions(component, args, this.handlerSchema, course.id, undefined, true); + } + + /** + * Set init result. + * + * @param result Result to set. + */ + setInitResult(result: CoreSitePluginsContent | null): void { + this.initResult = result; + + this.updatingDefer?.resolve(); + delete this.updatingDefer; + } + + /** + * Mark init being updated. + */ + updatingInit(): void { + this.updatingDefer = CoreUtils.promiseDefer(); + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/main-menu-handler.ts b/src/core/features/siteplugins/classes/handlers/main-menu-handler.ts new file mode 100644 index 000000000..becc84c29 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/main-menu-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 { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu/services/mainmenu-delegate'; +import { + CoreSitePluginsContent, + CoreSitePluginsMainMenuHandlerData, + CoreSitePluginsPlugin, +} from '@features/siteplugins/services/siteplugins'; +import { CoreSitePluginsBaseHandler } from './base-handler'; + +/** + * Handler to display a site plugin in the main menu. + */ +export class CoreSitePluginsMainMenuHandler extends CoreSitePluginsBaseHandler implements CoreMainMenuHandler { + + priority: number; + + constructor( + name: string, + protected title: string, + protected plugin: CoreSitePluginsPlugin, + protected handlerSchema: CoreSitePluginsMainMenuHandlerData, + protected initResult: CoreSitePluginsContent | null, + ) { + super(name); + + this.priority = handlerSchema.priority || 0; + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreMainMenuHandlerData { + return { + title: this.title, + icon: this.handlerSchema.displaydata?.icon || 'fas-question', + class: this.handlerSchema.displaydata?.class, + page: '@todo CoreSitePluginsPluginPage', + pageParams: { + title: this.title, + component: this.plugin.component, + method: this.handlerSchema.method, + initResult: this.initResult, + ptrEnabled: this.handlerSchema.ptrenabled, + }, + onlyInMore: true, + }; + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/message-output-handler.ts b/src/core/features/siteplugins/classes/handlers/message-output-handler.ts new file mode 100644 index 000000000..194797fab --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/message-output-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 { AddonMessageOutputHandler, AddonMessageOutputHandlerData } from '@addons/messageoutput/services/messageoutput-delegate'; +import { + CoreSitePluginsContent, + CoreSitePluginsMessageOutputHandlerData, + CoreSitePluginsPlugin, +} from '@features/siteplugins/services/siteplugins'; +import { CoreSitePluginsBaseHandler } from './base-handler'; + +/** + * Handler to display a message output settings option. + */ +export class CoreSitePluginsMessageOutputHandler extends CoreSitePluginsBaseHandler implements AddonMessageOutputHandler { + + constructor( + name: string, + public processorName: string, + protected title: string, + protected plugin: CoreSitePluginsPlugin, + protected handlerSchema: CoreSitePluginsMessageOutputHandlerData, + protected initResult: CoreSitePluginsContent | null, + ) { + super(name); + } + + /** + * @inheritdoc + */ + getDisplayData(): AddonMessageOutputHandlerData { + return { + priority: this.handlerSchema.priority || 0, + label: this.title, + icon: this.handlerSchema.displaydata?.icon || 'fas-question', + page: '@todo CoreSitePluginsPluginPage', + pageParams: { + title: this.title, + component: this.plugin.component, + method: this.handlerSchema.method, + initResult: this.initResult, + ptrEnabled: this.handlerSchema.ptrenabled, + }, + }; + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/module-handler.ts b/src/core/features/siteplugins/classes/handlers/module-handler.ts new file mode 100644 index 000000000..37de850a2 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/module-handler.ts @@ -0,0 +1,166 @@ +// (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 { Type } from '@angular/core'; + +import { CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course'; +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; +import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index'; +import { + CoreSitePlugins, + CoreSitePluginsContent, + CoreSitePluginsCourseModuleHandlerData, + CoreSitePluginsPlugin, +} from '@features/siteplugins/services/siteplugins'; +import { CoreNavigationOptions } from '@services/navigator'; +import { CoreLogger } from '@singletons/logger'; +import { CoreSitePluginsBaseHandler } from './base-handler'; + +/** + * Handler to support a module using a site plugin. + */ +export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler implements CoreCourseModuleHandler { + + supportedFeatures?: Record; + supportsFeature?: (feature: string) => unknown; + + protected logger: CoreLogger; + + constructor( + name: string, + public modName: string, + protected plugin: CoreSitePluginsPlugin, + protected handlerSchema: CoreSitePluginsCourseModuleHandlerData, + protected initResult: CoreSitePluginsContent | null, + ) { + super(name); + + this.logger = CoreLogger.getInstance('CoreSitePluginsModuleHandler'); + this.supportedFeatures = handlerSchema.supportedfeatures; + + if (initResult?.jsResult && initResult.jsResult.supportsFeature) { + // The init result defines a function to check if a feature is supported, use it. + this.supportsFeature = initResult.jsResult.supportsFeature.bind(initResult.jsResult); + } + } + + /** + * @inheritdoc + */ + getData( + module: CoreCourseAnyModuleData, + courseId: number, + sectionId?: number, + forCoursePage?: boolean, + ): CoreCourseModuleHandlerData { + const callMethod = forCoursePage && this.handlerSchema.coursepagemethod; + + if ('noviewlink' in module && module.noviewlink && !callMethod) { + // The module doesn't link to a new page (similar to label). Only display the description. + const title = module.description; + module.description = ''; + + return { + icon: this.getIconSrc(), + title: title || '', + a11yTitle: '', + class: this.handlerSchema.displaydata?.class, + }; + } + + const hasOffline = !!(this.handlerSchema.offlinefunctions && Object.keys(this.handlerSchema.offlinefunctions).length); + const showDowloadButton = this.handlerSchema.downloadbutton; + const handlerData: CoreCourseModuleHandlerData = { + title: module.name, + icon: this.getIconSrc(), + class: this.handlerSchema.displaydata?.class, + showDownloadButton: typeof showDowloadButton != 'undefined' ? showDowloadButton : hasOffline, + }; + + if (this.handlerSchema.method) { + // There is a method, add an action. + handlerData.action = (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => { + event.preventDefault(); + event.stopPropagation(); + + // @todo navCtrl.push('CoreSitePluginsModuleIndexPage', { + // title: module.name, + // module: module, + // courseId: courseId + // }, options); + }; + } + + if (callMethod && module.visibleoncoursepage !== 0) { + // Call the method to get the course page template. + this.loadCoursePageTemplate(module, courseId, handlerData); + } + + return handlerData; + } + + /** + * Load and use template for course page. + * + * @param module Module. + * @param courseId Course ID. + * @param handlerData Handler data. + * @return Promise resolved when done. + */ + protected async loadCoursePageTemplate( + module: CoreCourseAnyModuleData, + courseId: number, + handlerData: CoreCourseModuleHandlerData, + ): Promise { + // Call the method to get the course page template. + handlerData.loading = true; + + const args = { + courseid: courseId, + cmid: module.id, + }; + + try { + const result = await CoreSitePlugins.getContent( + this.plugin.component, + this.handlerSchema.coursepagemethod!, + args, + ); + + // Use the html returned. + handlerData.title = result.templates[0]?.html ?? ''; + ( module).description = ''; + } catch (error) { + this.logger.error('Error calling course page method:', error); + } finally { + handlerData.loading = false; + } + } + + /** + * @inheritdoc + */ + getIconSrc(): string | undefined { + return this.handlerSchema.displaydata?.icon; + } + + /** + * @inheritdoc + */ + async getMainComponent(): Promise> { + return CoreSitePluginsModuleIndexComponent; + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/module-prefetch-handler.ts b/src/core/features/siteplugins/classes/handlers/module-prefetch-handler.ts new file mode 100644 index 000000000..5353cb0b3 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/module-prefetch-handler.ts @@ -0,0 +1,236 @@ +// (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 { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreSitePlugins, CoreSitePluginsCourseModuleHandlerData } from '@features/siteplugins/services/siteplugins'; +import { CoreFilepool } from '@services/filepool'; +import { CoreFileSizeSum } from '@services/plugin-file-delegate'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; + +/** + * Handler to prefetch a module site plugin. + */ +export class CoreSitePluginsModulePrefetchHandler extends CoreCourseActivityPrefetchHandlerBase { + + protected isResource: boolean; + + constructor( + component: string, + name: string, + modName: string, + protected handlerSchema: CoreSitePluginsCourseModuleHandlerData, + ) { + super(); + + this.component = component; + this.name = name; + this.modName = modName; + this.isResource = !!handlerSchema.isresource; + + if (handlerSchema.updatesnames) { + try { + this.updatesNames = new RegExp(handlerSchema.updatesnames); + } catch (ex) { + // Ignore errors. + } + } + } + + /** + * @inheritdoc + */ + download(module: CoreCourseAnyModuleData, courseId: number, dirPath?: string): Promise { + const siteId = CoreSites.getCurrentSiteId(); + + return this.prefetchPackage( + module, + courseId, + this.downloadPrefetchPlugin.bind(this, module, courseId, false, dirPath, siteId), + siteId, + ); + } + + /** + * Download or prefetch the plugin, downloading the files and calling the needed WS. + * + * @param module The module object returned by WS. + * @param courseId Course ID. + * @param prefetch True to prefetch, false to download right away. + * @param dirPath Path of the directory where to store all the content files. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + protected async downloadPrefetchPlugin( + module: CoreCourseAnyModuleData, + courseId: number, + prefetch: boolean, + dirPath?: string, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const args = { + courseid: courseId, + cmid: module.id, + userid: site.getUserId(), + }; + + await Promise.all([ + // Download the files (if any). + this.downloadOrPrefetchFiles(site.getId(), module, courseId, prefetch, dirPath), + + // Call all the offline functions. + CoreSitePlugins.prefetchFunctions( + this.component, + args, + this.handlerSchema, + courseId, + module, + prefetch, + dirPath, + site, + ), + ]); + } + + /** + * Download or prefetch the plugin files. + * + * @param siteId Site ID. + * @param module The module object returned by WS. + * @param courseId Course ID. + * @param prefetch True to prefetch, false to download right away. + * @param dirPath Path of the directory where to store all the content files. + * @return Promise resolved when done. + */ + protected async downloadOrPrefetchFiles( + siteId: string, + module: CoreCourseAnyModuleData, + courseId: number, + prefetch: boolean, + dirPath?: string, + ): Promise { + // Load module contents (ignore cache so we always have the latest data). + await this.loadContents(module, courseId, true); + + // Get the intro files. + const introFiles = await this.getIntroFiles(module, courseId); + + const contentFiles = this.getContentDownloadableFiles(module); + + if (dirPath) { + await Promise.all([ + // Download intro files in filepool root folder. + CoreFilepool.downloadOrPrefetchFiles(siteId, introFiles, prefetch, false, this.component, module.id), + + // Download content files inside dirPath. + CoreFilepool.downloadOrPrefetchFiles( + siteId, + contentFiles, + prefetch, + false, + this.component, + module.id, + dirPath, + ), + ]); + } else { + // No dirPath, download everything in filepool root folder. + await CoreFilepool.downloadOrPrefetchFiles( + siteId, + introFiles.concat(contentFiles), + prefetch, + false, + this.component, + module.id, + ); + } + } + + /** + * @inheritdoc + */ + async getDownloadSize(): Promise { + // In most cases, to calculate the size we'll have to do all the WS calls. Just return unknown size. + return { size: -1, total: false }; + } + + /** + * @inheritdoc + */ + async invalidateContent(moduleId: number, courseId: number): Promise { + const currentSite = CoreSites.getCurrentSite(); + if (!currentSite) { + return; + } + + const promises: Promise[] = []; + const siteId = currentSite.getId(); + const args = { + courseid: courseId, + cmid: moduleId, + userid: currentSite.getUserId(), + }; + + // Invalidate files and the module. + promises.push(CoreFilepool.invalidateFilesByComponent(siteId, this.component, moduleId)); + promises.push(CoreCourse.invalidateModule(moduleId, siteId)); + + // Also invalidate all the WS calls. + for (const method in this.handlerSchema.offlinefunctions) { + if (currentSite.wsAvailable(method)) { + // The method is a WS. + promises.push(currentSite.invalidateWsCacheForKey(CoreSitePlugins.getCallWSCacheKey(method, args))); + } else { + // It's a method to get content. + promises.push(CoreSitePlugins.invalidateContent(this.component, method, args, siteId)); + } + } + + return CoreUtils.allPromises(promises); + } + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + async loadContents(module: CoreCourseAnyModuleData, courseId: number, ignoreCache?: boolean): Promise { + if (this.isResource) { + return CoreCourse.loadModuleContents(module, courseId, undefined, false, ignoreCache); + } + } + + /** + * @inheritdoc + */ + prefetch(module: CoreCourseAnyModuleData, courseId?: number, single?: boolean, dirPath?: string): Promise { + const siteId = CoreSites.getCurrentSiteId(); + + return this.prefetchPackage( + module, + courseId, + this.downloadPrefetchPlugin.bind(this, module, courseId, true, dirPath, siteId), + siteId, + ); + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/question-behaviour-handler.ts b/src/core/features/siteplugins/classes/handlers/question-behaviour-handler.ts new file mode 100644 index 000000000..9e3a1a991 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/question-behaviour-handler.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 { Type } from '@angular/core'; + +import { CoreQuestionBehaviourBaseHandler } from '@features/question/classes/base-behaviour-handler'; +import { CoreSitePluginsQuestionBehaviourComponent } from '@features/siteplugins/components/question-behaviour/question-behaviour'; + +/** + * Handler to display a question behaviour site plugin. + */ +export class CoreSitePluginsQuestionBehaviourHandler extends CoreQuestionBehaviourBaseHandler { + + constructor(public name: string, public type: string, public hasTemplate: boolean) { + super(); + } + + /** + * @inheritdoc + */ + handleQuestion(): undefined | Type[] { + if (this.hasTemplate) { + return [CoreSitePluginsQuestionBehaviourComponent]; + } + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/question-handler.ts b/src/core/features/siteplugins/classes/handlers/question-handler.ts new file mode 100644 index 000000000..79c8d9dc4 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/question-handler.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 { Type } from '@angular/core'; + +import { CoreQuestionBaseHandler } from '@features/question/classes/base-question-handler'; +import { CoreSitePluginsQuestionComponent } from '@features/siteplugins/components/question/question'; + +/** + * Handler to display a question site plugin. + */ +export class CoreSitePluginsQuestionHandler extends CoreQuestionBaseHandler { + + constructor(public name: string, public type: string) { + super(); + } + + /** + * @inheritdoc + */ + getComponent(): Type { + return CoreSitePluginsQuestionComponent; + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/quiz-access-rule-handler.ts b/src/core/features/siteplugins/classes/handlers/quiz-access-rule-handler.ts new file mode 100644 index 000000000..46af909b4 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/quiz-access-rule-handler.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 { Type } from '@angular/core'; + +import { AddonModQuizAccessRuleHandler } from '@addons/mod/quiz/services/access-rules-delegate'; +import { CoreSitePluginsQuizAccessRuleComponent } from '../../components/quiz-access-rule/quiz-access-rule'; + +/** + * Handler to display a quiz access rule site plugin. + */ +export class CoreSitePluginsQuizAccessRuleHandler implements AddonModQuizAccessRuleHandler { + + constructor(public name: string, public ruleName: string, public hasTemplate: boolean) { } + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + isPreflightCheckRequired(): boolean { + return this.hasTemplate; + } + + /** + * @inheritdoc + */ + getFixedPreflightData(): void { + // Nothing to do. + } + + /** + * @inheritdoc + */ + getPreflightComponent(): undefined | Type { + if (this.hasTemplate) { + return CoreSitePluginsQuizAccessRuleComponent; + } + } + + /** + * @inheritdoc + */ + notifyPreflightCheckPassed(): void { + // Nothing to do. + } + + /** + * @inheritdoc + */ + notifyPreflightCheckFailed(): void { + // Nothing to do. + } + + /** + * @inheritdoc + */ + shouldShowTimeLeft(): boolean { + return false; + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/settings-handler.ts b/src/core/features/siteplugins/classes/handlers/settings-handler.ts new file mode 100644 index 000000000..f1b9844e1 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/settings-handler.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 { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate'; +import { + CoreSitePluginsContent, + CoreSitePluginsPlugin, + CoreSitePluginsSettingsHandlerData, +} from '@features/siteplugins/services/siteplugins'; +import { CoreSitePluginsBaseHandler } from './base-handler'; + +/** + * Handler to display a site plugin in the settings. + */ +export class CoreSitePluginsSettingsHandler extends CoreSitePluginsBaseHandler implements CoreSettingsHandler { + + priority: number; + + constructor( + name: string, + protected title: string, + protected plugin: CoreSitePluginsPlugin, + protected handlerSchema: CoreSitePluginsSettingsHandlerData, + protected initResult: CoreSitePluginsContent | null, + ) { + super(name); + + this.priority = handlerSchema.priority || 0; + } + + /** + * Returns the data needed to render the handler. + * + * @return Data. + */ + getDisplayData(): CoreSettingsHandlerData { + return { + title: this.title, + icon: this.handlerSchema.displaydata?.icon, + class: this.handlerSchema.displaydata?.class, + page: '@todo CoreSitePluginsPluginPage', + params: { + title: this.title, + component: this.plugin.component, + method: this.handlerSchema.method, + initResult: this.initResult, + ptrEnabled: this.handlerSchema.ptrenabled, + }, + }; + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/user-handler.ts b/src/core/features/siteplugins/classes/handlers/user-handler.ts new file mode 100644 index 000000000..b19e8193f --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/user-handler.ts @@ -0,0 +1,127 @@ +// (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 { + CoreSitePlugins, + CoreSitePluginsContent, + CoreSitePluginsPlugin, + CoreSitePluginsUserHandlerData, +} from '@features/siteplugins/services/siteplugins'; +import { CoreUserProfile } from '@features/user/services/user'; +import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; +import { CoreSites } from '@services/sites'; +import { CoreUtils, PromiseDefer } from '@services/utils/utils'; +import { CoreSitePluginsBaseHandler } from './base-handler'; + +/** + * Handler to display a site plugin in the user profile. + */ +export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandler implements CoreUserProfileHandler { + + priority: number; + type: string; + + protected updatingDefer?: PromiseDefer; + + constructor( + name: string, + protected title: string, + protected plugin: CoreSitePluginsPlugin, + protected handlerSchema: CoreSitePluginsUserHandlerData, + protected initResult: CoreSitePluginsContent | null, + ) { + super(name); + + this.priority = handlerSchema.priority || 0; + + // Only support TYPE_COMMUNICATION and TYPE_NEW_PAGE. + this.type = handlerSchema.type != CoreUserDelegateService.TYPE_COMMUNICATION ? + CoreUserDelegateService.TYPE_NEW_PAGE : CoreUserDelegateService.TYPE_COMMUNICATION; + } + + /** + * @inheritdoc + */ + async isEnabledForUser( + user: CoreUserProfile, + courseId?: number, + ): Promise { + // First check if it's enabled for the user. + const enabledForUser = CoreSitePlugins.isHandlerEnabledForUser( + user.id, + this.handlerSchema.restricttocurrentuser, + this.initResult?.restrict, + ); + + if (!enabledForUser) { + return false; + } + + courseId = courseId || CoreSites.getCurrentSiteHomeId(); + + // Enabled for user, check if it's enabled for the course. + return CoreSitePlugins.isHandlerEnabledForCourse( + courseId, + this.handlerSchema.restricttoenrolledcourses, + this.initResult?.restrict, + ); + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreUserProfileHandlerData { + return { + title: this.title, + icon: this.handlerSchema.displaydata?.icon, + class: this.handlerSchema.displaydata?.class, + action: (event: Event, user: CoreUserProfile, courseId?: number): void => { + event.preventDefault(); + event.stopPropagation(); + + // @todo navCtrl.push('CoreSitePluginsPluginPage', { + // title: this.title, + // component: this.plugin.component, + // method: this.handlerSchema.method, + // args: { + // courseid: courseId, + // userid: user.id + // }, + // initResult: this.initResult, + // ptrEnabled: this.handlerSchema.ptrenabled, + // }); + }, + }; + } + + /** + * Set init result. + * + * @param result Result to set. + */ + setInitResult(result: CoreSitePluginsContent | null): void { + this.initResult = result; + + this.updatingDefer?.resolve(); + delete this.updatingDefer; + } + + /** + * Mark init being updated. + */ + updatingInit(): void { + this.updatingDefer = CoreUtils.promiseDefer(); + } + +} diff --git a/src/core/features/siteplugins/classes/handlers/user-profile-field-handler.ts b/src/core/features/siteplugins/classes/handlers/user-profile-field-handler.ts new file mode 100644 index 000000000..d051e0f49 --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/user-profile-field-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 { Type } from '@angular/core'; + +import { AuthEmailSignupProfileField } from '@features/login/services/login-helper'; +import { CoreSitePluginsUserProfileFieldComponent } from '@features/siteplugins/components/user-profile-field/user-profile-field'; +import { CoreUserProfileField } from '@features/user/services/user'; +import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@features/user/services/user-profile-field-delegate'; +import { CoreSitePluginsBaseHandler } from './base-handler'; + +/** + * Handler to display a site plugin in the user profile. + */ +export class CoreSitePluginsUserProfileFieldHandler extends CoreSitePluginsBaseHandler implements CoreUserProfileFieldHandler { + + constructor(name: string, public type: string) { + super(name); + } + + /** + * @inheritdoc + */ + getComponent(): Type { + return CoreSitePluginsUserProfileFieldComponent; + } + + /** + * @inheritdoc + */ + async getData( + field: AuthEmailSignupProfileField | CoreUserProfileField, + signup: boolean, + registerAuth: string, + formValues: Record, + ): Promise { + // No getData function implemented, use a default behaviour. + const name = 'profile_field_' + field.shortname; + + return { + type: 'type' in field ? field.type : field.datatype!, + name: name, + value: formValues[name], + }; + } + +} From a4046f5678c6cde80108e3ec6dd545adf8a10da9 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 3 Mar 2021 10:03:22 +0100 Subject: [PATCH 05/10] MOBILE-3664 site plugins: Implement pages --- .../pages/edit-event/edit-event.page.ts | 9 +- .../components/submission/submission.ts | 18 +-- src/addons/mod/assign/pages/edit/edit.ts | 9 +- .../submission-review/submission-review.ts | 5 +- .../mod/quiz/pages/player/player.page.ts | 9 +- src/core/features/features.module.ts | 2 + .../classes/handlers/course-option-handler.ts | 21 +-- .../classes/handlers/main-menu-handler.ts | 4 +- .../handlers/message-output-handler.ts | 4 +- .../classes/handlers/module-handler.ts | 14 +- .../classes/handlers/settings-handler.ts | 4 +- .../classes/handlers/user-handler.ts | 30 +++-- .../components/components.module.ts | 3 - .../only-title-block/only-title-block.ts | 33 +++-- .../plugin-content/plugin-content.ts | 31 +++-- .../core-siteplugins-course-option.html | 3 +- .../course-option/course-option.module.ts | 45 +++++++ .../course-option/course-option.ts | 72 +++++++++-- .../pages/module-index/module-index.html | 19 +++ .../pages/module-index/module-index.module.ts | 45 +++++++ .../pages/module-index/module-index.ts | 109 ++++++++++++++++ .../pages/plugin-page/plugin-page.html | 21 +++ .../pages/plugin-page/plugin-page.module.ts | 45 +++++++ .../pages/plugin-page/plugin-page.ts | 120 ++++++++++++++++++ .../siteplugins/siteplugins-lazy.module.ts | 32 +++++ .../siteplugins/siteplugins.module.ts | 54 ++++++++ src/core/pipes/pipes.module.ts | 3 + src/core/pipes/to-locale-string.ts | 66 ++++++++++ src/core/services/utils/utils.ts | 9 -- 29 files changed, 736 insertions(+), 103 deletions(-) rename src/core/features/siteplugins/{components => pages}/course-option/core-siteplugins-course-option.html (71%) create mode 100644 src/core/features/siteplugins/pages/course-option/course-option.module.ts rename src/core/features/siteplugins/{components => pages}/course-option/course-option.ts (51%) create mode 100644 src/core/features/siteplugins/pages/module-index/module-index.html create mode 100644 src/core/features/siteplugins/pages/module-index/module-index.module.ts create mode 100644 src/core/features/siteplugins/pages/module-index/module-index.ts create mode 100644 src/core/features/siteplugins/pages/plugin-page/plugin-page.html create mode 100644 src/core/features/siteplugins/pages/plugin-page/plugin-page.module.ts create mode 100644 src/core/features/siteplugins/pages/plugin-page/plugin-page.ts create mode 100644 src/core/features/siteplugins/siteplugins-lazy.module.ts create mode 100644 src/core/features/siteplugins/siteplugins.module.ts create mode 100644 src/core/pipes/to-locale-string.ts diff --git a/src/addons/calendar/pages/edit-event/edit-event.page.ts b/src/addons/calendar/pages/edit-event/edit-event.page.ts index 2321e24eb..d86fabc51 100644 --- a/src/addons/calendar/pages/edit-event/edit-event.page.ts +++ b/src/addons/calendar/pages/edit-event/edit-event.page.ts @@ -43,6 +43,7 @@ import { CoreFilterHelper } from '@features/filter/services/filter-helper'; import { AddonCalendarOfflineEventDBRecord } from '../../services/database/calendar-offline'; import { CoreError } from '@classes/errors/error'; import { CoreNavigator } from '@services/navigator'; +import { CanLeave } from '@guards/can-leave'; /** * Page that displays a form to create/edit an event. @@ -52,7 +53,7 @@ import { CoreNavigator } from '@services/navigator'; templateUrl: 'edit-event.html', styleUrls: ['edit-event.scss'], }) -export class AddonCalendarEditEventPage implements OnInit, OnDestroy { +export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave { @ViewChild(CoreEditorRichTextEditorComponent) descriptionEditor!: CoreEditorRichTextEditorComponent; @ViewChild('editEventForm') formElement!: ElementRef; @@ -605,15 +606,17 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { /** * Check if we can leave the page or not. * - * @return Resolved if we can leave it, rejected if not. + * @return Resolved with true if we can leave it, rejected if not. */ - async ionViewCanLeave(): Promise { + async canLeave(): Promise { if (AddonCalendarHelper.hasEventDataChanged(this.form.value, this.originalData)) { // Show confirmation if some data has been modified. await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit')); } CoreDomUtils.triggerFormCancelledEvent(this.formElement, this.currentSite.getId()); + + return true; } /** diff --git a/src/addons/mod/assign/components/submission/submission.ts b/src/addons/mod/assign/components/submission/submission.ts index 0e3afc1ee..bd98df951 100644 --- a/src/addons/mod/assign/components/submission/submission.ts +++ b/src/addons/mod/assign/components/submission/submission.ts @@ -56,6 +56,7 @@ import { CoreGroups } from '@services/groups'; import { CoreSync } from '@services/sync'; import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin'; import { AddonModAssignModuleHandlerService } from '../../services/handlers/module'; +import { CanLeave } from '@guards/can-leave'; /** * Component that displays an assignment submission. @@ -65,7 +66,7 @@ import { AddonModAssignModuleHandlerService } from '../../services/handlers/modu templateUrl: 'addon-mod-assign-submission.html', styleUrls: ['submission.scss'], }) -export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { +export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, CanLeave { @ViewChild(CoreTabsComponent) tabs!: CoreTabsComponent; @ViewChildren(AddonModAssignSubmissionPluginComponent) submissionComponents!: @@ -252,21 +253,20 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { /** * Check if the user can leave the view. If there are changes to be saved, it will ask for confirm. * - * @return Promise resolved if can leave the view, rejected otherwise. + * @return Promise resolved with true if can leave the view, rejected otherwise. */ - async canLeave(): Promise { + async canLeave(): Promise { // Check if there is data to save. const modified = await this.hasDataToSave(); if (modified) { // Modified, confirm user wants to go back. - try { - await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit')); - await this.discardDrafts(); - } catch { - // Cancelled by the user. - } + await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit')); + + await this.discardDrafts(); } + + return true; } /** diff --git a/src/addons/mod/assign/pages/edit/edit.ts b/src/addons/mod/assign/pages/edit/edit.ts index 6eb7739ac..31f2d9a7b 100644 --- a/src/addons/mod/assign/pages/edit/edit.ts +++ b/src/addons/mod/assign/pages/edit/edit.ts @@ -16,6 +16,7 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co import { ActivatedRoute } from '@angular/router'; import { CoreError } from '@classes/errors/error'; import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper'; +import { CanLeave } from '@guards/can-leave'; import { CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSync } from '@services/sync'; @@ -44,7 +45,7 @@ import { AddonModAssignSync } from '../../services/assign-sync'; selector: 'page-addon-mod-assign-edit', templateUrl: 'edit.html', }) -export class AddonModAssignEditPage implements OnInit, OnDestroy { +export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { @ViewChild('editSubmissionForm') formElement?: ElementRef; @@ -92,9 +93,9 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy { * * @return Resolved if we can leave it, rejected if not. */ - async ionViewCanLeave(): Promise { + async canLeave(): Promise { if (this.forceLeave) { - return; + return true; } // Check if data has changed. @@ -107,6 +108,8 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy { AddonModAssignHelper.clearSubmissionPluginTmpData(this.assign!, this.userSubmission, this.getInputData()); CoreDomUtils.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId()); + + return true; } /** diff --git a/src/addons/mod/assign/pages/submission-review/submission-review.ts b/src/addons/mod/assign/pages/submission-review/submission-review.ts index 8446a9c45..c1feef2cd 100644 --- a/src/addons/mod/assign/pages/submission-review/submission-review.ts +++ b/src/addons/mod/assign/pages/submission-review/submission-review.ts @@ -15,6 +15,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { CoreCourse } from '@features/course/services/course'; +import { CanLeave } from '@guards/can-leave'; import { IonRefresher } from '@ionic/angular'; import { CoreNavigator } from '@services/navigator'; import { CoreScreen } from '@services/screen'; @@ -29,7 +30,7 @@ import { AddonModAssign, AddonModAssignAssign } from '../../services/assign'; selector: 'page-addon-mod-assign-submission-review', templateUrl: 'submission-review.html', }) -export class AddonModAssignSubmissionReviewPage implements OnInit { +export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave { @ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent; @@ -71,7 +72,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit { * * @return Resolved if we can leave it, rejected if not. */ - ionViewCanLeave(): boolean | Promise { + async canLeave(): Promise { if (!this.submissionComponent || this.forceLeave) { return true; } diff --git a/src/addons/mod/quiz/pages/player/player.page.ts b/src/addons/mod/quiz/pages/player/player.page.ts index fc5029f7c..1e88da13a 100644 --- a/src/addons/mod/quiz/pages/player/player.page.ts +++ b/src/addons/mod/quiz/pages/player/player.page.ts @@ -44,6 +44,7 @@ import { } from '../../services/quiz'; import { AddonModQuizAttempt, AddonModQuizHelper } from '../../services/quiz-helper'; import { AddonModQuizSync } from '../../services/quiz-sync'; +import { CanLeave } from '@guards/can-leave'; /** * Page that allows attempting a quiz. @@ -53,7 +54,7 @@ import { AddonModQuizSync } from '../../services/quiz-sync'; templateUrl: 'player.html', styleUrls: ['player.scss'], }) -export class AddonModQuizPlayerPage implements OnInit, OnDestroy { +export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave { @ViewChild(IonContent) content?: IonContent; @ViewChildren(CoreQuestionComponent) questionComponents?: QueryList; @@ -144,9 +145,9 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { * * @return Resolved if we can leave it, rejected if not. */ - async ionViewCanLeave(): Promise { + async canLeave(): Promise { if (this.forceLeave || this.quizAborted || !this.questions.length || this.showSummary) { - return; + return true; } // Save answers. @@ -164,6 +165,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { } finally { modal.dismiss(); } + + return true; } /** diff --git a/src/core/features/features.module.ts b/src/core/features/features.module.ts index 9be76ae02..653f24f03 100644 --- a/src/core/features/features.module.ts +++ b/src/core/features/features.module.ts @@ -31,6 +31,7 @@ import { CoreXAPIModule } from './xapi/xapi.module'; import { CoreViewerModule } from './viewer/viewer.module'; import { CoreSearchModule } from './search/search.module'; import { CoreCommentsModule } from './comments/comments.module'; +import { CoreSitePluginsModule } from './siteplugins/siteplugins.module'; @NgModule({ imports: [ @@ -51,6 +52,7 @@ import { CoreCommentsModule } from './comments/comments.module'; CoreH5PModule, CoreViewerModule, CoreCommentsModule, + CoreSitePluginsModule, ], }) export class CoreFeaturesModule {} diff --git a/src/core/features/siteplugins/classes/handlers/course-option-handler.ts b/src/core/features/siteplugins/classes/handlers/course-option-handler.ts index 6e4cbf464..e3960190d 100644 --- a/src/core/features/siteplugins/classes/handlers/course-option-handler.ts +++ b/src/core/features/siteplugins/classes/handlers/course-option-handler.ts @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { Md5 } from 'ts-md5'; + import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData, @@ -74,10 +76,8 @@ export class CoreSitePluginsCourseOptionHandler extends CoreSitePluginsBaseHandl return { title: this.title, class: this.handlerSchema.displaydata?.class, - page: '@todo CoreSitePluginsCourseOptionComponent', - pageParams: { - handlerUniqueName: this.name, - }, + page: `siteplugins/${this.name}`, + pageParams: {}, }; } @@ -85,18 +85,19 @@ export class CoreSitePluginsCourseOptionHandler extends CoreSitePluginsBaseHandl * @inheritdoc */ getMenuDisplayData(course: CoreCourseAnyCourseDataWithOptions): CoreCourseOptionsMenuHandlerData { + const args = { + courseid: course.id, + }; + const hash = Md5.hashAsciiStr(JSON.stringify(args)); + return { title: this.title, class: this.handlerSchema.displaydata?.class, icon: this.handlerSchema.displaydata?.icon || '', - page: '@todo CoreSitePluginsPluginPage', + page: `siteplugins/${this.plugin.component}/${this.handlerSchema.method}/${hash}`, pageParams: { title: this.title, - component: this.plugin.component, - method: this.handlerSchema.method, - args: { - courseid: course.id, - }, + args, initResult: this.initResult, ptrEnabled: this.handlerSchema.ptrenabled, }, diff --git a/src/core/features/siteplugins/classes/handlers/main-menu-handler.ts b/src/core/features/siteplugins/classes/handlers/main-menu-handler.ts index becc84c29..aa118ccda 100644 --- a/src/core/features/siteplugins/classes/handlers/main-menu-handler.ts +++ b/src/core/features/siteplugins/classes/handlers/main-menu-handler.ts @@ -47,11 +47,9 @@ export class CoreSitePluginsMainMenuHandler extends CoreSitePluginsBaseHandler i title: this.title, icon: this.handlerSchema.displaydata?.icon || 'fas-question', class: this.handlerSchema.displaydata?.class, - page: '@todo CoreSitePluginsPluginPage', + page: `siteplugins/${this.plugin.component}/${this.handlerSchema.method}/0`, pageParams: { title: this.title, - component: this.plugin.component, - method: this.handlerSchema.method, initResult: this.initResult, ptrEnabled: this.handlerSchema.ptrenabled, }, diff --git a/src/core/features/siteplugins/classes/handlers/message-output-handler.ts b/src/core/features/siteplugins/classes/handlers/message-output-handler.ts index 194797fab..a67af0f94 100644 --- a/src/core/features/siteplugins/classes/handlers/message-output-handler.ts +++ b/src/core/features/siteplugins/classes/handlers/message-output-handler.ts @@ -44,11 +44,9 @@ export class CoreSitePluginsMessageOutputHandler extends CoreSitePluginsBaseHand priority: this.handlerSchema.priority || 0, label: this.title, icon: this.handlerSchema.displaydata?.icon || 'fas-question', - page: '@todo CoreSitePluginsPluginPage', + page: `siteplugins/${this.plugin.component}/${this.handlerSchema.method}/0`, pageParams: { title: this.title, - component: this.plugin.component, - method: this.handlerSchema.method, initResult: this.initResult, ptrEnabled: this.handlerSchema.ptrenabled, }, diff --git a/src/core/features/siteplugins/classes/handlers/module-handler.ts b/src/core/features/siteplugins/classes/handlers/module-handler.ts index 37de850a2..11009129f 100644 --- a/src/core/features/siteplugins/classes/handlers/module-handler.ts +++ b/src/core/features/siteplugins/classes/handlers/module-handler.ts @@ -24,7 +24,7 @@ import { CoreSitePluginsCourseModuleHandlerData, CoreSitePluginsPlugin, } from '@features/siteplugins/services/siteplugins'; -import { CoreNavigationOptions } from '@services/navigator'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreLogger } from '@singletons/logger'; import { CoreSitePluginsBaseHandler } from './base-handler'; @@ -95,11 +95,13 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp event.preventDefault(); event.stopPropagation(); - // @todo navCtrl.push('CoreSitePluginsModuleIndexPage', { - // title: module.name, - // module: module, - // courseId: courseId - // }, options); + options = options || {}; + options.params = { + title: module.name, + module, + }; + + CoreNavigator.navigateToSitePath(`siteplugins/module/${courseId}/${module.id}`, options); }; } diff --git a/src/core/features/siteplugins/classes/handlers/settings-handler.ts b/src/core/features/siteplugins/classes/handlers/settings-handler.ts index f1b9844e1..950f00039 100644 --- a/src/core/features/siteplugins/classes/handlers/settings-handler.ts +++ b/src/core/features/siteplugins/classes/handlers/settings-handler.ts @@ -49,11 +49,9 @@ export class CoreSitePluginsSettingsHandler extends CoreSitePluginsBaseHandler i title: this.title, icon: this.handlerSchema.displaydata?.icon, class: this.handlerSchema.displaydata?.class, - page: '@todo CoreSitePluginsPluginPage', + page: `siteplugins/${this.plugin.component}/${this.handlerSchema.method}/0`, params: { title: this.title, - component: this.plugin.component, - method: this.handlerSchema.method, initResult: this.initResult, ptrEnabled: this.handlerSchema.ptrenabled, }, diff --git a/src/core/features/siteplugins/classes/handlers/user-handler.ts b/src/core/features/siteplugins/classes/handlers/user-handler.ts index b19e8193f..86978a13e 100644 --- a/src/core/features/siteplugins/classes/handlers/user-handler.ts +++ b/src/core/features/siteplugins/classes/handlers/user-handler.ts @@ -20,8 +20,10 @@ import { } from '@features/siteplugins/services/siteplugins'; import { CoreUserProfile } from '@features/user/services/user'; import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; +import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreUtils, PromiseDefer } from '@services/utils/utils'; +import { Md5 } from 'ts-md5'; import { CoreSitePluginsBaseHandler } from './base-handler'; /** @@ -90,17 +92,23 @@ export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandle event.preventDefault(); event.stopPropagation(); - // @todo navCtrl.push('CoreSitePluginsPluginPage', { - // title: this.title, - // component: this.plugin.component, - // method: this.handlerSchema.method, - // args: { - // courseid: courseId, - // userid: user.id - // }, - // initResult: this.initResult, - // ptrEnabled: this.handlerSchema.ptrenabled, - // }); + const args = { + courseid: courseId, + userid: user.id, + }; + const hash = Md5.hashAsciiStr(JSON.stringify(args)); + + CoreNavigator.navigateToSitePath( + `siteplugins/${this.plugin.component}/${this.handlerSchema.method}/${hash}`, + { + params: { + title: this.title, + args, + initResult: this.initResult, + ptrEnabled: this.handlerSchema.ptrenabled, + }, + }, + ); }, }; } diff --git a/src/core/features/siteplugins/components/components.module.ts b/src/core/features/siteplugins/components/components.module.ts index 1145557fb..977cceb44 100644 --- a/src/core/features/siteplugins/components/components.module.ts +++ b/src/core/features/siteplugins/components/components.module.ts @@ -18,7 +18,6 @@ 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'; @@ -37,7 +36,6 @@ import { CoreSitePluginsOnlyTitleBlockComponent } from './only-title-block/only- CoreSitePluginsModuleIndexComponent, CoreSitePluginsBlockComponent, CoreSitePluginsOnlyTitleBlockComponent, - CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, CoreSitePluginsQuestionComponent, @@ -57,7 +55,6 @@ import { CoreSitePluginsOnlyTitleBlockComponent } from './only-title-block/only- CoreSitePluginsModuleIndexComponent, CoreSitePluginsBlockComponent, CoreSitePluginsOnlyTitleBlockComponent, - CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, CoreSitePluginsQuestionComponent, 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 index a492d2166..742c392d3 100644 --- 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 @@ -13,10 +13,12 @@ // limitations under the License. import { OnInit, Component } from '@angular/core'; +import { Md5 } from 'ts-md5'; import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; import { CoreBlockDelegate } from '@features/block/services/block-delegate'; -import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; +import { CoreSitePlugins, CoreSitePluginsUserHandlerData } from '@features/siteplugins/services/siteplugins'; +import { CoreNavigator } from '@services/navigator'; /** * Component to render blocks with only a title and link. @@ -51,18 +53,23 @@ export class CoreSitePluginsOnlyTitleBlockComponent extends CoreBlockBaseCompon 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, - // }); + const args = { + contextlevel: this.contextLevel, + instanceid: this.instanceId, + }; + const hash = Md5.hashAsciiStr(JSON.stringify(args)); + + CoreNavigator.navigateToSitePath( + `siteplugins/${handler.plugin.component}/${handler.handlerSchema.method}/${hash}`, + { + params: { + title: this.title, + args, + initResult: handler.initResult, + ptrEnabled: ( handler.handlerSchema).ptrenabled, + }, + }, + ); } } diff --git a/src/core/features/siteplugins/components/plugin-content/plugin-content.ts b/src/core/features/siteplugins/components/plugin-content/plugin-content.ts index 48b4cb749..24187a1a7 100644 --- a/src/core/features/siteplugins/components/plugin-content/plugin-content.ts +++ b/src/core/features/siteplugins/components/plugin-content/plugin-content.ts @@ -13,11 +13,14 @@ // limitations under the License. import { Component, OnInit, Input, Output, EventEmitter, DoCheck, KeyValueDiffers, ViewChild, KeyValueDiffer } from '@angular/core'; +import { Subject } from 'rxjs'; +import { Md5 } from 'ts-md5'; + import { CoreSiteWSPreSets } from '@classes/site'; import { CoreCompileHtmlComponent } from '@features/compile/components/compile-html/compile-html'; import { CoreSitePlugins, CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins'; +import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; -import { Subject } from 'rxjs'; /** * Component to render a site plugin content. @@ -145,17 +148,21 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck { 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, - // }); + component = component || this.component; + method = method || this.method; + args = args || {}; + const hash = Md5.hashAsciiStr(JSON.stringify(args)); + + CoreNavigator.navigateToSitePath(`siteplugins/${component}/${method}/${hash}`, { + params: { + title, + args, + initResult: this.initResult, + jsData, + preSets, + ptrEnabled, + }, + }); } /** diff --git a/src/core/features/siteplugins/components/course-option/core-siteplugins-course-option.html b/src/core/features/siteplugins/pages/course-option/core-siteplugins-course-option.html similarity index 71% rename from src/core/features/siteplugins/components/course-option/core-siteplugins-course-option.html rename to src/core/features/siteplugins/pages/course-option/core-siteplugins-course-option.html index e63a4ec08..bb6328cbb 100644 --- a/src/core/features/siteplugins/components/course-option/core-siteplugins-course-option.html +++ b/src/core/features/siteplugins/pages/course-option/core-siteplugins-course-option.html @@ -1,5 +1,6 @@ - + ; @@ -43,6 +43,9 @@ export class CoreSitePluginsCourseOptionComponent implements OnInit { * Component being initialized. */ ngOnInit(): void { + this.courseId = CoreNavigator.getRouteNumberParam('courseId'); + this.handlerUniqueName = CoreNavigator.getRouteParam('handlerUniqueName'); + if (!this.handlerUniqueName) { return; } @@ -75,4 +78,55 @@ export class CoreSitePluginsCourseOptionComponent implements OnInit { } } + /** + * The page is about to enter and become the active page. + */ + ionViewWillEnter(): void { + this.content?.callComponentFunction('ionViewWillEnter'); + } + + /** + * The page has fully entered and is now the active page. This event will fire, whether it was the first load or a cached page. + */ + ionViewDidEnter(): void { + this.content?.callComponentFunction('ionViewDidEnter'); + } + + /** + * The page is about to leave and no longer be the active page. + */ + ionViewWillLeave(): void { + this.content?.callComponentFunction('ionViewWillLeave'); + } + + /** + * The page has finished leaving and is no longer the active page. + */ + ionViewDidLeave(): void { + this.content?.callComponentFunction('ionViewDidLeave'); + } + + /** + * The page is about to be destroyed and have its elements removed. + */ + ionViewWillUnload(): void { + this.content?.callComponentFunction('ionViewWillUnload'); + } + + /** + * Check if we can leave the page or not. + * + * @return Resolved if we can leave it, rejected if not. + */ + async canLeave(): Promise { + if (!this.content) { + return true; + } + + + const result = await this.content.callComponentFunction('canLeave'); + + return result === undefined || result === null ? true : !!result; + } + } diff --git a/src/core/features/siteplugins/pages/module-index/module-index.html b/src/core/features/siteplugins/pages/module-index/module-index.html new file mode 100644 index 000000000..c1b98c79a --- /dev/null +++ b/src/core/features/siteplugins/pages/module-index/module-index.html @@ -0,0 +1,19 @@ + + + + + + {{ title }} + + + + + + + + + + + + diff --git a/src/core/features/siteplugins/pages/module-index/module-index.module.ts b/src/core/features/siteplugins/pages/module-index/module-index.module.ts new file mode 100644 index 000000000..edf9be25a --- /dev/null +++ b/src/core/features/siteplugins/pages/module-index/module-index.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 { RouterModule, Routes } from '@angular/router'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { CanLeaveGuard } from '@guards/can-leave'; +import { CoreSitePluginsModuleIndexPage } from './module-index'; +import { CoreSitePluginsComponentsModule } from '../../components/components.module'; + +const routes: Routes = [ + { + path: '', + component: CoreSitePluginsModuleIndexPage, + canDeactivate: [CanLeaveGuard], + }, +]; + +/** + * Module to lazy load the page. + */ +@NgModule({ + declarations: [ + CoreSitePluginsModuleIndexPage, + ], + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + CoreSitePluginsComponentsModule, + ], + exports: [RouterModule], +}) +export class CoreSitePluginsModuleIndexPageModule {} diff --git a/src/core/features/siteplugins/pages/module-index/module-index.ts b/src/core/features/siteplugins/pages/module-index/module-index.ts new file mode 100644 index 000000000..b4daca1f6 --- /dev/null +++ b/src/core/features/siteplugins/pages/module-index/module-index.ts @@ -0,0 +1,109 @@ +// (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, ViewChild } from '@angular/core'; +import { IonRefresher } from '@ionic/angular'; + +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CanLeave } from '@guards/can-leave'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSitePluginsModuleIndexComponent } from '../../components/module-index/module-index'; + +/** + * Page to render the index page of a module site plugin. + */ +@Component({ + selector: 'page-core-site-plugins-module-index', + templateUrl: 'module-index.html', +}) +export class CoreSitePluginsModuleIndexPage implements OnInit, CanLeave { + + @ViewChild(CoreSitePluginsModuleIndexComponent) content?: CoreSitePluginsModuleIndexComponent; + + title?: string; // Page title. + module?: CoreCourseModule; + courseId?: number; + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.title = CoreNavigator.getRouteParam('title'); + this.module = CoreNavigator.getRouteParam('module'); + this.courseId = CoreNavigator.getRouteNumberParam('courseId'); + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + */ + refreshData(refresher: IonRefresher): void { + this.content?.doRefresh().finally(() => { + refresher.complete(); + }); + } + + /** + * The page is about to enter and become the active page. + */ + ionViewWillEnter(): void { + this.content?.callComponentFunction('ionViewWillEnter'); + } + + /** + * The page has fully entered and is now the active page. This event will fire, whether it was the first load or a cached page. + */ + ionViewDidEnter(): void { + this.content?.callComponentFunction('ionViewDidEnter'); + } + + /** + * The page is about to leave and no longer be the active page. + */ + ionViewWillLeave(): void { + this.content?.callComponentFunction('ionViewWillLeave'); + } + + /** + * The page has finished leaving and is no longer the active page. + */ + ionViewDidLeave(): void { + this.content?.callComponentFunction('ionViewDidLeave'); + } + + /** + * The page is about to be destroyed and have its elements removed. + */ + ionViewWillUnload(): void { + this.content?.callComponentFunction('ionViewWillUnload'); + } + + /** + * Check if we can leave the page or not. + * + * @return Resolved if we can leave it, rejected if not. + */ + async canLeave(): Promise { + if (!this.content) { + return true; + } + + + const result = await this.content.callComponentFunction('canLeave'); + + return result === undefined || result === null ? true : !!result; + } + +} diff --git a/src/core/features/siteplugins/pages/plugin-page/plugin-page.html b/src/core/features/siteplugins/pages/plugin-page/plugin-page.html new file mode 100644 index 000000000..ea8d93c87 --- /dev/null +++ b/src/core/features/siteplugins/pages/plugin-page/plugin-page.html @@ -0,0 +1,21 @@ + + + + + + {{ title | translate }} + + + + + + + + + + + + + diff --git a/src/core/features/siteplugins/pages/plugin-page/plugin-page.module.ts b/src/core/features/siteplugins/pages/plugin-page/plugin-page.module.ts new file mode 100644 index 000000000..083771477 --- /dev/null +++ b/src/core/features/siteplugins/pages/plugin-page/plugin-page.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 { RouterModule, Routes } from '@angular/router'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { CanLeaveGuard } from '@guards/can-leave'; +import { CoreSitePluginsPluginPage } from './plugin-page'; +import { CoreSitePluginsComponentsModule } from '../../components/components.module'; + +const routes: Routes = [ + { + path: '', + component: CoreSitePluginsPluginPage, + canDeactivate: [CanLeaveGuard], + }, +]; + +/** + * Module to lazy load the page. + */ +@NgModule({ + declarations: [ + CoreSitePluginsPluginPage, + ], + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + CoreSitePluginsComponentsModule, + ], + exports: [RouterModule], +}) +export class CoreSitePluginsPluginPageModule {} diff --git a/src/core/features/siteplugins/pages/plugin-page/plugin-page.ts b/src/core/features/siteplugins/pages/plugin-page/plugin-page.ts new file mode 100644 index 000000000..0895d5157 --- /dev/null +++ b/src/core/features/siteplugins/pages/plugin-page/plugin-page.ts @@ -0,0 +1,120 @@ +// (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, ViewChild } from '@angular/core'; +import { IonRefresher } from '@ionic/angular'; + +import { CoreSiteWSPreSets } from '@classes/site'; +import { CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins'; +import { CanLeave } from '@guards/can-leave'; +import { CoreNavigator } from '@services/navigator'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreSitePluginsPluginContentComponent } from '../../components/plugin-content/plugin-content'; + +/** + * Page to render a site plugin page. + */ +@Component({ + selector: 'page-core-site-plugins-plugin', + templateUrl: 'plugin-page.html', +}) +export class CoreSitePluginsPluginPage implements OnInit, CanLeave { + + @ViewChild(CoreSitePluginsPluginContentComponent) content?: CoreSitePluginsPluginContentComponent; + + title?: string; // Page title. + component?: string; + method?: string; + args?: Record; + initResult?: CoreSitePluginsContent | null; + jsData?: Record; // JS variables to pass to the plugin so they can be used in the template or JS. + preSets?: CoreSiteWSPreSets; // The preSets for the WS call. + ptrEnabled = false; + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.title = CoreNavigator.getRouteParam('title'); + this.component = CoreNavigator.getRouteParam('component'); + this.method = CoreNavigator.getRouteParam('method'); + this.args = CoreNavigator.getRouteParam('args'); + this.initResult = CoreNavigator.getRouteParam('initResult'); + this.jsData = CoreNavigator.getRouteParam('jsData'); + this.preSets = CoreNavigator.getRouteParam('preSets'); + this.ptrEnabled = !CoreUtils.isFalseOrZero(CoreNavigator.getRouteBooleanParam('ptrEnabled')); + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + */ + refreshData(refresher: IonRefresher): void { + this.content?.refreshContent(false).finally(() => { + refresher.complete(); + }); + } + + /** + * The page is about to enter and become the active page. + */ + ionViewWillEnter(): void { + this.content?.callComponentFunction('ionViewWillEnter'); + } + + /** + * The page has fully entered and is now the active page. This event will fire, whether it was the first load or a cached page. + */ + ionViewDidEnter(): void { + this.content?.callComponentFunction('ionViewDidEnter'); + } + + /** + * The page is about to leave and no longer be the active page. + */ + ionViewWillLeave(): void { + this.content?.callComponentFunction('ionViewWillLeave'); + } + + /** + * The page has finished leaving and is no longer the active page. + */ + ionViewDidLeave(): void { + this.content?.callComponentFunction('ionViewDidLeave'); + } + + /** + * The page is about to be destroyed and have its elements removed. + */ + ionViewWillUnload(): void { + this.content?.callComponentFunction('ionViewWillUnload'); + } + + /** + * Check if we can leave the page or not. + * + * @return Resolved if we can leave it, rejected if not. + */ + async canLeave(): Promise { + if (!this.content) { + return true; + } + + const result = await this.content.callComponentFunction('canLeave'); + + return result === undefined || result === null ? true : !!result; + } + +} diff --git a/src/core/features/siteplugins/siteplugins-lazy.module.ts b/src/core/features/siteplugins/siteplugins-lazy.module.ts new file mode 100644 index 000000000..7b7e4a3dd --- /dev/null +++ b/src/core/features/siteplugins/siteplugins-lazy.module.ts @@ -0,0 +1,32 @@ +// (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 { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: 'module/:courseId/:cmId', + loadChildren: () => import('./pages/module-index/module-index.module').then( m => m.CoreSitePluginsModuleIndexPageModule), + }, + { + path: ':component/:method/:hash', + loadChildren: () => import('./pages/plugin-page/plugin-page.module').then( m => m.CoreSitePluginsPluginPageModule), + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], +}) +export class CoreSitePluginsLazyModule {} diff --git a/src/core/features/siteplugins/siteplugins.module.ts b/src/core/features/siteplugins/siteplugins.module.ts new file mode 100644 index 000000000..154e3feb0 --- /dev/null +++ b/src/core/features/siteplugins/siteplugins.module.ts @@ -0,0 +1,54 @@ +// (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 { Routes } from '@angular/router'; + +import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CoreSitePluginsComponentsModule } from './components/components.module'; +import { CoreSitePluginsHelper } from './services/siteplugins-helper'; + +const routes: Routes = [ + { + path: 'siteplugins', + loadChildren: () => import('@features/siteplugins/siteplugins-lazy.module').then(m => m.CoreSitePluginsLazyModule), + }, +]; + +const courseIndexRoutes: Routes = [ + { + path: 'siteplugins/:handlerUniqueName', + loadChildren: () => import('@features/siteplugins/pages/course-option/course-option.module') + .then(m => m.CoreSitePluginsCourseOptionModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }), + CoreSitePluginsComponentsModule, + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreSitePluginsHelper.initialize(); + }, + }, + ], +}) +export class CoreSitePluginsModule {} diff --git a/src/core/pipes/pipes.module.ts b/src/core/pipes/pipes.module.ts index 8e32caa7f..36e2e09c7 100644 --- a/src/core/pipes/pipes.module.ts +++ b/src/core/pipes/pipes.module.ts @@ -21,6 +21,7 @@ import { CoreTimeAgoPipe } from './time-ago'; import { CoreBytesToSizePipe } from './bytes-to-size'; import { CoreDurationPipe } from './duration'; import { CoreDateDayOrTimePipe } from './date-day-or-time'; +import { CoreToLocaleStringPipe } from './to-locale-string'; @NgModule({ declarations: [ @@ -32,6 +33,7 @@ import { CoreDateDayOrTimePipe } from './date-day-or-time'; CoreSecondsToHMSPipe, CoreDurationPipe, CoreDateDayOrTimePipe, + CoreToLocaleStringPipe, ], imports: [], exports: [ @@ -43,6 +45,7 @@ import { CoreDateDayOrTimePipe } from './date-day-or-time'; CoreSecondsToHMSPipe, CoreDurationPipe, CoreDateDayOrTimePipe, + CoreToLocaleStringPipe, ], }) export class CorePipesModule {} diff --git a/src/core/pipes/to-locale-string.ts b/src/core/pipes/to-locale-string.ts new file mode 100644 index 000000000..d64df8a6d --- /dev/null +++ b/src/core/pipes/to-locale-string.ts @@ -0,0 +1,66 @@ +// (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 { Pipe, PipeTransform } from '@angular/core'; +import { CoreTimeUtils } from '@services/utils/time'; + +import { CoreLogger } from '@singletons/logger'; + +/** + * Filter to format a timestamp to a locale string. Timestamp can be in seconds or milliseconds. + * + * @deprecated since 3.6. Use coreFormatDate instead. + */ +@Pipe({ + name: 'coreToLocaleString', +}) +export class CoreToLocaleStringPipe implements PipeTransform { + + protected logger: CoreLogger; + + constructor() { + this.logger = CoreLogger.getInstance('CoreToLocaleStringPipe'); + } + + /** + * Format a timestamp to a locale string. + * + * @param timestamp The timestamp (can be in seconds or milliseconds). + * @return Formatted time. + */ + transform(timestamp: number | string): string { + if (typeof timestamp == 'string') { + // Convert the value to a number. + const numberTimestamp = parseInt(timestamp, 10); + if (isNaN(numberTimestamp)) { + this.logger.error('Invalid value received', timestamp); + + return timestamp; + } + timestamp = numberTimestamp; + } + + if (timestamp < 0) { + // Date not valid. + return ''; + } + if (timestamp < 100000000000) { + // Timestamp is in seconds, convert it to milliseconds. + timestamp = timestamp * 1000; + } + + return CoreTimeUtils.userDate(timestamp, 'core.strftimedatetimeshort'); + } + +} diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index b5b23f0bf..e9ac75b7d 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -186,15 +186,6 @@ export class CoreUtilsProvider { } } - /** - * Blocks leaving a view. - * - * @deprecated, use ionViewCanLeave instead. - */ - blockLeaveView(): void { - return; - } - /** * Check if a URL has a redirect. * From f5a7da148ff23e7c8e8ab26769e37a15e5cdaa49 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 4 Mar 2021 08:15:44 +0100 Subject: [PATCH 06/10] MOBILE-3664 siteplugins: Implement directives --- src/core/features/compile/services/compile.ts | 49 +++---- .../classes/call-ws-click-directive.ts | 82 +++++++++++ .../siteplugins/classes/call-ws-directive.ts | 135 ++++++++++++++++++ .../directives/call-ws-new-content.ts | 119 +++++++++++++++ .../siteplugins/directives/call-ws-on-load.ts | 57 ++++++++ .../siteplugins/directives/call-ws.ts | 81 +++++++++++ .../directives/directives.module.ts | 37 +++++ .../siteplugins/directives/new-content.ts | 117 +++++++++++++++ 8 files changed, 651 insertions(+), 26 deletions(-) create mode 100644 src/core/features/siteplugins/classes/call-ws-click-directive.ts create mode 100644 src/core/features/siteplugins/classes/call-ws-directive.ts create mode 100644 src/core/features/siteplugins/directives/call-ws-new-content.ts create mode 100644 src/core/features/siteplugins/directives/call-ws-on-load.ts create mode 100644 src/core/features/siteplugins/directives/call-ws.ts create mode 100644 src/core/features/siteplugins/directives/directives.module.ts create mode 100644 src/core/features/siteplugins/directives/new-content.ts diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 704968c10..b44a96b8c 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -64,7 +64,7 @@ import { CORE_SITEHOME_SERVICES } from '@features/sitehome/sitehome.module'; import { CORE_TAG_SERVICES } from '@features/tag/tag.module'; import { CORE_USER_SERVICES } from '@features/user/user.module'; import { CORE_XAPI_SERVICES } from '@features/xapi/xapi.module'; -// @todo import { CoreSitePluginsProvider } from '@features/siteplugins/services/siteplugins'; +import { CoreSitePluginsProvider } from '@features/siteplugins/services/siteplugins'; // Import other libraries and providers. import { DomSanitizer } from '@angular/platform-browser'; @@ -93,7 +93,7 @@ import { CoreSharedModule } from '@/core/shared.module'; import { CoreCourseComponentsModule } from '@features/course/components/components.module'; import { CoreCourseDirectivesModule } from '@features/course/directives/directives.module'; import { CoreCoursesComponentsModule } from '@features/courses/components/components.module'; -// @todo import { CoreSitePluginsDirectivesModule } from '@features/siteplugins/directives/directives.module'; +import { CoreSitePluginsDirectivesModule } from '@features/siteplugins/directives/directives.module'; import { CoreUserComponentsModule } from '@features/user/components/components.module'; import { CoreQuestionComponentsModule } from '@features/question/components/components.module'; import { CoreBlockComponentsModule } from '@features/block/components/components.module'; @@ -103,17 +103,15 @@ import { CoreSearchComponentsModule } from '@features/search/components/componen // Import some components so they can be injected dynamically. import { CoreCourseUnsupportedModuleComponent } from '@features/course/components/unsupported-module/unsupported-module'; import { CoreCourseFormatSingleActivityComponent } from '@features/course/format/singleactivity/components/singleactivity'; -// @todo -// import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index'; -// import { CoreSitePluginsBlockComponent } from '@features/siteplugins/components/block/block'; -// import { CoreSitePluginsCourseOptionComponent } from '@features/siteplugins/components/course-option/course-option'; -// import { CoreSitePluginsCourseFormatComponent } from '@features/siteplugins/components/course-format/course-format'; -// import { CoreSitePluginsQuestionComponent } from '@features/siteplugins/components/question/question'; -// import { CoreSitePluginsQuestionBehaviourComponent } from '@features/siteplugins/components/question-behaviour/question-behaviour'; -// import { CoreSitePluginsUserProfileFieldComponent } from '@features/siteplugins/components/user-profile-field/user-profile-field'; -// import { CoreSitePluginsQuizAccessRuleComponent } from '@features/siteplugins/components/quiz-access-rule/quiz-access-rule'; -// import { CoreSitePluginsAssignFeedbackComponent } from '@features/siteplugins/components/assign-feedback/assign-feedback'; -// import { CoreSitePluginsAssignSubmissionComponent } from '@features/siteplugins/components/assign-submission/assign-submission'; +import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index'; +import { CoreSitePluginsBlockComponent } from '@features/siteplugins/components/block/block'; +import { CoreSitePluginsCourseFormatComponent } from '@features/siteplugins/components/course-format/course-format'; +import { CoreSitePluginsQuestionComponent } from '@features/siteplugins/components/question/question'; +import { CoreSitePluginsQuestionBehaviourComponent } from '@features/siteplugins/components/question-behaviour/question-behaviour'; +import { CoreSitePluginsUserProfileFieldComponent } from '@features/siteplugins/components/user-profile-field/user-profile-field'; +import { CoreSitePluginsQuizAccessRuleComponent } from '@features/siteplugins/components/quiz-access-rule/quiz-access-rule'; +import { CoreSitePluginsAssignFeedbackComponent } from '@features/siteplugins/components/assign-feedback/assign-feedback'; +import { CoreSitePluginsAssignSubmissionComponent } from '@features/siteplugins/components/assign-submission/assign-submission'; // Import addon providers. Do not import database module because it causes circular dependencies. import { ADDON_BADGES_SERVICES } from '@addons/badges/badges.module'; @@ -169,8 +167,8 @@ export class CoreCompileProvider { protected readonly IMPORTS = [ CoreSharedModule, CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreUserComponentsModule, CoreCourseDirectivesModule, CoreQuestionComponentsModule, AddonModAssignComponentsModule, - CoreBlockComponentsModule, CoreEditorComponentsModule, CoreSearchComponentsModule, - // @todo AddonModWorkshopComponentsModule, CoreSitePluginsDirectivesModule, + CoreBlockComponentsModule, CoreEditorComponentsModule, CoreSearchComponentsModule, CoreSitePluginsDirectivesModule, + // @todo AddonModWorkshopComponentsModule, ]; constructor(protected injector: Injector, compilerFactory: JitCompilerFactory) { @@ -272,7 +270,7 @@ export class CoreCompileProvider { ...CORE_SETTINGS_SERVICES, // @todo ...CORE_SHAREDFILES_SERVICES, ...CORE_SITEHOME_SERVICES, - // @todo ...CoreSitePluginsProvider, + CoreSitePluginsProvider, ...CORE_TAG_SERVICES, ...CORE_USER_SERVICES, ...CORE_XAPI_SERVICES, @@ -348,16 +346,15 @@ export class CoreCompileProvider { instance['CoreCourseResourcePrefetchHandlerBase'] = CoreCourseResourcePrefetchHandlerBase; instance['CoreCourseUnsupportedModuleComponent'] = CoreCourseUnsupportedModuleComponent; instance['CoreCourseFormatSingleActivityComponent'] = CoreCourseFormatSingleActivityComponent; - // @todo instance['CoreSitePluginsModuleIndexComponent'] = CoreSitePluginsModuleIndexComponent; - // instance['CoreSitePluginsBlockComponent'] = CoreSitePluginsBlockComponent; - // instance['CoreSitePluginsCourseOptionComponent'] = CoreSitePluginsCourseOptionComponent; - // instance['CoreSitePluginsCourseFormatComponent'] = CoreSitePluginsCourseFormatComponent; - // instance['CoreSitePluginsQuestionComponent'] = CoreSitePluginsQuestionComponent; - // instance['CoreSitePluginsQuestionBehaviourComponent'] = CoreSitePluginsQuestionBehaviourComponent; - // instance['CoreSitePluginsUserProfileFieldComponent'] = CoreSitePluginsUserProfileFieldComponent; - // instance['CoreSitePluginsQuizAccessRuleComponent'] = CoreSitePluginsQuizAccessRuleComponent; - // instance['CoreSitePluginsAssignFeedbackComponent'] = CoreSitePluginsAssignFeedbackComponent; - // instance['CoreSitePluginsAssignSubmissionComponent'] = CoreSitePluginsAssignSubmissionComponent; + instance['CoreSitePluginsModuleIndexComponent'] = CoreSitePluginsModuleIndexComponent; + instance['CoreSitePluginsBlockComponent'] = CoreSitePluginsBlockComponent; + instance['CoreSitePluginsCourseFormatComponent'] = CoreSitePluginsCourseFormatComponent; + instance['CoreSitePluginsQuestionComponent'] = CoreSitePluginsQuestionComponent; + instance['CoreSitePluginsQuestionBehaviourComponent'] = CoreSitePluginsQuestionBehaviourComponent; + instance['CoreSitePluginsUserProfileFieldComponent'] = CoreSitePluginsUserProfileFieldComponent; + instance['CoreSitePluginsQuizAccessRuleComponent'] = CoreSitePluginsQuizAccessRuleComponent; + instance['CoreSitePluginsAssignFeedbackComponent'] = CoreSitePluginsAssignFeedbackComponent; + instance['CoreSitePluginsAssignSubmissionComponent'] = CoreSitePluginsAssignSubmissionComponent; instance['CoreGeolocationError'] = CoreGeolocationError; instance['CoreGeolocationErrorReason'] = CoreGeolocationErrorReason; } diff --git a/src/core/features/siteplugins/classes/call-ws-click-directive.ts b/src/core/features/siteplugins/classes/call-ws-click-directive.ts new file mode 100644 index 000000000..4e822ba1c --- /dev/null +++ b/src/core/features/siteplugins/classes/call-ws-click-directive.ts @@ -0,0 +1,82 @@ +// (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 { Input, OnInit, ElementRef, Directive } from '@angular/core'; + +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; +import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; +import { CoreSitePluginsCallWSBaseDirective } from './call-ws-directive'; + +/** + * Base class for directives to call a WS when the element is clicked. + * + * The directives that inherit from this class will call a WS method when the element is clicked. + */ +@Directive() +export class CoreSitePluginsCallWSOnClickBaseDirective extends CoreSitePluginsCallWSBaseDirective implements OnInit { + + @Input() confirmMessage?: string; // Message to confirm the action. If not supplied, no confirmation. If empty, default message. + @Input() showError?: boolean | string; // Whether to show an error message if the WS call fails. Defaults to true. + + constructor( + element: ElementRef, + parentContent: CoreSitePluginsPluginContentComponent | null, + ) { + super(element, parentContent); + } + + /** + * @inheritdoc + */ + ngOnInit(): void { + super.ngOnInit(); + + this.element.addEventListener('click', async (ev: Event) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (typeof this.confirmMessage != 'undefined') { + // Ask for confirm. + try { + await CoreDomUtils.showConfirm(this.confirmMessage || Translate.instant('core.areyousure')); + } catch { + // User cancelled, stop. + return; + } + } + + this.callWS(); + }); + } + + /** + * @inheritdoc + */ + protected async callWS(): Promise { + const modal = await CoreDomUtils.showModalLoading(); + + try { + await super.callWS(); + } catch (error) { + if (typeof this.showError == 'undefined' || CoreUtils.isTrueOrOne(this.showError)) { + CoreDomUtils.showErrorModalDefault(error, 'core.serverconnection', true); + } + } finally { + modal.dismiss(); + } + } + +} diff --git a/src/core/features/siteplugins/classes/call-ws-directive.ts b/src/core/features/siteplugins/classes/call-ws-directive.ts new file mode 100644 index 000000000..90ae3ab2a --- /dev/null +++ b/src/core/features/siteplugins/classes/call-ws-directive.ts @@ -0,0 +1,135 @@ +// (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 { Input, OnInit, OnDestroy, ElementRef, Output, EventEmitter, Directive } from '@angular/core'; +import { Subscription } from 'rxjs'; + +import { CoreSiteWSPreSets } from '@classes/site'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; +import { CoreSitePlugins } from '../services/siteplugins'; +import { CoreLogger } from '@singletons/logger'; + +/** + * Base class for directives that need to call a WS. + */ +@Directive() +export class CoreSitePluginsCallWSBaseDirective implements OnInit, OnDestroy { + + @Input() name!: string; // The name of the WS to call. + @Input() params?: Record; // The params for the WS call. + @Input() preSets?: CoreSiteWSPreSets; // The preSets for the WS call. + @Input() useOtherDataForWS?: string[] | unknown; // Whether to include other data in the params for the WS. + @Input() form?: string; // ID or name to identify a form. The form data will be retrieved and sent to the WS. + @Output() onSuccess = new EventEmitter(); // Sends the result when the WS call succeeds. + @Output() onError = new EventEmitter(); // Sends the error when the WS call fails. + @Output() onDone = new EventEmitter(); // Notifies when the WS call is done (either success or fail). + + protected logger: CoreLogger; + protected element: HTMLElement; + protected invalidateObserver?: Subscription; + + constructor( + element: ElementRef, + protected parentContent: CoreSitePluginsPluginContentComponent | null, + ) { + this.element = element.nativeElement || element; + this.logger = CoreLogger.getInstance('CoreSitePluginsCallWS'); + } + + /** + * @inheritdoc + */ + ngOnInit(): void { + if (!this.parentContent?.invalidateObservable) { + return; + } + + this.invalidateObserver = this.parentContent.invalidateObservable.subscribe(() => { + this.invalidate(); + }); + } + + /** + * Call a WS. + * + * @return Promise resolved when done. + */ + protected async callWS(): Promise { + try { + const params = this.getParamsForWS(); + + const result = await CoreSitePlugins.callWS(this.name, params, this.preSets); + + this.onSuccess.emit(result); + + return this.wsCallSuccess(result); + } catch (error) { + this.onError.emit(error); + this.logger.error(`Error calling WS ${this.name}`, error); + + throw error; + } finally { + this.onDone.emit(); + } + } + + /** + * Get the params for the WS call. + * + * @return Params. + */ + protected getParamsForWS(): Record { + let params = this.params || {}; + + if (this.parentContent) { + params = CoreSitePlugins.loadOtherDataInArgs(params, this.parentContent.otherData, this.useOtherDataForWS); + } + + if (this.form && document.forms[this.form]) { + params = Object.assign(params, CoreDomUtils.getDataFromForm(document.forms[this.form])); + } + + return params; + } + + /** + * Function called when the WS call is successful. + * + * @param result Result of the WS call. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected wsCallSuccess(result: unknown): void { + // Function to be overridden. + } + + /** + * Invalidate the WS call. + * + * @return Promise resolved when done. + */ + invalidate(): Promise { + const params = this.getParamsForWS(); + + return CoreSitePlugins.instance.invalidateCallWS(this.name, params, this.preSets); + } + + /** + * Directive destroyed. + */ + ngOnDestroy(): void { + this.invalidateObserver?.unsubscribe(); + } + +} diff --git a/src/core/features/siteplugins/directives/call-ws-new-content.ts b/src/core/features/siteplugins/directives/call-ws-new-content.ts new file mode 100644 index 000000000..54c773fd9 --- /dev/null +++ b/src/core/features/siteplugins/directives/call-ws-new-content.ts @@ -0,0 +1,119 @@ +// (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 { Directive, Input, ElementRef, Optional } from '@angular/core'; +import { CoreSiteWSPreSets } from '@classes/site'; +import { CoreNavigator } from '@services/navigator'; +import { CoreUtils } from '@services/utils/utils'; +import { Md5 } from 'ts-md5'; + +import { CoreSitePluginsCallWSOnClickBaseDirective } from '../classes/call-ws-click-directive'; +import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; +import { CoreSitePlugins } from '../services/siteplugins'; + +/** + * Directive to call a WS when the element is clicked and load a new content passing the WS result as args. This new content + * can be displayed in a new page or in the same page (only if current page is already displaying a site plugin content). + * + * If you don't need to load some new content when done, @see CoreSitePluginsCallWSDirective. + * + * @see CoreSitePluginsCallWSOnClickBaseDirective. + * + * Example usages: + * + * A button to get some data from the server without using cache, showing default confirm and displaying a new page: + * + * + * {{ 'plugin.mod_certificate_coursecertificate.getissued' | translate }} + * + * + * A button to get some data from the server using cache, without confirm, displaying new content in same page and using + * userid from otherdata: + * + * + * {{ 'plugin.mod_certificate_coursecertificate.getissued' | translate }} + * + */ +@Directive({ + selector: '[core-site-plugins-call-ws-new-content]', +}) +export class CoreSitePluginsCallWSNewContentDirective extends CoreSitePluginsCallWSOnClickBaseDirective { + + @Input() component?: string; // The component of the new content. If not provided, use the same component as current page. + @Input() method?: string; // The method to get the new content. If not provided, use the same method as current page. + @Input() args?: Record; // The params to get the new content. + @Input() title?: string; // The title to display with the new content. Only if samePage=false. + @Input() samePage?: boolean | string; // Whether to display the content in same page or open a new one. Defaults to new page. + @Input() useOtherData?: string[] | unknown; // Whether to include other data in the args. + @Input() form?: string; // ID or name to identify a form. The form data will be retrieved and sent to the WS. + // JS variables to pass to the new page 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. + @Input() jsData?: Record | boolean; + @Input() newContentPreSets?: CoreSiteWSPreSets; // The preSets for the WS call of the new content. + @Input() ptrEnabled?: boolean | string; // Whether PTR should be enabled in the new page. Defaults to true. + + constructor( + element: ElementRef, + @Optional() parentContent: CoreSitePluginsPluginContentComponent, + ) { + super(element, parentContent); + } + + /** + * Function called when the WS call is successful. + * + * @param result Result of the WS call. + */ + protected wsCallSuccess(result: unknown): void { + let args = this.args || {}; + + if (this.parentContent) { + args = CoreSitePlugins.loadOtherDataInArgs(this.args, this.parentContent.otherData, this.useOtherData); + } + + // Add the properties from the WS call result to the args. + args = Object.assign(args, result); + + let jsData = this.jsData || {}; + if (jsData === true) { + jsData = this.parentContent?.data || {}; + } + + if (CoreUtils.isTrueOrOne(this.samePage)) { + // Update the parent content (if it exists). + this.parentContent?.updateContent(args, this.component, this.method, jsData, this.newContentPreSets); + } else { + const component = this.component || this.parentContent?.component; + const method = this.method || this.parentContent?.method; + const hash = Md5.hashAsciiStr(JSON.stringify(args)); + + CoreNavigator.navigateToSitePath(`siteplugins/${component}/${method}/${hash}`, { + params: { + title: this.title || this.parentContent?.pageTitle, + args, + initResult: this.parentContent?.initResult, + jsData, + preSets: this.newContentPreSets, + ptrEnabled: this.ptrEnabled, + }, + }); + } + } + +} diff --git a/src/core/features/siteplugins/directives/call-ws-on-load.ts b/src/core/features/siteplugins/directives/call-ws-on-load.ts new file mode 100644 index 000000000..50c3d3d88 --- /dev/null +++ b/src/core/features/siteplugins/directives/call-ws-on-load.ts @@ -0,0 +1,57 @@ +// (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 { Directive, OnInit, ElementRef, Optional } from '@angular/core'; + +import { CoreSitePluginsCallWSBaseDirective } from '../classes/call-ws-directive'; +import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; + +/** + * Directive to call a WS as soon as its loaded. + * This directive is meant for actions to do in the background, like calling logging WebServices. + * + * If you want to call a WS when the user clicks on a certain element, @see CoreSitePluginsCallWSDirective. + * + * @see CoreSitePluginsCallWSBaseDirective. + * + * Example usage: + * + * + */ +@Directive({ + selector: '[core-site-plugins-call-ws-on-load]', +}) +export class CoreSitePluginsCallWSOnLoadDirective extends CoreSitePluginsCallWSBaseDirective implements OnInit { + + constructor( + element: ElementRef, + @Optional() parentContent: CoreSitePluginsPluginContentComponent, + ) { + super(element, parentContent); + } + + /** + * @inheritdoc + */ + ngOnInit(): void { + super.ngOnInit(); + + // Call the WS immediately. + this.callWS().catch(() => { + // Ignore errors. + }); + } + +} diff --git a/src/core/features/siteplugins/directives/call-ws.ts b/src/core/features/siteplugins/directives/call-ws.ts new file mode 100644 index 000000000..71c5f82c3 --- /dev/null +++ b/src/core/features/siteplugins/directives/call-ws.ts @@ -0,0 +1,81 @@ +// (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 { Directive, Input, ElementRef, Optional } from '@angular/core'; + +import { Translate } from '@singletons'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSitePluginsCallWSOnClickBaseDirective } from '../classes/call-ws-click-directive'; +import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; + +/** + * Directive to call a WS when the element is clicked. The action to do when the WS call is successful depends on the input data: + * display a message, go back or refresh current view. + * + * If you want to load a new content when the WS call is done, @see CoreSitePluginsCallWSNewContentDirective. + * + * @see CoreSitePluginsCallWSOnClickBaseDirective. + * + * Example usages: + * + * A button to send some data to the server without using cache, displaying default messages and refreshing on success: + * + * + * {{ 'plugin.mod_certificate_coursecertificate.senddata' | translate }} + * + * + * A button to send some data to the server using cache, without confirm, going back on success and using userid from otherdata: + * + * + * {{ 'plugin.mod_certificate_coursecertificate.senddata' | translate }} + * + */ +@Directive({ + selector: '[core-site-plugins-call-ws]', +}) +export class CoreSitePluginsCallWSDirective extends CoreSitePluginsCallWSOnClickBaseDirective { + + @Input() successMessage?: string; // Message to show on success. If not supplied, no message. If empty, default message. + @Input() goBackOnSuccess?: boolean | string; // Whether to go back if the WS call is successful. + @Input() refreshOnSuccess?: boolean | string; // Whether to refresh the current view if the WS call is successful. + + constructor( + element: ElementRef, + @Optional() parentContent: CoreSitePluginsPluginContentComponent, + ) { + super(element, parentContent); + } + + /** + * @inheritdoc + */ + protected wsCallSuccess(): void { + if (typeof this.successMessage != 'undefined') { + // Display the success message. + CoreDomUtils.showToast(this.successMessage || Translate.instant('core.success')); + } + + if (CoreUtils.isTrueOrOne(this.goBackOnSuccess)) { + CoreNavigator.back(); + } else if (CoreUtils.isTrueOrOne(this.refreshOnSuccess) && this.parentContent) { + this.parentContent.refreshContent(true); + } + } + +} diff --git a/src/core/features/siteplugins/directives/directives.module.ts b/src/core/features/siteplugins/directives/directives.module.ts new file mode 100644 index 000000000..d35d8e0fe --- /dev/null +++ b/src/core/features/siteplugins/directives/directives.module.ts @@ -0,0 +1,37 @@ +// (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 { CoreSitePluginsCallWSDirective } from './call-ws'; +import { CoreSitePluginsCallWSNewContentDirective } from './call-ws-new-content'; +import { CoreSitePluginsCallWSOnLoadDirective } from './call-ws-on-load'; +import { CoreSitePluginsNewContentDirective } from './new-content'; + +@NgModule({ + declarations: [ + CoreSitePluginsCallWSDirective, + CoreSitePluginsCallWSNewContentDirective, + CoreSitePluginsCallWSOnLoadDirective, + CoreSitePluginsNewContentDirective, + ], + imports: [], + exports: [ + CoreSitePluginsCallWSDirective, + CoreSitePluginsCallWSNewContentDirective, + CoreSitePluginsCallWSOnLoadDirective, + CoreSitePluginsNewContentDirective, + ], +}) +export class CoreSitePluginsDirectivesModule {} diff --git a/src/core/features/siteplugins/directives/new-content.ts b/src/core/features/siteplugins/directives/new-content.ts new file mode 100644 index 000000000..6faee7123 --- /dev/null +++ b/src/core/features/siteplugins/directives/new-content.ts @@ -0,0 +1,117 @@ +// (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 { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core'; +import { Md5 } from 'ts-md5'; + +import { CoreSiteWSPreSets } from '@classes/site'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; +import { CoreSitePlugins } from '../services/siteplugins'; + +/** + * Directive to display a new site plugin content when clicked. This new content can be displayed in a new page or in the + * current page (only if the current page is already displaying a site plugin content). + * + * Example usages: + * + * A button to go to a new content page: + * + * + * {{ 'plugin.mod_certificate_coursecertificate.viewissued' | translate }} + * + * + * A button to load new content in current page using a param from otherdata: + * + * + * {{ 'plugin.mod_certificate_coursecertificate.viewissued' | translate }} + * + */ +@Directive({ + selector: '[core-site-plugins-new-content]', +}) +export class CoreSitePluginsNewContentDirective implements OnInit { + + @Input() component?: string; // The component of the new content. If not provided, use the same component as current page. + @Input() method?: string; // The method to get the new content. If not provided, use the same method as current page. + @Input() args?: Record; // The params to get the new content. + @Input() title?: string; // The title to display with the new content. Only if samePage=false. + @Input() samePage?: boolean | string; // Whether to display the content in same page or open a new one. Defaults to new page. + @Input() useOtherData?: string[] | unknown; // Whether to include other data in the args. + @Input() form?: string; // ID or name to identify a form. The form data will be retrieved and sent to the WS. + // JS variables to pass to the new page 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. + @Input() jsData?: Record | boolean; + @Input() preSets?: CoreSiteWSPreSets; // The preSets for the WS call of the new content. + @Input() ptrEnabled?: boolean | string; // Whether PTR should be enabled in the new page. Defaults to true. + + protected element: HTMLElement; + + constructor( + element: ElementRef, + @Optional() protected parentContent: CoreSitePluginsPluginContentComponent, + ) { + this.element = element.nativeElement || element; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.element.addEventListener('click', (ev: Event): void => { + ev.preventDefault(); + ev.stopPropagation(); + + let args = this.args || {}; + + if (this.parentContent) { + args = CoreSitePlugins.loadOtherDataInArgs(this.args, this.parentContent.otherData, this.useOtherData); + } + + if (this.form && document.forms[this.form]) { + args = Object.assign(args, CoreDomUtils.getDataFromForm(document.forms[this.form])); + } + + let jsData = this.jsData || {}; + if (jsData === true) { + jsData = this.parentContent?.data || {}; + } + + if (CoreUtils.isTrueOrOne(this.samePage)) { + // Update the parent content (if it exists). + this.parentContent?.updateContent(args, this.component, this.method, jsData, this.preSets); + } else { + const component = this.component || this.parentContent?.component; + const method = this.method || this.parentContent?.method; + const hash = Md5.hashAsciiStr(JSON.stringify(args)); + + CoreNavigator.navigateToSitePath(`siteplugins/${component}/${method}/${hash}`, { + params: { + title: this.title || this.parentContent?.pageTitle, + args, + initResult: this.parentContent?.initResult, + jsData, + preSets: this.preSets, + ptrEnabled: this.ptrEnabled, + }, + }); + } + }); + } + +} From 3c54bd3b72822e74a8d27971e604eeb8f44f5f6a Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 4 Mar 2021 12:55:12 +0100 Subject: [PATCH 07/10] MOBILE-3664 angular: Don't optimize scripts The scripts optimization needs to be disabled to make site plugins work --- angular.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/angular.json b/angular.json index 37fc46139..c44eabc41 100644 --- a/angular.json +++ b/angular.json @@ -40,7 +40,10 @@ }, "configurations": { "production": { - "optimization": true, + "optimization": { + "scripts": false, + "styles": true + }, "outputHashing": "all", "sourceMap": false, "extractCss": true, @@ -52,8 +55,8 @@ "budgets": [ { "type": "initial", - "maximumWarning": "2mb", - "maximumError": "5mb" + "maximumWarning": "50mb", + "maximumError": "100mb" } ] }, From 76f67faada3d17ec48528affbf8ac1eb1e7e1ed2 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 4 Mar 2021 14:45:05 +0100 Subject: [PATCH 08/10] MOBILE-3664 ios: Fix some inputs not focusable in iOS --- src/addons/calendar/pages/edit-event/edit-event.html | 3 ++- .../features/login/pages/credentials/credentials.html | 10 +++++----- .../pages/forgotten-password/forgotten-password.html | 9 ++++----- .../search/components/search-box/core-search-box.html | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/addons/calendar/pages/edit-event/edit-event.html b/src/addons/calendar/pages/edit-event/edit-event.html index 27a1912bf..193a65b58 100644 --- a/src/addons/calendar/pages/edit-event/edit-event.html +++ b/src/addons/calendar/pages/edit-event/edit-event.html @@ -15,7 +15,8 @@
-

+ +

{{ 'addon.calendar.eventname' | translate }}

diff --git a/src/core/features/login/pages/credentials/credentials.html b/src/core/features/login/pages/credentials/credentials.html index 3b393f0b0..db3f5ce46 100644 --- a/src/core/features/login/pages/credentials/credentials.html +++ b/src/core/features/login/pages/credentials/credentials.html @@ -31,11 +31,11 @@ - - - + + + diff --git a/src/core/features/login/pages/forgotten-password/forgotten-password.html b/src/core/features/login/pages/forgotten-password/forgotten-password.html index ed7c39de0..56a3aef48 100644 --- a/src/core/features/login/pages/forgotten-password/forgotten-password.html +++ b/src/core/features/login/pages/forgotten-password/forgotten-password.html @@ -29,11 +29,10 @@ - - - - + + + {{ 'core.courses.search' | translate }} diff --git a/src/core/features/search/components/search-box/core-search-box.html b/src/core/features/search/components/search-box/core-search-box.html index 70b6e55da..7f400ffa9 100644 --- a/src/core/features/search/components/search-box/core-search-box.html +++ b/src/core/features/search/components/search-box/core-search-box.html @@ -1,11 +1,11 @@ - - - + + + From 98bea1f50239ecadfe67defd8f5358f4188a49dc Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 5 Mar 2021 08:17:42 +0100 Subject: [PATCH 09/10] MOBILE-3664 course: Check site plugins when opening course --- src/core/features/course/services/course.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index c57d50745..4326094c3 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -41,6 +41,7 @@ import { CoreCourseHelper, CoreCourseModuleCompletionData } from './course-helpe import { CoreCourseFormatDelegate } from './format-delegate'; import { CoreCronDelegate } from '@services/cron'; import { CoreCourseLogCronHandler } from './handlers/log-cron'; +import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; const ROOT_CACHE_KEY = 'mmCourse:'; @@ -1026,7 +1027,7 @@ export class CoreCourseProvider { const loading = await CoreDomUtils.showModalLoading(); // Wait for site plugins to be fetched. - // @todo await this.sitePluginsProvider.waitFetchPlugins(); + await CoreSitePlugins.waitFetchPlugins(); if (!('format' in course) || typeof course.format == 'undefined') { const result = await CoreCourseHelper.getCourse(course.id); @@ -1034,7 +1035,9 @@ export class CoreCourseProvider { course = result.course; } - if (course) { // @todo Replace with: if (!this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) { + const format = 'format' in course && `format_${course.format}`; + + if (!format || !CoreSitePlugins.sitePluginPromiseExists(`format_${format}`)) { // No custom format plugin. We don't need to wait for anything. await CoreCourseFormatDelegate.openCourse( course, params); loading.dismiss(); @@ -1044,11 +1047,12 @@ export class CoreCourseProvider { // This course uses a custom format plugin, wait for the format plugin to finish loading. try { - /* @todo await this.sitePluginsProvider.sitePluginLoaded('format_' + course.format); + await CoreSitePlugins.sitePluginLoaded(format); + // The format loaded successfully, but the handlers wont be registered until all site plugins have loaded. - if (this.sitePluginsProvider.sitePluginsFinishedLoading) { - return CoreCourseFormatDelegate.openCourse(course, params); - }*/ + if (CoreSitePlugins.sitePluginsFinishedLoading) { + return CoreCourseFormatDelegate.openCourse( course, params); + } // Wait for plugins to be loaded. const deferred = CoreUtils.promiseDefer(); From 3249647f2a1bfd35b5fd98eb26fcc93b939fcbfe Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 5 Mar 2021 14:48:00 +0100 Subject: [PATCH 10/10] MOBILE-3664 siteplugins: Support home delegate for plugins --- src/core/classes/tabs.ts | 4 +- .../components/tabs-outlet/tabs-outlet.ts | 26 ++++----- .../handlers/main-menu-home-handler.ts | 58 +++++++++++++++++++ .../services/siteplugins-helper.ts | 42 ++++++++++++++ .../siteplugins/services/siteplugins.ts | 14 ++++- .../siteplugins/siteplugins.module.ts | 2 + 6 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 src/core/features/siteplugins/classes/handlers/main-menu-home-handler.ts diff --git a/src/core/classes/tabs.ts b/src/core/classes/tabs.ts index 8cc91af13..c04bea315 100644 --- a/src/core/classes/tabs.ts +++ b/src/core/classes/tabs.ts @@ -23,6 +23,7 @@ import { AfterViewInit, ViewChild, ElementRef, + SimpleChange, } from '@angular/core'; import { IonSlides } from '@ionic/angular'; import { BackButtonEvent } from '@ionic/core'; @@ -153,7 +154,8 @@ export class CoreTabsBaseComponent implements OnInit, Aft /** * Detect changes on input properties. */ - ngOnChanges(): void { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ngOnChanges(changes: Record): void { // Wait for ngAfterViewInit so it works in the case that each tab has its own component. if (!this.initialized && this.hideUntil && this.afterViewInitTriggered) { // Tabs should be shown, initialize them. diff --git a/src/core/components/tabs-outlet/tabs-outlet.ts b/src/core/components/tabs-outlet/tabs-outlet.ts index 0fd52aaa9..963cb3ced 100644 --- a/src/core/components/tabs-outlet/tabs-outlet.ts +++ b/src/core/components/tabs-outlet/tabs-outlet.ts @@ -21,6 +21,7 @@ import { AfterViewInit, ViewChild, ElementRef, + SimpleChange, } from '@angular/core'; import { IonTabs } from '@ionic/angular'; import { Subscription } from 'rxjs'; @@ -70,17 +71,6 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent { - super.ngOnInit(); - - this.tabs.forEach((tab) => { - this.initTab(tab); - }); - } - /** * Init tab info. * @@ -117,12 +107,16 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent { - this.initTab(tab); - }); + ngOnChanges(changes: Record): void { + if (changes.tabs) { + this.tabs.forEach((tab) => { + this.initTab(tab); + }); - super.ngOnChanges(); + this.calculateSlides(); + } + + super.ngOnChanges(changes); } /** diff --git a/src/core/features/siteplugins/classes/handlers/main-menu-home-handler.ts b/src/core/features/siteplugins/classes/handlers/main-menu-home-handler.ts new file mode 100644 index 000000000..1e2904b9d --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/main-menu-home-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 { CoreMainMenuHomeHandler, CoreMainMenuHomeHandlerData } from '@features/mainmenu/services/home-delegate'; +import { + CoreSitePluginsContent, + CoreSitePluginsMainMenuHomeHandlerData, + CoreSitePluginsPlugin, +} from '@features/siteplugins/services/siteplugins'; +import { CoreSitePluginsBaseHandler } from './base-handler'; + +/** + * Handler to display a site plugin in the main menu. + */ +export class CoreSitePluginsMainMenuHomeHandler extends CoreSitePluginsBaseHandler implements CoreMainMenuHomeHandler { + + priority: number; + + constructor( + name: string, + protected title: string, + protected plugin: CoreSitePluginsPlugin, + protected handlerSchema: CoreSitePluginsMainMenuHomeHandlerData, + protected initResult: CoreSitePluginsContent | null, + ) { + super(name); + + this.priority = handlerSchema.priority || 0; + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreMainMenuHomeHandlerData { + return { + title: this.title, + class: this.handlerSchema.displaydata?.class, + page: `siteplugins/${this.plugin.component}/${this.handlerSchema.method}/0`, + pageParams: { + title: this.title, + initResult: this.initResult, + ptrEnabled: this.handlerSchema.ptrenabled, + }, + }; + } + +} diff --git a/src/core/features/siteplugins/services/siteplugins-helper.ts b/src/core/features/siteplugins/services/siteplugins-helper.ts index 3c4c06eeb..439340e14 100644 --- a/src/core/features/siteplugins/services/siteplugins-helper.ts +++ b/src/core/features/siteplugins/services/siteplugins-helper.ts @@ -74,8 +74,11 @@ import { CoreSitePluginsBlockHandlerData, CoreSitePluginsHandlerCommonData, CoreSitePluginsInitHandlerData, + CoreSitePluginsMainMenuHomeHandlerData, } from './siteplugins'; import { makeSingleton } from '@singletons'; +import { CoreMainMenuHomeDelegate } from '@features/mainmenu/services/home-delegate'; +import { CoreSitePluginsMainMenuHomeHandler } from '../classes/handlers/main-menu-home-handler'; const HANDLER_DISABLED = 'core_site_plugins_helper_handler_disabled'; @@ -535,6 +538,10 @@ export class CoreSitePluginsHelperProvider { uniqueName = await this.registerWorkshopAssessmentStrategyHandler(plugin, handlerName, handlerSchema); break; + case 'CoreMainMenuHomeDelegate': + uniqueName = await this.registerMainMenuHomeHandler(plugin, handlerName, handlerSchema, initResult); + break; + default: // Nothing to do. } @@ -1130,6 +1137,41 @@ export class CoreSitePluginsHelperProvider { CoreEvents.trigger(CoreEvents.SITE_PLUGINS_COURSE_RESTRICT_UPDATED, {}); } + /** + * Given a handler in a plugin, register it in the main menu home delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @param initResult Result of the init WS call. + * @return A string to identify the handler. + */ + protected registerMainMenuHomeHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsMainMenuHomeHandlerData, + initResult: CoreSitePluginsContent | null, + ): string | undefined { + if (!handlerSchema.displaydata) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in main menu home delegate:', plugin, handlerSchema, initResult); + + // Create and register the handler. + const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); + const prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title || 'pluginname'); + + CoreMainMenuHomeDelegate.registerHandler( + new CoreSitePluginsMainMenuHomeHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult), + ); + + return uniqueName; + } + } export const CoreSitePluginsHelper = makeSingleton(CoreSitePluginsHelperProvider); diff --git a/src/core/features/siteplugins/services/siteplugins.ts b/src/core/features/siteplugins/services/siteplugins.ts index 1110755ef..cbbfbea40 100644 --- a/src/core/features/siteplugins/services/siteplugins.ts +++ b/src/core/features/siteplugins/services/siteplugins.ts @@ -784,7 +784,7 @@ export type CoreSitePluginsPlugin = CoreSitePluginsWSPlugin & { export type CoreSitePluginsHandlerData = CoreSitePluginsInitHandlerData | CoreSitePluginsCourseOptionHandlerData | CoreSitePluginsMainMenuHandlerData | CoreSitePluginsCourseModuleHandlerData | CoreSitePluginsCourseFormatHandlerData | CoreSitePluginsUserHandlerData | CoreSitePluginsSettingsHandlerData | CoreSitePluginsMessageOutputHandlerData | -CoreSitePluginsBlockHandlerData; +CoreSitePluginsBlockHandlerData | CoreSitePluginsMainMenuHomeHandlerData; /** * Plugin handler data common to all delegates. @@ -920,3 +920,15 @@ export type CoreSitePluginsInitHandlerData = CoreSitePluginsHandlerCommonData & methodJSResult?: any; // eslint-disable-line @typescript-eslint/no-explicit-any methodOtherdata?: Record; }; + +/** + * Main menu home handler specific data. + */ +export type CoreSitePluginsMainMenuHomeHandlerData = CoreSitePluginsHandlerCommonData & { + displaydata?: { + title?: string; + class?: string; + }; + priority?: number; + ptrenabled?: boolean; +}; diff --git a/src/core/features/siteplugins/siteplugins.module.ts b/src/core/features/siteplugins/siteplugins.module.ts index 154e3feb0..730d504e2 100644 --- a/src/core/features/siteplugins/siteplugins.module.ts +++ b/src/core/features/siteplugins/siteplugins.module.ts @@ -17,6 +17,7 @@ import { Routes } from '@angular/router'; import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CoreMainMenuHomeRoutingModule } from '@features/mainmenu/pages/home/home-routing.module'; import { CoreSitePluginsComponentsModule } from './components/components.module'; import { CoreSitePluginsHelper } from './services/siteplugins-helper'; @@ -39,6 +40,7 @@ const courseIndexRoutes: Routes = [ imports: [ CoreMainMenuTabRoutingModule.forChild(routes), CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }), + CoreMainMenuHomeRoutingModule.forChild({ children: routes }), CoreSitePluginsComponentsModule, ], providers: [