From 13679abc7ae05927e034ca2ef833dec25f72e706 Mon Sep 17 00:00:00 2001 From: Antoine Hurard Date: Mon, 22 Sep 2025 09:08:58 +0200 Subject: [PATCH 1/5] put back draft record --- .../lib/components/form-modal/form-modal.component.html | 8 ++++---- .../src/lib/components/form-modal/form-modal.component.ts | 2 ++ libs/shared/src/lib/components/form/form.component.html | 8 ++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/libs/shared/src/lib/components/form-modal/form-modal.component.html b/libs/shared/src/lib/components/form-modal/form-modal.component.html index bf9215eea0..f5935123b5 100644 --- a/libs/shared/src/lib/components/form-modal/form-modal.component.html +++ b/libs/shared/src/lib/components/form-modal/form-modal.component.html @@ -11,11 +11,11 @@ *ngIf="survey" [survey]="survey" > - + > - + }} - + > {{ 'common.next' | translate }} - + }} {{ 'common.save' | translate }} From 991a92479f49cd70b575a5379f316e02c55f86e5 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Tue, 23 Sep 2025 11:51:41 +0200 Subject: [PATCH 2/5] AB#120296 - Show the 'Save as Draft' button only when needed, deactivate it when editing existing records, and saving changes before loading draft records --- .../draft-record/draft-record.component.ts | 6 + .../form-modal/form-modal.component.html | 3 +- .../form-modal/form-modal.component.ts | 139 +++++++++++++++--- .../lib/components/form/form.component.html | 3 +- .../src/lib/components/form/form.component.ts | 53 ++++++- 5 files changed, 178 insertions(+), 26 deletions(-) diff --git a/libs/shared/src/lib/components/draft-record/draft-record.component.ts b/libs/shared/src/lib/components/draft-record/draft-record.component.ts index 92df8dda19..6926011e62 100644 --- a/libs/shared/src/lib/components/draft-record/draft-record.component.ts +++ b/libs/shared/src/lib/components/draft-record/draft-record.component.ts @@ -22,6 +22,8 @@ export class DraftRecordComponent extends UnsubscribeComponent { @Input() survey!: SurveyModel; /** Form input */ @Input() formId!: string; + /** Optional hook before opening drafts list. Return false to cancel. */ + @Input() beforeOpenDrafts?: () => Promise | boolean; /** Emit event when selecting draft */ @Output() loadDraft: EventEmitter = new EventEmitter(); @@ -38,6 +40,10 @@ export class DraftRecordComponent extends UnsubscribeComponent { * Open draft list. */ public async onOpenDrafts(): Promise { + const beforeOpenDrafts = await this.beforeOpenDrafts?.(); + if (!beforeOpenDrafts) { + return; + } // Lazy load modal const { DraftRecordListModalComponent } = await import( '../draft-record-list-modal/draft-record-list-modal.component' diff --git a/libs/shared/src/lib/components/form-modal/form-modal.component.html b/libs/shared/src/lib/components/form-modal/form-modal.component.html index f5935123b5..f864b35b96 100644 --- a/libs/shared/src/lib/components/form-modal/form-modal.component.html +++ b/libs/shared/src/lib/components/form-modal/form-modal.component.html @@ -14,6 +14,7 @@ @@ -71,7 +72,7 @@ {{ 'common.next' | translate }} - {{ + {{ 'components.form.draftRecords.save' | translate }} { // Allow user to save as draft this.disableSaveAsDraft = false; + this.ValueChanged = true; }); this.survey.onComplete.add(this.onComplete); if (this.storedMergedData) { @@ -432,28 +441,29 @@ export class FormModalComponent this.ngZone.run(() => { this.dialogRef.close(); }); - } else { - if (this.lastDraftRecord) { - const callback = () => { - this.lastDraftRecord = undefined; - }; - this.formHelpersService.deleteRecordDraft( - this.lastDraftRecord, - callback - ); - } - this.ngZone.run(() => { - this.dialogRef.close({ - template: this.data.template, - data: data?.addRecord, - } as any); - this.snackBar.openSnackBar( - this.translate.instant( - 'components.form.display.submissionMessage' - ) - ); - }); + } else { + if (this.lastDraftRecord) { + const callback = () => { + this.lastDraftRecord = undefined; + }; + this.formHelpersService.deleteRecordDraft( + this.lastDraftRecord, + callback + ); } + this.ValueChanged = false; + this.ngZone.run(() => { + this.dialogRef.close({ + template: this.data.template, + data: data?.addRecord, + } as any); + this.snackBar.openSnackBar( + this.translate.instant( + 'components.form.display.submissionMessage' + ) + ); + }); + } }, error: (err) => { this.snackBar.openSnackBar(err.message, { error: true }); @@ -558,6 +568,7 @@ export class FormModalComponent value: '', }) ); + this.ValueChanged = false; this.dialogRef.close({ template: this.form?.id, data: data[responseType], @@ -741,8 +752,92 @@ export class FormModalComponent public onLoadDraftRecord(id: string): void { this.lastDraftRecord = id; this.disableSaveAsDraft = true; + this.ValueChanged = false; } + /** + * Saves the current record data without closing the modal + */ + public async saveRecordSilently(): Promise { + return new Promise((resolve) => { + if (!this.data.recordId || !this.survey) { + resolve(false); + return; + } + + this.formHelpersService.uploadFiles( + this.survey, + this.temporaryFilesStorage, + this.form?.id as string + ).then(() => { + this.formHelpersService.createTemporaryRecords(this.survey).then(() => { + this.apollo + .mutate({ + mutation: EDIT_RECORD, + variables: { + id: this.isMultiEdition ? (this.data.recordId as string[])[0] : this.data.recordId, + data: this.survey.data, + template: this.data.template, + }, + }) + .subscribe({ + next: ({ errors, data }) => { + if (errors) { + this.snackBar.openSnackBar( + this.translate.instant('common.notifications.objectNotUpdated', { + type: this.translate.instant('common.record.one'), + error: errors[0].message, + }), + { error: true } + ); + resolve(false); + } else { + this.snackBar.openSnackBar( + this.translate.instant('common.notifications.objectUpdated', { + type: this.translate.instant('common.record.one'), + value: '', + }) + ); + this.ValueChanged = false; + resolve(true); + } + }, + error: (err) => { + this.snackBar.openSnackBar(err.message, { error: true }); + resolve(false); + }, + }); + }).catch(() => resolve(false)); + }).catch(() => resolve(false)); + }); + } + + /** + * When editing an existing record, + * ask the user whether to save current changes before loading a draft. + */ + public beforeOpenDrafts = async (): Promise => { + if (this.data.recordId && this.ValueChanged) { + const confirmMessage: ConfirmDialogData = { + title: this.translate.instant('components.form.update.exit'), + content: this.translate.instant( + 'components.form.update.exitMessage' + ), + confirmText: this.translate.instant('components.confirmModal.confirm'), + confirmVariant: 'primary' as 'primary', + }; + const dialogRef = this.confirmService.openConfirmModal(confirmMessage); + const value = await firstValueFrom(dialogRef.closed as any); + if (value) { + // Save the current changes before loading draft without closing the modal + const saveSuccess = await this.saveRecordSilently(); + return saveSuccess; + } + return false; + } + return true; + }; + /** * Clears the cache for the records created by resource questions */ diff --git a/libs/shared/src/lib/components/form/form.component.html b/libs/shared/src/lib/components/form/form.component.html index d8eae2eec8..75665dadfe 100644 --- a/libs/shared/src/lib/components/form/form.component.html +++ b/libs/shared/src/lib/components/form/form.component.html @@ -12,6 +12,7 @@ @@ -55,7 +56,7 @@ >{{ 'common.next' | translate }} - {{ + {{ 'components.form.draftRecords.save' | translate }} {{ diff --git a/libs/shared/src/lib/components/form/form.component.ts b/libs/shared/src/lib/components/form/form.component.ts index 4697afac60..8bfa8f97b3 100644 --- a/libs/shared/src/lib/components/form/form.component.ts +++ b/libs/shared/src/lib/components/form/form.component.ts @@ -18,7 +18,7 @@ import { EditRecordMutationResponse, Record as RecordModel, } from '../../models/record.model'; -import { BehaviorSubject, takeUntil } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, takeUntil } from 'rxjs'; import addCustomFunctions from '../../utils/custom-functions'; import { AuthService } from '../../services/auth/auth.service'; import { FormBuilderService } from '../../services/form-builder/form-builder.service'; @@ -28,6 +28,7 @@ import { UnsubscribeComponent } from '../utils/unsubscribe/unsubscribe.component import { FormHelpersService } from '../../services/form-helper/form-helper.service'; import { SnackbarService, UILayoutService } from '@oort-front/ui'; import { isNil } from 'lodash'; +import { ConfirmService } from '../../services/confirm/confirm.service'; /** * This component is used to display forms @@ -76,6 +77,13 @@ export class FormComponent public lastDraftRecord?: string; /** Disables the save as draft button */ public disableSaveAsDraft = false; + /** Track if form has been changed */ + private ValueChanged = false; + /** Whether Save as Draft button should be visible */ + public get showSaveAsDraft(): boolean { + // Show when creating a new record or when a draft is loaded + return !this.record || !!this.lastDraftRecord; + } /** Timeout for reset survey */ private resetTimeoutListener!: NodeJS.Timeout; /** As we save the draft record in the db, the local storage is no longer used */ @@ -105,7 +113,8 @@ export class FormComponent private layoutService: UILayoutService, private formBuilderService: FormBuilderService, public formHelpersService: FormHelpersService, - private translate: TranslateService + private translate: TranslateService, + private confirmService: ConfirmService ) { super(); } @@ -127,6 +136,8 @@ export class FormComponent this.record ); + this.ValueChanged = false; + this.survey.showCompletedPage = false; if (!this.record && !this.form.canCreateRecords) { this.survey.mode = 'display'; @@ -134,6 +145,7 @@ export class FormComponent this.survey.onValueChanged.add(() => { // Allow user to save as draft this.disableSaveAsDraft = false; + this.ValueChanged = true; }); this.survey.onComplete.add(() => { this.onComplete(); @@ -216,6 +228,7 @@ export class FormComponent /** Force reload of the survey so default value are being applied */ this.survey.fromJSON(this.survey.toJSON()); this.survey.showCompletedPage = false; + this.ValueChanged = false; this.save.emit({ completed: false }); if (this.resetTimeoutListener) { clearTimeout(this.resetTimeoutListener); @@ -344,6 +357,7 @@ export class FormComponent } else { this.survey.showCompletedPage = true; } + this.ValueChanged = false; this.save.emit({ completed: true, hideNewRecord: data.addRecord && data.addRecord.form.uniqueRecord, @@ -378,6 +392,7 @@ export class FormComponent this.formHelpersService.clearTemporaryFilesStorage( this.temporaryFilesStorage ); + this.ValueChanged = false; } /** @@ -405,6 +420,7 @@ export class FormComponent public onLoadDraftRecord(id: string): void { this.lastDraftRecord = id; this.disableSaveAsDraft = true; + this.ValueChanged = false; } /** @@ -449,6 +465,39 @@ export class FormComponent }); } + /** + * When editing an existing record, + * ask the user whether to save current changes before loading a draft. + */ + public beforeOpenDrafts = async (): Promise => { + if (this.record && this.ValueChanged) { + const dialogRef = this.confirmService.openConfirmModal({ + title: this.translate.instant('components.form.update.exit'), + content: this.translate.instant( + 'components.form.update.exitMessage' + ), + confirmText: this.translate.instant('components.confirmModal.confirm'), + confirmVariant: 'primary', + }); + const value = await firstValueFrom(dialogRef.closed as any); + if (value) { + // User confirmed, save the current changes before loading draft + try { + await this.onComplete(); + this.ValueChanged = false; + + return true; + } catch (error) { + // If save fails, don't proceed with loading draft + console.error('Failed to save changes before loading draft:', error); + return false; + } + } + return false; + } + return true; + }; + /** It removes the item from local storage, clears cached records, and discards the search. */ override ngOnDestroy(): void { super.ngOnDestroy(); From a7b8fb359ca5db3c94a8490487757426e5cf5f17 Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Tue, 23 Sep 2025 12:02:58 +0200 Subject: [PATCH 3/5] AB#120296 - Enables autosave for new records and drafts --- .../form-modal/form-modal.component.ts | 65 +++++++++++++++++-- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/libs/shared/src/lib/components/form-modal/form-modal.component.ts b/libs/shared/src/lib/components/form-modal/form-modal.component.ts index 5eed7aed2b..f4eb6ff0de 100644 --- a/libs/shared/src/lib/components/form-modal/form-modal.component.ts +++ b/libs/shared/src/lib/components/form-modal/form-modal.component.ts @@ -130,6 +130,10 @@ export class FormModalComponent public get showSaveAsDraft(): boolean { return !this.data.recordId || !!this.lastDraftRecord; } + /** Auto-save trigger subject */ + private autoSaveSubject = new Subject(); + /** Whether auto-save is enabled */ + private autoSaveEnabled = false; /** * Display a form instance in a modal. @@ -160,6 +164,17 @@ export class FormModalComponent protected ngZone: NgZone ) { super(); + + this.autoSaveSubject + .pipe( + debounceTime(500), // Wait 500ms after last change before auto-saving + takeUntil(this.destroy$) + ) + .subscribe(() => { + if (this.autoSaveEnabled) { + this.performAutoSave(); + } + }); } /** @@ -300,11 +315,15 @@ export class FormModalComponent ); this.ValueChanged = false; + this.autoSaveEnabled = !this.data.recordId || !!this.lastDraftRecord; this.survey.onValueChanged.add(() => { // Allow user to save as draft this.disableSaveAsDraft = false; this.ValueChanged = true; + if (this.autoSaveEnabled) { + this.autoSaveSubject.next(); + } }); this.survey.onComplete.add(this.onComplete); if (this.storedMergedData) { @@ -478,8 +497,9 @@ export class FormModalComponent * * @param id record id. * @param survey current survey. + * @param closeModal whether to close the modal after successful update */ - public updateData(id: any, survey: any): void { + public updateData(id: any, survey: any, closeModal: boolean = true): void { this.apollo .mutate({ mutation: EDIT_RECORD, @@ -491,7 +511,7 @@ export class FormModalComponent }) .subscribe({ next: ({ errors, data }) => { - this.handleRecordMutationResponse({ data, errors }, 'editRecord'); + this.handleRecordMutationResponse({ data, errors }, 'editRecord', closeModal); }, error: (err) => { this.snackBar.openSnackBar(err.message, { error: true }); @@ -542,10 +562,12 @@ export class FormModalComponent * @param response.data response data * @param response.errors response errors * @param responseType response type + * @param closeModal whether to close the modal after successful update */ private handleRecordMutationResponse( response: { data: any; errors: any }, - responseType: 'editRecords' | 'editRecord' + responseType: 'editRecords' | 'editRecord', + closeModal: boolean = true ) { const { data, errors } = response; const type = @@ -569,10 +591,12 @@ export class FormModalComponent }) ); this.ValueChanged = false; - this.dialogRef.close({ - template: this.form?.id, - data: data[responseType], - } as any); + if (closeModal) { + this.dialogRef.close({ + template: this.form?.id, + data: data[responseType], + } as any); + } } } } @@ -753,6 +777,7 @@ export class FormModalComponent this.lastDraftRecord = id; this.disableSaveAsDraft = true; this.ValueChanged = false; + this.autoSaveEnabled = true; } /** @@ -844,5 +869,31 @@ export class FormModalComponent override ngOnDestroy(): void { super.ngOnDestroy(); this.survey?.dispose(); + this.autoSaveSubject.complete(); + } + + /** + * Performs auto-save for new records and existing drafts + */ + private performAutoSave(): void { + if (!this.survey || !this.form?.id) { + return; + } + + if (!this.ValueChanged) { + return; + } + + this.formHelpersService.saveAsDraft( + this.survey, + this.form.id, + this.lastDraftRecord, + (details: any) => { + if (!this.lastDraftRecord && details?.id) { + this.lastDraftRecord = details.id; + this.disableSaveAsDraft = false; + } + } + ); } } From 457d621620e3d423a8ed2fc432d88639210db1ee Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Tue, 23 Sep 2025 12:14:58 +0200 Subject: [PATCH 4/5] AB#120296 - Formatting fixes --- .../form-modal/form-modal.component.ts | 173 ++++++++++-------- .../src/lib/components/form/form.component.ts | 9 +- 2 files changed, 104 insertions(+), 78 deletions(-) diff --git a/libs/shared/src/lib/components/form-modal/form-modal.component.ts b/libs/shared/src/lib/components/form-modal/form-modal.component.ts index f4eb6ff0de..b6eb1d0672 100644 --- a/libs/shared/src/lib/components/form-modal/form-modal.component.ts +++ b/libs/shared/src/lib/components/form-modal/form-modal.component.ts @@ -21,7 +21,13 @@ import { import { Apollo } from 'apollo-angular'; import isNil from 'lodash/isNil'; import omitBy from 'lodash/omitBy'; -import { BehaviorSubject, firstValueFrom, takeUntil, debounceTime, Subject } from 'rxjs'; +import { + BehaviorSubject, + firstValueFrom, + takeUntil, + debounceTime, + Subject, +} from 'rxjs'; import { SurveyModule } from 'survey-angular-ui'; import { SurveyModel } from 'survey-core'; import { Form, FormQueryResponse } from '../../models/form.model'; @@ -120,16 +126,21 @@ export class FormModalComponent public pages$ = this.pages.asObservable(); /** Is multi edition of records enabled ( for grid actions ) */ protected isMultiEdition = false; + /** Temporary storage of files */ protected temporaryFilesStorage: any = {}; + /** Stored merged data */ private storedMergedData: any; + /** Track if form has been changed */ private ValueChanged = false; + /** Whether Save as Draft button should be visible */ public get showSaveAsDraft(): boolean { return !this.data.recordId || !!this.lastDraftRecord; } + /** Auto-save trigger subject */ private autoSaveSubject = new Subject(); /** Whether auto-save is enabled */ @@ -164,7 +175,7 @@ export class FormModalComponent protected ngZone: NgZone ) { super(); - + this.autoSaveSubject .pipe( debounceTime(500), // Wait 500ms after last change before auto-saving @@ -313,10 +324,8 @@ export class FormModalComponent this.form?.metadata, this.record ); - this.ValueChanged = false; this.autoSaveEnabled = !this.data.recordId || !!this.lastDraftRecord; - this.survey.onValueChanged.add(() => { // Allow user to save as draft this.disableSaveAsDraft = false; @@ -460,29 +469,29 @@ export class FormModalComponent this.ngZone.run(() => { this.dialogRef.close(); }); - } else { - if (this.lastDraftRecord) { - const callback = () => { - this.lastDraftRecord = undefined; - }; - this.formHelpersService.deleteRecordDraft( - this.lastDraftRecord, - callback - ); + } else { + if (this.lastDraftRecord) { + const callback = () => { + this.lastDraftRecord = undefined; + }; + this.formHelpersService.deleteRecordDraft( + this.lastDraftRecord, + callback + ); + } + this.ValueChanged = false; + this.ngZone.run(() => { + this.dialogRef.close({ + template: this.data.template, + data: data?.addRecord, + } as any); + this.snackBar.openSnackBar( + this.translate.instant( + 'components.form.display.submissionMessage' + ) + ); + }); } - this.ValueChanged = false; - this.ngZone.run(() => { - this.dialogRef.close({ - template: this.data.template, - data: data?.addRecord, - } as any); - this.snackBar.openSnackBar( - this.translate.instant( - 'components.form.display.submissionMessage' - ) - ); - }); - } }, error: (err) => { this.snackBar.openSnackBar(err.message, { error: true }); @@ -511,7 +520,11 @@ export class FormModalComponent }) .subscribe({ next: ({ errors, data }) => { - this.handleRecordMutationResponse({ data, errors }, 'editRecord', closeModal); + this.handleRecordMutationResponse( + { data, errors }, + 'editRecord', + closeModal + ); }, error: (err) => { this.snackBar.openSnackBar(err.message, { error: true }); @@ -790,50 +803,64 @@ export class FormModalComponent return; } - this.formHelpersService.uploadFiles( - this.survey, - this.temporaryFilesStorage, - this.form?.id as string - ).then(() => { - this.formHelpersService.createTemporaryRecords(this.survey).then(() => { - this.apollo - .mutate({ - mutation: EDIT_RECORD, - variables: { - id: this.isMultiEdition ? (this.data.recordId as string[])[0] : this.data.recordId, - data: this.survey.data, - template: this.data.template, - }, + this.formHelpersService + .uploadFiles( + this.survey, + this.temporaryFilesStorage, + this.form?.id as string + ) + .then(() => { + this.formHelpersService + .createTemporaryRecords(this.survey) + .then(() => { + this.apollo + .mutate({ + mutation: EDIT_RECORD, + variables: { + id: this.isMultiEdition + ? (this.data.recordId as string[])[0] + : this.data.recordId, + data: this.survey.data, + template: this.data.template, + }, + }) + .subscribe({ + next: ({ errors }) => { + if (errors) { + this.snackBar.openSnackBar( + this.translate.instant( + 'common.notifications.objectNotUpdated', + { + type: this.translate.instant('common.record.one'), + error: errors[0].message, + } + ), + { error: true } + ); + resolve(false); + } else { + this.snackBar.openSnackBar( + this.translate.instant( + 'common.notifications.objectUpdated', + { + type: this.translate.instant('common.record.one'), + value: '', + } + ) + ); + this.ValueChanged = false; + resolve(true); + } + }, + error: (err) => { + this.snackBar.openSnackBar(err.message, { error: true }); + resolve(false); + }, + }); }) - .subscribe({ - next: ({ errors, data }) => { - if (errors) { - this.snackBar.openSnackBar( - this.translate.instant('common.notifications.objectNotUpdated', { - type: this.translate.instant('common.record.one'), - error: errors[0].message, - }), - { error: true } - ); - resolve(false); - } else { - this.snackBar.openSnackBar( - this.translate.instant('common.notifications.objectUpdated', { - type: this.translate.instant('common.record.one'), - value: '', - }) - ); - this.ValueChanged = false; - resolve(true); - } - }, - error: (err) => { - this.snackBar.openSnackBar(err.message, { error: true }); - resolve(false); - }, - }); - }).catch(() => resolve(false)); - }).catch(() => resolve(false)); + .catch(() => resolve(false)); + }) + .catch(() => resolve(false)); }); } @@ -845,11 +872,9 @@ export class FormModalComponent if (this.data.recordId && this.ValueChanged) { const confirmMessage: ConfirmDialogData = { title: this.translate.instant('components.form.update.exit'), - content: this.translate.instant( - 'components.form.update.exitMessage' - ), + content: this.translate.instant('components.form.update.exitMessage'), confirmText: this.translate.instant('components.confirmModal.confirm'), - confirmVariant: 'primary' as 'primary', + confirmVariant: 'primary' as const, }; const dialogRef = this.confirmService.openConfirmModal(confirmMessage); const value = await firstValueFrom(dialogRef.closed as any); diff --git a/libs/shared/src/lib/components/form/form.component.ts b/libs/shared/src/lib/components/form/form.component.ts index 8bfa8f97b3..946568ad83 100644 --- a/libs/shared/src/lib/components/form/form.component.ts +++ b/libs/shared/src/lib/components/form/form.component.ts @@ -79,11 +79,13 @@ export class FormComponent public disableSaveAsDraft = false; /** Track if form has been changed */ private ValueChanged = false; + /** Whether Save as Draft button should be visible */ public get showSaveAsDraft(): boolean { // Show when creating a new record or when a draft is loaded return !this.record || !!this.lastDraftRecord; } + /** Timeout for reset survey */ private resetTimeoutListener!: NodeJS.Timeout; /** As we save the draft record in the db, the local storage is no longer used */ @@ -104,6 +106,7 @@ export class FormComponent * @param formBuilderService This is the service that will be used to build forms. * @param formHelpersService This is the service that will handle forms. * @param translate This is the service used to translate text + * @param confirmService This is the service that will be used to display confirm window. */ constructor( public dialog: Dialog, @@ -473,9 +476,7 @@ export class FormComponent if (this.record && this.ValueChanged) { const dialogRef = this.confirmService.openConfirmModal({ title: this.translate.instant('components.form.update.exit'), - content: this.translate.instant( - 'components.form.update.exitMessage' - ), + content: this.translate.instant('components.form.update.exitMessage'), confirmText: this.translate.instant('components.confirmModal.confirm'), confirmVariant: 'primary', }); @@ -485,7 +486,7 @@ export class FormComponent try { await this.onComplete(); this.ValueChanged = false; - + return true; } catch (error) { // If save fails, don't proceed with loading draft From 4e86fe75b7f8d669a691f87690e16fe6f1b13b3d Mon Sep 17 00:00:00 2001 From: Jose Garcia Date: Tue, 30 Sep 2025 14:42:38 +0200 Subject: [PATCH 5/5] Drop the draftRecord model --- .../draft-record-list-modal.component.ts | 14 +- .../graphql/queries.ts | 8 +- .../form-modal/form-modal.component.ts | 120 +++++++++++------- .../form-modal/graphql/mutations.ts | 16 ++- .../lib/components/form/graphql/mutations.ts | 16 ++- libs/shared/src/lib/models/record.model.ts | 14 +- .../form-helper/form-helper.service.ts | 12 +- .../services/form-helper/graphql/mutations.ts | 16 ++- 8 files changed, 139 insertions(+), 77 deletions(-) diff --git a/libs/shared/src/lib/components/draft-record-list-modal/draft-record-list-modal.component.ts b/libs/shared/src/lib/components/draft-record-list-modal/draft-record-list-modal.component.ts index 2a010d99cb..1d4c6252a8 100644 --- a/libs/shared/src/lib/components/draft-record-list-modal/draft-record-list-modal.component.ts +++ b/libs/shared/src/lib/components/draft-record-list-modal/draft-record-list-modal.component.ts @@ -10,8 +10,8 @@ import { Dialog } from '@angular/cdk/dialog'; import { Apollo } from 'apollo-angular'; import { DraftRecordsQueryResponse, - DraftRecord, -} from '../../models/draft-record.model'; + Record as RecordModel, +} from '../../models/record.model'; import { TableModule, DialogModule, @@ -48,7 +48,7 @@ interface DialogData { }) export class DraftRecordListModalComponent implements OnInit { /** Array of available draft records */ - public draftRecords: Array = new Array(); + public draftRecords: Array = new Array(); /** Displayed table columns */ public displayedColumns = ['createdAt', 'actions']; /** Displayed skeleton table columns */ @@ -103,7 +103,7 @@ export class DraftRecordListModalComponent implements OnInit { .pipe() .subscribe(({ data }) => { this.form = data.form; - this.draftRecords = data.draftRecords; + this.draftRecords = data.records; this.loading = false; }); } @@ -113,7 +113,7 @@ export class DraftRecordListModalComponent implements OnInit { * * @param element draft record selected */ - async onPreview(element: DraftRecord) { + async onPreview(element: RecordModel) { const { DraftRecordModalComponent } = await import( '../draft-record-modal/draft-record-modal.component' ); @@ -130,7 +130,7 @@ export class DraftRecordListModalComponent implements OnInit { * * @param element Draft record to delete */ - onDelete(element: DraftRecord) { + onDelete(element: RecordModel) { const dialogRef = this.confirmService.openConfirmModal({ title: this.translate.instant( 'components.form.draftRecords.confirmModal.delete' @@ -160,7 +160,7 @@ export class DraftRecordListModalComponent implements OnInit { * * @param element Draft record to be returned to form component */ - onClose(element: DraftRecord): void { + onClose(element: RecordModel): void { const confirmDialogRef = this.confirmService.openConfirmModal({ title: this.translate.instant( 'components.form.draftRecords.confirmModal.load' diff --git a/libs/shared/src/lib/components/draft-record-list-modal/graphql/queries.ts b/libs/shared/src/lib/components/draft-record-list-modal/graphql/queries.ts index 7638caa351..0eb61ca7f9 100644 --- a/libs/shared/src/lib/components/draft-record-list-modal/graphql/queries.ts +++ b/libs/shared/src/lib/components/draft-record-list-modal/graphql/queries.ts @@ -3,10 +3,16 @@ import { gql } from 'apollo-angular'; /** Graphql request for getting draft records */ export const GET_DRAFT_RECORDS = gql` query GetDraftRecords($form: ID!) { - draftRecords(form: $form) { + records(draft: true) { id + incrementalId + draft createdAt data + form { + id + name + } } form(id: $form) { id diff --git a/libs/shared/src/lib/components/form-modal/form-modal.component.ts b/libs/shared/src/lib/components/form-modal/form-modal.component.ts index b6eb1d0672..58b93b0de2 100644 --- a/libs/shared/src/lib/components/form-modal/form-modal.component.ts +++ b/libs/shared/src/lib/components/form-modal/form-modal.component.ts @@ -452,51 +452,83 @@ export class FormModalComponent this.updateData(this.data.recordId, survey); } } else { - this.apollo - .mutate({ - mutation: ADD_RECORD, - variables: { - form: this.data.template, - data: survey.data, - }, - }) - .subscribe({ - next: ({ errors, data }) => { - if (errors) { - this.snackBar.openSnackBar(`Error. ${errors[0].message}`, { - error: true, - }); - this.ngZone.run(() => { - this.dialogRef.close(); - }); - } else { - if (this.lastDraftRecord) { - const callback = () => { - this.lastDraftRecord = undefined; - }; - this.formHelpersService.deleteRecordDraft( - this.lastDraftRecord, - callback - ); + if (this.lastDraftRecord) { + this.apollo + .mutate({ + mutation: EDIT_RECORD, + variables: { + id: this.lastDraftRecord, + data: survey.data, + updateDraftStatus: false, + }, + }) + .subscribe({ + next: ({ errors, data }) => { + if (errors) { + this.snackBar.openSnackBar(`Error. ${errors[0].message}`, { + error: true, + }); + this.ngZone.run(() => { + this.dialogRef.close(); + }); + } else { + this.lastDraftRecord = undefined; + this.ValueChanged = false; + this.ngZone.run(() => { + this.dialogRef.close({ + template: this.data.template, + data: data?.editRecord, + } as any); + this.snackBar.openSnackBar( + this.translate.instant( + 'components.form.display.submissionMessage' + ) + ); + }); } - this.ValueChanged = false; - this.ngZone.run(() => { - this.dialogRef.close({ - template: this.data.template, - data: data?.addRecord, - } as any); - this.snackBar.openSnackBar( - this.translate.instant( - 'components.form.display.submissionMessage' - ) - ); - }); - } - }, - error: (err) => { - this.snackBar.openSnackBar(err.message, { error: true }); - }, - }); + }, + error: (err) => { + this.snackBar.openSnackBar(err.message, { error: true }); + }, + }); + } else { + this.apollo + .mutate({ + mutation: ADD_RECORD, + variables: { + form: this.data.template, + data: survey.data, + }, + }) + .subscribe({ + next: ({ errors, data }) => { + if (errors) { + this.snackBar.openSnackBar(`Error. ${errors[0].message}`, { + error: true, + }); + this.ngZone.run(() => { + this.dialogRef.close(); + }); + } else { + this.ValueChanged = false; + this.ngZone.run(() => { + this.dialogRef.close({ + template: this.data.template, + data: data?.addRecord, + } as any); + this.snackBar.openSnackBar( + this.translate.instant( + 'components.form.display.submissionMessage' + ) + ); + }); + } + }, + error: (err) => { + this.snackBar.openSnackBar(err.message, { error: true }); + }, + }); + } } survey.showCompletedPage = true; } diff --git a/libs/shared/src/lib/components/form-modal/graphql/mutations.ts b/libs/shared/src/lib/components/form-modal/graphql/mutations.ts index c708115d80..6df59ce3b9 100644 --- a/libs/shared/src/lib/components/form-modal/graphql/mutations.ts +++ b/libs/shared/src/lib/components/form-modal/graphql/mutations.ts @@ -10,6 +10,8 @@ export const EDIT_RECORD = gql` $template: ID $display: Boolean $lang: String + $draft: Boolean + $updateDraftStatus: Boolean ) { editRecord( id: $id @@ -17,9 +19,12 @@ export const EDIT_RECORD = gql` version: $version template: $template lang: $lang + draft: $draft + updateDraftStatus: $updateDraftStatus ) { id incrementalId + draft data(display: $display) createdAt modifiedAt @@ -37,9 +42,16 @@ export const EDIT_RECORD = gql` // === ADD RECORD === /** Graphql request for adding a new record to a form */ export const ADD_RECORD = gql` - mutation addRecord($form: ID!, $data: JSON!, $display: Boolean) { - addRecord(form: $form, data: $data) { + mutation addRecord( + $form: ID! + $data: JSON! + $display: Boolean + $draft: Boolean + ) { + addRecord(form: $form, data: $data, draft: $draft) { id + incrementalId + draft createdAt modifiedAt createdBy { diff --git a/libs/shared/src/lib/components/form/graphql/mutations.ts b/libs/shared/src/lib/components/form/graphql/mutations.ts index dbf765592c..b548197ca0 100644 --- a/libs/shared/src/lib/components/form/graphql/mutations.ts +++ b/libs/shared/src/lib/components/form/graphql/mutations.ts @@ -2,9 +2,16 @@ import { gql } from 'apollo-angular'; /** Graphql request for adding a new record to a form */ export const ADD_RECORD = gql` - mutation addRecord($form: ID!, $data: JSON!, $display: Boolean) { - addRecord(form: $form, data: $data) { + mutation addRecord( + $form: ID! + $data: JSON! + $display: Boolean + $draft: Boolean + ) { + addRecord(form: $form, data: $data, draft: $draft) { id + incrementalId + draft createdAt modifiedAt createdBy { @@ -34,6 +41,8 @@ export const EDIT_RECORD = gql` $template: ID $display: Boolean $lang: String + $draft: Boolean + $updateDraftStatus: Boolean ) { editRecord( id: $id @@ -41,9 +50,12 @@ export const EDIT_RECORD = gql` version: $version template: $template lang: $lang + draft: $draft + updateDraftStatus: $updateDraftStatus ) { id incrementalId + draft data(display: $display) createdAt modifiedAt diff --git a/libs/shared/src/lib/models/record.model.ts b/libs/shared/src/lib/models/record.model.ts index 67d9a3f96f..7e7e94e9f6 100644 --- a/libs/shared/src/lib/models/record.model.ts +++ b/libs/shared/src/lib/models/record.model.ts @@ -1,4 +1,4 @@ -import { DraftRecord } from './draft-record.model'; +// import { DraftRecord } from './draft-record.model'; import { Form } from './form.model'; import { Resource } from './resource.model'; import { User } from './user.model'; @@ -18,6 +18,7 @@ export interface Record { createdAt?: Date; modifiedAt?: Date; deleted?: boolean; + draft?: boolean; data?: any; form?: Form; resource?: Resource; @@ -39,9 +40,9 @@ export interface AddRecordMutationResponse { addRecord: Record; } -/** Model for add draft record graphql mutation response */ -export interface AddDraftRecordMutationResponse { - addDraftRecord: Record; +/** Model for draft records graphql query response */ +export interface DraftRecordsQueryResponse { + records: Record[]; } /** Model for edit record graphql mutation response */ @@ -49,11 +50,6 @@ export interface EditRecordMutationResponse { editRecord: Record; } -/** Model for edit draft record graphql mutation response */ -export interface EditDraftRecordMutationResponse { - editDraftRecord: DraftRecord; -} - /** Model for delete record graphql mutation response */ export interface DeleteRecordMutationResponse { deleteRecord: Record; diff --git a/libs/shared/src/lib/services/form-helper/form-helper.service.ts b/libs/shared/src/lib/services/form-helper/form-helper.service.ts index 83e5496517..0ebcb68103 100644 --- a/libs/shared/src/lib/services/form-helper/form-helper.service.ts +++ b/libs/shared/src/lib/services/form-helper/form-helper.service.ts @@ -9,9 +9,9 @@ import { firstValueFrom } from 'rxjs'; import { PageModel, SurveyModel } from 'survey-core'; import { ADD_RECORD } from '../../components/form/graphql/mutations'; import { - AddDraftRecordMutationResponse, AddRecordMutationResponse, - EditDraftRecordMutationResponse, + EditRecordMutationResponse, + Record as RecordModel, } from '../../models/record.model'; import { Question } from '../../survey/types'; import { AuthService } from '../auth/auth.service'; @@ -573,7 +573,7 @@ export class FormHelpersService { // Check if a draft has already been loaded if (!draftId) { // Add a new draft record to the database - const mutation = this.apollo.mutate({ + const mutation = this.apollo.mutate({ mutation: ADD_DRAFT_RECORD, variables: { form: formId, @@ -599,7 +599,7 @@ export class FormHelpersService { // Callback to emit save but stay in record addition mode if (callback) { callback({ - id: data?.addDraftRecord.id, + id: data?.addRecord.id, save: { completed: false, hideNewRecord: true, @@ -613,7 +613,7 @@ export class FormHelpersService { }); } else { // Edit last added draft record in the database - const mutation = this.apollo.mutate({ + const mutation = this.apollo.mutate({ mutation: EDIT_DRAFT_RECORD, variables: { id: draftId, @@ -655,7 +655,7 @@ export class FormHelpersService { */ public deleteRecordDraft(draftId: string, callback?: any): void { this.apollo - .mutate({ + .mutate<{ deleteRecord: RecordModel }>({ mutation: DELETE_DRAFT_RECORD, variables: { id: draftId, diff --git a/libs/shared/src/lib/services/form-helper/graphql/mutations.ts b/libs/shared/src/lib/services/form-helper/graphql/mutations.ts index dc2252ec26..30e54c0fbc 100644 --- a/libs/shared/src/lib/services/form-helper/graphql/mutations.ts +++ b/libs/shared/src/lib/services/form-helper/graphql/mutations.ts @@ -2,9 +2,11 @@ import { gql } from 'apollo-angular'; /** Graphql request for adding a new draft record to a form */ export const ADD_DRAFT_RECORD = gql` - mutation addDraftRecord($form: ID!, $data: JSON!, $display: Boolean) { - addDraftRecord(form: $form, data: $data) { + mutation addRecord($form: ID!, $data: JSON!, $display: Boolean) { + addRecord(form: $form, data: $data, draft: true) { id + incrementalId + draft createdAt modifiedAt createdBy { @@ -27,9 +29,11 @@ export const ADD_DRAFT_RECORD = gql` /** Graphql request for editing a draft record by its id */ export const EDIT_DRAFT_RECORD = gql` - mutation editDraftRecord($id: ID!, $data: JSON) { - editDraftRecord(id: $id, data: $data) { + mutation editRecord($id: ID!, $data: JSON) { + editRecord(id: $id, data: $data) { id + incrementalId + draft data createdAt createdBy { @@ -41,8 +45,8 @@ export const EDIT_DRAFT_RECORD = gql` /** Delete draft record gql mutation definition */ export const DELETE_DRAFT_RECORD = gql` - mutation deleteDraftRecord($id: ID!) { - deleteDraftRecord(id: $id) { + mutation deleteRecord($id: ID!) { + deleteRecord(id: $id) { id } }