[ACS-8201] Knowledge Retrieval - getting AI response for one or more selected files (#10229)

* [ACS-8202] basic flow getting ai response for one or more selected files (#9944)

* ACS-8202 Getting list of agents

* ACS-8202 Mocked agents, used base api from hxi connector

* ACS-8202 Search Ai service

* ACS-8202 Small correction and mocked data

* ACS-8202 Renamed variable

* ACS-8202 Added documentation

* ACS-8202 Addressed PR comments

* ACS-8202 Type change

* ACS-8202 Reverted unwatend change

* ACS-8202 Reverted unwanted change

* ACS-8201 Small correction after rebasing with Angular 15

* [ACS-8398] Unit tests for agents and search ai  (#9974)

* ACS-8398 Unit tests for search ai api and agents api

* ACS-8398 Unit tests for getAnswer function from SearchAiApi, corrections for unit tests for SearchAiApi and AgentsApi

* ACS-8398 Unit tests for SearchAiService and AgentService

* [ACS-8210] Agent basic details popup (#9956)

* [ACS-8573] Allow user to ask question without file selection

* [ACS-8312] Display warning about losing response (#10059)

* [ACS-8312] Display warning about losing response

* [ACS-8312] Display warning about losing response - fixes

* [ACS-8432] Sending all file types to HX instead of only the text file types (#10087)

* ACS-8201 Fixed issues after rebase

* [ACS-8588] Navigation is triggered twice when leaving Knowledge Retrieval page (#10132)

* [ACS-8588] Navigation is triggered twice when leaving Knowledge Retrieval page

* [ACS-8588] Navigation is triggered twice when leaving Knowledge Retrieval page - review fixes

* [ACS-8588] Navigation is triggered twice when leaving Knowledge Retrieval page - review fixes 2

* [ACS-8399] Integrate all changes with backend (#10163)

* Answers endpoint fix (#10176)

* [ACS-8664] generic question redirection to hx insight (#10174)

* ACS-8664 Loading HX insight url

* ACS-8664 Added documentation for loading config of Knowledge Retrieval

* ACS-8664 Unit tests

* ACS-8664 Fixed unit tests

* ACS-8664 Fixed unit tests after rebase

* ACS-8664 Addressed comment

* ACS-8201 Fixed issues after rebase

* [ACS-8695] Getting Agent avatar (#10189)

* [ACS-8695] Getting Agent avatar

* [ACS-8695] Getting Agent avatar - on image load error

* [ACS-8695] Getting Agent avatar - removed getAgentAvatar call (#10209)

* [ACS-8201] Review fixes

---------

Co-authored-by: AleksanderSklorz <115619721+AleksanderSklorz@users.noreply.github.com>
Co-authored-by: Aleksander Sklorz <Aleksander.Sklorz@hyland.com>
This commit is contained in:
jacekpluta
2024-09-19 12:42:50 +02:00
committed by GitHub
parent 6a40e2a25e
commit 797b800bd6
51 changed files with 1848 additions and 71 deletions

View File

@@ -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

View File

@@ -1,5 +1,5 @@
<div class="adf-avatar">
<img *ngIf="src; else initialsTemplate" class="adf-avatar__image" [src]="src" [alt]="initials" [title]="tooltip" />
<img (error)="src = ''" *ngIf="src; else initialsTemplate" class="adf-avatar__image" [src]="src" [alt]="initials" [title]="tooltip" />
</div>
<ng-template #initialsTemplate>

View File

@@ -32,6 +32,8 @@ describe('AvatarComponent', () => {
fixture.detectChanges();
});
const getAvatarImageElement = (): HTMLImageElement => fixture.nativeElement.querySelector('.adf-avatar__image');
it('should create', () => {
expect(component).toBeTruthy();
});
@@ -46,8 +48,7 @@ describe('AvatarComponent', () => {
it('should display image when src is provided', () => {
component.src = 'path/to/image.jpg';
fixture.detectChanges();
const imgElement: HTMLImageElement = fixture.nativeElement.querySelector('.adf-avatar__image');
expect(imgElement.src).toContain(component.src);
expect(getAvatarImageElement().src).toContain(component.src);
});
it('should use default initials when not provided', () => {
@@ -67,7 +68,7 @@ describe('AvatarComponent', () => {
component.size = '48px';
fixture.detectChanges();
const style = getComputedStyle(fixture.nativeElement.querySelector('.adf-avatar__image'));
const style = getComputedStyle(getAvatarImageElement());
expect(style.width).toBe('48px');
expect(style.height).toBe('48px');
});
@@ -76,14 +77,22 @@ describe('AvatarComponent', () => {
component.cursor = 'pointer';
fixture.detectChanges();
const style = getComputedStyle(fixture.nativeElement.querySelector('.adf-avatar__image'));
const style = getComputedStyle(getAvatarImageElement());
expect(style.cursor).toBe('pointer');
});
it('should display tooltip when provided', () => {
component.tooltip = 'User Tooltip';
fixture.detectChanges();
const avatarElement: HTMLElement = fixture.nativeElement.querySelector('.adf-avatar__image');
expect(avatarElement.getAttribute('title')).toBe('User Tooltip');
expect(getAvatarImageElement().getAttribute('title')).toBe('User Tooltip');
});
it('should call onImageError when the image fails to load', () => {
component.src = 'path/to/image.jpg';
fixture.detectChanges();
getAvatarImageElement().dispatchEvent(new Event('error'));
expect(component.src).toEqual('');
});
});

View File

@@ -1,29 +1,35 @@
<h1 mat-dialog-title class="adf-unsaved-changes-dialog-title">
{{ 'CORE.DIALOG.UNSAVED_CHANGES.TITLE' | translate }}
<h1 mat-dialog-title class="adf-unsaved-changes-dialog-header">
{{ dialogData.headerText | translate }}
<button
data-automation-id="adf-unsaved-changes-dialog-close-button"
class="adf-unsaved-changes-dialog-header-close-button"
mat-icon-button
[title]="'CLOSE' | translate"
[mat-dialog-close]="false">
<mat-icon>close</mat-icon>
</button>
</h1>
<mat-dialog-content>
{{ 'CORE.DIALOG.UNSAVED_CHANGES.DESCRIPTION' | translate }}
<mat-dialog-content class="adf-unsaved-changes-dialog-content">
{{ dialogData.descriptionText | translate }}
<div class="adf-unsaved-changes-dialog-content-checkbox" *ngIf="dialogData.checkboxText.length">
<mat-checkbox data-automation-id="adf-unsaved-changes-dialog-content-checkbox"
(change)="onToggleCheckboxPreferences($event)"
>{{ dialogData.checkboxText | translate }}</mat-checkbox>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<mat-dialog-actions align="end" class="adf-unsaved-changes-dialog-actions">
<button
data-automation-id="adf-unsaved-changes-dialog-cancel-button"
mat-button
[mat-dialog-close]="false"
class="adf-unsaved-changes-dialog-cancel-button">
class="adf-unsaved-changes-dialog-actions-cancel-button">
{{ 'CANCEL' | translate | titlecase }}
</button>
<button
data-automation-id="adf-unsaved-changes-dialog-discard-changes-button"
mat-button
[mat-dialog-close]="true"
class="adf-unsaved-changes-dialog-discard-changes-button">
{{ 'CORE.DIALOG.UNSAVED_CHANGES.DISCARD_CHANGES_BUTTON' | translate }}
class="adf-unsaved-changes-dialog-actions-discard-changes-button">
{{ dialogData.confirmButtonText | translate }}
</button>
</mat-dialog-actions>

View File

@@ -1,32 +1,61 @@
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-danger-button-background);
min-width: 143px;
}
&-actions {
margin-top: 11px;
margin-bottom: 1px;
padding: 0;
align-items: flex-end;
&-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;
}
}
}
}

View File

@@ -16,39 +16,61 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CoreTestingModule, UnsavedChangesDialogComponent } from '@alfresco/adf-core';
import { AppConfigValues, CoreTestingModule, UnsavedChangesDialogComponent, UserPreferencesService } 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<UnsavedChangesDialogComponent>;
let userPreferencesService: UserPreferencesService;
let savePreferenceCheckbox: DebugElement;
beforeEach(() => {
const setupBeforeEach = (unsavedChangesDialogData?: UnsavedChangesDialogData) => {
TestBed.configureTestingModule({
imports: [CoreTestingModule]
imports: [CoreTestingModule],
providers: [
{
provide: MAT_DIALOG_DATA,
useValue: unsavedChangesDialogData ?? {}
}
]
});
userPreferencesService = TestBed.inject(UserPreferencesService);
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 +79,38 @@ 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', () => {
let userPreferencesServiceSetSpy: jasmine.Spy<(property: string, value: any) => void>;
beforeEach(() => {
setupBeforeEach({
headerText: 'headerText',
descriptionText: 'descriptionText',
confirmButtonText: 'confirmButtonText',
checkboxText: 'checkboxText'
});
userPreferencesServiceSetSpy = spyOn(userPreferencesService, 'set');
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 call UserPreferences Service and update it to true when checkbox is checked', () => {
const event = { checked: true };
savePreferenceCheckbox.triggerEventHandler('change', event);
expect(userPreferencesServiceSetSpy).toHaveBeenCalledWith(AppConfigValues.UNSAVED_CHANGES_MODAL_HIDDEN, 'true');
});
it('should call UserPreferences Service and update it to false when checkbox is unchecked', () => {
const event = { checked: false };
savePreferenceCheckbox.triggerEventHandler('change', event);
expect(userPreferencesServiceSetSpy).toHaveBeenCalledWith(AppConfigValues.UNSAVED_CHANGES_MODAL_HIDDEN, 'false');
});
});
});

View File

@@ -15,22 +15,55 @@
* 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 { UserPreferencesService } 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({
selector: 'adf-unsaved-changes-dialog',
standalone: true,
imports: [CommonModule, MatDialogModule, TranslateModule, MatButtonModule, MatIconModule],
selector: 'adf-unsaved-changes-dialog',
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 userPreferencesService: UserPreferencesService) {}
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_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.userPreferencesService.set(AppConfigValues.UNSAVED_CHANGES_MODAL_HIDDEN, savePreferences.checked.toString());
}
}

View File

@@ -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;
}

View File

@@ -91,5 +91,15 @@ describe('UnsavedChangesGuard', () => {
});
afterClosed$.next(true);
});
it('should return false if afterClosed subject value was undefined', (done) => {
guard.unsaved = true;
(guard.canDeactivate() as Observable<boolean>).subscribe((result) => {
expect(result).toBeFalse();
done();
});
afterClosed$.next(undefined);
});
});
});

View File

@@ -20,7 +20,8 @@ import { CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { UnsavedChangesDialogComponent } from './unsaved-changes-dialog.component';
import { tap } from 'rxjs/operators';
import { map, 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<any> {
unsaved = false;
data: UnsavedChangesDialogData;
constructor(private dialog: MatDialog) {}
@@ -39,9 +41,17 @@ export class UnsavedChangesGuard implements CanDeactivate<any> {
* @returns boolean | Observable<boolean> true when there is no unsaved changes or changes can be discarded, false otherwise.
*/
canDeactivate(): boolean | Observable<boolean> {
return this.unsaved ?
this.dialog.open<UnsavedChangesDialogComponent, undefined, boolean>(UnsavedChangesDialogComponent, {
maxWidth: 346
}).afterClosed().pipe(tap((confirmed) => this.unsaved = !confirmed)) : true;
return this.unsaved
? this.dialog
.open<UnsavedChangesDialogComponent>(UnsavedChangesDialogComponent, {
maxWidth: 346,
data: this.data
})
.afterClosed()
.pipe(
tap((confirmed) => (this.unsaved = !confirmed)),
map((confirmed) => !!confirmed)
)
: true;
}
}

View File

@@ -86,8 +86,9 @@
--adf-header-icon-button-hover-color: $adf-ref-header-icon-color,
--adf-header-icon-button-pressed-color: $adf-ref-header-icon-color,
--adf-header-icon-button-disabled-color: $adf-ref-header-icon-color,
--adf-danger-button-background: $adf-danger-button-background,
--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)
);

View File

@@ -121,6 +121,7 @@ $mat-notched-outline-trailing: '.mdc-notched-outline__trailing';
$mat-notched-outline-notch: '.mdc-notched-outline__notch';
$mat-evolution-chip-set: '.mdc-evolution-chip-set';
$mat-button-base: '.mat-mdc-button-base';
$mat-button-touch-target: '.mat-mdc-button-touch-target';
$mat-evolution-chip-text-label: '.mdc-evolution-chip__text-label';
$cdk-overlay-pane: '.cdk-overlay-pane';
$cdk-drag-preview: '.cdk-drag-preview';

View File

@@ -25,5 +25,6 @@ $adf-ref-metadata-property-panel-label-color: rgba(33, 33, 33, 0.24);
$adf-ref-metadata-property-panel-title-color: rgb(33, 33, 33);
$adf-ref-header-icon-color: inherit;
$adf-ref-header-icon-border-radius: 50%;
$adf-danger-button-background: #ba1b1b;
$adf-error-color: #ba1b1b;
$adf-secondary-button-background: #2121210d;
$adf-secondary-modal-text-color: #212121;