[ACS-8001] content enrichment menu component

This commit is contained in:
Mykyta Maliarchuk
2024-06-11 13:23:56 +02:00
committed by Mykyta Maliarchuk
parent 548592b329
commit a1cbcfca33
54 changed files with 1134 additions and 183 deletions

View File

@@ -96,6 +96,7 @@ A collection of Angular components for generic use.
| [Card View component](core/components/card-view.component.md) | Displays a configurable property list renderer. | [Source](../lib/core/src/lib/card-view/components/card-view/card-view.component.ts) |
| [Comment list component](core/components/comment-list.component.md) | Shows a list of comments. | [Source](../lib/core/src/lib/comments/comment-list/comment-list.component.ts) |
| [Comments Component](core/components/comments.component.md) | Displays comments from users involved in a specified environment and allows an involved user to add a comment to a environment. | [Source](../lib/core/src/lib/comments/comments.component.ts) |
| [Content Enrichment Menu Component](core/components/content-enrichment-menu.component.md) | Allows the user to handle AI predictions by confirming or rejecting changes. | [Source](../lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.ts) |
| [Data Column Component](core/components/data-column.component.md) | Defines column properties for DataTable, Tasklist, Document List and other components. | [Source](../lib/core/src/lib/datatable/data-column/data-column.component.ts) |
| [DataTable component](core/components/datatable.component.md) | Displays data as a table with customizable columns and presentation. | [Source](../lib/core/src/lib/datatable/components/datatable/datatable.component.ts) |
| [Dynamic Chip List component](core/components/dynamic-chip-list.component.md) | This component shows dynamic list of chips which render depending on free space. | [Source](../lib/core/src/lib/dynamic-chip-list/dynamic-chip-list.component.ts) |

View File

@@ -2,7 +2,7 @@
Title: Card View Content Update Service
Added: v6.0.0
Status: Active
Last reviewed: 2022-11-25
Last reviewed: 2024-06-11
---
# [Card View Content Update Service](../../../lib/content-services/src/lib/common/services/card-view-content-update.service.ts "Defined in card-view-content-update.service.ts")
@@ -25,6 +25,9 @@ Implements [`BaseCardViewContentUpdate`](../../../lib/content-services/src/lib/i
- **updateNodeAspect**(node: [`MinimalNode`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/NodeMinimalEntry.md))<br/>
Update node aspect observable.
- _node:_ [`MinimalNode`](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/NodeMinimalEntry.md) -
- **onPredictionStatusChanged**(notification: [`PredictionStatusUpdate[]`](../../core/interfaces/prediction-status-update.interface.md))<br/>
Clears predictions for properties and sets the previous value, if provided.
- _notification:_ [`PredictionStatusUpdate[]`](../../core/interfaces/prediction-status-update.interface.md) -
## Properties

View File

@@ -0,0 +1,32 @@
---
Title: Content Enrichment Menu Component
Added: v6.10.0
Status: Active
Last reviewed: 2024-06-11
---
# [Content Enrichment Menu Component](../../../lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.ts "Defined in content-enrichment-menu.component.ts")
Allows the user to handle AI predictions by confirming or rejecting changes.
![Content Enrichment Menu Component](../../docassets/images/content-enrichment-menu.png)
## Basic Usage
```html
<mat-form-field>
<mat-label>Form field</mat-label>
<adf-content-enrichment-menu matPrefix [prediction]="prediction"></adf-content-enrichment-menu>
<input matInput>
</mat-form-field>
```
### Properties
| Name | Type | Default value | Description |
|------------|------------------------------------------------------------------------------------------------------------------------------------------|---------------|-----------------------------------|
| prediction | [`Prediction`](https://github.com/Alfresco/alfresco-ng2-components/blob/develop/lib/js-api/src/api/hxi-connector-api/docs/Prediction.md) | | Prediction for the node property. |
## See also
- [Prediction Status Update Interface](../interfaces/prediction-status-update.interface.md)

View File

@@ -16,20 +16,23 @@ export interface BaseCardViewUpdate {
itemUpdated$: Subject<UpdateNotification>;
itemClicked$: Subject<ClickNotification>;
updateItem$: Subject<CardViewBaseItemModel>;
predictionStatusChanged$: Subject<PredictionStatusUpdate[]>;
update(property: CardViewBaseItemModel, newValue: any);
clicked(property: CardViewBaseItemModel);
updateElement(notification: CardViewBaseItemModel);
onPredictionStatusChanged(notification: PredictionStatusUpdate[]);
}
```
## Properties
| Name | Type | Description |
| ---- | ---- | ----------- |
| itemUpdated$ | [`Subject`](http://reactivex.io/documentation/subject.html)`<`[`UpdateNotification`](../../../lib/core/src/lib/card-view/interfaces/update-notification.interface.ts)`>` | The current updated item. |
| itemClicked$ | [`Subject`](http://reactivex.io/documentation/subject.html)`<`[`ClickNotification`](../../../lib/core/src/lib/card-view/interfaces/click-notification.interface.ts)`>` | The current clicked item. |
| updateItem$ | [`Subject`](http://reactivex.io/documentation/subject.html)`<`[`CardViewBaseItemModel`](../../../lib/core/src/lib/card-view/models/card-view-baseitem.model.ts)`>` | The current model for the update item. |
| Name | Type | Description |
|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|
| itemUpdated$ | [`Subject`](http://reactivex.io/documentation/subject.html)`<`[`UpdateNotification`](../../../lib/core/src/lib/card-view/interfaces/update-notification.interface.ts)`>` | The current updated item. |
| itemClicked$ | [`Subject`](http://reactivex.io/documentation/subject.html)`<`[`ClickNotification`](../../../lib/core/src/lib/card-view/interfaces/click-notification.interface.ts)`>` | The current clicked item. |
| updateItem$ | [`Subject`](http://reactivex.io/documentation/subject.html)`<`[`CardViewBaseItemModel`](../../../lib/core/src/lib/card-view/models/card-view-baseitem.model.ts)`>` | The current model for the update item. |
| predictionStatusChanged$ | [`Subject`](http://reactivex.io/documentation/subject.html)`<`[`PredictionStatusUpdate[]`](./prediction-status-update.interface.md)`>` | The current model for items with irrelevant predictions. |
### Methods
@@ -48,6 +51,10 @@ export interface BaseCardViewUpdate {
Update updateItem$ observable.
- notification:\_ [`CardViewBaseItemModel`](../../../lib/core/src/lib/card-view/models/card-view-baseitem.model.ts) - The notification.
- **onPredictionStatusChanged**(notification: [`PredictionStatusUpdate[]`](./prediction-status-update.interface.md))<br/>
Update predictionStatusChanged$ observable.
- notification:\_ [`PredictionStatusUpdate[]`](./prediction-status-update.interface.md) - The notification.
## See also
- [CardViewUpdate service](../services/card-view-update.service.md)

View File

@@ -2,7 +2,7 @@
Title: Card View Item interface
Added: v2.0.0
Status: Active
Last reviewed: 2018-05-08
Last reviewed: 2024-06-11
---
# [Card View Item interface](../../../lib/core/src/lib/card-view/interfaces/card-view-item.interface.ts "Defined in card-view-item.interface.ts")
@@ -22,22 +22,24 @@ export interface CardViewItem {
editable?: boolean;
icon?: string;
data?: any;
prediction?: Prediction;
}
```
### Properties
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| label | string | "" | Item label |
| value | any | | The original data value for the item |
| key | string | "" | Identifying key (important when editing the item) |
| default | any | | The default value to display if the value is empty |
| displayValue | string | "" | The value to display |
| editable | boolean | false | Toggles whether the item is editable |
| clickable | boolean | false | Toggles whether the item is clickable |
| icon | string | | The material icon to show beside clickable items |
| data | any | null | Any custom data which is needed to be provided and stored in the model for any reason. During an update or a click event this can be a container of any custom data which can be useful for 3rd party codes. |
| Name | Type | Default | Description |
|--------------|------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| label | string | "" | Item label |
| value | any | | The original data value for the item |
| key | string | "" | Identifying key (important when editing the item) |
| default | any | | The default value to display if the value is empty |
| displayValue | string | "" | The value to display |
| editable | boolean | false | Toggles whether the item is editable |
| clickable | boolean | false | Toggles whether the item is clickable |
| icon | string | | The material icon to show beside clickable items |
| data | any | null | Any custom data which is needed to be provided and stored in the model for any reason. During an update or a click event this can be a container of any custom data which can be useful for 3rd party codes. |
| prediction | Prediction | | Property prediction |
## Details

View File

@@ -0,0 +1,28 @@
---
Title: Prediction Status Update Interface
Added: v6.10.0
Status: Active
Last reviewed: 2024-06-11
---
# [Prediction Status Update Interface](../../../lib/core/src/lib/prediction/interfaces/prediction-status-update.interface.ts "Defined in prediction-status-update.interface.ts")
## Basic usage
```ts
export interface PredictionStatusUpdate {
key: string;
previousValue?: any;
}
```
## Properties
| Name | Type | Description |
|---------------|----------|-------------------------------|
| key | `string` | Key of the property. |
| previousValue | `any` | Previous human entered value. |
## See also
- [BaseCardViewUpdate interface](../interfaces/base-card-view-update.interface.md)

View File

@@ -137,8 +137,27 @@ Example
this.cardViewUpdateService.updateElement(cardViewBaseItemModel)
```
## Clear predictions for properties and set previous value
`onPredictionStatusChanged` function helps to clear predictions and set previous value (if provided) for the card view item. It takes the [`PredictionStatusUpdate[]`](../interfaces/prediction-status-update.interface.md) as a parameter.
Example
```javascript
this.cardViewUpdateService.onPredictionStatusChanged(notification);
```
You can subscribe to the `predictionStatusChanged$` to be informed about prediction status changes.
```ts
ngOnInit() {
this.cardViewUpdateService.predictionStatusChanged$.subscribe(this.respondToPredictionStatusChange.bind(this));
}
```
## See also
- [Card view component](../components/card-view.component.md)
- [UpdateNotification interface](../interfaces/update-notification.interface.md)
- [ClickNotification interface](../interfaces/click-notification.interface.md)
- [PredictionStatusUpdate interface](../interfaces/prediction-status-update.interface.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -46,6 +46,14 @@ backend services have been tested with each released version of ADF.
- [v2.1.0](#v210)
- [v2.0.0](#v200)
## v6.10.0
<!--6100 start-->
- [Content Enrichment Menu Component](core/components/content-enrichment-menu.component.md)
<!--6100 end-->
## v6.8.0
<!--680 start-->

View File

@@ -18,12 +18,15 @@
import { Node } from '@alfresco/js-api';
import { fakeAsync, TestBed } from '@angular/core/testing';
import { CardViewContentUpdateService } from './card-view-content-update.service';
import { CardViewUpdateService, PredictionStatusUpdate } from '@alfresco/adf-core';
describe('CardViewContentUpdateService', () => {
let cardViewContentUpdateService: CardViewContentUpdateService;
let cardViewUpdateService: CardViewUpdateService;
beforeEach(() => {
cardViewContentUpdateService = TestBed.inject(CardViewContentUpdateService);
cardViewUpdateService = TestBed.inject(CardViewUpdateService);
});
it('should send updated node when aspect changed', fakeAsync(() => {
@@ -34,4 +37,12 @@ describe('CardViewContentUpdateService', () => {
cardViewContentUpdateService.updateNodeAspect(fakeNode);
}));
it('should call onPredictionStatusChanged on cardViewUpdateService', () => {
spyOn(cardViewUpdateService, 'onPredictionStatusChanged');
const mockNotification: PredictionStatusUpdate[] = [{ key: 'test', previousValue: 'value' }];
cardViewContentUpdateService.onPredictionStatusChanged(mockNotification);
expect(cardViewUpdateService.onPredictionStatusChanged).toHaveBeenCalledWith(mockNotification);
});
});

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { UpdateNotification, CardViewBaseItemModel, CardViewUpdateService } from '@alfresco/adf-core';
import { UpdateNotification, CardViewBaseItemModel, CardViewUpdateService, PredictionStatusUpdate } from '@alfresco/adf-core';
import { Node } from '@alfresco/js-api';
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@@ -40,6 +40,10 @@ export class CardViewContentUpdateService implements BaseCardViewContentUpdate {
this.cardViewUpdateService.updateElement(notification);
}
onPredictionStatusChanged(notification: PredictionStatusUpdate[]) {
this.cardViewUpdateService.onPredictionStatusChanged(notification);
}
updateNodeAspect(node: Node) {
this.updatedAspect$.next(node);
}

View File

@@ -34,6 +34,10 @@ $panel-properties-height: 56px !default;
font-size: 19px;
line-height: 20px;
}
adf-content-enrichment-menu button.adf-ai-button {
margin-left: -10px;
}
}
}

View File

@@ -18,7 +18,17 @@
import { ComponentFixture, discardPeriodicTasks, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
import { SimpleChange } from '@angular/core';
import { By } from '@angular/platform-browser';
import { Category, CategoryPaging, ClassesApi, Node, Tag, TagBody, TagEntry, TagPaging, TagPagingList } from '@alfresco/js-api';
import {
Category,
CategoryPaging,
ClassesApi,
Node, Prediction, PredictionPaging, ReviewStatus,
Tag,
TagBody,
TagEntry,
TagPaging,
TagPagingList
} from '@alfresco/js-api';
import { ContentMetadataComponent } from './content-metadata.component';
import { ContentMetadataService } from '../../services/content-metadata.service';
import {
@@ -30,10 +40,12 @@ import {
PipeModule,
TranslationMock,
TranslationService,
UpdateNotification
UpdateNotification,
PredictionService,
PredictionStatusUpdate
} from '@alfresco/adf-core';
import { NodesApiService } from '../../../common/services/nodes-api.service';
import { EMPTY, of, throwError } from 'rxjs';
import { EMPTY, of, throwError, Subject } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
import { CardViewContentUpdateService } from '../../../common/services/card-view-content-update.service';
import { PropertyGroup } from '../../interfaces/property-group.interface';
@@ -61,12 +73,14 @@ describe('ContentMetadataComponent', () => {
let fixture: ComponentFixture<ContentMetadataComponent>;
let contentMetadataService: ContentMetadataService;
let updateService: CardViewContentUpdateService;
let predictionService: PredictionService;
let nodesApiService: NodesApiService;
let node: Node;
let folderNode: Node;
let tagService: TagService;
let categoryService: CategoryService;
let getClassSpy: jasmine.Spy;
let getBasicPropertiesSpy: jasmine.Spy;
let notificationService: NotificationService;
let getGroupedPropertiesSpy: jasmine.Spy;
@@ -215,6 +229,13 @@ describe('ContentMetadataComponent', () => {
linkNodeToCategory: () => EMPTY,
unlinkNodeFromCategory: () => EMPTY
}
},
{
provide: PredictionService,
useValue: {
getPredictions: () => EMPTY,
predictionStatusUpdated$: new Subject<PredictionStatusUpdate>()
}
}
]
});
@@ -222,6 +243,7 @@ describe('ContentMetadataComponent', () => {
component = fixture.componentInstance;
contentMetadataService = TestBed.inject(ContentMetadataService);
updateService = TestBed.inject(CardViewContentUpdateService);
predictionService = TestBed.inject(PredictionService);
nodesApiService = TestBed.inject(NodesApiService);
tagService = TestBed.inject(TagService);
categoryService = TestBed.inject(CategoryService);
@@ -250,7 +272,8 @@ describe('ContentMetadataComponent', () => {
component.node = node;
component.preset = preset;
spyOn(contentMetadataService, 'getContentTypeProperty').and.returnValue(of([]));
getGroupedPropertiesSpy = spyOn(contentMetadataService, 'getGroupedProperties');
getGroupedPropertiesSpy = spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of([]));
getBasicPropertiesSpy = spyOn(contentMetadataService, 'getBasicProperties').and.returnValue(of([]));
getClassSpy = spyOn(classesApi, 'getClass');
fixture.detectChanges();
});
@@ -267,6 +290,10 @@ describe('ContentMetadataComponent', () => {
it('should have expanded input param as false by default', () => {
expect(component.expanded).toBeFalse();
});
it('should have display predictions param as false by default', () => {
expect(component.displayPredictions).toBeFalse();
});
});
describe('Folder', () => {
@@ -300,8 +327,6 @@ describe('ContentMetadataComponent', () => {
it('nodeAspectUpdate', fakeAsync(() => {
const fakeNode = { id: 'fake-minimal-node', aspectNames: ['ft:a', 'ft:b', 'ft:c'], name: 'fake-node' } as Node;
getGroupedPropertiesSpy.and.stub();
spyOn(contentMetadataService, 'getBasicProperties').and.stub();
updateService.updateNodeAspect(fakeNode);
tick(600);
@@ -335,6 +360,18 @@ describe('ContentMetadataComponent', () => {
expect(component.node).toEqual(expectedNode);
}));
it('should call onPredictionStatusChanged with updated property keys to clear predictions if displayPredictions=true', () => {
spyOn(updateService, 'onPredictionStatusChanged');
const expectedNode: Node = { ...node, name: 'some-modified-value' };
spyOn(nodesApiService, 'updateNode').and.returnValue(of(expectedNode));
component.displayPredictions = true;
component.changedProperties = { properties: { key1: 'value1', key2: 'value2' } };
component.saveChanges();
expect(updateService.onPredictionStatusChanged).toHaveBeenCalledWith([{ key: 'key1' }, { key: 'key2' }]);
});
it('should call removeTag and assignTagsToNode on TagService on save click', fakeAsync(() => {
component.displayTags = true;
const property = { key: 'properties.property-key', value: 'original-value' } as CardViewBaseItemModel;
@@ -685,8 +722,6 @@ describe('ContentMetadataComponent', () => {
});
it('should load the basic properties on node change', () => {
spyOn(contentMetadataService, 'getBasicProperties');
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
expect(contentMetadataService.getContentTypeProperty).toHaveBeenCalledWith(expectedNode);
@@ -697,7 +732,7 @@ describe('ContentMetadataComponent', () => {
const expectedProperties = [];
component.expanded = false;
spyOn(contentMetadataService, 'getBasicProperties').and.returnValue(of(expectedProperties));
getBasicPropertiesSpy.and.returnValue(of(expectedProperties));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
@@ -714,7 +749,7 @@ describe('ContentMetadataComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
spyOn(contentMetadataService, 'getBasicProperties').and.returnValue(of([]));
getBasicPropertiesSpy.and.returnValue(of([]));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
@@ -726,10 +761,7 @@ describe('ContentMetadataComponent', () => {
});
it('should load the group properties on node change', () => {
getGroupedPropertiesSpy.and.stub();
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
expect(contentMetadataService.getGroupedProperties).toHaveBeenCalledWith(expectedNode, 'custom-preset');
});
@@ -750,8 +782,6 @@ describe('ContentMetadataComponent', () => {
}
];
component.preset = presetConfig;
getGroupedPropertiesSpy.and.stub();
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
expect(contentMetadataService.getGroupedProperties).toHaveBeenCalledWith(expectedNode, presetConfig);
@@ -813,7 +843,6 @@ describe('ContentMetadataComponent', () => {
it('should revert reload properties for general info panel on cancel', () => {
component.readOnly = false;
fixture.detectChanges();
spyOn(contentMetadataService, 'getBasicProperties');
toggleEditModeForGeneralInfo();
findCancelButton().click();
@@ -1002,6 +1031,7 @@ describe('ContentMetadataComponent', () => {
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
await fixture.whenStable();
await component.groupedProperties$.toPromise();
fixture.detectChanges();
@@ -1022,6 +1052,7 @@ describe('ContentMetadataComponent', () => {
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
await fixture.whenStable();
await component.groupedProperties$.toPromise();
fixture.detectChanges();
@@ -1042,6 +1073,7 @@ describe('ContentMetadataComponent', () => {
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
await fixture.whenStable();
await component.groupedProperties$.toPromise();
fixture.detectChanges();
@@ -1063,6 +1095,7 @@ describe('ContentMetadataComponent', () => {
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
await fixture.whenStable();
await component.groupedProperties$.toPromise();
fixture.detectChanges();
@@ -1083,6 +1116,7 @@ describe('ContentMetadataComponent', () => {
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
await fixture.whenStable();
await component.groupedProperties$.toPromise();
fixture.detectChanges();
@@ -1107,6 +1141,7 @@ describe('ContentMetadataComponent', () => {
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
await fixture.whenStable();
await component.groupedProperties$.toPromise();
fixture.detectChanges();
@@ -1141,6 +1176,7 @@ describe('ContentMetadataComponent', () => {
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
fixture.detectChanges();
await fixture.whenStable();
await component.groupedProperties$.toPromise();
fixture.detectChanges();
@@ -1640,4 +1676,136 @@ describe('ContentMetadataComponent', () => {
expect(customComponent).toBeDefined();
});
});
describe('Predictions', () => {
const getMockPrediction = (reviewStatus: ReviewStatus): Prediction => ({
confidenceLevel: 0.9,
predictionDateTime: new Date(2024, 1, 1),
modelId: 'test-model-id',
property: 'test:test',
id: 'test-prediction-id',
previousValue: 'previous value',
predictionValue: 'new value',
updateType: 'AUTOCORRECT',
reviewStatus: reviewStatus
});
const getMockPredictionPaging = (predictions: Prediction[]): PredictionPaging => ({
list: {
entries: predictions.map(prediction => ({entry: prediction}))
}
});
let getPredictionsSpy: jasmine.Spy;
beforeEach(() => {
component.node = node;
component.displayPredictions = true;
getPredictionsSpy = spyOn(predictionService, 'getPredictions').and.returnValue(of(getMockPredictionPaging([getMockPrediction(ReviewStatus.UNREVIEWED)])));
fixture.detectChanges();
});
it('should load predictions when displayPredictions is true', () => {
component.ngOnInit();
expect(predictionService.getPredictions).toHaveBeenCalledWith(node.id);
});
it('should map predictions to basic properties', (done) => {
getBasicPropertiesSpy.and.returnValue(
of([
{
key: 'properties.test:test',
editable: true,
value: 'new value',
title: 'test'
}
])
);
component.ngOnInit();
component.basicProperties$.subscribe(properties => {
expect(properties[0].prediction).toEqual(getMockPrediction(ReviewStatus.UNREVIEWED));
done();
});
});
it('should map predictions to grouped properties', (done) => {
getGroupedPropertiesSpy.and.returnValue(
of([
{
editable: true,
title: 'test',
properties: [{
key: 'properties.test:test',
editable: true,
value: 'new value',
title: 'test'
}]
}
])
);
component.ngOnInit();
component.groupedProperties$.subscribe(properties => {
expect(properties[0].properties[0].prediction).toEqual(getMockPrediction(ReviewStatus.UNREVIEWED));
done();
});
});
it('should not map predictions when reviewStatus other then UNREVIEWED', (done) => {
getPredictionsSpy.and.returnValue(of(getMockPredictionPaging([getMockPrediction(ReviewStatus.REJECTED), getMockPrediction(ReviewStatus.CONFIRMED)])));
getBasicPropertiesSpy.and.returnValue(
of([
{
key: 'properties.test:test',
editable: true,
value: 'new value',
title: 'test'
}
])
);
component.ngOnInit();
component.basicProperties$.subscribe(properties => {
expect(properties[0].prediction).toBeNull();
done();
});
});
it('should not map predictions to properties if the property value is different from the prediction value', (done) => {
getBasicPropertiesSpy.and.returnValue(
of([
{
key: 'properties.test:test',
editable: true,
value: 'different value',
title: 'test'
}
])
);
component.ngOnInit();
component.basicProperties$.subscribe(properties => {
expect(properties[0].prediction).toBeNull();
done();
});
});
it('should set updated node when prediction status has changed', () => {
const updatedNode = {...node, name: 'new test name'};
const getNodeSpy = spyOn(nodesApiService, 'getNode').and.returnValue(of(updatedNode));
component.ngOnInit();
predictionService.predictionStatusUpdated$.next({key: 'test:test', previousValue: 'previous value'});
expect(getNodeSpy).toHaveBeenCalledWith(node.id);
expect(component.node).toEqual(updatedNode);
});
it('should call onPredictionStatusChanged when prediction status has changed', () => {
const onPredictionStatusChangedSpy = spyOn(updateService, 'onPredictionStatusChanged');
const notification = {key: 'test:test', previousValue: 'previous value'};
component.ngOnInit();
predictionService.predictionStatusUpdated$.next(notification);
expect(onPredictionStatusChangedSpy).toHaveBeenCalledWith([notification]);
});
});
});

View File

@@ -16,7 +16,7 @@
*/
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Category, CategoryEntry, CategoryLinkBody, CategoryPaging, Node, TagBody, TagEntry, TagPaging } from '@alfresco/js-api';
import { Category, CategoryEntry, CategoryLinkBody, CategoryPaging, Node, TagBody, TagEntry, TagPaging, Prediction, ReviewStatus } from '@alfresco/js-api';
import { forkJoin, Observable, of, Subject, zip } from 'rxjs';
import {
AppConfigService,
@@ -24,6 +24,7 @@ import {
CardViewItem,
CardViewModule,
NotificationService,
PredictionService,
TranslationService,
UpdateNotification
} from '@alfresco/adf-core';
@@ -89,14 +90,14 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
/** Toggles whether to display empty values in the card view */
@Input()
displayEmpty: boolean = false;
displayEmpty = false;
/**
* Toggles between expanded (ie, full information) and collapsed
* (ie, reduced information) in the display
*/
@Input()
expanded: boolean = false;
expanded = false;
/** The multi parameter of the underlying material expansion panel, set to true to allow multi accordion to be expanded at the same time */
@Input()
@@ -108,19 +109,23 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
/** Toggles whether the metadata properties should be shown */
@Input()
displayDefaultProperties: boolean = true;
displayDefaultProperties = true;
/** (optional) shows the given aspect in the expanded card */
@Input()
displayAspect: string = null;
/** Toggles whether or not to enable copy to clipboard action. */
/** Toggles whether to enable copy to clipboard action. */
@Input()
copyToClipboardAction: boolean = true;
copyToClipboardAction = true;
/** Toggles whether or not to enable chips for multivalued properties. */
/** Toggles whether AI predictions should be shown. */
@Input()
useChipsForMultiValueProperty: boolean = true;
displayPredictions = false;
/** Toggles whether to enable chips for multivalued properties. */
@Input()
useChipsForMultiValueProperty = true;
/** True if tags should be displayed, false otherwise */
@Input()
@@ -176,7 +181,8 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
private tagService: TagService,
private categoryService: CategoryService,
private contentService: ContentService,
private notificationService: NotificationService
private notificationService: NotificationService,
private predictionService: PredictionService
) {
this.copyToClipboardAction = this.appConfig.get<boolean>('content-metadata.copy-to-clipboard-action');
this.multiValueSeparator = this.appConfig.get<string>('content-metadata.multi-value-pipe-separator') || DEFAULT_SEPARATOR;
@@ -197,6 +203,15 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
this.loadProperties(node);
});
if (this.displayPredictions) {
this.predictionService.predictionStatusUpdated$.pipe(takeUntil(this.onDestroy$)).subscribe(({key, previousValue}) => {
this.cardViewContentUpdateService.onPredictionStatusChanged([{key, previousValue}]);
this.nodesApiService.getNode(this.node.id).subscribe((node) => {
Object.assign(this.node, node);
});
});
}
this.loadProperties(this.node);
this.verifyAllowableOperations();
@@ -407,6 +422,9 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
)
.subscribe((result: any) => {
if (result) {
if (this.displayPredictions) {
this.cardViewContentUpdateService.onPredictionStatusChanged(Object.keys(this.changedProperties['properties']).map(key => ({key})));
}
this.updateUndefinedNodeProperties(result.updatedNode);
if (this.hasContentTypeChanged(this.changedProperties)) {
this.cardViewContentUpdateService.updateNodeAspect(this.node);
@@ -439,13 +457,37 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
private loadProperties(node: Node, loadBasicProps = true, loadGroupedProps = true, loadTags = true, loadCategories = true) {
if (node) {
const requests = {};
if (loadBasicProps) {
this.basicProperties$ = this.getProperties(node);
requests['properties'] = this.getProperties(node);
}
if (loadGroupedProps) {
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(node, this.preset);
requests['groupedProperties'] = this.contentMetadataService.getGroupedProperties(node, this.preset);
}
if (this.displayPredictions) {
requests['predictions'] = this.loadPredictionsForNode(this.node.id);
}
forkJoin(requests).subscribe(({ predictions, properties, groupedProperties }: ({ predictions: Prediction[]; properties: CardViewItem[]; groupedProperties: CardViewGroup[] })) => {
if (loadBasicProps && properties) {
this.basicProperties$ = predictions
? of(properties.map(property => this.mapPredictionsToProperty(property, predictions)))
: of(properties);
}
if (loadGroupedProps && groupedProperties) {
this.groupedProperties$ = predictions
? of(groupedProperties.map(group => {
group.properties = group.properties.map(property => this.mapPredictionsToProperty(property, predictions));
return group;
}))
: of(groupedProperties);
}
});
if (this.displayTags && loadTags) {
this.loadTagsForNode(node.id);
}
@@ -462,7 +504,7 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
}
}
private getProperties(node: Node) {
private getProperties(node: Node): Observable<CardViewItem[]> {
const properties$ = this.contentMetadataService.getBasicProperties(node);
const contentTypeProperty$ = this.contentMetadataService.getContentTypeProperty(node);
return zip(properties$, contentTypeProperty$).pipe(
@@ -540,4 +582,18 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
}
return observables;
}
private mapPredictionsToProperty(property: CardViewItem, predictions: Prediction[]): CardViewItem {
const propertyKey = property.key.split('.')[1];
const filteredPrediction = predictions.find(prediction => prediction.property === propertyKey && prediction.reviewStatus === ReviewStatus.UNREVIEWED && prediction.predictionValue === property.value);
property.prediction = filteredPrediction || null;
return property;
}
private loadPredictionsForNode(nodeId: string): Observable<Prediction[]> {
return this.predictionService.getPredictions(nodeId).pipe(
map(predictionPaging => predictionPaging.list.entries.map(predictionEntry => predictionEntry.entry))
);
}
}

View File

@@ -42,7 +42,6 @@ export * from './lib/category/index';
export * from './lib/viewer/index';
export * from './lib/security/index';
export * from './lib/infinite-scroll-datasource';
export * from './lib/prediction/index';
export * from './lib/content.module';
export * from './lib/testing/content.testing.module';

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 8.99987L17.75 6.24987L15 4.99987L17.75 3.74987L19 0.999873L20.25 3.74987L23 4.99987L20.25 6.24987L19 8.99987ZM19 22.9999L17.75 20.2499L15 18.9999L17.75 17.7499L19 14.9999L20.25 17.7499L23 18.9999L20.25 20.2499L19 22.9999ZM8.99998 19.9999L6.49998 14.4999L0.999984 11.9999L6.49998 9.49987L8.99998 3.99987L11.5 9.49987L17 11.9999L11.5 14.4999L8.99998 19.9999ZM8.99998 15.1499L9.99998 12.9999L12.15 11.9999L9.99998 10.9999L8.99998 8.84987L7.99998 10.9999L5.84998 11.9999L7.99998 12.9999L8.99998 15.1499Z" fill="#212328" fill-opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 653 B

View File

@@ -43,6 +43,7 @@ import { CardViewKeyValuePairsItemComponent } from './components/card-view-keyva
import { CardViewSelectItemComponent } from './components/card-view-selectitem/card-view-selectitem.component';
import { CardViewArrayItemComponent } from './components/card-view-arrayitem/card-view-arrayitem.component';
import { SelectFilterInputComponent } from './components/card-view-selectitem/select-filter-input/select-filter-input.component';
import { ContentEnrichmentMenuComponent } from '../prediction/components/content-enrichment-menu/content-enrichment-menu.component';
@NgModule({
imports: [
@@ -63,7 +64,8 @@ import { SelectFilterInputComponent } from './components/card-view-selectitem/se
MatCardModule,
MatDatetimepickerModule,
MatNativeDatetimeModule,
MatSlideToggleModule
MatSlideToggleModule,
ContentEnrichmentMenuComponent
],
declarations: [
CardViewComponent,

View File

@@ -40,6 +40,18 @@ export abstract class BaseCardView<T extends CardViewItem> implements OnDestroy
this.property.value = itemModel.value;
}
});
this.cardViewUpdateService.predictionStatusChanged$.pipe(takeUntil(this.destroy$)).subscribe((items) => {
items.map(item => {
if (this.property.key.split('.')[1] === item.key) {
this.property.prediction = null;
if (item.previousValue !== null && item.previousValue !== undefined) {
this.property.value = item.previousValue;
}
}
});
});
}
get isEditable(): boolean {

View File

@@ -1,13 +1,16 @@
<ng-container *ngIf="!property.isEmpty() || isEditable">
<div class="adf-property-value">
<mat-checkbox [attr.data-automation-id]="'card-boolean-' + property.key"
[attr.title]="'CORE.METADATA.ACTIONS.TOGGLE' | translate"
[checked]="property.displayValue"
[disabled]="!isEditable"
color="primary"
(change)="changed($event)">
<div [attr.data-automation-id]="'card-boolean-label-' + property.key"
class="adf-property-label">{{ property.label | translate }}</div>
</mat-checkbox>
<div class="adf-property-value-wrapper">
<adf-content-enrichment-menu matPrefix *ngIf="hasContentEnrichment" [prediction]="property.prediction"></adf-content-enrichment-menu>
<div class="adf-property-value">
<mat-checkbox [attr.data-automation-id]="'card-boolean-' + property.key"
[attr.title]="'CORE.METADATA.ACTIONS.TOGGLE' | translate"
[checked]="property.displayValue"
[disabled]="!isEditable"
color="primary"
(change)="changed($event)">
<div [attr.data-automation-id]="'card-boolean-label-' + property.key"
class="adf-property-label">{{ property.label | translate }}</div>
</mat-checkbox>
</div>
</div>
</ng-container>

View File

@@ -25,10 +25,12 @@ import { BaseCardView } from '../base-card-view';
templateUrl: './card-view-boolitem.component.html',
styles: [
`
.adf-property-value {
padding: 15px 0;
}
`
.adf-property-value {
padding: 15px 0;
display: flex;
align-content: center;
}
`
]
})
@@ -36,6 +38,9 @@ export class CardViewBoolItemComponent extends BaseCardView<CardViewBoolItemMode
@Input()
editable: boolean;
@Input()
hasContentEnrichment = false;
changed(change: MatCheckboxChange) {
this.cardViewUpdateService.update({ ...this.property } as CardViewBoolItemModel, change.checked );
this.property.value = change.checked;

View File

@@ -8,108 +8,62 @@
>
{{ property.label | translate }}
</label>
<div class="adf-property-value" [ngClass]="{ 'adf-property-value-editable': editable, 'adf-property-readonly-value': isReadonlyProperty }">
<span *ngIf="!isEditable && !property.multivalued" [attr.data-automation-id]="'card-' + property.type + '-value-' + property.key">
<span
*ngIf="showProperty"
[attr.data-automation-id]="'card-dateitem-' + property.key"
(dblclick)="copyToClipboard(property.displayValue)"
matTooltipShowDelay="1000"
[title]="'CORE.METADATA.ACTIONS.COPY_TO_CLIPBOARD' | translate"
>{{ property.displayValue }}</span
>
</span>
<div *ngIf="isEditable && !property.multivalued" class="adf-dateitem-editable">
<div class="adf-dateitem-editable-controls">
<div class="adf-property-value-wrapper">
<adf-content-enrichment-menu matPrefix *ngIf="hasContentEnrichment" [prediction]="property.prediction"></adf-content-enrichment-menu>
<div class="adf-property-value" [ngClass]="{ 'adf-property-value-editable': editable, 'adf-property-readonly-value': isReadonlyProperty }">
<span *ngIf="!isEditable && !property.multivalued" [attr.data-automation-id]="'card-' + property.type + '-value-' + property.key">
<span
class="adf-datepicker-toggle"
[attr.data-automation-id]="'datepicker-label-toggle-' + property.key"
(click)="showDatePicker()"
tabindex="0"
role="button"
(keyup.enter)="showDatePicker()"
*ngIf="showProperty"
[attr.data-automation-id]="'card-dateitem-' + property.key"
(dblclick)="copyToClipboard(property.displayValue)"
matTooltipShowDelay="1000"
[title]="'CORE.METADATA.ACTIONS.COPY_TO_CLIPBOARD' | translate"
>{{ property.displayValue }}</span
>
<span *ngIf="showProperty; else elseEmptyValueBlock" [attr.data-automation-id]="'card-' + property.type + '-value-' + property.key">
{{ property.displayValue }}</span
</span>
<div *ngIf="isEditable && !property.multivalued" class="adf-dateitem-editable">
<div class="adf-dateitem-editable-controls">
<span
class="adf-datepicker-toggle"
[attr.data-automation-id]="'datepicker-label-toggle-' + property.key"
(click)="showDatePicker()"
tabindex="0"
role="button"
(keyup.enter)="showDatePicker()"
>
</span>
<span *ngIf="showProperty; else elseEmptyValueBlock" [attr.data-automation-id]="'card-' + property.type + '-value-' + property.key">
{{ property.displayValue }}</span
>
</span>
<mat-icon
*ngIf="showClearAction"
class="adf-date-reset-icon"
(click)="onDateClear()"
[attr.title]="'CORE.METADATA.ACTIONS.CLEAR' | translate"
[attr.data-automation-id]="'datepicker-date-clear-' + property.key"
>
clear
</mat-icon>
<mat-icon
*ngIf="showClearAction"
class="adf-date-reset-icon"
(click)="onDateClear()"
[attr.title]="'CORE.METADATA.ACTIONS.CLEAR' | translate"
[attr.data-automation-id]="'datepicker-date-clear-' + property.key"
>
clear
</mat-icon>
<mat-datetimepicker-toggle
[attr.tabindex]="-1"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker"
>
</mat-datetimepicker-toggle>
</div>
<mat-datetimepicker-toggle
[attr.tabindex]="-1"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker"
>
</mat-datetimepicker-toggle>
</div>
<input
class="adf-invisible-date-input"
[attr.tabIndex]="-1"
[matDatetimepicker]="datetimePicker"
[value]="valueDate"
(dateChange)="onDateChanged($event)"
[attr.id]="'card-view-dateitem-' + property.key"
/>
<mat-datetimepicker
#datetimePicker
[type]="$any(property).type"
[timeInterval]="5"
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate"
>
</mat-datetimepicker>
</div>
<ng-template #elseEmptyValueBlock>
{{ property.default | translate }}
</ng-template>
<div *ngIf="property.multivalued"
class="adf-property-field adf-dateitem-chip-list-container adf-dateitem-editable">
<mat-chip-listbox #chipList class="adf-textitem-chip-list">
<mat-chip-option
*ngFor="let propertyValue of property.displayValue; let idx = index"
[removable]="isEditable"
(removed)="removeValueFromList(idx)">
{{ propertyValue }}
<mat-icon *ngIf="isEditable" matChipRemove>cancel</mat-icon>
</mat-chip-option>
</mat-chip-listbox>
<div
*ngIf="isEditable"
class="adf-property-field adf-dateitem-editable-controls"
(click)="showDatePicker()"
tabindex="0"
role="button"
(keyup.enter)="showDatePicker()"
>
<input
class="adf-invisible-date-input"
[attr.tabIndex]="-1"
[matDatetimepicker]="datetimePicker"
(dateChange)="addDateToList($event)"
[value]="valueDate"
(dateChange)="onDateChanged($event)"
[attr.id]="'card-view-dateitem-' + property.key"
/>
<mat-datetimepicker-toggle
[attr.tabindex]="-1"
matSuffix
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker"
>
</mat-datetimepicker-toggle>
<mat-datetimepicker
#datetimePicker
[type]="$any(property).type"
@@ -119,5 +73,54 @@
>
</mat-datetimepicker>
</div>
<ng-template #elseEmptyValueBlock>
{{ property.default | translate }}
</ng-template>
<div *ngIf="property.multivalued" class="adf-property-field adf-dateitem-chip-list-container adf-dateitem-editable">
<mat-chip-list #chipList class="adf-textitem-chip-list">
<mat-chip
*ngFor="let propertyValue of property.displayValue; let idx = index"
[removable]="isEditable"
(removed)="removeValueFromList(idx)"
>
{{ propertyValue }}
<mat-icon *ngIf="isEditable" matChipRemove>cancel</mat-icon>
</mat-chip>
</mat-chip-list>
<div
*ngIf="isEditable"
class="adf-property-field adf-dateitem-editable-controls"
(click)="showDatePicker()"
tabindex="0"
role="button"
(keyup.enter)="showDatePicker()"
>
<input
class="adf-invisible-date-input"
[attr.tabIndex]="-1"
[matDatetimepicker]="datetimePicker"
(dateChange)="addDateToList($event)"
[attr.id]="'card-view-dateitem-' + property.key"
/>
<mat-datetimepicker-toggle
[attr.tabindex]="-1"
matSuffix
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker"
>
</mat-datetimepicker-toggle>
<mat-datetimepicker
#datetimePicker
[type]="$any(property).type"
[timeInterval]="5"
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate"
>
</mat-datetimepicker>
</div>
</div>
</div>
</div>

View File

@@ -49,6 +49,9 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
@Input()
displayClearAction = true;
@Input()
hasContentEnrichment = false;
@ViewChild('datetimePicker')
public datepicker: MatDatetimepickerComponent<any>;

View File

@@ -32,27 +32,30 @@ export class CardViewItemDispatcherComponent implements OnChanges {
editable: boolean;
@Input()
displayEmpty: boolean = true;
displayEmpty = true;
@Input()
displayNoneOption: boolean = true;
displayNoneOption = true;
@Input()
displayClearAction: boolean = true;
displayClearAction = true;
@Input()
copyToClipboardAction: boolean = true;
copyToClipboardAction = true;
@Input()
useChipsForMultiValueProperty: boolean = true;
useChipsForMultiValueProperty = true;
@Input()
multiValueSeparator: string = DEFAULT_SEPARATOR;
multiValueSeparator = DEFAULT_SEPARATOR;
@Input()
displayLabelForChips: boolean = false;
displayLabelForChips = false;
private loaded: boolean = false;
@Input()
hasContentEnrichment = false;
private loaded = false;
private componentReference: any = null;
public ngOnInit;
@@ -104,6 +107,7 @@ export class CardViewItemDispatcherComponent implements OnChanges {
this.componentReference.instance.useChipsForMultiValueProperty = this.useChipsForMultiValueProperty;
this.componentReference.instance.multiValueSeparator = this.multiValueSeparator;
this.componentReference.instance.displayLabelForChips = this.displayLabelForChips;
this.componentReference.instance.hasContentEnrichment = this.hasContentEnrichment;
}
private proxy(methodName, ...args) {

View File

@@ -12,9 +12,12 @@
*ngIf="!isEditable"
class="adf-property-value adf-property-read-only"
[attr.data-automation-id]="'select-readonly-value-' + property.key"
data-automation-class="read-only-value">{{ (property.displayValue | async) | translate }}
data-automation-class="read-only-value">
<adf-content-enrichment-menu *ngIf="hasContentEnrichment" [prediction]="property.prediction" class="adf-content-enrichment-trigger"></adf-content-enrichment-menu>
{{ (property.displayValue | async) | translate }}
</div>
<div *ngIf="isEditable">
<div *ngIf="isEditable" class="adf-property-editable">
<adf-content-enrichment-menu matPrefix *ngIf="hasContentEnrichment" [prediction]="property.prediction"></adf-content-enrichment-menu>
<mat-form-field class="adf-property-value" [ngClass]="{'adf-property-value-editable': isEditable}">
<mat-select
[(value)]="value"

View File

@@ -47,6 +47,18 @@
padding: 6px 0;
border-bottom: 1px solid var(--adf-metadata-property-panel-border-color);
color: var(--adf-metadata-property-panel-title-color);
adf-content-enrichment-menu.adf-content-enrichment-trigger {
display: inline-block;
height: 17px;
margin-left: -3px;
margin-right: -7px;
}
}
.adf-property-editable {
display: flex;
align-items: center;
}
.mdc-line-ripple {

View File

@@ -38,10 +38,13 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
@Input() options$: Observable<CardViewSelectItemOption<string | number>[]>;
@Input()
displayNoneOption: boolean = true;
displayNoneOption = true;
@Input()
displayEmpty: boolean = true;
displayEmpty = true;
@Input()
hasContentEnrichment = false;
value: string | number;
filter$ = new BehaviorSubject<string>('');
@@ -50,6 +53,7 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
ngOnChanges(): void {
this.value = this.property.value;
this.property.setValue(this.property.value);
}
ngOnInit() {

View File

@@ -37,6 +37,7 @@
[attr.data-automation-id]="'card-textitem-value-' + property.key"
(keydown)="undoText($event)"
/>
<adf-content-enrichment-menu matPrefix *ngIf="hasContentEnrichment" [prediction]="property.prediction"></adf-content-enrichment-menu>
<textarea
matInput
*ngIf="property.multiline"
@@ -119,6 +120,7 @@
>
{{ property.label | translate }}
</mat-label>
<!-- <adf-content-enrichment-menu matPrefix *ngIf="hasContentEnrichment && !isEditable" [prediction]="property.prediction"></adf-content-enrichment-menu>-->
<input
matInput
[type]="property.inputType"

View File

@@ -57,6 +57,9 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
@Input()
displayLabelForChips = false;
@Input()
hasContentEnrichment = false;
editedValue: string | string[];
errors: CardViewItemValidator[];
templateType: string;

View File

@@ -10,7 +10,8 @@
[copyToClipboardAction]="copyToClipboardAction"
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty"
[multiValueSeparator]="multiValueSeparator"
[displayLabelForChips]="displayLabelForChips">
[displayLabelForChips]="displayLabelForChips"
[hasContentEnrichment]="!!property.prediction">
</adf-card-view-item-dispatcher>
</div>
</div>

View File

@@ -24,6 +24,16 @@
margin-bottom: 12px;
}
.adf-property-value-wrapper {
display: flex;
align-items: center;
border-bottom: 1px solid var(--adf-metadata-property-panel-border-color);
adf-content-enrichment-menu button.adf-ai-button {
margin-left: -10px;
}
}
.adf-property {
.adf-property-field {
width: 100%;
@@ -48,6 +58,7 @@
#{$mat-form--text-field-infix} {
border-top-width: 0;
position: initial;
}
#{$mat-form-field-flex} {

View File

@@ -19,13 +19,16 @@ import { Subject } from 'rxjs';
import { CardViewBaseItemModel } from '../models/card-view-baseitem.model';
import { UpdateNotification } from './update-notification.interface';
import { ClickNotification } from './click-notification.interface';
import { PredictionStatusUpdate } from '../../prediction';
export interface BaseCardViewUpdate {
itemUpdated$: Subject<UpdateNotification>;
itemClicked$: Subject<ClickNotification>;
updateItem$: Subject<CardViewBaseItemModel>;
predictionStatusChanged$: Subject<PredictionStatusUpdate[]>;
update(property: CardViewBaseItemModel, newValue: any);
clicked(property: CardViewBaseItemModel);
updateElement(notification: CardViewBaseItemModel);
onPredictionStatusChanged(notification: PredictionStatusUpdate[]);
}

View File

@@ -16,6 +16,7 @@
*/
import { CardViewItemValidator } from './card-view-item-validator.interface';
import { Prediction } from '@alfresco/js-api';
export interface CardViewItemProperties {
label: string;
@@ -36,4 +37,5 @@ export interface CardViewItemProperties {
parameters?: { [key: string]: any };
}>;
multivalued?: boolean;
prediction?: Prediction;
}

View File

@@ -15,6 +15,8 @@
* limitations under the License.
*/
import { Prediction } from '@alfresco/js-api';
export interface CardViewItem {
label: string;
value: any;
@@ -24,4 +26,5 @@ export interface CardViewItem {
displayValue: any;
editable?: boolean;
icon?: string;
prediction?: Prediction;
}

View File

@@ -17,6 +17,7 @@
import { CardViewItemProperties, CardViewItemValidator } from '../interfaces/card-view.interfaces';
import validatorsMap from '../validators/validators.map';
import { Prediction } from '@alfresco/js-api';
export abstract class CardViewBaseItemModel<T = any> {
label: string;
@@ -31,6 +32,7 @@ export abstract class CardViewBaseItemModel<T = any> {
data?: any;
type?: string;
multivalued?: boolean;
prediction?: Prediction;
constructor(props: CardViewItemProperties) {
this.label = props.label || '';
@@ -44,6 +46,7 @@ export abstract class CardViewBaseItemModel<T = any> {
this.validators = props.validators || [];
this.data = props.data || null;
this.multivalued = !!props.multivalued;
this.prediction = props.prediction || null;
if (props?.constraints?.length ?? 0) {
for (const constraint of props.constraints) {

View File

@@ -57,5 +57,14 @@ describe('CardViewSelectItemModel', () => {
expect(itemModel.displayNoneOption).toBe(false);
}));
it('should update the value when new value is set', (done) => {
const itemModel = new CardViewSelectItemModel(properties);
itemModel.setValue('three');
itemModel.displayValue.subscribe((value) => {
expect(value).toBe(mockData[2].label);
done();
});
});
});
});

View File

@@ -19,8 +19,8 @@ import { CardViewItem } from '../interfaces/card-view-item.interface';
import { DynamicComponentModel } from '../../common/services/dynamic-component-mapper.service';
import { CardViewBaseItemModel } from './card-view-baseitem.model';
import { CardViewSelectItemProperties, CardViewSelectItemOption } from '../interfaces/card-view.interfaces';
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
export class CardViewSelectItemModel<T> extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel {
type = 'select';
@@ -29,6 +29,8 @@ export class CardViewSelectItemModel<T> extends CardViewBaseItemModel implements
valueFetch$: Observable<string> = null;
private valueSubject = new BehaviorSubject<any>(this.value);
constructor(cardViewSelectItemProperties: CardViewSelectItemProperties<T>) {
super(cardViewSelectItemProperties);
@@ -36,11 +38,13 @@ export class CardViewSelectItemModel<T> extends CardViewBaseItemModel implements
this.options$ = cardViewSelectItemProperties.options$;
this.valueFetch$ = this.options$.pipe(
switchMap((options) => {
const option = options.find((o) => o.key === this.value?.toString());
return of(option ? option.label : '');
})
this.valueFetch$ = this.valueSubject.pipe(
switchMap((value) => this.options$.pipe(
map((options) => {
const option = options.find((o) => o.key === value?.toString());
return option ? option.label : '';
})
))
);
}
@@ -50,5 +54,6 @@ export class CardViewSelectItemModel<T> extends CardViewBaseItemModel implements
setValue(value: any) {
this.value = value;
this.valueSubject.next(value);
}
}

View File

@@ -18,6 +18,7 @@
import { fakeAsync, TestBed } from '@angular/core/testing';
import { CardViewBaseItemModel } from '../models/card-view-baseitem.model';
import { CardViewUpdateService, transformKeyToObject } from './card-view-update.service';
import { PredictionStatusUpdate } from '@alfresco/adf-core';
describe('CardViewUpdateService', () => {
@@ -82,5 +83,14 @@ describe('CardViewUpdateService', () => {
);
cardViewUpdateService.clicked(property);
}));
it('should emit predictionStatusChanged$ when onPredictionStatusChanged is called', (done) => {
const mockPredictionStatusUpdate: PredictionStatusUpdate[] = [{ key: 'test', previousValue: 'value' }];
cardViewUpdateService.predictionStatusChanged$.subscribe((value) => {
expect(value).toEqual(mockPredictionStatusUpdate);
done();
});
cardViewUpdateService.onPredictionStatusChanged(mockPredictionStatusUpdate);
});
});
});

View File

@@ -21,6 +21,7 @@ import { BaseCardViewUpdate } from '../interfaces/base-card-view-update.interfac
import { ClickNotification } from '../interfaces/click-notification.interface';
import { UpdateNotification } from '../interfaces/update-notification.interface';
import { CardViewBaseItemModel } from '../models/card-view-baseitem.model';
import { PredictionStatusUpdate } from '../../prediction/interfaces/prediction-status-update.interface';
export const transformKeyToObject = (key: string, value): any => {
const objectLevels: string[] = key.split('.').reverse();
@@ -36,6 +37,7 @@ export class CardViewUpdateService implements BaseCardViewUpdate {
itemUpdated$ = new Subject<UpdateNotification>();
itemClicked$ = new Subject<ClickNotification>();
updateItem$ = new Subject<CardViewBaseItemModel>();
predictionStatusChanged$ = new Subject<PredictionStatusUpdate[]>();
update(property: CardViewBaseItemModel, newValue: any) {
this.itemUpdated$.next({
@@ -59,4 +61,7 @@ export class CardViewUpdateService implements BaseCardViewUpdate {
this.updateItem$.next(notification);
}
onPredictionStatusChanged(notification: PredictionStatusUpdate[]) {
this.predictionStatusChanged$.next(notification);
}
}

View File

@@ -158,7 +158,8 @@ export class ThumbnailService {
'save-as': './assets/images/save-as.svg',
save: './assets/images/save.svg',
task: './assets/images/task.svg',
'multipart/related': './assets/images/ft_ic_website.svg'
'multipart/related': './assets/images/ft_ic_website.svg',
'ai-sparkles': './assets/images/ai-sparkles.svg'
};
constructor(matIconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {

View File

@@ -248,6 +248,19 @@
"DATEPICKER": "Use the arrow keys to navigate between dates. Up and down move to the next or previous week but on the same day. Left and right move to the next or previous day. Press Enter or Return to select a date.",
"COPY_TO_CLIPBOARD_MESSAGE": "Value copied to clipboard",
"EDIT_ASPECTS": "Edit Aspect"
},
"CONTENT_ENRICHMENT": {
"ACCURACY_LEVEL": "Accuracy Level",
"ACCURACY_DESCRIPTION": "This score represents the quality of the suggestion.",
"LAST_VALUE": "Last user value",
"CURRENT_VALUE": "Current Value",
"ACTIONS": {
"REVERT": "Revert",
"CONFIRM": "Confirm"
},
"ARIA-LABEL": {
"OPEN-MENU": "Open content enrichment menu"
}
}
},
"SEARCH": {

View File

@@ -0,0 +1,49 @@
<button mat-icon-button [attr.aria-label]="'CORE.METADATA.CONTENT_ENRICHMENT.ACTIONS.CONFIRM' | translate" #menuTrigger="matMenuTrigger" class="adf-ai-button" [matMenuTriggerFor]="menu" (menuOpened)="onMenuOpen()">
<adf-icon [value]="'adf:ai-sparkles'"></adf-icon>
</button>
<mat-menu #menu="matMenu" class="adf-ai-mat-menu" (closed)="onClosed()">
<div class="adf-content-enrichment-menu" #menuContainer (click)="$event.stopPropagation()" (keydown.tab)="$event.stopPropagation()" (keydown.shift.tab)="$event.stopPropagation()" tabindex="-1">
<div class="adf-content-enrichment-menu__accuracy">
<div class="adf-content-enrichment-menu__accuracy__title">
<span>{{ 'CORE.METADATA.CONTENT_ENRICHMENT.ACCURACY_LEVEL' | translate }}</span>
<p>{{ 'CORE.METADATA.CONTENT_ENRICHMENT.ACCURACY_DESCRIPTION' | translate }}</p>
</div>
<div class="adf-content-enrichment-menu__accuracy__level">
<span>{{ confidencePercentage }}</span>
<mat-progress-spinner
class="adf-accuracy-level-spinner"
diameter="49"
title="{{ 'CORE.METADATA.CONTENT_ENRICHMENT.ACCURACY_LEVEL' | translate }}"
color="primary"
mode="determinate"
[value]="confidencePercentage">
</mat-progress-spinner>
</div>
</div>
<div class="adf-content-enrichment-menu__content">
<div class="adf-content-enrichment-menu__content__last">
<p class="adf-content-enrichment-menu__content__label">{{ 'CORE.METADATA.CONTENT_ENRICHMENT.LAST_VALUE' | translate }}</p>
<div class="adf-content-enrichment-menu__content__last__value">{{ previousValue || 'None' }}</div>
</div>
<mat-icon>arrow_downward</mat-icon>
<div class="adf-content-enrichment-menu__content__current">
<p class="adf-content-enrichment-menu__content__label">{{ 'CORE.METADATA.CONTENT_ENRICHMENT.CURRENT_VALUE' | translate }}
<span>({{ predictionDateTime | adfLocalizedDate: 'MM/dd/yyyy' }})</span>
</p>
<div class="adf-content-enrichment-menu__content__current__value">
<adf-icon [value]="'adf:ai-sparkles'"></adf-icon>
<span>{{ predictionValue }}</span>
</div>
</div>
</div>
<div class="adf-content-enrichment-menu__footer">
<button mat-flat-button (click)="onRevert()">
{{ 'CORE.METADATA.CONTENT_ENRICHMENT.ACTIONS.REVERT' | translate }}
</button>
<button mat-button color="primary" (click)="onConfirm()" class="adf-confirm-button">
{{ 'CORE.METADATA.CONTENT_ENRICHMENT.ACTIONS.CONFIRM' | translate }}
</button>
</div>
</div>
</mat-menu>

View File

@@ -0,0 +1,136 @@
adf-content-enrichment-menu {
svg {
height: 17px;
width: 17px;
}
}
.adf-ai-mat-menu {
border: 1px solid transparent;
border-radius: 6px;
background-image: linear-gradient(var(--adf-theme-background-dialog-color), var(--adf-theme-background-dialog-color)),
var(--adf-ai-border-gradient);
background-origin: border-box;
background-clip: content-box, border-box;
}
.adf-content-enrichment-menu {
padding: 5px 10px;
&__accuracy {
display: flex;
align-items: center;
margin-bottom: 18px;
justify-content: space-between;
&__title {
span {
font-size: 14px;
font-weight: 600;
line-height: 16px;
letter-spacing: 0.5px;
text-align: left;
}
p {
font-size: 12px;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.5px;
text-align: left;
margin: 0;
}
}
&__level {
display: flex;
align-items: center;
span {
width: 0;
position: relative;
left: 14px;
font-size: 17.96px;
font-weight: 700;
line-height: 25.66px;
letter-spacing: 0.1924px;
}
.adf-accuracy-level-spinner circle {
stroke: var(--theme-accent-color);
}
}
}
&__content {
padding: 18px 10px;
border: 1px solid var(--adf-metadata-property-panel-border-color);
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
&__label {
font-size: 14px;
font-weight: 600;
line-height: 20px;
letter-spacing: 0.25px;
text-align: left;
margin: 0 0 4px;
span {
font-size: 10px;
font-weight: 500;
line-height: 16px;
letter-spacing: 0.2px;
text-align: left;
}
}
&__last {
width: 100%;
&__value {
background: var(--adf-theme-foreground-text-color-007);
border-radius: 6px;
padding: 7px;
min-height: 19px;
}
}
&__current {
width: 100%;
&__value {
padding: 7px;
border: 2px dashed var(--adf-metadata-property-panel-border-color);
border-radius: 6px;
display: flex;
align-items: center;
mat-icon {
width: 17px;
height: 17px;
margin-right: 10px;
}
}
}
mat-icon {
color: var(--adf-theme-foreground-text-color-054);
}
}
&__footer {
margin-top: 18px;
text-align: end;
.adf-confirm-button{
margin-left: 8px;
color: var(--adf-theme-foreground-base-color);
background-color: var(--theme-grey-text-background-color);
}
}
}

View File

@@ -0,0 +1,172 @@
/*!
* @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.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs';
import { ContentEnrichmentMenuComponent } from './content-enrichment-menu.component';
import { PredictionService } from '../../services';
import { CoreTestingModule } from '../../../testing/core.testing.module';
import { LocalizedDatePipe } from '../../../pipes/localized-date.pipe';
import { Prediction, ReviewStatus } from '@alfresco/js-api';
describe('ContentEnrichmentMenuComponent', () => {
let component: ContentEnrichmentMenuComponent;
let fixture: ComponentFixture<ContentEnrichmentMenuComponent>;
let predictionService: PredictionService;
let localizedDatePipe: LocalizedDatePipe;
const predictionMock: Prediction = {
confidenceLevel: 0.9,
predictionDateTime: new Date(2022, 1, 1),
modelId: 'test-model-id',
property: 'test:test',
id: 'test-prediction-id',
previousValue: 'previous value',
predictionValue: 'new value',
updateType: 'AUTOCORRECT',
reviewStatus: ReviewStatus.UNREVIEWED
};
const openMenu = () => {
const button = fixture.debugElement.query(By.css('.adf-ai-button')).nativeElement;
button.click();
fixture.detectChanges();
};
beforeEach(async () => {
const predictionServiceMock = {
reviewPrediction: jasmine.createSpy('reviewPrediction').and.returnValue(of(null)),
predictionStatusUpdated$: {next: jasmine.createSpy('next')}
};
await TestBed.configureTestingModule({
imports: [ContentEnrichmentMenuComponent, CoreTestingModule, MatProgressSpinnerModule],
providers: [{provide: PredictionService, useValue: predictionServiceMock}]
})
.compileComponents();
fixture = TestBed.createComponent(ContentEnrichmentMenuComponent);
component = fixture.componentInstance;
component.prediction = {...predictionMock};
predictionService = TestBed.inject(PredictionService);
localizedDatePipe = TestBed.inject(LocalizedDatePipe);
fixture.detectChanges();
});
it('should initialize properties when prediction is provided', () => {
expect(component.confidencePercentage).toBe(90);
expect(component.previousValue).toBe('previous value');
expect(component.predictionValue).toBe('new value');
expect(component.predictionDateTime).toEqual(predictionMock.predictionDateTime);
});
it('should initialize properties with default values when prediction is not provided', () => {
component.prediction = null;
component.ngOnInit();
expect(component.confidencePercentage).toBe(0);
expect(component.previousValue).toBe('');
expect(component.predictionValue).toBe('');
expect(component.predictionDateTime).toBeNull();
});
it('should correctly set boolean values', () => {
component.prediction.previousValue = false;
component.prediction.predictionValue = true;
component.ngOnInit();
expect(component.previousValue).toBeFalse();
expect(component.predictionValue).toBeTrue();
});
it('should call reviewPrediction with REJECTED on revert', () => {
component.onRevert();
expect(predictionService.reviewPrediction).toHaveBeenCalledWith(component.prediction.id, ReviewStatus.REJECTED);
});
it('should call reviewPrediction with CONFIRMED on confirm', () => {
component.onConfirm();
expect(predictionService.reviewPrediction).toHaveBeenCalledWith(component.prediction.id, ReviewStatus.CONFIRMED);
});
it('should emit predictionStatusUpdated$ on confirm', () => {
component.onConfirm();
expect(predictionService.predictionStatusUpdated$.next).toHaveBeenCalledWith({key: component.prediction.property});
});
it('should emit predictionStatusUpdated$ on revert', () => {
component.onRevert();
expect(predictionService.predictionStatusUpdated$.next).toHaveBeenCalledWith({
key: component.prediction.property,
previousValue: component.previousValue
});
});
it('should close the menu on revert', () => {
const menuTriggerSpy = spyOn(component.menuTrigger, 'closeMenu');
component.onRevert();
expect(menuTriggerSpy).toHaveBeenCalled();
});
it('should close the menu on confirm', () => {
const menuTriggerSpy = spyOn(component.menuTrigger, 'closeMenu');
component.onConfirm();
expect(menuTriggerSpy).toHaveBeenCalled();
});
it('should open the menu on button click', () => {
spyOn(component, 'onMenuOpen');
openMenu();
expect(component.onMenuOpen).toHaveBeenCalled();
});
it('should correctly format predictionDateTime using adfLocalizedDate', () => {
const formattedDate = localizedDatePipe.transform(component.predictionDateTime, 'MM/dd/yyyy');
openMenu();
const dateElement = fixture.debugElement.query(By.css('.adf-content-enrichment-menu__content__label span')).nativeElement;
expect(dateElement.textContent).toContain(formattedDate);
});
it('should render progress spinner with correct value', () => {
openMenu();
const spinnerElement = fixture.debugElement.query(By.css('.adf-accuracy-level-spinner'));
expect(spinnerElement.nativeElement).toBeDefined();
expect(spinnerElement.componentInstance.value).toEqual(component.confidencePercentage);
});
it('should render previous human entered value', () => {
openMenu();
const previousValueElement = fixture.debugElement.query(By.css('.adf-content-enrichment-menu__content__last__value')).nativeElement;
expect(previousValueElement.textContent).toContain(component.previousValue);
});
it('should display "None" as previous value when previousValue is not defined', () => {
component.prediction.previousValue = null;
component.ngOnInit();
openMenu();
const previousValueElement = fixture.debugElement.query(By.css('.adf-content-enrichment-menu__content__last__value')).nativeElement;
expect(previousValueElement.textContent).toContain('None');
});
it('should render predicted value', () => {
openMenu();
const predictionValueElement = fixture.debugElement.query(By.css('.adf-content-enrichment-menu__content__current__value span')).nativeElement;
expect(predictionValueElement.textContent).toContain(component.predictionValue);
});
});

View File

@@ -0,0 +1,93 @@
/*!
* @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.
*/
import { Component, ElementRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
import { TranslateModule } from '@ngx-translate/core';
import { Prediction, ReviewStatus } from '@alfresco/js-api';
import { IconComponent } from '../../../icon';
import { LocalizedDatePipe } from '../../../pipes';
import { PredictionService } from '../../services';
@Component({
standalone: true,
selector: 'adf-content-enrichment-menu',
templateUrl: './content-enrichment-menu.component.html',
styleUrls: ['./content-enrichment-menu.component.scss'],
encapsulation: ViewEncapsulation.None,
imports: [MatProgressSpinnerModule, MatTooltipModule, MatButtonModule, IconComponent, MatMenuModule, MatIconModule, TranslateModule, LocalizedDatePipe]
})
export class ContentEnrichmentMenuComponent implements OnInit {
@Input()
prediction: Prediction;
@ViewChild('menuContainer', { static: false })
menuContainer: ElementRef;
@ViewChild('menuTrigger', { static: false })
menuTrigger: MatMenuTrigger;
focusTrap: ConfigurableFocusTrap;
confidencePercentage: number;
previousValue: any;
predictionValue: any;
predictionDateTime: Date;
constructor(private predictionService: PredictionService, private focusTrapFactory: ConfigurableFocusTrapFactory) {}
ngOnInit() {
this.confidencePercentage = this.prediction?.confidenceLevel * 100 || 0;
this.previousValue = this.isDefined(this.prediction?.previousValue) ? this.prediction.previousValue : '';
this.predictionValue = this.isDefined(this.prediction?.predictionValue) ? this.prediction.predictionValue : '';
this.predictionDateTime = this.prediction?.predictionDateTime || null;
}
onMenuOpen() {
if (this.menuContainer && !this.focusTrap) {
this.focusTrap = this.focusTrapFactory.create(this.menuContainer.nativeElement);
}
}
onRevert() {
this.predictionService.reviewPrediction(this.prediction.id, ReviewStatus.REJECTED).subscribe(() => {
this.predictionService.predictionStatusUpdated$.next({key: this.prediction.property, previousValue: this.previousValue});
this.menuTrigger.closeMenu();
});
}
onConfirm() {
this.predictionService.reviewPrediction(this.prediction.id, ReviewStatus.CONFIRMED).subscribe(() => {
this.predictionService.predictionStatusUpdated$.next({key: this.prediction.property});
this.menuTrigger.closeMenu();
});
}
onClosed() {
this.focusTrap.destroy();
this.focusTrap = null;
}
private isDefined(value: string): boolean {
return value !== undefined && value !== null;
}
}

View File

@@ -0,0 +1,21 @@
/*!
* @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 PredictionStatusUpdate {
key: string;
previousValue?: any;
}

View File

@@ -16,3 +16,4 @@
*/
export * from './services';
export * from './interfaces/prediction-status-update.interface';

View File

@@ -16,8 +16,8 @@
*/
import { PredictionService } from './prediction.service';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { TestBed } from '@angular/core/testing';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { Prediction, PredictionEntry, PredictionPaging, PredictionPagingList, ReviewStatus } from '@alfresco/js-api';
describe('PredictionService', () => {
@@ -33,7 +33,7 @@ describe('PredictionService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ContentTestingModule]
imports: [CoreTestingModule]
});
service = TestBed.inject(PredictionService);
});

View File

@@ -16,14 +16,20 @@
*/
import { Injectable } from '@angular/core';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { PredictionsApi, PredictionPaging, ReviewStatus } from '@alfresco/js-api';
import { from, Observable } from 'rxjs';
import { from, Observable, Subject } from 'rxjs';
import { PredictionStatusUpdate } from '../interfaces/prediction-status-update.interface';
@Injectable({ providedIn: 'root' })
export class PredictionService {
private _predictionsApi: PredictionsApi;
/**
* Gets emitted when prediction status updated
*/
predictionStatusUpdated$ = new Subject<PredictionStatusUpdate>();
get predictionsApi(): PredictionsApi {
this._predictionsApi = this._predictionsApi ?? new PredictionsApi(this.apiService.getInstance());
return this._predictionsApi;

View File

@@ -89,7 +89,8 @@
--adf-danger-button-background: $adf-danger-button-background,
--adf-secondary-button-background: $adf-secondary-button-background,
--adf-display-external-property-widget-preview-selection-color: mat.get-color-from-palette($foreground, secondary-text)
--adf-display-external-property-widget-preview-selection-color: mat.get-color-from-palette($foreground, secondary-text),
--adf-ai-border-gradient: $adf-ref-ai-border-gradient
);
// propagates SCSS variables into the CSS variables scope

View File

@@ -27,3 +27,4 @@ $adf-ref-header-icon-color: inherit;
$adf-ref-header-icon-border-radius: 50%;
$adf-danger-button-background: #ba1b1b;
$adf-secondary-button-background: #2121210d;
$adf-ref-ai-border-gradient: linear-gradient(180deg, #94B7FF 0%, #9E00FF 100%);

View File

@@ -49,6 +49,7 @@ export * from './lib/blank-page/index';
export * from './lib/search-text/index';
export * from './lib/snackbar-content/index';
export * from './lib/translation/index';
export * from './lib/prediction/index';
export * from './lib/common/utils/index';
export * from './lib/interface/index';

View File

@@ -45,21 +45,24 @@ export class PredictionsApi extends BaseApi {
*
* @param predictionId The identifier of a prediction.
* @param reviewStatus New status to apply for prediction. Can be either 'confirmed' or 'rejected'.
* @returns Promise<void>
* @returns Promise<{}>
*/
reviewPrediction(predictionId: string, reviewStatus: ReviewStatus): Promise<void> {
throwIfNotDefined(predictionId, 'predictionId');
throwIfNotDefined(reviewStatus, 'reviewStatus');
const pathParams = {
predictionId,
reviewStatus
predictionId
};
const queryParams = {
reviewStatus
}
return this.post({
path: '/predictions/{predictionId}/review',
pathParams,
returnType: Promise<void>
queryParams
});
}
}