diff --git a/libs/shared/src/i18n/en.json b/libs/shared/src/i18n/en.json index 0b2d681456..9f0efe6961 100644 --- a/libs/shared/src/i18n/en.json +++ b/libs/shared/src/i18n/en.json @@ -1557,6 +1557,21 @@ }, "tooltip": "When submitting a record from the widget, the form will be automatically populated with the record's data after the submission" }, + "completionPopup": { + "label": "Completion popup", + "text": { + "label": "Popup message", + "placeholder": "Enter the message to show after submission", + "tooltip": "Custom text displayed after submission" + }, + "title": "Completion message", + "titleField": { + "label": "Popup title", + "placeholder": "Enter the popup title", + "tooltip": "Custom title displayed in the popup" + }, + "tooltip": "Show a popup after the form is submitted" + }, "directions": { "both": "Both changes on the state or the question will be reflected on the other", "questionToState": "Changes on the question will be reflected on the state", diff --git a/libs/shared/src/i18n/fr.json b/libs/shared/src/i18n/fr.json index da23367773..b863d8dd1f 100644 --- a/libs/shared/src/i18n/fr.json +++ b/libs/shared/src/i18n/fr.json @@ -1569,6 +1569,21 @@ }, "tooltip": "Lors de la soumission d'un enregistrement à partir du widget, le formulaire sera automatiquement rempli avec les données de l'enregistrement après la soumission" }, + "completionPopup": { + "label": "Fenêtre de confirmation", + "text": { + "label": "Message de la fenêtre", + "placeholder": "Saisissez le message à afficher après la soumission", + "tooltip": "Texte personnalisé affiché après la soumission" + }, + "title": "Message de confirmation", + "titleField": { + "label": "Titre de la fenêtre", + "placeholder": "Saisissez le titre de la fenêtre", + "tooltip": "Titre personnalisé affiché dans la fenêtre" + }, + "tooltip": "Afficher une fenêtre après la soumission du formulaire" + }, "directions": { "both": "Les modifications apportées à l'état ou à la question seront répercutées sur l'autre", "questionToState": "Les modifications apportées à la question seront répercutées sur l'état", diff --git a/libs/shared/src/i18n/test.json b/libs/shared/src/i18n/test.json index 6639cf3800..03aa8b9317 100644 --- a/libs/shared/src/i18n/test.json +++ b/libs/shared/src/i18n/test.json @@ -1557,6 +1557,21 @@ }, "tooltip": "******" }, + "completionPopup": { + "label": "******", + "text": { + "label": "******", + "placeholder": "******", + "tooltip": "******" + }, + "title": "******", + "titleField": { + "label": "******", + "placeholder": "******", + "tooltip": "******" + }, + "tooltip": "******" + }, "directions": { "both": "******", "questionToState": "******", diff --git a/libs/shared/src/lib/components/widgets/form-settings/form-settings.forms.ts b/libs/shared/src/lib/components/widgets/form-settings/form-settings.forms.ts index 5cf56a534c..50544a2c08 100644 --- a/libs/shared/src/lib/components/widgets/form-settings/form-settings.forms.ts +++ b/libs/shared/src/lib/components/widgets/form-settings/form-settings.forms.ts @@ -28,6 +28,11 @@ export const createFormWidgetFormGroup = (id: string, configuration: any) => { autoPopulateOmitQuestions: [ get(configuration, 'autoPopulateOmitQuestions', []), ], + completionPopup: fb.group({ + enabled: [get(configuration, 'completionPopup.enabled', false)], + title: [get(configuration, 'completionPopup.title', '')], + text: [get(configuration, 'completionPopup.text', '')], + }), floatingActions: [get(configuration, 'floatingActions', false)], loadRecord: fb.group({ enabled: [get(configuration, 'loadRecord.enabled', false)], diff --git a/libs/shared/src/lib/components/widgets/form-settings/tab-main/tab-main.component.html b/libs/shared/src/lib/components/widgets/form-settings/tab-main/tab-main.component.html index acc5f75e95..43fc933a5f 100644 --- a/libs/shared/src/lib/components/widgets/form-settings/tab-main/tab-main.component.html +++ b/libs/shared/src/lib/components/widgets/form-settings/tab-main/tab-main.component.html @@ -90,6 +90,81 @@

{{ 'common.general' | translate }}

+
+
+ + + {{ 'components.widget.form.completionPopup.label' | translate }} + + + +
+ +
+ + +
+ +
+ + +
+
+
@@ -27,3 +27,14 @@ }} + + + + +

{{ data.title }}

+
+ +

{{ data?.text }}

+
+
+
\ No newline at end of file diff --git a/libs/shared/src/lib/components/widgets/form/form-widget.component.ts b/libs/shared/src/lib/components/widgets/form/form-widget.component.ts index 17bded7b51..095ee9da45 100644 --- a/libs/shared/src/lib/components/widgets/form/form-widget.component.ts +++ b/libs/shared/src/lib/components/widgets/form/form-widget.component.ts @@ -5,6 +5,7 @@ import { TemplateRef, ViewChild, } from '@angular/core'; +import { Dialog, DialogRef } from '@angular/cdk/dialog'; import { Apollo } from 'apollo-angular'; import { Form, FormQueryResponse } from '../../../models/form.model'; import { Record, RecordQueryResponse } from '../../../models/record.model'; @@ -22,6 +23,38 @@ import { import { DashboardService } from '../../../services/dashboard/dashboard.service'; import { isNil, omit } from 'lodash'; +/** Completion popup settings. */ +interface CompletionPopupSettings { + enabled?: boolean; + title?: string; + text?: string; +} + +/** Mapping rule between a question and a dashboard state. */ +interface MapQuestionToState { + question: string; + state: string; + direction: 'questionToState' | 'stateToQuestion' | 'both'; +} + +/** Form widget settings. */ +interface FormWidgetSettings { + title?: string; + form?: string; + floatingActions?: boolean; + mapQuestionState?: MapQuestionToState[]; + autoPopulateOnSubmit?: boolean; + autoPopulateOmitQuestions?: string[]; + completionPopup?: CompletionPopupSettings; + loadRecord?: { + enabled?: boolean; + canUpdate?: boolean; + update?: boolean; + state?: string | null; + }; + contextFilters?: string; +} + /** * Form widget component. */ @@ -35,11 +68,17 @@ export class FormWidgetComponent implements OnInit { /** Widget settings */ - @Input() settings: any; + @Input() settings: FormWidgetSettings = {}; /** Should show padding */ @Input() usePadding = true; /** Widget header template reference */ @ViewChild('headerTemplate') headerTemplate!: TemplateRef; + /** Completion popup template reference */ + @ViewChild('completionPopupTemplate') + private completionPopupTemplate!: TemplateRef<{ + text: string; + title: string; + }>; /** Loaded form */ public form!: Form; /** Loaded record, if any */ @@ -62,6 +101,22 @@ export class FormWidgetComponent @ViewChild(FormComponent) private formComponent?: FormComponent; + /** Completion popup dialog reference */ + private completionPopupRef?: DialogRef< + unknown, + { text: string; title: string } + >; + + /** @returns map question state settings */ + get mapQuestionState(): MapQuestionToState[] { + return this.settings.mapQuestionState || []; + } + + /** @returns floating actions setting */ + get floatingActions(): boolean { + return this.settings.floatingActions ?? false; + } + /** * Form widget component. * @@ -70,38 +125,41 @@ export class FormWidgetComponent * @param translate This is the service that allows us to translate the text in our application. * @param contextService Shared context service * @param dashboardService Shared dashboard service + * @param dialog Dialog service */ constructor( private apollo: Apollo, protected snackBar: SnackbarService, protected translate: TranslateService, private contextService: ContextService, - private dashboardService: DashboardService + private dashboardService: DashboardService, + private dialog: Dialog ) { super(); } async ngOnInit(): Promise { - const promises: Promise[] = - []; - // Fetch template if (this.settings.form) { - promises.push( - firstValueFrom( + try { + const { data, loading } = await firstValueFrom( this.apollo.query({ query: GET_SHORT_FORM_BY_ID, variables: { id: this.settings.form, }, }) - ).then(({ data, loading }) => { - this.form = data.form; - this.loading = loading; - }) - ); - - await Promise.all(promises); + ); + this.form = data.form; + this.loading = loading; + } catch (err) { + this.loading = false; + const message = + err instanceof Error + ? err.message + : this.translate.instant('common.notifications.dataNotRecovered'); + this.snackBar.openSnackBar(message, { error: true }); + } } // Load record from loadRecord state @@ -124,19 +182,31 @@ export class FormWidgetComponent }, }) .pipe(takeUntil(this.destroy$)) - .subscribe(({ data }) => { - this.loading = false; - if (data) { - this.record = - !this.settings.loadRecord.canUpdate || - this.settings.loadRecord.update - ? data.record - : omit(data.record, 'id'); - } + .subscribe({ + next: ({ data }) => { + this.loading = false; + if (data) { + this.record = + !this.settings.loadRecord?.canUpdate || + this.settings.loadRecord?.update + ? data.record + : omit(data.record, 'id'); + } + }, + error: (err) => { + this.loading = false; + const message = + err instanceof Error + ? err.message + : this.translate.instant( + 'common.notifications.dataNotRecovered' + ); + this.snackBar.openSnackBar(message, { error: true }); + }, }); } }); - if (this.settings.loadRecord.canUpdate) { + if (this.settings.loadRecord?.canUpdate) { this.mode = 'edit'; } } else { @@ -151,16 +221,15 @@ export class FormWidgetComponent this.contextService.filter$ .pipe(debounceTime(500), takeUntil(this.destroy$)) .subscribe(({ previous, current }) => { - if ( - this.contextService.filterRegex.test(this.settings.contextFilters) - ) { + const contextFilters = this.settings.contextFilters ?? ''; + if (this.contextService.filterRegex.test(contextFilters)) { if ( this.contextService.shouldRefresh(this.settings, previous, current) ) { - const contextFilters = this.contextService.injectContext( + const resolvedFilters = this.contextService.injectContext( this.contextFilters ); - this.getRecordFromFilters(contextFilters); + this.getRecordFromFilters(resolvedFilters); } } }); @@ -177,15 +246,21 @@ export class FormWidgetComponent this.completed = e.completed; this.hideNewRecord = this.hideNewRecord || e.hideNewRecord || false; + if (e.completed === true) { + this.openCompletionPopup(); + } + if (this.settings.autoPopulateOnSubmit && e.completed === true) { // Reset the form setTimeout(() => { const data = structuredClone( this.formComponent?.survey.getParsedData?.() || {} ); - this.settings.autoPopulateOmitQuestions.forEach((question: string) => { - delete data[question]; - }); + (this.settings.autoPopulateOmitQuestions || []).forEach( + (question: string) => { + delete data[question]; + } + ); this.clearForm(); if (this.formComponent) { this.formComponent.survey.data = data; @@ -224,12 +299,55 @@ export class FormWidgetComponent id: recordId, }, }) - .subscribe(({ data }) => { - if (data) { - this.record = data.record; - } + .subscribe({ + next: ({ data }) => { + if (data) { + this.record = data.record; + } + }, + error: (err) => { + const message = + err instanceof Error + ? err.message + : this.translate.instant( + 'common.notifications.dataNotRecovered' + ); + this.snackBar.openSnackBar(message, { error: true }); + }, }); } } } + + /** + * Opens the completion popup if enabled. + */ + private openCompletionPopup(): void { + if (!this.settings.completionPopup?.enabled) { + return; + } + if (!this.completionPopupTemplate) { + return; + } + + const text = + this.settings.completionPopup.text?.trim() || + this.translate.instant('components.form.display.submissionMessage'); + const title = + this.settings.completionPopup?.title?.trim() || + this.translate.instant('components.widget.form.completionPopup.title'); + + if (!text) { + return; + } + + if (this.completionPopupRef) { + this.completionPopupRef.close(); + } + + this.completionPopupRef = this.dialog.open(this.completionPopupTemplate, { + data: { text, title }, + autoFocus: false, + }); + } } diff --git a/libs/shared/src/lib/components/widgets/form/form-widget.module.ts b/libs/shared/src/lib/components/widgets/form/form-widget.module.ts index bd3eeb4b6d..5e1dd1a598 100644 --- a/libs/shared/src/lib/components/widgets/form/form-widget.module.ts +++ b/libs/shared/src/lib/components/widgets/form/form-widget.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormWidgetComponent } from './form-widget.component'; import { FormModule } from '../../form/form.module'; -import { ButtonModule, SpinnerModule } from '@oort-front/ui'; +import { ButtonModule, DialogModule, SpinnerModule } from '@oort-front/ui'; import { TranslateModule } from '@ngx-translate/core'; /** Module for the form widget component */ @@ -12,6 +12,7 @@ import { TranslateModule } from '@ngx-translate/core'; CommonModule, FormModule, ButtonModule, + DialogModule, TranslateModule, SpinnerModule, ],