From 77966d6fb48f695d0f2516c6123a0844e248b8e5 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 21 Apr 2021 13:12:32 +0200 Subject: [PATCH] MOBILE-3641 feedback: Migrate form page --- .../mod/feedback/feedback-lazy.module.ts | 4 + src/addons/mod/feedback/pages/form/form.html | 178 ++++++++ .../mod/feedback/pages/form/form.module.ts | 39 ++ src/addons/mod/feedback/pages/form/form.scss | 11 + src/addons/mod/feedback/pages/form/form.ts | 428 ++++++++++++++++++ .../mark-required/mark-required.scss | 4 +- src/theme/theme.base.scss | 4 + 7 files changed, 667 insertions(+), 1 deletion(-) create mode 100644 src/addons/mod/feedback/pages/form/form.html create mode 100644 src/addons/mod/feedback/pages/form/form.module.ts create mode 100644 src/addons/mod/feedback/pages/form/form.scss create mode 100644 src/addons/mod/feedback/pages/form/form.ts diff --git a/src/addons/mod/feedback/feedback-lazy.module.ts b/src/addons/mod/feedback/feedback-lazy.module.ts index 8315c3afa..c083bea13 100644 --- a/src/addons/mod/feedback/feedback-lazy.module.ts +++ b/src/addons/mod/feedback/feedback-lazy.module.ts @@ -23,6 +23,10 @@ const routes: Routes = [ path: ':courseId/:cmId', component: AddonModFeedbackIndexPage, }, + { + path: ':courseId/:cmId/form', + loadChildren: () => import('./pages/form/form.module').then(m => m.AddonModFeedbackFormPageModule), + }, ]; @NgModule({ diff --git a/src/addons/mod/feedback/pages/form/form.html b/src/addons/mod/feedback/pages/form/form.html new file mode 100644 index 000000000..c96b3c5a7 --- /dev/null +++ b/src/addons/mod/feedback/pages/form/form.html @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + +

{{ 'addon.mod_feedback.mode' | translate }}

+

{{ 'addon.mod_feedback.anonymous' | translate }}

+

{{ 'addon.mod_feedback.non_anonymous' | translate }}

+
+
+ + + + + + + +

+ {{item.itemnumber}}. + + + {{item.postfix}} +

+

+ + +

+
+ + + + + + + + + {{ 'addon.mod_feedback.numberoutofrange' | translate }} [{{item.rangefrom}} + , {{item.rangeto}}] + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + {{ 'addon.mod_feedback.captchaofflinewarning' | translate }} + +
+
+
+
+ + + + + + {{ 'addon.mod_feedback.previous_page' | translate }} + + + + + {{ 'addon.mod_feedback.next_page' | translate }} + + + + + + {{ 'addon.mod_feedback.save_entries' | translate }} + + + + +
+
+ + + + + +

+ {{ 'addon.mod_feedback.this_feedback_is_already_submitted' | translate }} +

+

+ {{ 'addon.mod_feedback.feedback_submitted_offline' | translate }} +

+

+ + +

+
+
+
+ + + + + + + + {{ 'addon.mod_feedback.completed_feedbacks' | translate }} + + + + + {{ 'core.continue' | translate }} + + + + + + +
+
diff --git a/src/addons/mod/feedback/pages/form/form.module.ts b/src/addons/mod/feedback/pages/form/form.module.ts new file mode 100644 index 000000000..9dd8eea72 --- /dev/null +++ b/src/addons/mod/feedback/pages/form/form.module.ts @@ -0,0 +1,39 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CanLeaveGuard } from '@guards/can-leave'; +import { AddonModFeedbackFormPage } from './form'; + +const routes: Routes = [ + { + path: '', + component: AddonModFeedbackFormPage, + canDeactivate: [CanLeaveGuard], + }, +]; + +@NgModule({ + declarations: [ + AddonModFeedbackFormPage, + ], + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + ], + exports: [RouterModule], +}) +export class AddonModFeedbackFormPageModule {} diff --git a/src/addons/mod/feedback/pages/form/form.scss b/src/addons/mod/feedback/pages/form/form.scss new file mode 100644 index 000000000..b64a532ef --- /dev/null +++ b/src/addons/mod/feedback/pages/form/form.scss @@ -0,0 +1,11 @@ +:host { + .addon-mod_feedback-item ion-label.label-stacked { + margin: 11px 0px 10px; + transform: none; + } + + .addon-mod_feedback-item-error { + padding-top: 5px; + padding-bottom: 8px; + } +} diff --git a/src/addons/mod/feedback/pages/form/form.ts b/src/addons/mod/feedback/pages/form/form.ts new file mode 100644 index 000000000..306ccb2c7 --- /dev/null +++ b/src/addons/mod/feedback/pages/form/form.ts @@ -0,0 +1,428 @@ +// (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, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { CoreSite } from '@classes/site'; +import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; +import { CoreCourse, CoreCourseCommonModWSOptions, CoreCourseWSModule } from '@features/course/services/course'; +import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { CanLeave } from '@guards/can-leave'; +import { IonContent } from '@ionic/angular'; +import { CoreApp } from '@services/app'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { Network, NgZone, Translate } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { Subscription } from 'rxjs'; +import { + AddonModFeedback, + AddonModFeedbackGetFeedbackAccessInformationWSResponse, + AddonModFeedbackPageItems, + AddonModFeedbackProvider, + AddonModFeedbackResponseValue, + AddonModFeedbackWSFeedback, +} from '../../services/feedback'; +import { AddonModFeedbackFormItem, AddonModFeedbackHelper } from '../../services/feedback-helper'; +import { AddonModFeedbackSync } from '../../services/feedback-sync'; +import { AddonModFeedbackModuleHandlerService } from '../../services/handlers/module'; + +/** + * Page that displays feedback form. + */ +@Component({ + selector: 'page-addon-mod-feedback-form', + templateUrl: 'form.html', + styleUrls: ['form.scss'], +}) +export class AddonModFeedbackFormPage implements OnInit, OnDestroy, CanLeave { + + @ViewChild(IonContent) content?: IonContent; + + protected module?: CoreCourseWSModule; + protected currentPage?: number; + protected siteAfterSubmit?: string; + protected onlineObserver: Subscription; + protected originalData?: Record; + protected currentSite: CoreSite; + protected forceLeave = false; + + title?: string; + preview = false; + cmId!: number; + courseId!: number; + feedback?: AddonModFeedbackWSFeedback; + completionPageContents?: string; + component = AddonModFeedbackProvider.COMPONENT; + offline = false; + feedbackLoaded = false; + access?: AddonModFeedbackGetFeedbackAccessInformationWSResponse; + items: AddonModFeedbackFormItem[] = []; + hasPrevPage = false; + hasNextPage = false; + completed = false; + completedOffline = false; + + constructor() { + this.currentSite = CoreSites.getCurrentSite()!; + + // Refresh online status when changes. + this.onlineObserver = Network.onChange().subscribe(() => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + NgZone.run(() => { + this.offline = !CoreApp.isOnline(); + }); + }); + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + this.cmId = CoreNavigator.getRouteNumberParam('cmId')!; + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.currentPage = CoreNavigator.getRouteNumberParam('page'); + this.title = CoreNavigator.getRouteParam('title'); + this.preview = !!CoreNavigator.getRouteBooleanParam('preview'); + + await this.fetchData(); + + if (!this.feedback) { + return; + } + + try { + await AddonModFeedback.logView(this.feedback.id, this.feedback.name, true); + + CoreCourse.checkModuleCompletion(this.courseId, this.module!.completiondata); + } catch { + // Ignore errors. + } + } + + /** + * View entered. + */ + ionViewDidEnter(): void { + this.forceLeave = false; + } + + /** + * @inheritdoc + */ + async canLeave(): Promise { + if (this.forceLeave) { + return true; + } + + if (!this.preview) { + const responses = AddonModFeedbackHelper.getPageItemsResponses(this.items); + + if (this.items && !this.completed && this.originalData) { + // Form submitted. Check if there is any change. + if (!CoreUtils.basicLeftCompare(responses, this.originalData, 3)) { + await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit')); + } + } + } + + return true; + } + + /** + * Fetch all the data required for the view. + * + * @return Promise resolved when done. + */ + protected async fetchData(): Promise { + try { + this.module = await CoreCourse.getModule(this.cmId, this.courseId, undefined, true, false, this.currentSite.getId()); + + this.offline = !CoreApp.isOnline(); + const options = { + cmId: this.cmId, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + siteId: this.currentSite.getId(), + }; + + this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.cmId); + + this.title = this.feedback.name || this.title; + + await this.fetchAccessData(options); + + let page = 0; + + if (!this.preview && this.access!.cansubmit && !this.access!.isempty) { + page = this.currentPage ?? await this.fetchResumePage(options); + } else { + this.preview = true; + } + + await this.fetchFeedbackPageData(page); + } catch (message) { + CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); + this.forceLeave = true; + CoreNavigator.back(); + } finally { + this.feedbackLoaded = true; + } + } + + /** + * Fetch access information. + * + * @param options Options. + * @return Promise resolved when done. + */ + protected async fetchAccessData(options: CoreCourseCommonModWSOptions): Promise { + try { + this.access = await AddonModFeedback.getFeedbackAccessInformation(this.feedback!.id, options); + } catch (error) { + if (this.offline || CoreUtils.isWebServiceError(error)) { + // Already offline or shouldn't go offline, fail. + throw error; + } + + // If it fails, go offline. + this.offline = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; + + this.access = await AddonModFeedback.getFeedbackAccessInformation(this.feedback!.id, options); + } + } + + /** + * Get resume page from WS. + * + * @param options Options. + * @return Promise resolved with the page to resume. + */ + protected async fetchResumePage(options: CoreCourseCommonModWSOptions): Promise { + try { + return await AddonModFeedback.getResumePage(this.feedback!.id, options); + } catch (error) { + if (this.offline || CoreUtils.isWebServiceError(error)) { + // Already offline or shouldn't go offline, fail. + throw error; + } + + // Go offline. + this.offline = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; + + return AddonModFeedback.getResumePage(this.feedback!.id, options); + } + } + + /** + * Fetch page data. + * + * @param page Page to load. + * @return Promise resolved when done. + */ + protected async fetchFeedbackPageData(page: number = 0): Promise { + this.items = []; + const response = await this.fetchPageItems(page); + + this.items = response.items + .map((itemData) => AddonModFeedbackHelper.getItemForm(itemData, this.preview)) + .filter((itemData) => itemData); // Filter items with errors. + + if (!this.preview) { + const itemsCopy = CoreUtils.clone(this.items); // Copy the array to avoid modifications. + this.originalData = AddonModFeedbackHelper.getPageItemsResponses(itemsCopy); + } + } + + /** + * Fetch page items. + * + * @param page Page to get. + * @return Promise resolved with WS response. + */ + protected async fetchPageItems(page: number): Promise { + const options = { + cmId: this.cmId, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + siteId: this.currentSite.getId(), + }; + + if (this.preview) { + const response = await AddonModFeedback.getItems(this.feedback!.id, options); + + return { + items: response.items, + warnings: response.warnings, + hasnextpage: false, + hasprevpage: false, + }; + } + + this.currentPage = page; + let response: AddonModFeedbackPageItems; + + try { + response = await AddonModFeedback.getPageItemsWithValues(this.feedback!.id, page, options); + } catch (error) { + if (this.offline || CoreUtils.isWebServiceError(error)) { + // Already offline or shouldn't go offline, fail. + throw error; + } + + // Go offline. + this.offline = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; + + response = await AddonModFeedback.getPageItemsWithValues(this.feedback!.id, page, options); + } + + this.hasPrevPage = !!response.hasprevpage; + this.hasNextPage = !!response.hasnextpage; + + return response; + } + + /** + * Function to allow page navigation through the questions form. + * + * @param goPrevious If true it will go back to the previous page, if false, it will go forward. + * @return Resolved when done. + */ + async gotoPage(goPrevious: boolean): Promise { + this.content?.scrollToTop(); + this.feedbackLoaded = false; + + const responses = AddonModFeedbackHelper.getPageItemsResponses(this.items); + const formHasErrors = this.items.some((item) => item.isEmpty || item.hasError); + + try { + // Sync other pages first. + await CoreUtils.ignoreErrors(AddonModFeedbackSync.syncFeedback(this.feedback!.id)); + + const response = await AddonModFeedback.processPage(this.feedback!.id, this.currentPage!, responses, { + goPrevious, + formHasErrors, + courseId: this.courseId, + cmId: this.cmId, + }); + + if (response.completed) { + // Form is completed, show completion message and buttons. + this.items = []; + this.completed = true; + this.completedOffline = !!response.offline; + this.completionPageContents = response.completionpagecontents; + this.siteAfterSubmit = response.siteaftersubmit; + + CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'feedback' }); + + // Invalidate access information so user will see home page updated (continue form or completion messages). + await Promise.all([ + AddonModFeedback.invalidateFeedbackAccessInformationData(this.feedback!.id), + AddonModFeedback.invalidateResumePageData(this.feedback!.id), + ]); + + // If form has been submitted, the info has been already invalidated but we should update index view. + CoreEvents.trigger(AddonModFeedbackProvider.FORM_SUBMITTED, { + feedbackId: this.feedback!.id, + tab: 'overview', + offline: this.completedOffline, + }); + + await this.fetchAccessData({ + cmId: this.cmId, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + siteId: this.currentSite.getId(), + }); + } else if (typeof response.jumpto != 'number' || response.jumpto == this.currentPage) { + // Errors on questions, stay in page. + } else { + // Invalidate access information so user will see home page updated (continue form). + await AddonModFeedback.invalidateResumePageData(this.feedback!.id); + + CoreEvents.trigger(AddonModFeedbackProvider.FORM_SUBMITTED, { + feedbackId: this.feedback!.id, + tab: 'overview', + offline: this.completedOffline, + }); + + // Fetch the new page. + await this.fetchFeedbackPageData(response.jumpto); + } + } catch (message) { + CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); + } finally { + this.feedbackLoaded = true; + } + } + + /** + * Function to link implemented features. + */ + showAnalysis(): void { + const indexPath = AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.cmId}`; + const previousPath = CoreNavigator.getPreviousPath(); + + if (previousPath.match(new RegExp(indexPath + '$'))) { + // Previous page is the index page, go back. + CoreEvents.trigger(AddonModFeedbackProvider.FORM_SUBMITTED, { + feedbackId: this.feedback!.id, + tab: 'analysis', + offline: this.completedOffline, + }); + + CoreNavigator.back(); + + return; + } + + CoreNavigator.navigateToSitePath(indexPath, { + params: { + module: this.module, + tab: 'analysis', + }, + }); + } + + /** + * Function to go to the page after submit. + */ + async continue(): Promise { + if (!this.siteAfterSubmit) { + return CoreCourseHelper.getAndOpenCourse(this.courseId, {}, this.currentSite.getId()); + } + + const modal = await CoreDomUtils.showModalLoading(); + + try { + const treated = await CoreContentLinksHelper.handleLink(this.siteAfterSubmit); + + if (!treated) { + await this.currentSite.openInBrowserWithAutoLoginIfSameSite(this.siteAfterSubmit); + } + } finally { + modal.dismiss(); + } + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.onlineObserver.unsubscribe(); + } + +} diff --git a/src/core/components/mark-required/mark-required.scss b/src/core/components/mark-required/mark-required.scss index def9d3ecb..dd1292e5e 100644 --- a/src/core/components/mark-required/mark-required.scss +++ b/src/core/components/mark-required/mark-required.scss @@ -1,8 +1,10 @@ +@import "~theme/globals"; + :host { .core-input-required-asterisk { font-size: 8px; - --padding-start: 4px; line-height: 100%; vertical-align: top; + @include padding-horizontal(4px, null); } } diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index af5fa751f..49955b3bf 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -487,3 +487,7 @@ ion-button.core-button-select { @include padding(null, null, null, 15px * $i + 16px); } } + +textarea:not([core-auto-rows]) { + height: 200px; +}