Skip to content

Commit

Permalink
NAS-125917 / 25.04 / Ask user when they try to close the form after m…
Browse files Browse the repository at this point in the history
…aking edits (#10844)

* NAS-125917: Ask user when they try to close the form after making edits

* NAS-125917: PR Update

* NAS-125917: PR Update

* NAS-125917: PR Update
  • Loading branch information
AlexKarpov98 authored Oct 15, 2024
1 parent 751868c commit d9ab633
Show file tree
Hide file tree
Showing 94 changed files with 329 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import {
createHostFactory, SpectatorHost, mockProvider,
} from '@ngneat/spectator/jest';
import { of, Subject } from 'rxjs';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { IxSlideInRef } from 'app/modules/forms/ix-forms/components/ix-slide-in/ix-slide-in-ref';
import { WarnAboutUnsavedChangesDirective } from './warn-about-unsaved-changes.directive';

describe('WarnAboutUnsavedChangesDirective', () => {
let spectator: SpectatorHost<WarnAboutUnsavedChangesDirective<unknown>>;

const createHost = createHostFactory({
component: WarnAboutUnsavedChangesDirective,
imports: [ReactiveFormsModule],
providers: [
mockProvider(DialogService, {
confirm: jest.fn(() => of(true)),
}),
{
provide: IxSlideInRef,
useFactory: () => ({
close: jest.fn(),
slideInClosed$: new Subject<void>(),
}),
},
],
});

beforeEach(() => {
spectator = createHost(`
<form [formGroup]="form" warnAboutUnsavedChanges></form>
`, {
hostProps: {
form: new FormGroup({}),
},
});
});

it('should set formChanged to true when form value changes', () => {
spectator.component.formGroup.markAsPristine();

spectator.component.formGroup.valueChanges.subscribe(() => {
expect(spectator.component.formChanged).toBe(true);
});
});

it('should emit close event if there are no unsaved changes', () => {
spectator.component.formGroup.markAsPristine();

spectator.detectChanges();

spectator.component.closeWithConfirmation().subscribe((shouldClose) => {
expect(shouldClose).toBe(true);
});
});

it('should call confirmation dialog if there are unsaved changes', () => {
const dialogService = spectator.inject(DialogService);

spectator.component.closeWithConfirmation().subscribe(() => {
expect(dialogService.confirm).toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
Directive, Input, OnInit,
} from '@angular/core';
import { FormGroup } from '@angular/forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateService } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { IxSlideInRef } from 'app/modules/forms/ix-forms/components/ix-slide-in/ix-slide-in-ref';

@UntilDestroy()
@Directive({
selector: '[warnAboutUnsavedChanges]',
standalone: true,
})
export class WarnAboutUnsavedChangesDirective<T> implements OnInit {
@Input() formGroup: FormGroup;

formChanged = false;

constructor(
private translate: TranslateService,
private dialogService: DialogService,
private slideInRef: IxSlideInRef<T>,
) {}

ngOnInit(): void {
this.trackFormChanges();
this.overrideSlideInClose();
}

closeWithConfirmation(response?: T): Observable<boolean> {
if (!this.formChanged) {
this.emitClose(response);
return of(true);
}

return this.showConfirmDialog().pipe(
switchMap((shouldClose) => {
if (shouldClose) {
this.formChanged = false;
this.emitClose(response);
}
return of(shouldClose);
}),
);
}

private trackFormChanges(): void {
this.formGroup.valueChanges
.pipe(
filter(() => !this.formGroup.pristine),
untilDestroyed(this),
)
.subscribe(() => {
this.formChanged = true;
});
}

private overrideSlideInClose(): void {
this.slideInRef.close = (response?: T) => this.closeWithConfirmation(response)
.pipe(untilDestroyed(this))
.subscribe();
}

private showConfirmDialog(): Observable<boolean> {
return this.dialogService.confirm({
title: this.translate.instant('Unsaved Changes'),
message: this.translate.instant('You have unsaved changes. Are you sure you want to close?'),
cancelText: this.translate.instant('No'),
buttonText: this.translate.instant('Yes'),
buttonColor: 'red',
hideCheckbox: true,
});
}

private emitClose(response?: T): void {
this.slideInRef.slideInClosed$.next(response);
this.slideInRef.slideInClosed$.complete();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<ix-modal-header [requiredRoles]="requiredRoles" [title]="title" [loading]="isFormLoading"></ix-modal-header>

<form class="ix-form-container" [formGroup]="form" (submit)="onSubmit()">
<form class="ix-form-container" warnAboutUnsavedChanges [formGroup]="form" (submit)="onSubmit()">
<ix-fieldset [title]="'Identification' | translate">
<div class="columns">
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from 'rxjs/operators';
import { allCommands } from 'app/constants/all-commands.constant';
import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive';
import { WarnAboutUnsavedChangesDirective } from 'app/directives/warn-about-unsaved-changes/warn-about-unsaved-changes.directive';
import { Role } from 'app/enums/role.enum';
import { choicesToOptions } from 'app/helpers/operators/options.operators';
import { helptextUsers } from 'app/helptext/account/user-form';
Expand Down Expand Up @@ -88,6 +89,7 @@ const defaultHomePath = '/var/empty';
MatButton,
TestDirective,
TranslateModule,
WarnAboutUnsavedChangesDirective,
],
})
export class UserFormComponent implements OnInit {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<mat-card>
<mat-card-content>
<form class="ix-form-container" [formGroup]="form" (submit)="onSubmit()">
<form class="ix-form-container" warnAboutUnsavedChanges [formGroup]="form" (submit)="onSubmit()">
<ix-fieldset [title]="'General Options' | translate">
<ix-input
formControlName="destination"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive';
import { WarnAboutUnsavedChangesDirective } from 'app/directives/warn-about-unsaved-changes/warn-about-unsaved-changes.directive';
import { Role } from 'app/enums/role.enum';
import { helptextStaticRoutes } from 'app/helptext/network/static-routes/static-routes';
import { StaticRoute, UpdateStaticRoute } from 'app/interfaces/static-route.interface';
Expand Down Expand Up @@ -41,6 +42,7 @@ import { WebSocketService } from 'app/services/ws.service';
MatButton,
TestDirective,
TranslateModule,
WarnAboutUnsavedChangesDirective,
],
})
export class StaticRouteFormComponent implements OnInit {
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/af.json
Original file line number Diff line number Diff line change
Expand Up @@ -4577,6 +4577,7 @@
"Unlock with Key file": "",
"Unlocked": "",
"Unlocking Datasets": "",
"Unsaved Changes": "",
"Unselect All": "",
"Unset": "",
"Unset <i>Generate Encryption Key</i> to instead import a custom Hex key.": "",
Expand Down Expand Up @@ -4924,6 +4925,7 @@
"You can search both for local groups as well as groups from Active Directory. Press ENTER to separate entries.": "",
"You can search both for local users as well as users from Active Directory.Press ENTER to separate entries.": "",
"You have left the domain.": "",
"You have unsaved changes. Are you sure you want to close?": "",
"You may enter a specific IP address (e.g., 192.168.1.1) for individual access, or use an IP address with a subnet mask (e.g., 192.168.1.0/24) to define a range of addresses.": "",
"Your dashboard is currently empty!": "",
"ZFS": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -4577,6 +4577,7 @@
"Unlock with Key file": "",
"Unlocked": "",
"Unlocking Datasets": "",
"Unsaved Changes": "",
"Unselect All": "",
"Unset": "",
"Unset <i>Generate Encryption Key</i> to instead import a custom Hex key.": "",
Expand Down Expand Up @@ -4924,6 +4925,7 @@
"You can search both for local groups as well as groups from Active Directory. Press ENTER to separate entries.": "",
"You can search both for local users as well as users from Active Directory.Press ENTER to separate entries.": "",
"You have left the domain.": "",
"You have unsaved changes. Are you sure you want to close?": "",
"You may enter a specific IP address (e.g., 192.168.1.1) for individual access, or use an IP address with a subnet mask (e.g., 192.168.1.0/24) to define a range of addresses.": "",
"Your dashboard is currently empty!": "",
"ZFS": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/ast.json
Original file line number Diff line number Diff line change
Expand Up @@ -4577,6 +4577,7 @@
"Unlock with Key file": "",
"Unlocked": "",
"Unlocking Datasets": "",
"Unsaved Changes": "",
"Unselect All": "",
"Unset": "",
"Unset <i>Generate Encryption Key</i> to instead import a custom Hex key.": "",
Expand Down Expand Up @@ -4924,6 +4925,7 @@
"You can search both for local groups as well as groups from Active Directory. Press ENTER to separate entries.": "",
"You can search both for local users as well as users from Active Directory.Press ENTER to separate entries.": "",
"You have left the domain.": "",
"You have unsaved changes. Are you sure you want to close?": "",
"You may enter a specific IP address (e.g., 192.168.1.1) for individual access, or use an IP address with a subnet mask (e.g., 192.168.1.0/24) to define a range of addresses.": "",
"Your dashboard is currently empty!": "",
"ZFS": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/az.json
Original file line number Diff line number Diff line change
Expand Up @@ -4577,6 +4577,7 @@
"Unlock with Key file": "",
"Unlocked": "",
"Unlocking Datasets": "",
"Unsaved Changes": "",
"Unselect All": "",
"Unset": "",
"Unset <i>Generate Encryption Key</i> to instead import a custom Hex key.": "",
Expand Down Expand Up @@ -4924,6 +4925,7 @@
"You can search both for local groups as well as groups from Active Directory. Press ENTER to separate entries.": "",
"You can search both for local users as well as users from Active Directory.Press ENTER to separate entries.": "",
"You have left the domain.": "",
"You have unsaved changes. Are you sure you want to close?": "",
"You may enter a specific IP address (e.g., 192.168.1.1) for individual access, or use an IP address with a subnet mask (e.g., 192.168.1.0/24) to define a range of addresses.": "",
"Your dashboard is currently empty!": "",
"ZFS": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/be.json
Original file line number Diff line number Diff line change
Expand Up @@ -4577,6 +4577,7 @@
"Unlock with Key file": "",
"Unlocked": "",
"Unlocking Datasets": "",
"Unsaved Changes": "",
"Unselect All": "",
"Unset": "",
"Unset <i>Generate Encryption Key</i> to instead import a custom Hex key.": "",
Expand Down Expand Up @@ -4924,6 +4925,7 @@
"You can search both for local groups as well as groups from Active Directory. Press ENTER to separate entries.": "",
"You can search both for local users as well as users from Active Directory.Press ENTER to separate entries.": "",
"You have left the domain.": "",
"You have unsaved changes. Are you sure you want to close?": "",
"You may enter a specific IP address (e.g., 192.168.1.1) for individual access, or use an IP address with a subnet mask (e.g., 192.168.1.0/24) to define a range of addresses.": "",
"Your dashboard is currently empty!": "",
"ZFS": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/bg.json
Original file line number Diff line number Diff line change
Expand Up @@ -4577,6 +4577,7 @@
"Unlock with Key file": "",
"Unlocked": "",
"Unlocking Datasets": "",
"Unsaved Changes": "",
"Unselect All": "",
"Unset": "",
"Unset <i>Generate Encryption Key</i> to instead import a custom Hex key.": "",
Expand Down Expand Up @@ -4924,6 +4925,7 @@
"You can search both for local groups as well as groups from Active Directory. Press ENTER to separate entries.": "",
"You can search both for local users as well as users from Active Directory.Press ENTER to separate entries.": "",
"You have left the domain.": "",
"You have unsaved changes. Are you sure you want to close?": "",
"You may enter a specific IP address (e.g., 192.168.1.1) for individual access, or use an IP address with a subnet mask (e.g., 192.168.1.0/24) to define a range of addresses.": "",
"Your dashboard is currently empty!": "",
"ZFS": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/bn.json
Original file line number Diff line number Diff line change
Expand Up @@ -4577,6 +4577,7 @@
"Unlock with Key file": "",
"Unlocked": "",
"Unlocking Datasets": "",
"Unsaved Changes": "",
"Unselect All": "",
"Unset": "",
"Unset <i>Generate Encryption Key</i> to instead import a custom Hex key.": "",
Expand Down Expand Up @@ -4924,6 +4925,7 @@
"You can search both for local groups as well as groups from Active Directory. Press ENTER to separate entries.": "",
"You can search both for local users as well as users from Active Directory.Press ENTER to separate entries.": "",
"You have left the domain.": "",
"You have unsaved changes. Are you sure you want to close?": "",
"You may enter a specific IP address (e.g., 192.168.1.1) for individual access, or use an IP address with a subnet mask (e.g., 192.168.1.0/24) to define a range of addresses.": "",
"Your dashboard is currently empty!": "",
"ZFS": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/br.json
Original file line number Diff line number Diff line change
Expand Up @@ -4577,6 +4577,7 @@
"Unlock with Key file": "",
"Unlocked": "",
"Unlocking Datasets": "",
"Unsaved Changes": "",
"Unselect All": "",
"Unset": "",
"Unset <i>Generate Encryption Key</i> to instead import a custom Hex key.": "",
Expand Down Expand Up @@ -4924,6 +4925,7 @@
"You can search both for local groups as well as groups from Active Directory. Press ENTER to separate entries.": "",
"You can search both for local users as well as users from Active Directory.Press ENTER to separate entries.": "",
"You have left the domain.": "",
"You have unsaved changes. Are you sure you want to close?": "",
"You may enter a specific IP address (e.g., 192.168.1.1) for individual access, or use an IP address with a subnet mask (e.g., 192.168.1.0/24) to define a range of addresses.": "",
"Your dashboard is currently empty!": "",
"ZFS": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/bs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4577,6 +4577,7 @@
"Unlock with Key file": "",
"Unlocked": "",
"Unlocking Datasets": "",
"Unsaved Changes": "",
"Unselect All": "",
"Unset": "",
"Unset <i>Generate Encryption Key</i> to instead import a custom Hex key.": "",
Expand Down Expand Up @@ -4924,6 +4925,7 @@
"You can search both for local groups as well as groups from Active Directory. Press ENTER to separate entries.": "",
"You can search both for local users as well as users from Active Directory.Press ENTER to separate entries.": "",
"You have left the domain.": "",
"You have unsaved changes. Are you sure you want to close?": "",
"You may enter a specific IP address (e.g., 192.168.1.1) for individual access, or use an IP address with a subnet mask (e.g., 192.168.1.0/24) to define a range of addresses.": "",
"Your dashboard is currently empty!": "",
"ZFS": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -4577,6 +4577,7 @@
"Unlock with Key file": "",
"Unlocked": "",
"Unlocking Datasets": "",
"Unsaved Changes": "",
"Unselect All": "",
"Unset": "",
"Unset <i>Generate Encryption Key</i> to instead import a custom Hex key.": "",
Expand Down Expand Up @@ -4924,6 +4925,7 @@
"You can search both for local groups as well as groups from Active Directory. Press ENTER to separate entries.": "",
"You can search both for local users as well as users from Active Directory.Press ENTER to separate entries.": "",
"You have left the domain.": "",
"You have unsaved changes. Are you sure you want to close?": "",
"You may enter a specific IP address (e.g., 192.168.1.1) for individual access, or use an IP address with a subnet mask (e.g., 192.168.1.0/24) to define a range of addresses.": "",
"Your dashboard is currently empty!": "",
"ZFS": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -3904,6 +3904,7 @@
"Unlock with Key file": "",
"Unlocked": "",
"Unlocking Datasets": "",
"Unsaved Changes": "",
"Unselect All": "",
"Unset": "",
"Unset <i>Generate Encryption Key</i> to instead import a custom Hex key.": "",
Expand Down Expand Up @@ -4048,6 +4049,7 @@
"Xen: Extent block size 512b, TPC enabled, Xen compat mode enabled, SSD speed": "",
"You are using an insecure connection. Switch to HTTPS for secure access.": "",
"You can also vote for new features <a target=\"_blank\" href=\"https://forums.truenas.com/feature-requests\">on our forum.</a>": "",
"You have unsaved changes. Are you sure you want to close?": "",
"ZFS Errors": "",
"iXsystems does not audit or otherwise validate the contents of third-party applications catalogs. It is incumbent on the user to verify that the new catalog is from a trusted source and that the third-party properly audits its chart contents. Failure to exercise due diligence may expose the user and their data to some or all of the following:<br/> <ul>\n <li>Malicious software</li>\n <li>Broken services on TrueNAS host</li>\n <li>Service disruption on TrueNAS host</li>\n <li>Broken filesystem permissions on Host or within application</li>\n <li>Unexpected deletion of user data</li>\n <li>Unsafe service configuration in application</li>\n <li>Degradation of TrueNAS host performance and stability</li>\n </ul>": "",
"{n, plural, one {# CPU} other {# CPUs}}": "",
Expand Down
2 changes: 2 additions & 0 deletions src/assets/i18n/cy.json
Original file line number Diff line number Diff line change
Expand Up @@ -4577,6 +4577,7 @@
"Unlock with Key file": "",
"Unlocked": "",
"Unlocking Datasets": "",
"Unsaved Changes": "",
"Unselect All": "",
"Unset": "",
"Unset <i>Generate Encryption Key</i> to instead import a custom Hex key.": "",
Expand Down Expand Up @@ -4924,6 +4925,7 @@
"You can search both for local groups as well as groups from Active Directory. Press ENTER to separate entries.": "",
"You can search both for local users as well as users from Active Directory.Press ENTER to separate entries.": "",
"You have left the domain.": "",
"You have unsaved changes. Are you sure you want to close?": "",
"You may enter a specific IP address (e.g., 192.168.1.1) for individual access, or use an IP address with a subnet mask (e.g., 192.168.1.0/24) to define a range of addresses.": "",
"Your dashboard is currently empty!": "",
"ZFS": "",
Expand Down
Loading

0 comments on commit d9ab633

Please sign in to comment.