diff --git a/lib/core/src/lib/app-config/app-config.service.ts b/lib/core/src/lib/app-config/app-config.service.ts index 1efe6a50fa..1e5de6d1fb 100644 --- a/lib/core/src/lib/app-config/app-config.service.ts +++ b/lib/core/src/lib/app-config/app-config.service.ts @@ -47,7 +47,8 @@ export enum AppConfigValues { STORAGE_PREFIX = 'application.storagePrefix', NOTIFY_DURATION = 'notificationDefaultDuration', CONTENT_TICKET_STORAGE_LABEL = 'ticket-ECM', - PROCESS_TICKET_STORAGE_LABEL = 'ticket-BPM' + PROCESS_TICKET_STORAGE_LABEL = 'ticket-BPM', + UNSAVED_CHANGES_MODAL_HIDDEN = 'unsaved_changes__modal_hidden' } // eslint-disable-next-line no-shadow diff --git a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.html b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.html index 3de4be2a69..3d4a7d0363 100644 --- a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.html +++ b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.html @@ -1,29 +1,35 @@ -

- {{ 'CORE.DIALOG.UNSAVED_CHANGES.TITLE' | translate }} +

+ {{ dialogData.headerText | translate }}

- - {{ 'CORE.DIALOG.UNSAVED_CHANGES.DESCRIPTION' | translate }} + + {{ dialogData.descriptionText | translate }} +
+ {{ dialogData.checkboxText | translate }} +
- + diff --git a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.scss b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.scss index 043db20626..94b5cd89b6 100644 --- a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.scss +++ b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.scss @@ -1,33 +1,60 @@ -adf-unsaved-changes-dialog { - margin-top: -4px; - display: block; - +.adf-unsaved-changes-dialog { .adf-unsaved-changes-dialog { - &-title { + &-header { display: flex; align-items: center; justify-content: space-between; - font-size: 16px; - font-weight: bold; + font-size: 18px; + font-weight: 700; + margin-bottom: 16px; + height: 24px; + + &-close-button { + margin-right: -16px; + margin-bottom: 2px; + } + + &::before { + display: none; + } } - &-cancel-button { - background-color: var(--adf-secondary-button-background); - margin-right: 4px; + &-content { + padding: 0 8px 0 0; + overflow: unset; + color: var(--adf-secondary-modal-text-color); + + &-checkbox { + margin-top: 20px; + + label { + color: var(--adf-secondary-modal-text-color); + } + } } - &-discard-changes-button { - color: var(--theme-warn-color-default-contrast); - background-color: var(--adf-error-color); - min-width: 143px; - } + &-actions { + margin-top: 18px; + margin-bottom: 4px; + padding: 0; - &-cancel-button, - &-discard-changes-button { - padding: 4px 14px; - height: 32px; - display: flex; - align-items: center; + &-cancel-button { + background-color: var(--adf-secondary-button-background); + margin-right: 4px; + } + + &-discard-changes-button { + color: white; + background-color: var(--adf-error-color); + } + + &-cancel-button, + &-discard-changes-button { + padding: 4px 12px; + height: 32px; + display: flex; + align-items: center; + } } } } diff --git a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.spec.ts b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.spec.ts index 459c9a2bac..da49768f76 100644 --- a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.spec.ts +++ b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.spec.ts @@ -16,39 +16,66 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CoreTestingModule, UnsavedChangesDialogComponent } from '@alfresco/adf-core'; +import { CoreTestingModule, StorageService, UnsavedChangesDialogComponent } from '@alfresco/adf-core'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; -import { MatDialogClose } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialogClose } from '@angular/material/dialog'; +import { UnsavedChangesDialogData } from './unsaved-changes-dialog.model'; describe('UnsavedChangesDialog', () => { let fixture: ComponentFixture; + let storageServiceMock: any; + let savePreferenceCheckbox: DebugElement; + + const setupBeforeEach = (unsavedChangesDialogData?: UnsavedChangesDialogData) => { + storageServiceMock = { + getItem: jasmine.createSpy('getItem'), + setItem: jasmine.createSpy('setItem') + }; - beforeEach(() => { TestBed.configureTestingModule({ - imports: [CoreTestingModule] + imports: [CoreTestingModule], + providers: [ + { provide: StorageService, useValue: storageServiceMock }, + { + provide: MAT_DIALOG_DATA, + useValue: unsavedChangesDialogData ?? {} + } + ] }); + fixture = TestBed.createComponent(UnsavedChangesDialogComponent); fixture.detectChanges(); - }); + savePreferenceCheckbox = fixture.debugElement.query(By.css('[data-automation-id="adf-unsaved-changes-dialog-content-checkbox"]')); + }; - describe('Close icon button', () => { - let closeIconButton: DebugElement; + const getElements = (): { header: HTMLElement; content: HTMLElement; discardChangesButton: HTMLElement } => { + const header = fixture.nativeElement.querySelector('.adf-unsaved-changes-dialog-header'); + const content = fixture.nativeElement.querySelector('.adf-unsaved-changes-dialog-content'); + const discardChangesButton = fixture.nativeElement.querySelector('.adf-unsaved-changes-dialog-actions-discard-changes-button'); + return { header, content, discardChangesButton }; + }; + describe('when data is not present in dialog', () => { beforeEach(() => { - closeIconButton = fixture.debugElement.query(By.css('[data-automation-id="adf-unsaved-changes-dialog-close-button"]')); + setupBeforeEach(); }); - it('should have assigned dialog close button with false as result', () => { - expect(closeIconButton.injector.get(MatDialogClose).dialogResult).toBeFalse(); + it('should display correct text if there is no data object', () => { + const { header, content, discardChangesButton } = getElements(); + expect(header.textContent).toContain('CORE.DIALOG.UNSAVED_CHANGES.TITLE'); + expect(content.textContent).toContain('CORE.DIALOG.UNSAVED_CHANGES.DESCRIPTION'); + expect(discardChangesButton.textContent).toContain('CORE.DIALOG.UNSAVED_CHANGES.DISCARD_CHANGES_BUTTON'); }); - it('should have displayed correct icon', () => { - expect(closeIconButton.nativeElement.textContent).toBe('close'); + it('should have assigned dialog close button with true as result', () => { + expect( + fixture.debugElement + .query(By.css('[data-automation-id="adf-unsaved-changes-dialog-discard-changes-button"]')) + .injector.get(MatDialogClose).dialogResult + ).toBeTrue(); }); - }); - describe('Cancel button', () => { it('should have assigned dialog close button with false as result', () => { expect( fixture.debugElement.query(By.css('[data-automation-id="adf-unsaved-changes-dialog-cancel-button"]')).injector.get(MatDialogClose) @@ -57,13 +84,35 @@ describe('UnsavedChangesDialog', () => { }); }); - describe('Discard changes button', () => { - it('should have assigned dialog close button with true as result', () => { - expect( - fixture.debugElement - .query(By.css('[data-automation-id="adf-unsaved-changes-dialog-discard-changes-button"]')) - .injector.get(MatDialogClose).dialogResult - ).toBeTrue(); + describe('when data is present in dialog', () => { + beforeEach(() => { + setupBeforeEach({ + headerText: 'headerText', + descriptionText: 'descriptionText', + confirmButtonText: 'confirmButtonText', + checkboxText: 'checkboxText' + }); + fixture.detectChanges(); + }); + + it('should display correct text if there is data object', () => { + const { header, content, discardChangesButton } = getElements(); + + expect(header.textContent).toContain('headerText'); + expect(content.textContent).toContain('descriptionText checkboxText'); + expect(discardChangesButton.textContent).toContain('confirmButtonText'); + }); + + it('should update storageService to true when checkbox is checked', () => { + const event = { checked: true }; + savePreferenceCheckbox.triggerEventHandler('change', event); + expect(storageServiceMock.setItem).toHaveBeenCalledWith(UnsavedChangesDialogComponent.UNSAVED_CHANGES_MODAL_HIDDEN, 'true'); + }); + + it('should update storageService to false when checkbox is unchecked', () => { + const event = { checked: false }; + savePreferenceCheckbox.triggerEventHandler('change', event); + expect(storageServiceMock.setItem).toHaveBeenCalledWith(UnsavedChangesDialogComponent.UNSAVED_CHANGES_MODAL_HIDDEN, 'false'); }); }); }); diff --git a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.ts b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.ts index e178f06827..46bf1064c5 100644 --- a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.ts +++ b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.component.ts @@ -15,22 +15,57 @@ * limitations under the License. */ -import { Component, ViewEncapsulation } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { MatDialogModule } from '@angular/material/dialog'; +import { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { UnsavedChangesDialogData } from './unsaved-changes-dialog.model'; +import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox'; +import { ReactiveFormsModule } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; +import { CommonModule } from '@angular/common'; +import { StorageService } from '../../common'; +import { AppConfigValues } from '../../app-config'; /** * Dialog which informs about unsaved changes. Allows discard them and proceed or close dialog and stop proceeding. + * Can be customized with data object - UnsavedChangesDialogData. + * If data.checkboxText is provided, checkbox will be displayed with the checkbox description. + * If data.confirmButtonText is provided, it will be displayed on the confirm button. + * If data.headerText is provided, it will be displayed as the header. + * If data.descriptionText is provided, it will be displayed as dialog content. */ @Component({ + standalone: true, selector: 'adf-unsaved-changes-dialog', standalone: true, imports: [CommonModule, MatDialogModule, TranslateModule, MatButtonModule, MatIconModule], encapsulation: ViewEncapsulation.None, templateUrl: './unsaved-changes-dialog.component.html', - styleUrls: ['./unsaved-changes-dialog.component.scss'] + styleUrls: ['./unsaved-changes-dialog.component.scss'], + host: { class: 'adf-unsaved-changes-dialog' }, + imports: [MatDialogModule, TranslateModule, MatButtonModule, MatIconModule, CommonModule, MatCheckboxModule, ReactiveFormsModule] }) -export class UnsavedChangesDialogComponent {} +export class UnsavedChangesDialogComponent implements OnInit { + dialogData: UnsavedChangesDialogData; + + constructor(@Inject(MAT_DIALOG_DATA) public data: UnsavedChangesDialogData, private storageService: StorageService) {} + + ngOnInit() { + this.dialogData = { + headerText: this.data?.headerText ?? 'CORE.DIALOG.UNSAVED_CHANGES.TITLE', + descriptionText: this.data?.descriptionText ?? 'CORE.DIALOG.UNSAVED_CHANGES.DESCRIPTION', + confirmButtonText: this.data?.confirmButtonText ?? 'CORE.DIALOG.UNSAVED_CHANGES.DISCARD_CHANGES_BUTTON', + checkboxText: this.data?.checkboxText ?? '' + }; + } + + /** + * Sets 'unsaved_ai_changes__modal_visible' checked state (true or false string) as new item in local storage. + * + * @param savePreferences - MatCheckboxChange object with information about checkbox state. + */ + onToggleCheckboxPreferences(savePreferences: MatCheckboxChange) { + this.storageService.setItem(AppConfigValues.UNSAVED_CHANGES_MODAL_HIDDEN, savePreferences.checked.toString()); + } +} diff --git a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.model.ts b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.model.ts new file mode 100644 index 0000000000..fb753c3229 --- /dev/null +++ b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes-dialog.model.ts @@ -0,0 +1,23 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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. + */ + +export interface UnsavedChangesDialogData { + checkboxText?: string; + confirmButtonText?: string; + descriptionText?: string; + headerText?: string; +} diff --git a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.ts b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.ts index d18353b3c2..3106edbf39 100644 --- a/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.ts +++ b/lib/core/src/lib/dialogs/unsaved-changes-dialog/unsaved-changes.guard.ts @@ -21,6 +21,7 @@ import { Observable } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; import { UnsavedChangesDialogComponent } from './unsaved-changes-dialog.component'; import { tap } from 'rxjs/operators'; +import { UnsavedChangesDialogData } from './unsaved-changes-dialog.model'; /** * Guard responsible for protecting leaving page with unsaved changes. @@ -30,6 +31,7 @@ import { tap } from 'rxjs/operators'; }) export class UnsavedChangesGuard implements CanDeactivate { unsaved = false; + data: UnsavedChangesDialogData; constructor(private dialog: MatDialog) {} @@ -39,9 +41,14 @@ export class UnsavedChangesGuard implements CanDeactivate { * @returns boolean | Observable true when there is no unsaved changes or changes can be discarded, false otherwise. */ canDeactivate(): boolean | Observable { - return this.unsaved ? - this.dialog.open(UnsavedChangesDialogComponent, { - maxWidth: 346 - }).afterClosed().pipe(tap((confirmed) => this.unsaved = !confirmed)) : true; + return this.unsaved + ? this.dialog + .open(UnsavedChangesDialogComponent, { + maxWidth: 346, + data: this.data + }) + .afterClosed() + .pipe(tap((confirmed) => (this.unsaved = !confirmed))) + : true; } } diff --git a/lib/core/src/lib/styles/_components-variables.scss b/lib/core/src/lib/styles/_components-variables.scss index 45aa397f66..4df3beb1ea 100644 --- a/lib/core/src/lib/styles/_components-variables.scss +++ b/lib/core/src/lib/styles/_components-variables.scss @@ -88,6 +88,7 @@ --adf-header-icon-button-disabled-color: $adf-ref-header-icon-color, --adf-error-color: $adf-error-color, --adf-secondary-button-background: $adf-secondary-button-background, + --adf-secondary-modal-text-color: $adf-secondary-modal-text-color, --adf-display-external-property-widget-preview-selection-color: mat.get-color-from-palette($foreground, secondary-text) ); diff --git a/lib/core/src/lib/styles/_reference-variables.scss b/lib/core/src/lib/styles/_reference-variables.scss index 8795d28591..dc662e7780 100644 --- a/lib/core/src/lib/styles/_reference-variables.scss +++ b/lib/core/src/lib/styles/_reference-variables.scss @@ -27,3 +27,4 @@ $adf-ref-header-icon-color: inherit; $adf-ref-header-icon-border-radius: 50%; $adf-error-color: #ba1b1b; $adf-secondary-button-background: #2121210d; +$adf-secondary-modal-text-color: #212121;