mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-09-17 14:21:29 +00:00
[ACS-8001] content enrichment menu component
This commit is contained in:
committed by
Mykyta Maliarchuk
parent
548592b329
commit
a1cbcfca33
@@ -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) |
|
||||
|
@@ -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
|
||||
|
||||
|
32
docs/core/components/content-enrichment-menu.component.md
Normal file
32
docs/core/components/content-enrichment-menu.component.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
## 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)
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
||||
|
28
docs/core/interfaces/prediction-status-update.interface.md
Normal file
28
docs/core/interfaces/prediction-status-update.interface.md
Normal 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)
|
@@ -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)
|
||||
|
BIN
docs/docassets/images/content-enrichment-menu.png
Normal file
BIN
docs/docassets/images/content-enrichment-menu.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
@@ -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-->
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
|
3
lib/core/src/lib/assets/images/ai-sparkles.svg
Normal file
3
lib/core/src/lib/assets/images/ai-sparkles.svg
Normal 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 |
@@ -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,
|
||||
|
@@ -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 {
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -49,6 +49,9 @@ export class CardViewDateItemComponent extends BaseCardView<CardViewDateItemMode
|
||||
@Input()
|
||||
displayClearAction = true;
|
||||
|
||||
@Input()
|
||||
hasContentEnrichment = false;
|
||||
|
||||
@ViewChild('datetimePicker')
|
||||
public datepicker: MatDatetimepickerComponent<any>;
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -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"
|
||||
|
@@ -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 {
|
||||
|
@@ -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() {
|
||||
|
@@ -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"
|
||||
|
@@ -57,6 +57,9 @@ export class CardViewTextItemComponent extends BaseCardView<CardViewTextItemMode
|
||||
@Input()
|
||||
displayLabelForChips = false;
|
||||
|
||||
@Input()
|
||||
hasContentEnrichment = false;
|
||||
|
||||
editedValue: string | string[];
|
||||
errors: CardViewItemValidator[];
|
||||
templateType: string;
|
||||
|
@@ -10,7 +10,8 @@
|
||||
[copyToClipboardAction]="copyToClipboardAction"
|
||||
[useChipsForMultiValueProperty]="useChipsForMultiValueProperty"
|
||||
[multiValueSeparator]="multiValueSeparator"
|
||||
[displayLabelForChips]="displayLabelForChips">
|
||||
[displayLabelForChips]="displayLabelForChips"
|
||||
[hasContentEnrichment]="!!property.prediction">
|
||||
</adf-card-view-item-dispatcher>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -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} {
|
||||
|
@@ -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[]);
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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": {
|
||||
|
@@ -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>
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
});
|
||||
});
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -16,3 +16,4 @@
|
||||
*/
|
||||
|
||||
export * from './services';
|
||||
export * from './interfaces/prediction-status-update.interface';
|
@@ -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);
|
||||
});
|
@@ -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;
|
@@ -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
|
||||
|
@@ -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%);
|
||||
|
@@ -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';
|
||||
|
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user