forked from CIT/Vmeda.Online
		
	MOBILE-3641 feedback: Migrate form page
This commit is contained in:
		
							parent
							
								
									d3e689aa81
								
							
						
					
					
						commit
						77966d6fb4
					
				@ -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({
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										178
									
								
								src/addons/mod/feedback/pages/form/form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/addons/mod/feedback/pages/form/form.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,178 @@
 | 
			
		||||
<ion-header>
 | 
			
		||||
    <ion-toolbar>
 | 
			
		||||
        <ion-buttons slot="start">
 | 
			
		||||
            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
			
		||||
        </ion-buttons>
 | 
			
		||||
        <ion-title>
 | 
			
		||||
            <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
 | 
			
		||||
            </core-format-text>
 | 
			
		||||
        </ion-title>
 | 
			
		||||
    </ion-toolbar>
 | 
			
		||||
</ion-header>
 | 
			
		||||
<ion-content>
 | 
			
		||||
    <core-loading [hideUntil]="feedbackLoaded">
 | 
			
		||||
        <ng-container *ngIf="items && items.length">
 | 
			
		||||
            <ion-list class="ion-no-margin">
 | 
			
		||||
                <ion-item class="ion-text-wrap">
 | 
			
		||||
                    <ion-label>
 | 
			
		||||
                        <h2>{{ 'addon.mod_feedback.mode' | translate }}</h2>
 | 
			
		||||
                        <p *ngIf="access!.isanonymous">{{ 'addon.mod_feedback.anonymous' | translate }}</p>
 | 
			
		||||
                        <p *ngIf="!access!.isanonymous">{{ 'addon.mod_feedback.non_anonymous' | translate }}</p>
 | 
			
		||||
                    </ion-label>
 | 
			
		||||
                </ion-item>
 | 
			
		||||
                <ng-container *ngFor="let item of items">
 | 
			
		||||
                    <ion-item-divider *ngIf="item.typ == 'pagebreak'">
 | 
			
		||||
                        <ion-label></ion-label>
 | 
			
		||||
                    </ion-item-divider>
 | 
			
		||||
                    <ng-container *ngIf="item.typ != 'pagebreak'">
 | 
			
		||||
                        <ion-item class="ion-text-wrap addon-mod_feedback-item" [color]="item.dependitem > 0 ? 'light' : ''"
 | 
			
		||||
                            [class.core-danger-item]="item.isEmpty || item.hasError">
 | 
			
		||||
                            <ion-label [position]="item.hasTextInput ? 'stacked' : undefined">
 | 
			
		||||
                                <p *ngIf="item.name" [core-mark-required]="item.required">
 | 
			
		||||
                                    <span *ngIf="feedback!.autonumbering && item.itemnumber">{{item.itemnumber}}. </span>
 | 
			
		||||
                                    <core-format-text [component]="component" [componentId]="cmId" [text]="item.name"
 | 
			
		||||
                                        contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId" [wsNotFiltered]="true">
 | 
			
		||||
                                    </core-format-text>
 | 
			
		||||
                                    <span *ngIf="item.postfix" class="addon-mod_feedback-postfix">{{item.postfix}}</span>
 | 
			
		||||
                                </p>
 | 
			
		||||
                                <p *ngIf="item.templateName == 'label'">
 | 
			
		||||
                                    <core-format-text [component]="component" [componentId]="cmId"
 | 
			
		||||
                                        [text]="item.presentation" contextLevel="module" [contextInstanceId]="cmId"
 | 
			
		||||
                                        [wsNotFiltered]="true" [courseId]="courseId">
 | 
			
		||||
                                    </core-format-text>
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </ion-label>
 | 
			
		||||
 | 
			
		||||
                            <ion-input *ngIf="item.templateName == 'textfield'" type="text" [(ngModel)]="item.value"
 | 
			
		||||
                                autocorrect="off" name="{{item.typ}}_{{item.id}}" maxlength="{{item.length}}"
 | 
			
		||||
                                [required]="item.required">
 | 
			
		||||
                            </ion-input>
 | 
			
		||||
 | 
			
		||||
                            <ng-container *ngIf="item.templateName == 'numeric'">
 | 
			
		||||
                                <ion-input type="number" [(ngModel)]="item.value" name="{{item.typ}}_{{item.id}}"
 | 
			
		||||
                                    [required]="item.required">
 | 
			
		||||
                                </ion-input>
 | 
			
		||||
                                <ion-text *ngIf="item.hasError" color="danger" class="addon-mod_feedback-item-error">
 | 
			
		||||
                                    {{ 'addon.mod_feedback.numberoutofrange' | translate }} [{{item.rangefrom}}
 | 
			
		||||
                                    <span *ngIf="item.rangefrom && item.rangeto">, </span>{{item.rangeto}}]
 | 
			
		||||
                                </ion-text>
 | 
			
		||||
                            </ng-container>
 | 
			
		||||
 | 
			
		||||
                            <ion-textarea *ngIf="item.templateName == 'textarea'" [required]="item.required"
 | 
			
		||||
                                name="{{item.typ}}_{{item.id}}" [attr.aria-multiline]="true" [(ngModel)]="item.value">
 | 
			
		||||
                            </ion-textarea>
 | 
			
		||||
 | 
			
		||||
                            <ion-select *ngIf="item.templateName == 'multichoice-d'" [required]="item.required"
 | 
			
		||||
                                name="{{item.typ}}_{{item.id}}" [(ngModel)]="item.value" interface="action-sheet">
 | 
			
		||||
                                <ion-select-option *ngFor="let option of item.choices" [value]="option.value">
 | 
			
		||||
                                    <core-format-text [component]="component" [componentId]="cmId" [text]="option.label"
 | 
			
		||||
                                        contextLevel="module" [contextInstanceId]="cmId" [wsNotFiltered]="true"
 | 
			
		||||
                                        [courseId]="courseId">
 | 
			
		||||
                                    </core-format-text>
 | 
			
		||||
                                </ion-select-option>
 | 
			
		||||
                            </ion-select>
 | 
			
		||||
                        </ion-item>
 | 
			
		||||
 | 
			
		||||
                        <ion-radio-group *ngIf="item.templateName == 'multichoice-r'" [(ngModel)]="item.value"
 | 
			
		||||
                            [required]="item.required" name="{{item.typ}}_{{item.id}}">
 | 
			
		||||
                            <ion-item *ngFor="let option of item.choices">
 | 
			
		||||
                                <ion-label>
 | 
			
		||||
                                    <core-format-text  [component]="component" [componentId]="cmId"
 | 
			
		||||
                                        [text]="option.label" contextLevel="module" [contextInstanceId]="cmId"
 | 
			
		||||
                                        [wsNotFiltered]="true" [courseId]="courseId">
 | 
			
		||||
                                    </core-format-text>
 | 
			
		||||
                                </ion-label>
 | 
			
		||||
                                <ion-radio slot="start" [value]="option.value"></ion-radio>
 | 
			
		||||
                            </ion-item>
 | 
			
		||||
                        </ion-radio-group>
 | 
			
		||||
 | 
			
		||||
                        <ng-container *ngIf="item.templateName == 'multichoice-c'">
 | 
			
		||||
                            <ion-item *ngFor="let option of item.choices">
 | 
			
		||||
                                <ion-label>
 | 
			
		||||
                                    <core-format-text [component]="component" [componentId]="cmId" [text]="option.label"
 | 
			
		||||
                                        contextLevel="module" [contextInstanceId]="cmId" [wsNotFiltered]="true"
 | 
			
		||||
                                        [courseId]="courseId">
 | 
			
		||||
                                    </core-format-text>
 | 
			
		||||
                                </ion-label>
 | 
			
		||||
                                <ion-checkbox [required]="item.required" name="{{item.typ}}_{{item.id}}"
 | 
			
		||||
                                    [(ngModel)]="option.checked" value="option.value">
 | 
			
		||||
                                </ion-checkbox>
 | 
			
		||||
                            </ion-item>
 | 
			
		||||
                        </ng-container>
 | 
			
		||||
 | 
			
		||||
                        <ng-container *ngIf="item.templateName == 'captcha'">
 | 
			
		||||
                            <core-recaptcha *ngIf="!preview && !offline" [publicKey]="item.captcha.recaptchapublickey"
 | 
			
		||||
                                [model]="item" modelValueName="value">
 | 
			
		||||
                            </core-recaptcha>
 | 
			
		||||
                            <div *ngIf="!preview && (!item.captcha || offline)" class="core-warning-card">
 | 
			
		||||
                                <ion-item>
 | 
			
		||||
                                    <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
			
		||||
                                    <ion-label>{{ 'addon.mod_feedback.captchaofflinewarning' | translate }}</ion-label>
 | 
			
		||||
                                </ion-item>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </ng-container>
 | 
			
		||||
                    </ng-container>
 | 
			
		||||
                </ng-container>
 | 
			
		||||
                <ion-grid *ngIf="!preview">
 | 
			
		||||
                    <ion-row class="ion-align-items-center">
 | 
			
		||||
                        <ion-col *ngIf="hasPrevPage">
 | 
			
		||||
                            <ion-button expand="block" fill="outline" (click)="gotoPage(true)" class="ion-text-wrap">
 | 
			
		||||
                                <ion-icon name="fas-chevron-left" slot="start"></ion-icon>
 | 
			
		||||
                                {{ 'addon.mod_feedback.previous_page' | translate }}
 | 
			
		||||
                            </ion-button>
 | 
			
		||||
                        </ion-col>
 | 
			
		||||
                        <ion-col *ngIf="hasNextPage">
 | 
			
		||||
                            <ion-button expand="block" (click)="gotoPage(false)" class="ion-text-wrap">
 | 
			
		||||
                                {{ 'addon.mod_feedback.next_page' | translate }}
 | 
			
		||||
                                <ion-icon name="fas-chevron-right" slot="end"></ion-icon>
 | 
			
		||||
                            </ion-button>
 | 
			
		||||
                        </ion-col>
 | 
			
		||||
                        <ion-col *ngIf="!hasNextPage">
 | 
			
		||||
                            <ion-button expand="block" (click)="gotoPage(false)" class="ion-text-wrap">
 | 
			
		||||
                                {{ 'addon.mod_feedback.save_entries' | translate }}
 | 
			
		||||
                            </ion-button>
 | 
			
		||||
                        </ion-col>
 | 
			
		||||
                    </ion-row>
 | 
			
		||||
                </ion-grid>
 | 
			
		||||
            </ion-list>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
 | 
			
		||||
        <ion-card class="core-success-card" *ngIf="completed">
 | 
			
		||||
            <ion-item>
 | 
			
		||||
                <ion-icon name="fas-check" slot="start"></ion-icon>
 | 
			
		||||
                <ion-label>
 | 
			
		||||
                    <p *ngIf="!completionPageContents && !completedOffline">
 | 
			
		||||
                        {{ 'addon.mod_feedback.this_feedback_is_already_submitted' | translate }}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p *ngIf="!completionPageContents && completedOffline">
 | 
			
		||||
                        {{ 'addon.mod_feedback.feedback_submitted_offline' | translate }}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p *ngIf="completionPageContents">
 | 
			
		||||
                        <core-format-text  [component]="component" componentId="componentId" [text]="completionPageContents"
 | 
			
		||||
                            contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
 | 
			
		||||
                        </core-format-text>
 | 
			
		||||
                    </p>
 | 
			
		||||
                </ion-label>
 | 
			
		||||
            </ion-item>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
 | 
			
		||||
        <ion-card>
 | 
			
		||||
            <ion-grid *ngIf="completed">
 | 
			
		||||
                <ion-row class="ion-align-items-center">
 | 
			
		||||
                    <ion-col *ngIf="access!.canviewanalysis">
 | 
			
		||||
                        <ion-button expand="block" fill="outline" (click)="showAnalysis()" class="ion-text-wrap">
 | 
			
		||||
                            <ion-icon name="fas-chart-bar" slot="start"></ion-icon>
 | 
			
		||||
                            {{ 'addon.mod_feedback.completed_feedbacks' | translate }}
 | 
			
		||||
                        </ion-button>
 | 
			
		||||
                    </ion-col>
 | 
			
		||||
                    <ion-col *ngIf="hasNextPage">
 | 
			
		||||
                        <ion-button expand="block" (click)="continue()" class="ion-text-wrap">
 | 
			
		||||
                            {{ 'core.continue' | translate }}
 | 
			
		||||
                            <ion-icon name="fas-chevron-right" slot="end"></ion-icon>
 | 
			
		||||
                        </ion-button>
 | 
			
		||||
                    </ion-col>
 | 
			
		||||
                </ion-row>
 | 
			
		||||
            </ion-grid>
 | 
			
		||||
        </ion-card>
 | 
			
		||||
    </core-loading>
 | 
			
		||||
</ion-content>
 | 
			
		||||
							
								
								
									
										39
									
								
								src/addons/mod/feedback/pages/form/form.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/addons/mod/feedback/pages/form/form.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -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 {}
 | 
			
		||||
							
								
								
									
										11
									
								
								src/addons/mod/feedback/pages/form/form.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/addons/mod/feedback/pages/form/form.scss
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										428
									
								
								src/addons/mod/feedback/pages/form/form.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										428
									
								
								src/addons/mod/feedback/pages/form/form.ts
									
									
									
									
									
										Normal file
									
								
							@ -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<string, AddonModFeedbackResponseValue>;
 | 
			
		||||
    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<void> {
 | 
			
		||||
        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<boolean> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        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<number> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        this.items = [];
 | 
			
		||||
        const response = await this.fetchPageItems(page);
 | 
			
		||||
 | 
			
		||||
        this.items = <AddonModFeedbackFormItem[]> 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<AddonModFeedbackPageItems> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        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<void> {
 | 
			
		||||
        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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -487,3 +487,7 @@ ion-button.core-button-select {
 | 
			
		||||
        @include padding(null, null, null, 15px * $i + 16px);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
textarea:not([core-auto-rows]) {
 | 
			
		||||
    height: 200px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user