-
+
+
+
+
{{ property.displayValue }}
-
- {{ property.displayValue }}
+
+
+
-
+
+ {{ property.displayValue }}
+
-
- clear
-
+
+ clear
+
-
-
-
+
+
+
-
-
-
-
-
-
- {{ property.default | translate }}
-
-
-
-
-
- {{ propertyValue }}
- cancel
-
-
-
-
-
-
+
+
+ {{ property.default | translate }}
+
+
+
+
+
+ {{ propertyValue }}
+ cancel
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts
index 6f83a51cbb..d214556bc4 100644
--- a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts
+++ b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.ts
@@ -49,6 +49,9 @@ export class CardViewDateItemComponent extends BaseCardView
;
diff --git a/lib/core/src/lib/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.ts b/lib/core/src/lib/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.ts
index 0217cbc9b8..122bf2801b 100644
--- a/lib/core/src/lib/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.ts
+++ b/lib/core/src/lib/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.ts
@@ -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) {
diff --git a/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.html b/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.html
index 8aeacf3806..0e4e88991b 100644
--- a/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.html
+++ b/lib/core/src/lib/card-view/components/card-view-selectitem/card-view-selectitem.component.html
@@ -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">
+
+ {{ (property.displayValue | async) | translate }}
-
+
+
[]>;
@Input()
- displayNoneOption: boolean = true;
+ displayNoneOption = true;
@Input()
- displayEmpty: boolean = true;
+ displayEmpty = true;
+
+ @Input()
+ hasContentEnrichment = false;
value: string | number;
filter$ = new BehaviorSubject('');
@@ -50,6 +53,7 @@ export class CardViewSelectItemComponent extends BaseCardView
+
diff --git a/lib/core/src/lib/card-view/components/card-view/card-view.component.scss b/lib/core/src/lib/card-view/components/card-view/card-view.component.scss
index d1e713bd53..dbcfe8aa0d 100644
--- a/lib/core/src/lib/card-view/components/card-view/card-view.component.scss
+++ b/lib/core/src/lib/card-view/components/card-view/card-view.component.scss
@@ -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} {
diff --git a/lib/core/src/lib/card-view/interfaces/base-card-view-update.interface.ts b/lib/core/src/lib/card-view/interfaces/base-card-view-update.interface.ts
index b23c4f52c4..b4f61d461d 100644
--- a/lib/core/src/lib/card-view/interfaces/base-card-view-update.interface.ts
+++ b/lib/core/src/lib/card-view/interfaces/base-card-view-update.interface.ts
@@ -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
;
itemClicked$: Subject;
updateItem$: Subject;
+ predictionStatusChanged$: Subject;
update(property: CardViewBaseItemModel, newValue: any);
clicked(property: CardViewBaseItemModel);
updateElement(notification: CardViewBaseItemModel);
+ onPredictionStatusChanged(notification: PredictionStatusUpdate[]);
}
diff --git a/lib/core/src/lib/card-view/interfaces/card-view-item-properties.interface.ts b/lib/core/src/lib/card-view/interfaces/card-view-item-properties.interface.ts
index 04d49ef525..cc5db8d91f 100644
--- a/lib/core/src/lib/card-view/interfaces/card-view-item-properties.interface.ts
+++ b/lib/core/src/lib/card-view/interfaces/card-view-item-properties.interface.ts
@@ -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;
}
diff --git a/lib/core/src/lib/card-view/interfaces/card-view-item.interface.ts b/lib/core/src/lib/card-view/interfaces/card-view-item.interface.ts
index 0dccaa0e3b..846305a87b 100644
--- a/lib/core/src/lib/card-view/interfaces/card-view-item.interface.ts
+++ b/lib/core/src/lib/card-view/interfaces/card-view-item.interface.ts
@@ -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;
}
diff --git a/lib/core/src/lib/card-view/models/card-view-baseitem.model.ts b/lib/core/src/lib/card-view/models/card-view-baseitem.model.ts
index 953d131b17..331678b69f 100644
--- a/lib/core/src/lib/card-view/models/card-view-baseitem.model.ts
+++ b/lib/core/src/lib/card-view/models/card-view-baseitem.model.ts
@@ -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 {
label: string;
@@ -31,6 +32,7 @@ export abstract class CardViewBaseItemModel {
data?: any;
type?: string;
multivalued?: boolean;
+ prediction?: Prediction;
constructor(props: CardViewItemProperties) {
this.label = props.label || '';
@@ -44,6 +46,7 @@ export abstract class CardViewBaseItemModel {
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) {
diff --git a/lib/core/src/lib/card-view/models/card-view-selectitem.model.spec.ts b/lib/core/src/lib/card-view/models/card-view-selectitem.model.spec.ts
index e0715c5a84..9b56397eca 100644
--- a/lib/core/src/lib/card-view/models/card-view-selectitem.model.spec.ts
+++ b/lib/core/src/lib/card-view/models/card-view-selectitem.model.spec.ts
@@ -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();
+ });
+ });
});
});
diff --git a/lib/core/src/lib/card-view/models/card-view-selectitem.model.ts b/lib/core/src/lib/card-view/models/card-view-selectitem.model.ts
index 836f6bcd39..fb2b4d4783 100644
--- a/lib/core/src/lib/card-view/models/card-view-selectitem.model.ts
+++ b/lib/core/src/lib/card-view/models/card-view-selectitem.model.ts
@@ -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 extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel {
type = 'select';
@@ -29,6 +29,8 @@ export class CardViewSelectItemModel extends CardViewBaseItemModel implements
valueFetch$: Observable = null;
+ private valueSubject = new BehaviorSubject(this.value);
+
constructor(cardViewSelectItemProperties: CardViewSelectItemProperties) {
super(cardViewSelectItemProperties);
@@ -36,11 +38,13 @@ export class CardViewSelectItemModel 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 extends CardViewBaseItemModel implements
setValue(value: any) {
this.value = value;
+ this.valueSubject.next(value);
}
}
diff --git a/lib/core/src/lib/card-view/services/card-view-update.service.spec.ts b/lib/core/src/lib/card-view/services/card-view-update.service.spec.ts
index 9cafb71390..160369dfe2 100644
--- a/lib/core/src/lib/card-view/services/card-view-update.service.spec.ts
+++ b/lib/core/src/lib/card-view/services/card-view-update.service.spec.ts
@@ -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);
+ });
});
});
diff --git a/lib/core/src/lib/card-view/services/card-view-update.service.ts b/lib/core/src/lib/card-view/services/card-view-update.service.ts
index 5a625b3dba..1c6b61ab71 100644
--- a/lib/core/src/lib/card-view/services/card-view-update.service.ts
+++ b/lib/core/src/lib/card-view/services/card-view-update.service.ts
@@ -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();
itemClicked$ = new Subject();
updateItem$ = new Subject();
+ predictionStatusChanged$ = new Subject();
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);
+ }
}
diff --git a/lib/core/src/lib/common/services/thumbnail.service.ts b/lib/core/src/lib/common/services/thumbnail.service.ts
index 053e794b7a..d54345db42 100644
--- a/lib/core/src/lib/common/services/thumbnail.service.ts
+++ b/lib/core/src/lib/common/services/thumbnail.service.ts
@@ -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) {
diff --git a/lib/core/src/lib/i18n/en.json b/lib/core/src/lib/i18n/en.json
index 72e69bb173..0442aa1452 100644
--- a/lib/core/src/lib/i18n/en.json
+++ b/lib/core/src/lib/i18n/en.json
@@ -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": {
diff --git a/lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.html b/lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.html
new file mode 100644
index 0000000000..dc55d7e37b
--- /dev/null
+++ b/lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.html
@@ -0,0 +1,49 @@
+
+
+
diff --git a/lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.scss b/lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.scss
new file mode 100644
index 0000000000..e69cf87aba
--- /dev/null
+++ b/lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.scss
@@ -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);
+ }
+ }
+}
diff --git a/lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.spec.ts b/lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.spec.ts
new file mode 100644
index 0000000000..e89870e66a
--- /dev/null
+++ b/lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.spec.ts
@@ -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;
+ 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);
+ });
+});
diff --git a/lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.ts b/lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.ts
new file mode 100644
index 0000000000..48976ac4a7
--- /dev/null
+++ b/lib/core/src/lib/prediction/components/content-enrichment-menu/content-enrichment-menu.component.ts
@@ -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;
+ }
+}
diff --git a/lib/content-services/src/lib/prediction/index.ts b/lib/core/src/lib/prediction/index.ts
similarity index 100%
rename from lib/content-services/src/lib/prediction/index.ts
rename to lib/core/src/lib/prediction/index.ts
diff --git a/lib/core/src/lib/prediction/interfaces/prediction-status-update.interface.ts b/lib/core/src/lib/prediction/interfaces/prediction-status-update.interface.ts
new file mode 100644
index 0000000000..3c5227da52
--- /dev/null
+++ b/lib/core/src/lib/prediction/interfaces/prediction-status-update.interface.ts
@@ -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;
+}
diff --git a/lib/content-services/src/lib/prediction/public-api.ts b/lib/core/src/lib/prediction/public-api.ts
similarity index 91%
rename from lib/content-services/src/lib/prediction/public-api.ts
rename to lib/core/src/lib/prediction/public-api.ts
index fda9d52bde..4a47573a5c 100644
--- a/lib/content-services/src/lib/prediction/public-api.ts
+++ b/lib/core/src/lib/prediction/public-api.ts
@@ -16,3 +16,4 @@
*/
export * from './services';
+export * from './interfaces/prediction-status-update.interface';
diff --git a/lib/content-services/src/lib/prediction/services/index.ts b/lib/core/src/lib/prediction/services/index.ts
similarity index 100%
rename from lib/content-services/src/lib/prediction/services/index.ts
rename to lib/core/src/lib/prediction/services/index.ts
diff --git a/lib/content-services/src/lib/prediction/services/prediction.service.spec.ts b/lib/core/src/lib/prediction/services/prediction.service.spec.ts
similarity index 94%
rename from lib/content-services/src/lib/prediction/services/prediction.service.spec.ts
rename to lib/core/src/lib/prediction/services/prediction.service.spec.ts
index 8e88c50393..817979f0ce 100644
--- a/lib/content-services/src/lib/prediction/services/prediction.service.spec.ts
+++ b/lib/core/src/lib/prediction/services/prediction.service.spec.ts
@@ -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);
});
diff --git a/lib/content-services/src/lib/prediction/services/prediction.service.ts b/lib/core/src/lib/prediction/services/prediction.service.ts
similarity index 83%
rename from lib/content-services/src/lib/prediction/services/prediction.service.ts
rename to lib/core/src/lib/prediction/services/prediction.service.ts
index 020749f819..5d59d7334d 100644
--- a/lib/content-services/src/lib/prediction/services/prediction.service.ts
+++ b/lib/core/src/lib/prediction/services/prediction.service.ts
@@ -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();
+
get predictionsApi(): PredictionsApi {
this._predictionsApi = this._predictionsApi ?? new PredictionsApi(this.apiService.getInstance());
return this._predictionsApi;
diff --git a/lib/core/src/lib/styles/_components-variables.scss b/lib/core/src/lib/styles/_components-variables.scss
index 06e7cc9ec5..1e27bba715 100644
--- a/lib/core/src/lib/styles/_components-variables.scss
+++ b/lib/core/src/lib/styles/_components-variables.scss
@@ -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
diff --git a/lib/core/src/lib/styles/_reference-variables.scss b/lib/core/src/lib/styles/_reference-variables.scss
index 6f72688cbe..9d250d4c01 100644
--- a/lib/core/src/lib/styles/_reference-variables.scss
+++ b/lib/core/src/lib/styles/_reference-variables.scss
@@ -27,3 +27,4 @@ $adf-ref-header-icon-color: inherit;
$adf-ref-header-icon-border-radius: 50%;
$adf-danger-button-background: #ba1b1b;
$adf-secondary-button-background: #2121210d;
+$adf-ref-ai-border-gradient: linear-gradient(180deg, #94B7FF 0%, #9E00FF 100%);
diff --git a/lib/core/src/public-api.ts b/lib/core/src/public-api.ts
index 645edf1c46..6e9374494b 100644
--- a/lib/core/src/public-api.ts
+++ b/lib/core/src/public-api.ts
@@ -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';
diff --git a/lib/js-api/src/api/hxi-connector-api/api/predictions.api.ts b/lib/js-api/src/api/hxi-connector-api/api/predictions.api.ts
index eb97b1a25f..640af6424e 100644
--- a/lib/js-api/src/api/hxi-connector-api/api/predictions.api.ts
+++ b/lib/js-api/src/api/hxi-connector-api/api/predictions.api.ts
@@ -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
+ * @returns Promise<{}>
*/
reviewPrediction(predictionId: string, reviewStatus: ReviewStatus): Promise {
throwIfNotDefined(predictionId, 'predictionId');
throwIfNotDefined(reviewStatus, 'reviewStatus');
const pathParams = {
- predictionId,
- reviewStatus
+ predictionId
};
+ const queryParams = {
+ reviewStatus
+ }
+
return this.post({
path: '/predictions/{predictionId}/review',
pathParams,
- returnType: Promise
+ queryParams
});
}
}