diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7403a29a8..be4c9d431 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,7 +45,7 @@ jobs: uses: android-actions/setup-android@v2 - name: Fix Android directories - run: rm -R /usr/local/lib/android/sdk/build-tools/31.0.0/ && rm -R /usr/local/lib/android/sdk/build-tools/32.0.0/ + run: rm -R /usr/local/lib/android/sdk/build-tools/31.0.0/ && rm -R /usr/local/lib/android/sdk/build-tools/32.0.0/ && rm -R /usr/local/lib/android/sdk/build-tools/33.0.0/ - name: Setup Node uses: actions/setup-node@v2 diff --git a/src/app/pages/questions/components/introduction/introduction.component.scss b/src/app/pages/questions/components/introduction/introduction.component.scss index c3fa4f9f4..93864ee35 100644 --- a/src/app/pages/questions/components/introduction/introduction.component.scss +++ b/src/app/pages/questions/components/introduction/introduction.component.scss @@ -6,4 +6,8 @@ introduction { .icons-big { width: 30%; } + + h2 { + font-size: 15px !important; + } } diff --git a/src/app/pages/questions/components/question/audio-input/audio-input.component.ts b/src/app/pages/questions/components/question/audio-input/audio-input.component.ts index bd7a6bf5b..3b460f0c6 100644 --- a/src/app/pages/questions/components/question/audio-input/audio-input.component.ts +++ b/src/app/pages/questions/components/question/audio-input/audio-input.component.ts @@ -27,6 +27,8 @@ import { AudioRecordService } from '../../../services/audio-record.service' export class AudioInputComponent implements OnDestroy, OnInit { @Output() valueChange: EventEmitter = new EventEmitter() + @Output() + onRecordStart: EventEmitter = new EventEmitter() @Input() text: string @Input() @@ -77,6 +79,7 @@ export class AudioInputComponent implements OnDestroy, OnInit { this.startRecording().catch(e => this.showTaskInterruptedAlert()) } else { this.stopRecording() + this.onRecordStart.emit(false) if (this.recordAttempts == DefaultMaxAudioAttemptsAllowed) this.finishRecording().catch(e => this.showTaskInterruptedAlert()) else this.showAfterAttemptAlert() @@ -95,6 +98,7 @@ export class AudioInputComponent implements OnDestroy, OnInit { this.permissionUtil.getRecordAudio_Permission(), this.permissionUtil.getWriteExternalStorage_permission() ]).then(res => { + this.onRecordStart.emit(true) this.usage.sendGeneralEvent(UsageEventType.RECORDING_STARTED, true) return res[0] && res[1] ? this.audioRecordService.startAudioRecording() diff --git a/src/app/pages/questions/components/question/question.component.html b/src/app/pages/questions/components/question/question.component.html index 606b42131..726de2a5b 100755 --- a/src/app/pages/questions/components/question/question.component.html +++ b/src/app/pages/questions/components/question/question.component.html @@ -47,14 +47,14 @@ @@ -64,7 +64,7 @@ [max]="question.range.max" [labelLeft]="question.range.labelLeft" [labelRight]="question.range.labelRight" - (valueChange)="onValueChange($event)" + (valueChange)="emitAnswer($event)" > @@ -73,7 +73,7 @@ [min]="question.range.min" [max]="question.range.max" [responses]="question.select_choices_or_calculations" - (valueChange)="onValueChange($event)" + (valueChange)="emitAnswer($event)" > @@ -84,7 +84,7 @@ [step]="question.range.step" [labelLeft]="question.range.labelLeft" [labelRight]="question.range.labelRight" - (valueChange)="onValueChange($event)" + (valueChange)="emitAnswer($event)" > @@ -93,7 +93,7 @@ [sections]="question.select_choices_or_calculations" [hasFieldLabel]="question.field_label.length > 0" [image]="question.field_annotation?.image" - (valueChange)="onValueChange($event)" + (valueChange)="emitAnswer($event)" [currentlyShown]="currentlyShown" > @@ -102,7 +102,8 @@ *ngSwitchCase="'audio'" [text]="question.field_label" [currentlyShown]="currentlyShown" - (valueChange)="onValueChange($event)" + (valueChange)="emitAnswer($event)" + (onRecordStart)="onAudioRecordStart($event)" > @@ -112,14 +113,14 @@ [image]="question.field_annotation.image" [timer]="question.field_annotation.timer" [currentlyShown]="currentlyShown" - (valueChange)="onValueChange($event)" + (valueChange)="emitAnswer($event)" > @@ -130,14 +131,14 @@ { code: '1', label: 'Yes' }, { code: '0', label: 'No' } ]" - (valueChange)="onValueChange($event)" + (valueChange)="emitAnswer($event)" > @@ -147,7 +148,7 @@ [responses]="question.select_choices_or_calculations" [currentlyShown]="currentlyShown" [previouslyShown]="previouslyShown" - (valueChange)="onValueChange($event)" + (valueChange)="emitAnswer($event)" > diff --git a/src/app/pages/questions/components/question/question.component.scss b/src/app/pages/questions/components/question/question.component.scss index 5467a66b6..4831c1aa7 100755 --- a/src/app/pages/questions/components/question/question.component.scss +++ b/src/app/pages/questions/components/question/question.component.scss @@ -70,11 +70,6 @@ question { .input-scroll { overflow-y: auto !important; - mask-image: linear-gradient(to top, transparent 3%, black 7%), - linear-gradient(to bottom, transparent 0%, black 7%); - mask-size: 100% 25%, 100% 80%; - mask-position: bottom, top; - mask-repeat: no-repeat, no-repeat; } .spinner-container { diff --git a/src/app/pages/questions/components/question/question.component.ts b/src/app/pages/questions/components/question/question.component.ts index 582ee9cdd..6bda9b0a2 100755 --- a/src/app/pages/questions/components/question/question.component.ts +++ b/src/app/pages/questions/components/question/question.component.ts @@ -9,9 +9,12 @@ import { } from '@angular/core' import { Dialogs } from '@ionic-native/dialogs/ngx' import { Vibration } from '@ionic-native/vibration/ngx' -import { Content, Keyboard } from 'ionic-angular' import * as smoothscroll from 'smoothscroll-polyfill' +import { + KeyboardEventType, + NextButtonEventType +} from '../../../../shared/enums/events' import { Answer } from '../../../../shared/models/answer' import { Question, QuestionType } from '../../../../shared/models/question' import { Task } from '../../../../shared/models/task' @@ -34,8 +37,13 @@ export class QuestionComponent implements OnInit, OnChanges { task: Task @Input() isSectionHeaderHidden: boolean + // isNextAutomatic: automatically slide to next upon answer + @Input() + isNextAutomatic: boolean @Output() answer: EventEmitter = new EventEmitter() + @Output() + nextAction: EventEmitter = new EventEmitter() value: any currentlyShown = false @@ -79,11 +87,7 @@ export class QuestionComponent implements OnInit, OnChanges { QuestionType.checkbox ]) - constructor( - private vibration: Vibration, - private dialogs: Dialogs, - private keyboard: Keyboard - ) { + constructor(private vibration: Vibration, private dialogs: Dialogs) { smoothscroll.polyfill() this.value = null } @@ -118,31 +122,20 @@ export class QuestionComponent implements OnInit, OnChanges { else this.previouslyShown = false this.currentlyShown = false } - // this.evalBeep() } - emitAnswer() { - this.answer.emit({ - id: this.question.field_name, - value: this.value, - type: this.question.field_type - }) - } - - onValueChange(event: any) { + emitAnswer(event: any) { // NOTE: On init the component fires the event once - if (event === undefined) { - return - } - this.value = event - this.emitAnswer() - } - - evalBeep() { - if (this.currentlyShown && this.question.field_label.includes('beep')) { - console.log('Beep!') - this.dialogs.beep(1) - this.vibration.vibrate(600) + if (event && event !== undefined) { + this.value = event + this.answer.emit({ + id: this.question.field_name, + value: this.value, + type: this.question.field_type + }) + if (this.question.isAutoNext) + this.nextAction.emit(NextButtonEventType.AUTO) + else this.nextAction.emit(NextButtonEventType.ENABLE) } } @@ -153,12 +146,14 @@ export class QuestionComponent implements OnInit, OnChanges { ) { const min = this.question.select_choices_or_calculations[0].code const minLabel = this.question.select_choices_or_calculations[0].label - const max = this.question.select_choices_or_calculations[ - this.question.select_choices_or_calculations.length - 1 - ].code - const maxLabel = this.question.select_choices_or_calculations[ - this.question.select_choices_or_calculations.length - 1 - ].label + const max = + this.question.select_choices_or_calculations[ + this.question.select_choices_or_calculations.length - 1 + ].code + const maxLabel = + this.question.select_choices_or_calculations[ + this.question.select_choices_or_calculations.length - 1 + ].label this.question.range = { min: parseInt(min.trim()), max: parseInt(max.trim()), @@ -168,16 +163,26 @@ export class QuestionComponent implements OnInit, OnChanges { } } - onTextInputFocus(value) { - if (value) { - // Add delay for keyboard to show up - setTimeout(() => { - this.content.nativeElement.style = `padding-bottom:${this.keyboardInputOffset}px;` - this.content.nativeElement.scrollTop = this.keyboardInputOffset - }, 100) - } else { - this.content.nativeElement.style = '' - this.content.nativeElement.scrollTop = 0 + onKeyboardEvent(value) { + switch (value) { + case KeyboardEventType.FOCUS: + // Add delay for keyboard to show up + setTimeout(() => { + this.content.nativeElement.style = `padding-bottom:${this.keyboardInputOffset}px;` + this.content.nativeElement.scrollTop = this.keyboardInputOffset + }, 100) + break + case KeyboardEventType.BLUR: { + this.content.nativeElement.style = '' + this.content.nativeElement.scrollTop = 0 + break + } + case KeyboardEventType.ENTER: { + this.nextAction.emit(NextButtonEventType.AUTO) + break + } + default: + break } } @@ -210,4 +215,9 @@ export class QuestionComponent implements OnInit, OnChanges { behavior: 'smooth' }) } + + onAudioRecordStart(start: boolean) { + if (start) this.nextAction.emit(NextButtonEventType.DISABLE) + else this.nextAction.emit(NextButtonEventType.ENABLE) + } } diff --git a/src/app/pages/questions/components/question/text-input/text-input.component.html b/src/app/pages/questions/components/question/text-input/text-input.component.html index cd6d8c70a..129e33d7c 100644 --- a/src/app/pages/questions/components/question/text-input/text-input.component.html +++ b/src/app/pages/questions/components/question/text-input/text-input.component.html @@ -29,10 +29,12 @@ diff --git a/src/app/pages/questions/components/question/text-input/text-input.component.ts b/src/app/pages/questions/components/question/text-input/text-input.component.ts index 70fd94bd6..8e7e6f1d2 100644 --- a/src/app/pages/questions/components/question/text-input/text-input.component.ts +++ b/src/app/pages/questions/components/question/text-input/text-input.component.ts @@ -6,13 +6,14 @@ import { Output, ViewChild } from '@angular/core' -import { IonicFormInput } from 'ionic-angular' +import { Keyboard } from '@ionic-native/keyboard/ngx' import { LocalizationService } from '../../../../../core/services/misc/localization.service' +import { KeyboardEventType } from '../../../../../shared/enums/events' @Component({ selector: 'text-input', - templateUrl: 'text-input.component.html', + templateUrl: 'text-input.component.html' }) export class TextInputComponent implements OnInit { @ViewChild('content') content @@ -20,7 +21,7 @@ export class TextInputComponent implements OnInit { @Output() valueChange: EventEmitter = new EventEmitter() @Output() - textInputFocus: EventEmitter = new EventEmitter() + keyboardEvent: EventEmitter = new EventEmitter() @Input() type: string @Input() @@ -45,12 +46,15 @@ export class TextInputComponent implements OnInit { hour: 'Hour', minute: 'Minute', second: 'Second', - ampm: 'AM/PM', + ampm: 'AM/PM' } value = {} - constructor(private localization: LocalizationService) {} + constructor( + private localization: LocalizationService, + private keyboard: Keyboard + ) {} ngOnInit() { if (this.type.length) { @@ -112,13 +116,16 @@ export class TextInputComponent implements OnInit { } emitAnswer(value) { + if (!value) return if (typeof value !== 'string') { this.value = Object.assign(this.value, value) this.valueChange.emit(JSON.stringify(this.value)) } else this.valueChange.emit(value) } - emitTextInputFocus(value) { - this.textInputFocus.emit(value) + emitKeyboardEvent(value) { + if (value == KeyboardEventType.ENTER) this.keyboard.hide() + + this.keyboardEvent.emit(value) } } diff --git a/src/app/pages/questions/components/toolbar/toolbar.component.ts b/src/app/pages/questions/components/toolbar/toolbar.component.ts index a6d3d1eab..39804a8ff 100644 --- a/src/app/pages/questions/components/toolbar/toolbar.component.ts +++ b/src/app/pages/questions/components/toolbar/toolbar.component.ts @@ -31,6 +31,8 @@ export class ToolbarComponent implements OnChanges { finish: EventEmitter = new EventEmitter() @Output() close: EventEmitter = new EventEmitter() + @Output() + tappedDisabledButton: EventEmitter = new EventEmitter() textValues = { next: this.localization.translateKey(LocKeys.BTN_NEXT), @@ -67,7 +69,10 @@ export class ToolbarComponent implements OnChanges { } rightButtonHandler() { - if (this.isRightButtonDisabled) return + if (this.isRightButtonDisabled) { + this.tappedDisabledButton.emit() + return + } switch (this.rightButtonText) { case this.textValues.next: return this.next.emit() diff --git a/src/app/pages/questions/containers/questions-page.component.html b/src/app/pages/questions/containers/questions-page.component.html index e42b365df..4469a21d0 100755 --- a/src/app/pages/questions/containers/questions-page.component.html +++ b/src/app/pages/questions/containers/questions-page.component.html @@ -29,6 +29,7 @@ [currentIndex]="currentQuestionGroupId" [isSectionHeaderHidden]="j != currentQuestionIndices[0]" (answer)="onAnswer($event)" + (nextAction)="nextAction($event)" > @@ -68,6 +69,7 @@ (previous)="previousQuestion()" (close)="exitQuestionnaire()" (finish)="navigateToFinishPage()" + (tappedDisabledButton)="showDisabledButtonAlert()" [isLeftButtonDisabled]="isLeftButtonDisabled" [isRightButtonDisabled]="isRightButtonDisabled" [currentQuestionId]="currentQuestionGroupId" diff --git a/src/app/pages/questions/containers/questions-page.component.ts b/src/app/pages/questions/containers/questions-page.component.ts index 26de37296..838776c85 100644 --- a/src/app/pages/questions/containers/questions-page.component.ts +++ b/src/app/pages/questions/containers/questions-page.component.ts @@ -2,16 +2,24 @@ import { Component, OnInit, ViewChild } from '@angular/core' import { Insomnia } from '@ionic-native/insomnia/ngx' import { NavController, NavParams, Platform, Slides } from 'ionic-angular' +import { AlertService } from '../../../core/services/misc/alert.service' import { LocalizationService } from '../../../core/services/misc/localization.service' import { UsageService } from '../../../core/services/usage/usage.service' -import { UsageEventType } from '../../../shared/enums/events' +import { + NextButtonEventType, + UsageEventType +} from '../../../shared/enums/events' import { LocKeys } from '../../../shared/enums/localisations' import { Assessment, AssessmentType, ShowIntroductionType } from '../../../shared/models/assessment' -import { ExternalApp, Question } from '../../../shared/models/question' +import { + ExternalApp, + Question, + QuestionType +} from '../../../shared/models/question' import { Task } from '../../../shared/models/task' import { HomePageComponent } from '../../home/containers/home-page.component' import { AppLauncherService } from '../services/app-launcher.service' @@ -65,7 +73,8 @@ export class QuestionsPageComponent implements OnInit { private platform: Platform, private insomnia: Insomnia, private localization: LocalizationService, - private appLauncher: AppLauncherService + private appLauncher: AppLauncherService, + private alertService: AlertService ) { this.platform.registerBackButtonAction(() => { this.sendCompletionLog() @@ -168,13 +177,7 @@ export class QuestionsPageComponent implements OnInit { } onAnswer(event) { - if (event.id) { - this.questionsService.submitAnswer(event) - setTimeout(() => this.updateToolbarButtons(), 100) - } - if (this.questionsService.getIsNextAutomatic(event.type)) { - this.nextQuestion() - } + if (event.id) this.questionsService.submitAnswer(event) } slideQuestion() { @@ -200,7 +203,17 @@ export class QuestionsPageComponent implements OnInit { ) } + nextAction(event) { + if (event == NextButtonEventType.AUTO) return this.nextQuestion() + if (event == NextButtonEventType.ENABLE) + return setTimeout(() => this.updateToolbarButtons(), 100) + if (event == NextButtonEventType.DISABLE) + return (this.isRightButtonDisabled = true) + } + nextQuestion() { + if (this.isRightButtonDisabled) return + const questionPosition = this.questionsService.getNextQuestion( this.groupedQuestions, this.currentQuestionGroupId @@ -284,6 +297,23 @@ export class QuestionsPageComponent implements OnInit { return 1 } + showDisabledButtonAlert() { + const currentQuestionType = this.getCurrentQuestions()[0].field_type + // NOTE: Show alert when next is tapped without finishing audio question + if (currentQuestionType == QuestionType.audio) + this.alertService.showAlert({ + message: this.localization.translateKey( + LocKeys.AUDIO_TASK_BUTTON_ALERT_DESC + ), + buttons: [ + { + text: this.localization.translateKey(LocKeys.BTN_DISMISS), + handler: () => {} + } + ] + }) + } + private checkIfQuestionnaireHasAppLaunch() { if ( this.externalApp && diff --git a/src/app/pages/questions/services/questions.service.ts b/src/app/pages/questions/services/questions.service.ts index 6feec6049..f67c6e256 100644 --- a/src/app/pages/questions/services/questions.service.ts +++ b/src/app/pages/questions/services/questions.service.ts @@ -125,11 +125,10 @@ export class QuestionsService { } } - processQuestions(title, questions: any[]) { - if (title.includes('ESM28Q')) - if (new Date().getHours() > 10) return Promise.resolve(questions.slice(1)) - - return Promise.resolve(questions) + processQuestions(title, questions: Question[]) { + return questions.map(q => + Object.assign(q, { isAutoNext: this.getIsNextAutomatic(q.field_type) }) + ) } isAnswered(question: Question) { @@ -222,13 +221,11 @@ export class QuestionsService { const type = task.type return this.questionnaire .getAssessmentForTask(type, task) - .then(assessment => - this.processQuestions( + .then(assessment => { + const questions = this.processQuestions( assessment.name, assessment.questions - ).then(questions => [assessment, questions]) - ) - .then(([assessment, questions]) => { + ) return { title: assessment.name, introduction: this.localization.chooseText(assessment.startText), diff --git a/src/app/shared/enums/events.ts b/src/app/shared/enums/events.ts index 1bafe00f0..b81a5aec7 100644 --- a/src/app/shared/enums/events.ts +++ b/src/app/shared/enums/events.ts @@ -47,3 +47,15 @@ export enum NotificationEventType { RESCHEDULED = 'notification_rescheduled', TEST = 'notification_test' } + +export enum KeyboardEventType { + FOCUS = 'focus', + BLUR = 'blur', + ENTER = 'enter' +} + +export enum NextButtonEventType { + AUTO = 'auto', + DISABLE = 'disable', + ENABLE = 'enable' +} diff --git a/src/app/shared/enums/localisations.ts b/src/app/shared/enums/localisations.ts index 10fd5ccd0..122ce17a8 100644 --- a/src/app/shared/enums/localisations.ts +++ b/src/app/shared/enums/localisations.ts @@ -168,6 +168,7 @@ export class LocKeys { static CONFIG_ERROR_DESC = new LocKeys('CONFIG_ERROR_DESC') static AUDIO_TASK_ALERT = new LocKeys('AUDIO_TASK_ALERT') static AUDIO_TASK_ALERT_DESC = new LocKeys('AUDIO_TASK_ALERT_DESC') + static AUDIO_TASK_BUTTON_ALERT_DESC = new LocKeys('AUDIO_TASK_ALERT_DESC') static AUDIO_TASK_ATTEMPT_ALERT = new LocKeys('AUDIO_TASK_ATTEMPT_ALERT') static AUDIO_TASK_HAPPY_ALERT = new LocKeys('AUDIO_TASK_HAPPY_ALERT') static SPLASH_STATUS_UPDATING_CONFIG = new LocKeys( diff --git a/src/app/shared/models/question.ts b/src/app/shared/models/question.ts index 85a9ea6a5..1445cbff9 100755 --- a/src/app/shared/models/question.ts +++ b/src/app/shared/models/question.ts @@ -20,6 +20,7 @@ export interface Question { text_validation_type_or_show_slider_number?: string type?: string range?: Range + isAutoNext?: boolean } export interface ExternalApp { diff --git a/src/assets/data/localisations.ts b/src/assets/data/localisations.ts index 28e692959..1eb68ec69 100644 --- a/src/assets/data/localisations.ts +++ b/src/assets/data/localisations.ts @@ -1429,6 +1429,21 @@ export const Localisations = { pl: 'Zadanie zostało przerwane. Zacznij od nowa.', hb: 'המשימה הופסקה, התחל מחדש' }, + AUDIO_TASK_BUTTON_ALERT_DESC: { + da: + 'Stop venligst optagelsen, når du er færdig for at aktivere den næste knap.', + de: + 'Bitte stoppen Sie die Aufnahme, wenn Sie fertig sind, um die Schaltfläche „Weiter“ zu aktivieren.', + en: + 'Please stop the recording once you are done in order to enable the next button.', + es: + 'Detenga la grabación una vez que haya terminado para habilitar el siguiente botón.', + it: + 'Interrompi la registrazione una volta che hai finito per abilitare il pulsante successivo.', + nl: 'Stop de opname als u klaar bent om de volgende knop in te schakelen.', + pl: 'Po zakończeniu zatrzymaj nagrywanie, aby włączyć następny przycisk.', + hb: 'אנא עצור את ההקלטה לאחר שתסיים כדי להפעיל את הכפתור הבא.' + }, AUDIO_TASK_ATTEMPT_ALERT: { da: 'Tilbageværende forsøg', de: 'Verbleibende Versuche',