mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-10-01 14:41:14 +00:00
[ACS-8210] Agent basic details popup (#3942)
* [ACS-8210] Agent basic details popup * [ACS-8210] Agent basic details popup - review fixes --------- Co-authored-by: Aleksander Sklorz <aleksander.sklorz@hyland.com>
This commit is contained in:
committed by
Aleksander Sklorz
parent
28880a0de6
commit
5bfa638684
@@ -26,8 +26,10 @@
|
||||
[attr.data-automation-id]="'aca-agents-button-agent-' + agent.id"
|
||||
[value]="agent">
|
||||
<div class="aca-agents-button-menu-list-agent-content">
|
||||
<adf-avatar [initials]="initialsByAgentId[agent.id]"></adf-avatar>
|
||||
{{ agent.name }}
|
||||
<adf-avatar [src]="agent?.avatar" [initials]="initialsByAgentId[agent.id]"></adf-avatar>
|
||||
<span class="aca-agents-button-menu-list-agent-content-name" [matTooltip]="agent.name" [matTooltipPosition]="'right'">
|
||||
{{ agent.name }}
|
||||
</span>
|
||||
</div>
|
||||
</mat-list-option>
|
||||
</mat-selection-list>
|
||||
|
@@ -46,7 +46,14 @@ aca-agents-button.aca-agents-button {
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
|
||||
&-name {
|
||||
width: 120px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
adf-avatar {
|
||||
@@ -54,6 +61,10 @@ aca-agents-button.aca-agents-button {
|
||||
margin-bottom: 2px;
|
||||
padding-left: 1px;
|
||||
padding-top: 1px;
|
||||
|
||||
.adf-avatar__image {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { AgentsButtonComponent } from './agents-button.component';
|
||||
import { AgentService, ContentTestingModule, SearchAiService } from '@alfresco/adf-content-services';
|
||||
import { AgentPaging } from '@alfresco/js-api';
|
||||
import { AgentWithAvatar } from '@alfresco/js-api';
|
||||
import { Subject } from 'rxjs';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
@@ -38,12 +38,13 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { MatSelectionListHarness } from '@angular/material/list/testing';
|
||||
import { MatMenuHarness } from '@angular/material/menu/testing';
|
||||
import { MatSelectionList } from '@angular/material/list';
|
||||
import { MatSnackBarRef } from '@angular/material/snack-bar';
|
||||
|
||||
describe('AgentsButtonComponent', () => {
|
||||
let component: AgentsButtonComponent;
|
||||
let fixture: ComponentFixture<AgentsButtonComponent>;
|
||||
let agents$: Subject<AgentPaging>;
|
||||
let agentPaging: AgentPaging;
|
||||
let agents$: Subject<AgentWithAvatar[]>;
|
||||
let agentsWithAvatar: AgentWithAvatar[];
|
||||
let checkSearchAvailabilitySpy: jasmine.Spy<(selectedNodesState: SelectionState, maxSelectedNodes?: number) => string>;
|
||||
let selectionState: SelectionState;
|
||||
let store: MockStore;
|
||||
@@ -61,26 +62,22 @@ describe('AgentsButtonComponent', () => {
|
||||
fixture = TestBed.createComponent(AgentsButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
store = TestBed.inject(MockStore);
|
||||
agents$ = new Subject<AgentPaging>();
|
||||
agents$ = new Subject<AgentWithAvatar[]>();
|
||||
spyOn(TestBed.inject(AgentService), 'getAgents').and.returnValue(agents$);
|
||||
agentPaging = {
|
||||
list: {
|
||||
entries: [
|
||||
{
|
||||
entry: {
|
||||
id: '1',
|
||||
name: 'HR Agent'
|
||||
}
|
||||
},
|
||||
{
|
||||
entry: {
|
||||
id: '2',
|
||||
name: 'Policy Agent'
|
||||
}
|
||||
}
|
||||
]
|
||||
agentsWithAvatar = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'HR Agent',
|
||||
description: 'Test 1',
|
||||
avatar: undefined
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Policy Agent',
|
||||
description: 'Test 2',
|
||||
avatar: undefined
|
||||
}
|
||||
};
|
||||
];
|
||||
checkSearchAvailabilitySpy = spyOn(TestBed.inject(SearchAiService), 'checkSearchAvailability');
|
||||
selectionState = {
|
||||
nodes: [],
|
||||
@@ -93,30 +90,55 @@ describe('AgentsButtonComponent', () => {
|
||||
});
|
||||
|
||||
describe('Button', () => {
|
||||
let notificationServiceSpy: jasmine.Spy<(message: string) => MatSnackBarRef<any>>;
|
||||
|
||||
beforeEach(() => {
|
||||
const notificationService = TestBed.inject(NotificationService);
|
||||
notificationServiceSpy = spyOn(notificationService, 'showError').and.callThrough();
|
||||
});
|
||||
|
||||
it('should be rendered if any agents are loaded', () => {
|
||||
agents$.next(agentPaging);
|
||||
agents$.next(agentsWithAvatar);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getAgentsButton()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should get agents on component init', () => {
|
||||
agents$.next(agentsWithAvatar);
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.initialsByAgentId).toEqual({ 1: 'HA', 2: 'PA' });
|
||||
expect(component.agents).toEqual(agentsWithAvatar);
|
||||
expect(notificationServiceSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show notification error on getAgents error', () => {
|
||||
agents$.error('error');
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.agents).toEqual([]);
|
||||
expect(component.initialsByAgentId).toEqual({});
|
||||
expect(notificationServiceSpy).toHaveBeenCalledWith('KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.AGENTS_FETCHING');
|
||||
});
|
||||
|
||||
it('should not be rendered if none agent is loaded', () => {
|
||||
agentPaging.list.entries = [];
|
||||
agents$.next(agentPaging);
|
||||
agentsWithAvatar = [];
|
||||
agents$.next(agentsWithAvatar);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(getAgentsButton()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should have correct label', () => {
|
||||
agents$.next(agentPaging);
|
||||
agents$.next(agentsWithAvatar);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getAgentsButton().textContent.trim()).toBe('KNOWLEDGE_RETRIEVAL.SEARCH.AGENTS_BUTTON.LABEL');
|
||||
});
|
||||
|
||||
it('should contain stars icon', () => {
|
||||
agents$.next(agentPaging);
|
||||
agents$.next(agentsWithAvatar);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.debugElement.query(By.css('.aca-agents-menu-button adf-icon')).componentInstance.value).toBe('adf:colored-stars-ai');
|
||||
@@ -135,7 +157,7 @@ describe('AgentsButtonComponent', () => {
|
||||
: new KeyboardEvent(eventName, {
|
||||
key: 'Enter'
|
||||
});
|
||||
agents$.next(agentPaging);
|
||||
agents$.next(agentsWithAvatar);
|
||||
});
|
||||
|
||||
it('should display notification if checkSearchAvailability from SearchAiService returns message', () => {
|
||||
@@ -195,7 +217,7 @@ describe('AgentsButtonComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
agents$.next(agentPaging);
|
||||
agents$.next(agentsWithAvatar);
|
||||
fixture.detectChanges();
|
||||
checkSearchAvailabilitySpy.and.returnValue('');
|
||||
const button = getAgentsButton();
|
||||
|
@@ -33,12 +33,13 @@ import { takeUntil } from 'rxjs/operators';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatListModule, MatSelectionListChange } from '@angular/material/list';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { Agent } from '@alfresco/js-api';
|
||||
import { AgentWithAvatar } from '@alfresco/js-api';
|
||||
import { AgentService, SearchAiService } from '@alfresco/adf-content-services';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatMenuModule, MatListModule, TranslateModule, AvatarComponent, IconComponent],
|
||||
imports: [CommonModule, MatMenuModule, MatListModule, TranslateModule, AvatarComponent, IconComponent, MatTooltipModule],
|
||||
selector: 'aca-agents-button',
|
||||
templateUrl: './agents-button.component.html',
|
||||
styleUrls: ['./agents-button.component.scss'],
|
||||
@@ -50,12 +51,12 @@ export class AgentsButtonComponent implements OnInit, OnDestroy {
|
||||
data: { trigger: string };
|
||||
|
||||
private selectedNodesState: SelectionState;
|
||||
private _agents: Agent[] = [];
|
||||
private _agents: AgentWithAvatar[] = [];
|
||||
private onDestroy$ = new Subject<void>();
|
||||
private _disabled = true;
|
||||
private _initialsByAgentId: { [key: string]: string } = {};
|
||||
|
||||
get agents(): Agent[] {
|
||||
get agents(): AgentWithAvatar[] {
|
||||
return this._agents;
|
||||
}
|
||||
|
||||
@@ -71,8 +72,8 @@ export class AgentsButtonComponent implements OnInit, OnDestroy {
|
||||
private store: Store<AppStore>,
|
||||
private notificationService: NotificationService,
|
||||
private searchAiService: SearchAiService,
|
||||
private translateService: TranslateService,
|
||||
private agentService: AgentService
|
||||
private agentService: AgentService,
|
||||
private translateService: TranslateService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -82,12 +83,13 @@ export class AgentsButtonComponent implements OnInit, OnDestroy {
|
||||
.subscribe((selection) => {
|
||||
this.selectedNodesState = selection;
|
||||
});
|
||||
|
||||
this.agentService
|
||||
.getAgents()
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(
|
||||
(paging) => {
|
||||
this._agents = paging.list.entries.map((agentEntry) => agentEntry.entry);
|
||||
(agents) => {
|
||||
this._agents = agents;
|
||||
if (this.agents.length) {
|
||||
this._initialsByAgentId = this.agents.reduce((initials, agent) => {
|
||||
const words = agent.name.split(' ').filter((word) => !word.match(/[^a-zA-Z]+/g));
|
||||
|
@@ -1,27 +1,49 @@
|
||||
<mat-select
|
||||
[formControl]="agentControl"
|
||||
class="aca-search-ai-input-agent-select"
|
||||
panelClass="aca-search-ai-input-agent-select-options aca-search-ai-input-agent-select-agents"
|
||||
data-automation-id="aca-search-ai-agents-select"
|
||||
<div class="aca-search-ai-input-agent-container">
|
||||
<mat-select
|
||||
[formControl]="agentControl"
|
||||
class="aca-search-ai-input-agent-select"
|
||||
panelClass="aca-search-ai-input-agent-select-options aca-search-ai-input-agent-select-agents"
|
||||
data-automation-id="aca-search-ai-agents-select"
|
||||
[hideSingleSelectionIndicator]="true">
|
||||
<mat-select-trigger class="aca-search-ai-input-agent-select-displayed-value">
|
||||
<adf-avatar
|
||||
[initials]="initialsByAgentId[agentControl.value.id]"
|
||||
size="26px">
|
||||
</adf-avatar>
|
||||
{{ agentControl.value.name }}
|
||||
</mat-select-trigger>
|
||||
<mat-option
|
||||
*ngFor="let agent of agents"
|
||||
[value]="agent"
|
||||
class="aca-search-ai-input-agent-select-options-option"
|
||||
[attr.data-automation-id]="'aca-search-ai-input-agent-' + agent.id">
|
||||
<div class="aca-search-ai-input-agent-select-options-option-content">
|
||||
<adf-avatar [initials]="initialsByAgentId[agent.id]"></adf-avatar>
|
||||
{{ agent.name }}
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-select-trigger class="aca-search-ai-input-agent-select-displayed-value">
|
||||
<adf-avatar
|
||||
[src]="agentControl.value?.avatar"
|
||||
[initials]="initialsByAgentId[agentControl.value?.id]"
|
||||
size="26px">
|
||||
</adf-avatar>
|
||||
<span class="aca-search-ai-input-agent-select-displayed-value-text">{{ agentControl.value?.name }}</span>
|
||||
</mat-select-trigger>
|
||||
<mat-option
|
||||
*ngFor="let agent of agents"
|
||||
[value]="agent"
|
||||
class="aca-search-ai-input-agent-select-options-option"
|
||||
[attr.data-automation-id]="'aca-search-ai-input-agent-' + agent.id"><div class="aca-search-ai-input-agent-select-options-option-content">
|
||||
<adf-avatar [src]="agent?.avatar" [initials]="initialsByAgentId[agent.id]"></adf-avatar>
|
||||
<span class="aca-search-ai-input-agent-select-options-option-content-text" [matTooltip]="agent.name" [matTooltipPosition]="'right'">{{ agent.name }}</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
|
||||
<div class="aca-search-ai-input-agent-popup-hover-card">
|
||||
<mat-card class="aca-search-ai-input-agent-popup-hover-card-container">
|
||||
<mat-card-title class="aca-search-ai-input-agent-popup-hover-card-container-title">
|
||||
<adf-avatar
|
||||
[initials]="initialsByAgentId[agentControl.value?.id]"
|
||||
[src]="agentControl.value?.avatar"
|
||||
size="50px">
|
||||
</adf-avatar>
|
||||
<span class="aca-search-ai-input-agent-popup-hover-card-container-title-name"
|
||||
[matTooltipPosition]="'right'"
|
||||
[matTooltip]="agentControl.value?.name">
|
||||
{{ agentControl.value?.name }}
|
||||
</span>
|
||||
</mat-card-title>
|
||||
<mat-card-content class="aca-search-ai-input-agent-popup-hover-card-container-content">
|
||||
{{ agentControl.value?.description }}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
class="aca-search-ai-input-text"
|
||||
matInput
|
||||
|
@@ -6,6 +6,7 @@ aca-search-ai-input {
|
||||
align-items: center;
|
||||
|
||||
.aca-search-ai-input-text {
|
||||
margin-top: 4px;
|
||||
flex: 1;
|
||||
font-size: 20px;
|
||||
margin-right: 167px;
|
||||
@@ -54,13 +55,24 @@ aca-search-ai-input {
|
||||
font-size: 15px;
|
||||
margin-right: 26px;
|
||||
|
||||
#{$mat-select-trigger} {
|
||||
height: auto;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: -webkit-focus-ring-color auto 1px;
|
||||
}
|
||||
|
||||
&-displayed-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
|
||||
&-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
adf-avatar {
|
||||
@@ -68,6 +80,10 @@ aca-search-ai-input {
|
||||
margin-right: 6px;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 3px;
|
||||
|
||||
.adf-avatar__image {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,14 +97,83 @@ aca-search-ai-input {
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
|
||||
&-text {
|
||||
width: 120px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
adf-avatar {
|
||||
margin-right: 12px;
|
||||
padding-left: 1px;
|
||||
|
||||
.adf-avatar__image {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aca-search-ai-input-agent-container {
|
||||
position: relative;
|
||||
|
||||
.aca-search-ai-input-agent-popup-hover-card {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
|
||||
&-container {
|
||||
width: 315px;
|
||||
height: fit-content;
|
||||
border-radius: 12px;
|
||||
margin-top: 4px;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
padding: 16px 16px 8px;
|
||||
gap: 4px;
|
||||
|
||||
&-name {
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
min-width: 50px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
color: var(--theme-content-color);
|
||||
text-align: justify;
|
||||
text-justify: inter-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.aca-search-ai-input-agent-popup-hover-card {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -29,8 +29,8 @@ import { By } from '@angular/platform-browser';
|
||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
import { AgentService, ContentTestingModule, SearchAiService } from '@alfresco/adf-content-services';
|
||||
import { getAppSelection, SearchByTermAiAction } from '@alfresco/aca-shared/store';
|
||||
import { of } from 'rxjs';
|
||||
import { NodeEntry } from '@alfresco/js-api';
|
||||
import { Subject } from 'rxjs';
|
||||
import { AgentWithAvatar, NodeEntry } from '@alfresco/js-api';
|
||||
import { FormControlDirective } from '@angular/forms';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { AvatarComponent, IconComponent, NotificationService, UserPreferencesService } from '@alfresco/adf-core';
|
||||
@@ -42,6 +42,22 @@ import { MatInput } from '@angular/material/input';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatInputHarness } from '@angular/material/input/testing';
|
||||
import { SelectionState } from '@alfresco/adf-extensions';
|
||||
import { MatSnackBarRef } from '@angular/material/snack-bar';
|
||||
|
||||
const agentWithAvatarList: AgentWithAvatar[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'HR Agent',
|
||||
description: 'Test 1',
|
||||
avatar: undefined
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Policy Agent',
|
||||
description: 'Test 2',
|
||||
avatar: undefined
|
||||
}
|
||||
];
|
||||
|
||||
describe('SearchAiInputComponent', () => {
|
||||
let component: SearchAiInputComponent;
|
||||
@@ -49,6 +65,7 @@ describe('SearchAiInputComponent', () => {
|
||||
let loader: HarnessLoader;
|
||||
let selectionState: SelectionState;
|
||||
let store: MockStore;
|
||||
let agents$: Subject<AgentWithAvatar[]>;
|
||||
|
||||
const prepareBeforeTest = (): void => {
|
||||
selectionState = {
|
||||
@@ -73,34 +90,19 @@ describe('SearchAiInputComponent', () => {
|
||||
component = fixture.componentInstance;
|
||||
store = TestBed.inject(MockStore);
|
||||
loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
spyOn(TestBed.inject(AgentService), 'getAgents').and.returnValue(
|
||||
of({
|
||||
list: {
|
||||
entries: [
|
||||
{
|
||||
entry: {
|
||||
id: '1',
|
||||
name: 'HR Agent'
|
||||
}
|
||||
},
|
||||
{
|
||||
entry: {
|
||||
id: '2',
|
||||
name: 'Policy Agent'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
);
|
||||
agents$ = new Subject<AgentWithAvatar[]>();
|
||||
spyOn(TestBed.inject(AgentService), 'getAgents').and.returnValue(agents$);
|
||||
prepareBeforeTest();
|
||||
});
|
||||
|
||||
describe('Agent select box', () => {
|
||||
let selectElement: DebugElement;
|
||||
let notificationServiceSpy: jasmine.Spy<(message: string) => MatSnackBarRef<any>>;
|
||||
|
||||
beforeEach(() => {
|
||||
selectElement = fixture.debugElement.query(By.directive(MatSelect));
|
||||
const notificationService = TestBed.inject(NotificationService);
|
||||
notificationServiceSpy = spyOn(notificationService, 'showError').and.callThrough();
|
||||
});
|
||||
|
||||
it('should have assigned formControl', () => {
|
||||
@@ -111,8 +113,26 @@ describe('SearchAiInputComponent', () => {
|
||||
expect(selectElement.componentInstance.hideSingleSelectionIndicator).toBeTrue();
|
||||
});
|
||||
|
||||
it('should get agents on init', () => {
|
||||
agents$.next(agentWithAvatarList);
|
||||
component.ngOnInit();
|
||||
expect(component.agents).toEqual(agentWithAvatarList);
|
||||
expect(component.initialsByAgentId).toEqual({ 1: 'HA', 2: 'PA' });
|
||||
expect(notificationServiceSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show notification on getAgents error', () => {
|
||||
agents$.error('error');
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.agents).toEqual([]);
|
||||
expect(component.initialsByAgentId).toEqual({});
|
||||
expect(notificationServiceSpy).toHaveBeenCalledWith('KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.AGENTS_FETCHING');
|
||||
});
|
||||
|
||||
it('should have selected correct agent', async () => {
|
||||
expect(await (await loader.getHarness(MatSelectHarness)).getValueText()).toBe('PA Policy Agent');
|
||||
agents$.next(agentWithAvatarList);
|
||||
expect(await (await loader.getHarness(MatSelectHarness)).getValueText()).toBe('PAPolicy Agent');
|
||||
const avatar = selectElement.query(By.directive(AvatarComponent))?.componentInstance;
|
||||
expect(avatar.initials).toBe('PA');
|
||||
expect(avatar.size).toBe('26px');
|
||||
@@ -126,6 +146,7 @@ describe('SearchAiInputComponent', () => {
|
||||
.componentInstance;
|
||||
|
||||
beforeEach(async () => {
|
||||
agents$.next(agentWithAvatarList);
|
||||
const selectHarness = await loader.getHarness(MatSelectHarness);
|
||||
await selectHarness.open();
|
||||
options = await selectHarness.getOptions();
|
||||
@@ -136,8 +157,8 @@ describe('SearchAiInputComponent', () => {
|
||||
});
|
||||
|
||||
it('should have correct agent names', async () => {
|
||||
expect(await options[0].getText()).toBe('HA HR Agent');
|
||||
expect(await options[1].getText()).toBe('PA Policy Agent');
|
||||
expect(await options[0].getText()).toBe('HAHR Agent');
|
||||
expect(await options[1].getText()).toBe('PAPolicy Agent');
|
||||
});
|
||||
|
||||
it('should display avatar for each agent', () => {
|
||||
@@ -157,6 +178,7 @@ describe('SearchAiInputComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
queryInput = fixture.debugElement.query(By.directive(MatInput));
|
||||
agents$.next(agentWithAvatarList);
|
||||
});
|
||||
|
||||
it('should have assigned formControl', () => {
|
||||
@@ -181,6 +203,7 @@ describe('SearchAiInputComponent', () => {
|
||||
beforeEach(async () => {
|
||||
submitButton = fixture.debugElement.query(By.directive(MatButton));
|
||||
queryInput = await loader.getHarness(MatInputHarness);
|
||||
agents$.next(agentWithAvatarList);
|
||||
});
|
||||
|
||||
it('should be disabled by default', () => {
|
||||
@@ -324,7 +347,7 @@ describe('SearchAiInputComponent', () => {
|
||||
await (
|
||||
await loader.getHarness(MatSelectHarness)
|
||||
).clickOptions({
|
||||
text: 'HA HR Agent'
|
||||
text: 'HAHR Agent'
|
||||
});
|
||||
submittingTrigger();
|
||||
|
||||
|
@@ -38,8 +38,20 @@ import { AiSearchByTermPayload, AppStore, getAppSelection, SearchByTermAiAction
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { SelectionState } from '@alfresco/adf-extensions';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { Agent } from '@alfresco/js-api';
|
||||
import { AgentWithAvatar } from '@alfresco/js-api';
|
||||
import { AgentService, SearchAiService } from '@alfresco/adf-content-services';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import {
|
||||
MAT_TOOLTIP_DEFAULT_OPTIONS,
|
||||
MAT_TOOLTIP_DEFAULT_OPTIONS_FACTORY,
|
||||
MatTooltipDefaultOptions,
|
||||
MatTooltipModule
|
||||
} from '@angular/material/tooltip';
|
||||
|
||||
const MatTooltipOptions: MatTooltipDefaultOptions = {
|
||||
...MAT_TOOLTIP_DEFAULT_OPTIONS_FACTORY(),
|
||||
disableTooltipInteractivity: true
|
||||
};
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -55,12 +67,15 @@ import { AgentService, SearchAiService } from '@alfresco/adf-content-services';
|
||||
ReactiveFormsModule,
|
||||
MatSelectModule,
|
||||
IconComponent,
|
||||
AvatarComponent
|
||||
AvatarComponent,
|
||||
MatCardModule,
|
||||
MatTooltipModule
|
||||
],
|
||||
selector: 'aca-search-ai-input',
|
||||
templateUrl: './search-ai-input.component.html',
|
||||
styleUrls: ['./search-ai-input.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
providers: [{ provide: MAT_TOOLTIP_DEFAULT_OPTIONS, useValue: MatTooltipOptions }]
|
||||
})
|
||||
export class SearchAiInputComponent implements OnInit, OnDestroy {
|
||||
@Input()
|
||||
@@ -75,18 +90,18 @@ export class SearchAiInputComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly storedNodesKey = 'knowledgeRetrievalNodes';
|
||||
|
||||
private _agentControl = new FormControl<Agent>(null);
|
||||
private _agents: Agent[] = [];
|
||||
private _agentControl = new FormControl<AgentWithAvatar>(null);
|
||||
private _agents: AgentWithAvatar[] = [];
|
||||
private onDestroy$ = new Subject<void>();
|
||||
private selectedNodesState: SelectionState;
|
||||
private _queryControl = new FormControl('');
|
||||
private _initialsByAgentId: { [key: string]: string } = {};
|
||||
|
||||
get agentControl(): FormControl<Agent> {
|
||||
get agentControl(): FormControl<AgentWithAvatar> {
|
||||
return this._agentControl;
|
||||
}
|
||||
|
||||
get agents(): Agent[] {
|
||||
get agents(): AgentWithAvatar[] {
|
||||
return this._agents;
|
||||
}
|
||||
|
||||
@@ -103,8 +118,8 @@ export class SearchAiInputComponent implements OnInit, OnDestroy {
|
||||
private searchAiService: SearchAiService,
|
||||
private notificationService: NotificationService,
|
||||
private agentService: AgentService,
|
||||
private translateService: TranslateService,
|
||||
private userPreferencesService: UserPreferencesService
|
||||
private userPreferencesService: UserPreferencesService,
|
||||
private translateService: TranslateService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -118,13 +133,14 @@ export class SearchAiInputComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
this.selectedNodesState = JSON.parse(this.userPreferencesService.get(this.storedNodesKey));
|
||||
}
|
||||
|
||||
this.agentService
|
||||
.getAgents()
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe(
|
||||
(paging) => {
|
||||
this._agents = paging.list.entries.map((agentEntry) => agentEntry.entry);
|
||||
this.agentControl.setValue(this.agents.find((agent) => agent.id === this.agentId));
|
||||
(agents) => {
|
||||
this._agents = agents;
|
||||
this.agentControl.setValue(agents.find((agent) => agent.id === this.agentId));
|
||||
this._initialsByAgentId = this.agents.reduce((initials, agent) => {
|
||||
const words = agent.name.split(' ').filter((word) => !word.match(/[^a-zA-Z]+/g));
|
||||
initials[agent.id] = `${words[0][0]}${words[1][0] || ''}`;
|
||||
|
@@ -61,7 +61,7 @@
|
||||
width: 100%;
|
||||
|
||||
&-response {
|
||||
margin-bottom: 17px;
|
||||
margin: 17px 0;
|
||||
padding-left: 6px;
|
||||
padding-right: 5px;
|
||||
overflow-wrap: break-word;
|
||||
@@ -98,7 +98,7 @@
|
||||
padding-left: 8px;
|
||||
|
||||
&-header {
|
||||
margin-top: 8px;
|
||||
margin-top: 16px;
|
||||
color: var(--theme-text-light-color);
|
||||
font-weight: 400;
|
||||
margin-bottom: 3px;
|
||||
|
@@ -0,0 +1,102 @@
|
||||
/*!
|
||||
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Alfresco Example Content Application
|
||||
*
|
||||
* This file is part of the Alfresco Example Content Application.
|
||||
* If the software was purchased under a paid Alfresco license, the terms of
|
||||
* the paid license agreement will prevail. Otherwise, the software is
|
||||
* provided under the following open source license terms:
|
||||
*
|
||||
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* from Hyland Software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
||||
import { SearchAiResultsComponent } from './search-ai-results.component';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { Subject } from 'rxjs';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { UserPreferencesService } from '@alfresco/adf-core';
|
||||
import { AppTestingModule } from '../../../../testing/app-testing.module';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
|
||||
describe('SearchAiResultsComponent', () => {
|
||||
const knowledgeRetrievalNodes = '{"isEmpty":"false","nodes":[{"entry":{"id": "someId","isFolder":"true"}}]}';
|
||||
let fixture: ComponentFixture<SearchAiResultsComponent>;
|
||||
let component: SearchAiResultsComponent;
|
||||
let userPreferencesService: UserPreferencesService;
|
||||
let mockQueryParams = new Subject<Params>();
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [AppTestingModule, SearchAiResultsComponent, MatSnackBarModule, MatDialogModule],
|
||||
providers: [
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
queryParams: mockQueryParams.asObservable()
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(SearchAiResultsComponent);
|
||||
userPreferencesService = TestBed.inject(UserPreferencesService);
|
||||
component = fixture.componentInstance;
|
||||
component.ngOnInit();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockQueryParams = new Subject<Params>();
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
describe('query params change', () => {
|
||||
it('should perform ai search and sets agents on query params change', () => {
|
||||
spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
|
||||
mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
|
||||
|
||||
expect(component.searchQuery).toBe('test');
|
||||
expect(component.agentId).toBe('agentId1');
|
||||
expect(component.hasError).toBeFalse();
|
||||
});
|
||||
|
||||
it('should throw an error if searchQuery not available', () => {
|
||||
spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
|
||||
mockQueryParams.next({ agentId: 'agentId1' });
|
||||
|
||||
expect(component.searchQuery).toBe('');
|
||||
expect(component.agentId).toBe('agentId1');
|
||||
expect(component.hasError).toBeTrue();
|
||||
});
|
||||
|
||||
it('should throw an error if selectedNodesState nodes not available', () => {
|
||||
spyOn(userPreferencesService, 'get').and.returnValue('{}');
|
||||
mockQueryParams.next({ query: 'test', agentId: 'agentId1' });
|
||||
|
||||
expect(component.searchQuery).toBe('test');
|
||||
expect(component.agentId).toBe('agentId1');
|
||||
expect(component.hasError).toBeTrue();
|
||||
});
|
||||
|
||||
it('should throw an error if agentId not available', () => {
|
||||
spyOn(userPreferencesService, 'get').and.returnValue(knowledgeRetrievalNodes);
|
||||
mockQueryParams.next({ query: 'test' });
|
||||
|
||||
expect(component.searchQuery).toBe('test');
|
||||
expect(component.agentId).toBe(undefined);
|
||||
expect(component.hasError).toBeTrue();
|
||||
});
|
||||
});
|
||||
});
|
@@ -23,10 +23,17 @@
|
||||
*/
|
||||
|
||||
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { PageComponent, PageLayoutComponent, ToolbarActionComponent, ToolbarComponent } from '@alfresco/aca-shared';
|
||||
import { finalize, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { ClipboardService, EmptyContentComponent, ThumbnailService, ToolbarModule, UserPreferencesService } from '@alfresco/adf-core';
|
||||
import {
|
||||
AvatarComponent,
|
||||
ClipboardService,
|
||||
EmptyContentComponent,
|
||||
ThumbnailService,
|
||||
ToolbarModule,
|
||||
UserPreferencesService
|
||||
} from '@alfresco/adf-core';
|
||||
import { AiAnswer, Node } from '@alfresco/js-api';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SearchAiInputContainerComponent } from '../search-ai-input-container/search-ai-input-container.component';
|
||||
@@ -37,6 +44,8 @@ import { SelectionState } from '@alfresco/adf-extensions';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -51,7 +60,10 @@ import { MatListModule } from '@angular/material/list';
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatListModule,
|
||||
EmptyContentComponent
|
||||
EmptyContentComponent,
|
||||
MatCardModule,
|
||||
AvatarComponent,
|
||||
MatTooltipModule
|
||||
],
|
||||
selector: 'aca-search-ai-results',
|
||||
templateUrl: './search-ai-results.component.html',
|
||||
@@ -66,7 +78,7 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit, O
|
||||
private _loading = true;
|
||||
private _mimeTypeIconsByNodeId: { [key: string]: string } = {};
|
||||
private _nodes: Node[] = [];
|
||||
private selectedNodesState: SelectionState;
|
||||
private _selectedNodesState: SelectionState;
|
||||
private _searchQuery = '';
|
||||
private _queryAnswer: AiAnswer;
|
||||
|
||||
@@ -114,11 +126,12 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit, O
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params: Params) => {
|
||||
this.route.queryParams.pipe(takeUntil(this.onDestroy$)).subscribe((params) => {
|
||||
this._agentId = params.agentId;
|
||||
this._searchQuery = params.query ? decodeURIComponent(params.query) : '';
|
||||
this.selectedNodesState = JSON.parse(this.userPreferencesService.get('knowledgeRetrievalNodes'));
|
||||
if (!this.searchQuery || !this.selectedNodesState?.nodes?.length || !this.agentId) {
|
||||
this._selectedNodesState = JSON.parse(this.userPreferencesService.get('knowledgeRetrievalNodes'));
|
||||
|
||||
if (!this.searchQuery || !this._selectedNodesState?.nodes?.length || !this.agentId) {
|
||||
this._hasError = true;
|
||||
return;
|
||||
}
|
||||
@@ -144,7 +157,7 @@ export class SearchAiResultsComponent extends PageComponent implements OnInit, O
|
||||
this.searchAiService
|
||||
.ask({
|
||||
question: this.searchQuery,
|
||||
nodeIds: this.selectedNodesState.nodes.map((node) => node.entry.id)
|
||||
nodeIds: this._selectedNodesState.nodes.map((node) => node.entry.id)
|
||||
})
|
||||
.pipe(
|
||||
switchMap((response) => this.searchAiService.getAnswer(response.questionId)),
|
||||
|
@@ -28,6 +28,7 @@ $blue-save-button-background: #1f74db;
|
||||
$blue-checkbox-background: rgb(10, 96, 206);
|
||||
$blue-active-table-row: rgb(10, 96, 206, 0.24);
|
||||
$black-heading: #4e4c4c;
|
||||
$light-grey-content: #4b5563;
|
||||
$theme-dropdown-background: darken($background-color, 5%);
|
||||
$theme-dropdown-background-hover: darken($background-color, 10%);
|
||||
$grey-divider: rgba(0, 0, 0, 0.22);
|
||||
@@ -77,6 +78,7 @@ $defaults: (
|
||||
--theme-blue-checkbox-color: $blue-checkbox-background,
|
||||
--theme-blue-active-table-row-color: $blue-active-table-row,
|
||||
--theme-heading-color: $black-heading,
|
||||
--theme-content-color: $light-grey-content,
|
||||
--theme-dropdown-color: $theme-dropdown-background,
|
||||
--theme-dropdown-background-hover: $theme-dropdown-background-hover,
|
||||
--theme-grey-divider-color: $grey-divider,
|
||||
|
Reference in New Issue
Block a user