[AAE-10773] Make Form core process agonostic (#8032)

* move form list in a component

* move things in the right place

* move last pice in the right place

* move things in the right place

* move people and group in the right place

* move radio and typehead
form service start remove responsibilities

* remove model service and editor service from formService

* move dropdwon in process-service
finish remove service from form service

* fix some wrong import

* move activiti

* fix double quote imports

* move dynamic table

* fix shell

* move unit test

* [ci:force] fix lint issues

* fix build and some unit test

* fix process spec type spy problems [ci:foce]

* fix

* fix broken tests

* fix lint issues

* fix cloud dropdown test

* cleanup process-service-cloud tests

* fix people process

* improve e2e test

Co-authored-by: Kasia Biernat <kasia.biernat@hyland.com>
This commit is contained in:
Eugenio Romano
2022-12-21 15:12:38 +00:00
committed by GitHub
parent eb27d38eba
commit a535af667b
180 changed files with 1971 additions and 3260 deletions

View File

@@ -1,8 +0,0 @@
<adf-datatable *ngIf="!isEmpty()"
[rows]="forms">
<data-columns>
<data-column key="name" type="text" title="Name" class="adf-ellipsis-cell" [sortable]="true"></data-column>
<data-column key="lastUpdatedByFullName" type="text" title="User" class="adf-ellipsis-cell" [sortable]="true"></data-column>
<data-column key="lastUpdated" type="date" format="shortDate" title="Date"></data-column>
</data-columns>
</adf-datatable>

View File

@@ -1,60 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { of } from 'rxjs';
import { FormService } from '../services/form.service';
import { FormListComponent } from './form-list.component';
import { setupTestBed } from '../../testing/setup-test-bed';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
describe('TaskAttachmentList', () => {
let component: FormListComponent;
let fixture: ComponentFixture<FormListComponent>;
let service: FormService;
let element: HTMLElement;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
fixture = TestBed.createComponent(FormListComponent);
component = fixture.componentInstance;
element = fixture.debugElement.nativeElement;
service = TestBed.inject(FormService);
});
it('should show the forms as a list', async () => {
spyOn(service, 'getForms').and.returnValue(of([
{ name: 'FakeName-1', lastUpdatedByFullName: 'FakeUser-1', lastUpdated: '2017-01-02' },
{ name: 'FakeName-2', lastUpdatedByFullName: 'FakeUser-2', lastUpdated: '2017-01-03' }
]));
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelectorAll('.adf-datatable-body > .adf-datatable-row').length).toBe(2);
});
});

View File

@@ -1,49 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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, Input, OnChanges, ViewEncapsulation } from '@angular/core';
import { FormService } from '../services/form.service';
@Component({
selector: 'adf-form-list',
templateUrl: './form-list.component.html',
encapsulation: ViewEncapsulation.None
})
export class FormListComponent implements OnChanges {
/** The array that contains the information to show inside the list. */
@Input()
forms: any [] = [];
constructor(protected formService: FormService) {
}
ngOnChanges() {
this.getForms();
}
isEmpty(): boolean {
return this.forms && this.forms.length === 0;
}
getForms() {
this.formService.getForms().subscribe((forms) => {
this.forms.push(...forms);
});
}
}

View File

@@ -1,22 +0,0 @@
<mat-card class="adf-content-container" *ngIf="content">
<mat-card-content *ngIf="showDocumentContent">
<div *ngIf="content.isThumbnailSupported()" >
<img id="thumbnailPreview" class="adf-img-upload-widget" [src]="content.thumbnailUrl" alt="{{content.name}}">
</div>
<div *ngIf="!content.isThumbnailSupported()">
<mat-icon>image</mat-icon>
<div id="unsupported-thumbnail" class="adf-content-widget-preview-text">{{ 'FORM.PREVIEW.IMAGE_NOT_AVAILABLE' | translate }}
</div>
</div>
<div class="mdl-card__supporting-text upload-widget__content-text">{{content.name | translate }}</div>
</mat-card-content>
<mat-card-actions>
<button mat-icon-button id="view" (click)="openViewer(content)">
<mat-icon class="mat-24">zoom_in</mat-icon>
</button>
<button mat-icon-button id="download" (click)="download(content)">
<mat-icon class="mat-24">file_download</mat-icon>
</button>
</mat-card-actions>
</mat-card>

View File

@@ -1,15 +0,0 @@
.adf {
&-img-upload-widget {
width: 100%;
height: 100%;
border: 1px solid rgba(117, 117, 117, 0.57);
box-shadow: 1px 1px 2px #ddd;
background-color: #fff;
}
&-content-widget-preview-text {
word-wrap: break-word;
word-break: break-all;
text-align: center;
}
}

View File

@@ -1,283 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { SimpleChange } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ContentService } from '../../../../services';
import { of } from 'rxjs';
import { ProcessContentService } from '../../../services/process-content.service';
import { ContentLinkModel } from '../index';
import { ContentWidgetComponent } from './content.widget';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { CoreTestingModule } from '../../../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
declare let jasmine: any;
describe('ContentWidgetComponent', () => {
let component: ContentWidgetComponent;
let fixture: ComponentFixture<ContentWidgetComponent>;
let element: HTMLElement;
let processContentService: ProcessContentService;
let serviceContent: ContentService;
const createFakeImageBlob = () => {
const data = atob('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
return new Blob([data], { type: 'image/png' });
};
const createFakePdfBlob = (): Blob => {
const pdfData = atob(
'JVBERi0xLjcKCjEgMCBvYmogICUgZW50cnkgcG9pbnQKPDwKICAvVHlwZSAvQ2F0YWxvZwog' +
'IC9QYWdlcyAyIDAgUgo+PgplbmRvYmoKCjIgMCBvYmoKPDwKICAvVHlwZSAvUGFnZXMKICAv' +
'TWVkaWFCb3ggWyAwIDAgMjAwIDIwMCBdCiAgL0NvdW50IDEKICAvS2lkcyBbIDMgMCBSIF0K' +
'Pj4KZW5kb2JqCgozIDAgb2JqCjw8CiAgL1R5cGUgL1BhZ2UKICAvUGFyZW50IDIgMCBSCiAg' +
'L1Jlc291cmNlcyA8PAogICAgL0ZvbnQgPDwKICAgICAgL0YxIDQgMCBSIAogICAgPj4KICA+' +
'PgogIC9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKCjQgMCBvYmoKPDwKICAvVHlwZSAvRm9u' +
'dAogIC9TdWJ0eXBlIC9UeXBlMQogIC9CYXNlRm9udCAvVGltZXMtUm9tYW4KPj4KZW5kb2Jq' +
'Cgo1IDAgb2JqICAlIHBhZ2UgY29udGVudAo8PAogIC9MZW5ndGggNDQKPj4Kc3RyZWFtCkJU' +
'CjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8sIHdvcmxkISkgVGoKRVQKZW5kc3RyZWFtCmVu' +
'ZG9iagoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDEwIDAwMDAwIG4g' +
'CjAwMDAwMDAwNzkgMDAwMDAgbiAKMDAwMDAwMDE3MyAwMDAwMCBuIAowMDAwMDAwMzAxIDAw' +
'MDAwIG4gCjAwMDAwMDAzODAgMDAwMDAgbiAKdHJhaWxlcgo8PAogIC9TaXplIDYKICAvUm9v' +
'dCAxIDAgUgo+PgpzdGFydHhyZWYKNDkyCiUlRU9G');
return new Blob([pdfData], { type: 'application/pdf' });
};
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
serviceContent = TestBed.inject(ContentService);
processContentService = TestBed.inject(ProcessContentService);
});
beforeEach(() => {
fixture = TestBed.createComponent(ContentWidgetComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
fixture.detectChanges();
});
describe('Rendering tests', () => {
beforeEach(() => {
jasmine.Ajax.install();
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
it('should display content thumbnail', () => {
component.showDocumentContent = true;
component.content = new ContentLinkModel();
fixture.detectChanges();
const content = fixture.debugElement.query(By.css('div.upload-widget__content-thumbnail'));
expect(content).toBeDefined();
});
it('should load the thumbnail preview of the png image', fakeAsync(() => {
const blob = createFakeImageBlob();
spyOn(processContentService, 'getFileRawContent').and.returnValue(of(blob));
component.thumbnailLoaded.subscribe((res) => {
fixture.detectChanges();
expect(res).toBeDefined();
expect(res.changingThisBreaksApplicationSecurity).toBeDefined();
expect(res.changingThisBreaksApplicationSecurity).toContain('blob');
const thumbnailPreview: any = element.querySelector('#thumbnailPreview');
expect(thumbnailPreview.src).toContain('blob');
});
const contentId = 1;
const change = new SimpleChange(null, contentId, true);
component.ngOnChanges({ id: change });
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: {
id: 4004,
name: 'Useful expressions - Email_English.png',
created: 1490354907883,
createdBy: {
id: 2,
firstName: 'admin', lastName: 'admin', email: 'administrator@admin.com'
},
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'image/png',
simpleType: 'image',
previewStatus: 'unsupported',
thumbnailStatus: 'unsupported'
}
});
}));
it('should load the thumbnail preview of a pdf', fakeAsync(() => {
const blob = createFakePdfBlob();
spyOn(processContentService, 'getContentThumbnail').and.returnValue(of(blob));
component.thumbnailLoaded.subscribe((res) => {
fixture.detectChanges();
expect(res).toBeDefined();
expect(res.changingThisBreaksApplicationSecurity).toBeDefined();
expect(res.changingThisBreaksApplicationSecurity).toContain('blob');
const thumbnailPreview: any = element.querySelector('#thumbnailPreview');
expect(thumbnailPreview.src).toContain('blob');
});
const contentId = 1;
const change = new SimpleChange(null, contentId, true);
component.ngOnChanges({ id: change });
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: {
id: 4004,
name: 'FakeBlob.pdf',
created: 1490354907883,
createdBy: {
id: 2,
firstName: 'admin', lastName: 'admin', email: 'administrator@admin.com'
},
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'application/pdf',
simpleType: 'pdf',
previewStatus: 'created',
thumbnailStatus: 'created'
}
});
}));
it('should show unsupported preview with unsupported file', fakeAsync(() => {
const contentId = 1;
const change = new SimpleChange(null, contentId, true);
component.ngOnChanges({ id: change });
component.contentLoaded.subscribe(() => {
fixture.detectChanges();
const thumbnailPreview: any = element.querySelector('#unsupported-thumbnail');
expect(thumbnailPreview).toBeDefined();
expect(element.querySelector('div.upload-widget__content-text').innerHTML).toEqual('FakeBlob.zip');
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: {
id: 4004,
name: 'FakeBlob.zip',
created: 1490354907883,
createdBy: {
id: 2,
firstName: 'admin', lastName: 'admin', email: 'administrator@admin.com'
},
relatedContent: false,
contentAvailable: false,
link: false,
mimeType: 'application/zip',
simpleType: 'zip',
previewStatus: 'unsupported',
thumbnailStatus: 'unsupported'
}
});
}));
it('should open the viewer when the view button is clicked', () => {
const blob = createFakePdfBlob();
spyOn(processContentService, 'getContentPreview').and.returnValue(of(blob));
spyOn(processContentService, 'getFileRawContent').and.returnValue(of(blob));
component.content = new ContentLinkModel({
id: 4004,
name: 'FakeBlob.pdf',
created: 1490354907883,
createdBy: {
id: 2,
firstName: 'admin', lastName: 'admin', email: 'administrator@admin.com'
},
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'application/pdf',
simpleType: 'pdf',
previewStatus: 'created',
thumbnailStatus: 'created'
});
component.content.thumbnailUrl = '/alfresco-logo.svg';
component.contentClick.subscribe((content) => {
expect(content.contentBlob).toBe(blob);
expect(content.mimeType).toBe('application/pdf');
expect(content.name).toBe('FakeBlob.pdf');
});
fixture.detectChanges();
const viewButton: any = element.querySelector('#view');
viewButton.click();
});
it('should download the pdf when the download button is clicked', () => {
const blob = createFakePdfBlob();
spyOn(processContentService, 'getFileRawContent').and.returnValue(of(blob));
spyOn(serviceContent, 'downloadBlob').and.callThrough();
component.content = new ContentLinkModel({
id: 4004,
name: 'FakeBlob.pdf',
created: 1490354907883,
createdBy: {
id: 2,
firstName: 'admin', lastName: 'admin', email: 'administrator@admin.com'
},
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'application/pdf',
simpleType: 'pdf',
previewStatus: 'created',
thumbnailStatus: 'created'
});
component.content.thumbnailUrl = '/alfresco-logo.svg';
fixture.detectChanges();
const downloadButton: any = element.querySelector('#download');
downloadButton.click();
expect(serviceContent.downloadBlob).toHaveBeenCalledWith(blob, 'FakeBlob.pdf');
});
});
});

View File

@@ -1,142 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { ContentService } from '../../../../services/content.service';
import { LogService } from '../../../../services/log.service';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Observable } from 'rxjs';
import { ProcessContentService } from '../../../services/process-content.service';
import { ContentLinkModel } from '../core/content-link.model';
import { FormService } from '../../../services/form.service';
@Component({
selector: 'adf-content',
templateUrl: './content.widget.html',
styleUrls: ['./content.widget.scss'],
encapsulation: ViewEncapsulation.None
})
export class ContentWidgetComponent implements OnChanges {
/** The content id to show. */
@Input()
id: string;
/** Toggles showing document content. */
@Input()
showDocumentContent: boolean = true;
/** Emitted when the content is clicked. */
@Output()
contentClick = new EventEmitter();
/** Emitted when the thumbnail has loaded. */
@Output()
thumbnailLoaded: EventEmitter<any> = new EventEmitter<any>();
/** Emitted when the content has loaded. */
@Output()
contentLoaded: EventEmitter<any> = new EventEmitter<any>();
/** Emitted when an error occurs. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
content: ContentLinkModel;
constructor(protected formService: FormService,
private logService: LogService,
private contentService: ContentService,
private processContentService: ProcessContentService) {
}
ngOnChanges(changes: SimpleChanges) {
const contentId = changes['id'];
if (contentId && contentId.currentValue) {
this.loadContent(contentId.currentValue);
}
}
loadContent(id: number) {
this.processContentService
.getFileContent(id)
.subscribe(
(response: ContentLinkModel) => {
this.content = new ContentLinkModel(response);
this.contentLoaded.emit(this.content);
this.loadThumbnailUrl(this.content);
},
(error) => {
this.error.emit(error);
}
);
}
loadThumbnailUrl(content: ContentLinkModel) {
if (this.content.isThumbnailSupported()) {
let observable: Observable<any>;
if (this.content.isTypeImage()) {
observable = this.processContentService.getFileRawContent(content.id);
} else {
observable = this.processContentService.getContentThumbnail(content.id);
}
if (observable) {
observable.subscribe(
(response: Blob) => {
this.content.thumbnailUrl = this.contentService.createTrustedUrl(response);
this.thumbnailLoaded.emit(this.content.thumbnailUrl);
},
(error) => {
this.error.emit(error);
}
);
}
}
}
openViewer(content: ContentLinkModel): void {
let fetch = this.processContentService.getContentPreview(content.id);
if (content.isTypeImage() || content.isTypePdf()) {
fetch = this.processContentService.getFileRawContent(content.id);
}
fetch.subscribe(
(blob: Blob) => {
content.contentBlob = blob;
this.contentClick.emit(content);
this.logService.info('Content clicked' + content.id);
this.formService.formContentClicked.next(content);
},
(error) => {
this.error.emit(error);
}
);
}
/**
* Invoke content download.
*/
download(content: ContentLinkModel): void {
this.processContentService.getFileRawContent(content.id).subscribe(
(blob: Blob) => this.contentService.downloadBlob(blob, content.name),
(error) => {
this.error.emit(error);
}
);
}
}

View File

@@ -27,13 +27,10 @@ import { TabModel } from './tab.model';
import { fakeMetadataForm, fakeViewerForm } from '../../mock/form.mock';
import { Node } from '@alfresco/js-api';
import { UploadWidgetContentLinkModel } from './upload-widget-content-link.model';
import { AlfrescoApiService } from '../../../../services';
import { TestBed } from '@angular/core/testing';
import { CoreTestingModule, setupTestBed } from '../../../../testing';
describe('FormModel', () => {
let formService: FormService;
let alfrescoApiService: AlfrescoApiService;
setupTestBed({
imports: [
@@ -42,9 +39,7 @@ describe('FormModel', () => {
});
beforeEach(() => {
alfrescoApiService = TestBed.inject(AlfrescoApiService);
formService = new FormService(null, alfrescoApiService, null);
formService = new FormService();
});
it('should store original json', () => {

View File

@@ -1,5 +0,0 @@
<div class="adf-form-document-widget {{field.className}}">
<ng-container *ngIf="hasFile">
<adf-content [id]="fileId" [showDocumentContent]="true"></adf-content>
</ng-container>
</div>

View File

@@ -1,60 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService } from '../../../services/form.service';
import { WidgetComponent } from '../widget.component';
@Component({
selector: 'adf-form-document-widget',
templateUrl: './document.widget.html',
host: {
'(click)': 'event($event)',
'(blur)': 'event($event)',
'(change)': 'event($event)',
'(focus)': 'event($event)',
'(focusin)': 'event($event)',
'(focusout)': 'event($event)',
'(input)': 'event($event)',
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
encapsulation: ViewEncapsulation.None
})
export class DocumentWidgetComponent extends WidgetComponent implements OnInit {
fileId: string = null;
hasFile: boolean = false;
constructor(public formService: FormService) {
super(formService);
}
ngOnInit() {
if (this.field) {
const file = this.field.value;
if (file) {
this.fileId = file.id;
this.hasFile = true;
} else {
this.fileId = null;
this.hasFile = false;
}
}
}
}

View File

@@ -1,21 +0,0 @@
<div class="adf-dropdown-widget {{field.className}}"
[class.adf-invalid]="!field.isValid && isTouched()" [class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<mat-form-field>
<mat-select class="adf-select"
[id]="field.id"
[(ngModel)]="field.value"
[disabled]="field.readOnly"
(ngModelChange)="onFieldChanged(field)"
(blur)="markAsTouched()">
<mat-option *ngFor="let opt of field.options"
[value]="getOptionValue(opt, field.value)"
[id]="opt.id">{{opt.name}}
</mat-option>
<mat-option id="readonlyOption" *ngIf="isReadOnlyType()" [value]="field.value">{{field.value}}</mat-option>
</mat-select>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget class="adf-dropdown-required-message" *ngIf="showRequiredMessage()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -1,18 +0,0 @@
.adf {
&-dropdown-widget {
width: 100%;
.adf-select {
padding-top: 0 !important;
width: 100%;
}
.mat-select-value-text {
font-size: var(--theme-body-1-font-size);
}
&-select {
width: 100%;
}
}
}

View File

@@ -1,329 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { By } from '@angular/platform-browser';
import { Observable, of } from 'rxjs';
import { FormService } from '../../../services/form.service';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
import { FormFieldOption } from '../core/form-field-option';
import { FormFieldModel } from '../core/form-field.model';
import { FormModel } from '../core/form.model';
import { FormFieldTypes } from '../core/form-field-types';
import { DropdownWidgetComponent } from './dropdown.widget';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { CoreTestingModule } from '../../../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
describe('DropdownWidgetComponent', () => {
let formService: FormService;
let widget: DropdownWidgetComponent;
let visibilityService: WidgetVisibilityService;
let fixture: ComponentFixture<DropdownWidgetComponent>;
let element: HTMLElement;
const openSelect = () => {
const dropdown = fixture.debugElement.nativeElement.querySelector('.mat-select-trigger');
dropdown.click();
};
const fakeOptionList: FormFieldOption[] = [
{ id: 'opt_1', name: 'option_1' },
{ id: 'opt_2', name: 'option_2' },
{ id: 'opt_3', name: 'option_3' }];
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
fixture = TestBed.createComponent(DropdownWidgetComponent);
widget = fixture.componentInstance;
element = fixture.nativeElement;
formService = TestBed.inject(FormService);
visibilityService = TestBed.inject(WidgetVisibilityService);
widget.field = new FormFieldModel(new FormModel());
});
it('should require field with restUrl', () => {
spyOn(formService, 'getRestFieldValues').and.stub();
widget.field = null;
widget.ngOnInit();
expect(formService.getRestFieldValues).not.toHaveBeenCalled();
widget.field = new FormFieldModel(null, { restUrl: null });
widget.ngOnInit();
expect(formService.getRestFieldValues).not.toHaveBeenCalled();
});
it('should request field values from service', () => {
const taskId = '<form-id>';
const fieldId = '<field-id>';
const form = new FormModel({
taskId
});
widget.field = new FormFieldModel(form, {
id: fieldId,
restUrl: '<url>'
});
spyOn(formService, 'getRestFieldValues').and.returnValue(
new Observable((observer) => {
observer.next(null);
observer.complete();
})
);
widget.ngOnInit();
expect(formService.getRestFieldValues).toHaveBeenCalledWith(taskId, fieldId);
});
it('should preserve empty option when loading fields', () => {
const restFieldValue: FormFieldOption = { id: '1', name: 'Option1' } as FormFieldOption;
spyOn(formService, 'getRestFieldValues').and.callFake(() => new Observable((observer) => {
observer.next([restFieldValue]);
observer.complete();
}));
const form = new FormModel({ taskId: '<id>' });
const emptyOption: FormFieldOption = { id: 'empty', name: 'Empty' } as FormFieldOption;
widget.field = new FormFieldModel(form, {
id: '<id>',
restUrl: '/some/url/address',
hasEmptyValue: true,
options: [emptyOption]
});
widget.ngOnInit();
expect(formService.getRestFieldValues).toHaveBeenCalled();
expect(widget.field.options.length).toBe(2);
expect(widget.field.options[0]).toBe(emptyOption);
expect(widget.field.options[1]).toBe(restFieldValue);
});
describe('when is required', () => {
beforeEach(() => {
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
type: FormFieldTypes.DROPDOWN,
required: true
});
});
it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
it('should be invalid if no default option after interaction', async () => {
expect(element.querySelector('.adf-invalid')).toBeFalsy();
const dropdownSelect = element.querySelector('.adf-select');
dropdownSelect.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-invalid')).toBeTruthy();
});
it('should be valid if default option', async () => {
widget.field.options = fakeOptionList;
widget.field.value = fakeOptionList[0].id;
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-invalid')).toBeFalsy();
});
});
describe('when template is ready', () => {
describe('and dropdown is populated via taskId', () => {
beforeEach(() => {
spyOn(visibilityService, 'refreshVisibility').and.stub();
spyOn(formService, 'getRestFieldValues').and.callFake(() => of(fakeOptionList));
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id' }), {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
restUrl: 'fake-rest-url'
});
widget.field.emptyOption = { id: 'empty', name: 'Choose one...' };
widget.field.isVisible = true;
fixture.detectChanges();
});
it('should show visible dropdown widget', async () => {
expect(element.querySelector('#dropdown-id')).toBeDefined();
expect(element.querySelector('#dropdown-id')).not.toBeNull();
openSelect();
const optOne = fixture.debugElement.queryAll(By.css('[id="mat-option-1"]'));
const optTwo = fixture.debugElement.queryAll(By.css('[id="mat-option-2"]'));
const optThree = fixture.debugElement.queryAll(By.css('[id="mat-option-3"]'));
expect(optOne).not.toBeNull();
expect(optTwo).not.toBeNull();
expect(optThree).not.toBeNull();
});
it('should select the default value when an option is chosen as default', async () => {
widget.field.value = 'option_2';
widget.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
const dropDownElement: any = element.querySelector('#dropdown-id');
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('option_2');
expect(dropDownElement.attributes['ng-reflect-model'].textContent).toBe('option_2');
});
it('should select the empty value when no default is chosen', async () => {
widget.field.value = 'empty';
widget.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
openSelect();
fixture.detectChanges();
await fixture.whenStable();
const dropDownElement: any = element.querySelector('#dropdown-id');
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('empty');
});
});
describe('and dropdown is populated via processDefinitionId', () => {
beforeEach(() => {
spyOn(visibilityService, 'refreshVisibility').and.stub();
spyOn(formService, 'getRestFieldValuesByProcessId').and.callFake(() => of(fakeOptionList));
widget.field = new FormFieldModel(new FormModel({ processDefinitionId: 'fake-process-id' }), {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
restUrl: 'fake-rest-url'
});
widget.field.emptyOption = { id: 'empty', name: 'Choose one...' };
widget.field.isVisible = true;
fixture.detectChanges();
});
it('should show visible dropdown widget', () => {
expect(element.querySelector('#dropdown-id')).toBeDefined();
expect(element.querySelector('#dropdown-id')).not.toBeNull();
openSelect();
const optOne = fixture.debugElement.queryAll(By.css('[id="mat-option-1"]'));
const optTwo = fixture.debugElement.queryAll(By.css('[id="mat-option-2"]'));
const optThree = fixture.debugElement.queryAll(By.css('[id="mat-option-3"]'));
expect(optOne).not.toBeNull();
expect(optTwo).not.toBeNull();
expect(optThree).not.toBeNull();
});
it('should select the default value when an option is chosen as default', async () => {
widget.field.value = 'option_2';
widget.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
const dropDownElement: any = element.querySelector('#dropdown-id');
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('option_2');
expect(dropDownElement.attributes['ng-reflect-model'].textContent).toBe('option_2');
});
it('should select the empty value when no default is chosen', async () => {
widget.field.value = 'empty';
widget.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
openSelect();
fixture.detectChanges();
await fixture.whenStable();
const dropDownElement: any = element.querySelector('#dropdown-id');
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('empty');
});
it('should be disabled when the field is readonly', async () => {
widget.field = new FormFieldModel(new FormModel({ processDefinitionId: 'fake-process-id' }), {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'true',
restUrl: 'fake-rest-url'
});
fixture.detectChanges();
await fixture.whenStable();
const dropDownElement = element.querySelector<HTMLSelectElement>('#dropdown-id');
expect(dropDownElement).not.toBeNull();
expect(dropDownElement.getAttribute('aria-disabled')).toBe('true');
});
it('should show the option value when the field is readonly', async () => {
widget.field = new FormFieldModel(new FormModel({ processDefinitionId: 'fake-process-id' }), {
id: 'dropdown-id',
name: 'date-name',
type: 'readonly',
value: 'FakeValue',
readOnly: true,
params: { field: { name: 'date-name', type: 'dropdown' } }
});
openSelect();
fixture.detectChanges();
await fixture.whenStable();
const options = fixture.debugElement.queryAll(By.css('.mat-option-text'));
expect(options.length).toBe(1);
const option = options[0].nativeElement;
expect(option.innerText).toEqual('FakeValue');
});
});
});
});

View File

@@ -1,119 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { LogService } from '../../../../services/log.service';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService } from '../../../services/form.service';
import { FormFieldOption } from '../core/form-field-option';
import { WidgetComponent } from '../widget.component';
@Component({
selector: 'dropdown-widget',
templateUrl: './dropdown.widget.html',
styleUrls: ['./dropdown.widget.scss'],
host: {
'(click)': 'event($event)',
'(blur)': 'event($event)',
'(change)': 'event($event)',
'(focus)': 'event($event)',
'(focusin)': 'event($event)',
'(focusout)': 'event($event)',
'(input)': 'event($event)',
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
encapsulation: ViewEncapsulation.None
})
export class DropdownWidgetComponent extends WidgetComponent implements OnInit {
constructor(public formService: FormService,
private logService: LogService) {
super(formService);
}
ngOnInit() {
if (this.field && this.field.restUrl) {
if (this.field.form.taskId) {
this.getValuesByTaskId();
} else {
this.getValuesByProcessDefinitionId();
}
}
}
getValuesByTaskId() {
this.formService
.getRestFieldValues(
this.field.form.taskId,
this.field.id
)
.subscribe(
(formFieldOption: FormFieldOption[]) => {
const options = [];
if (this.field.emptyOption) {
options.push(this.field.emptyOption);
}
this.field.options = options.concat((formFieldOption || []));
this.field.updateForm();
},
(err) => this.handleError(err)
);
}
getValuesByProcessDefinitionId() {
this.formService
.getRestFieldValuesByProcessId(
this.field.form.processDefinitionId,
this.field.id
)
.subscribe(
(formFieldOption: FormFieldOption[]) => {
const options = [];
if (this.field.emptyOption) {
options.push(this.field.emptyOption);
}
this.field.options = options.concat((formFieldOption || []));
this.field.updateForm();
},
(err) => this.handleError(err)
);
}
getOptionValue(option: FormFieldOption, fieldValue: string): string {
let optionValue: string = '';
if (option.id === 'empty' || option.name !== fieldValue) {
optionValue = option.id;
} else {
optionValue = option.name;
}
return optionValue;
}
handleError(error: any) {
this.logService.error(error);
}
isReadOnlyType(): boolean {
return this.field.type === 'readonly';
}
showRequiredMessage(): boolean {
return (this.isInvalidFieldRequired() || this.field.value === 'empty') && this.isTouched();
}
}

View File

@@ -1,29 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
import { DynamicRowValidationSummary } from './dynamic-row-validation-summary.model';
export interface CellValidator {
isSupported(column: DynamicTableColumn): boolean;
validate(row: DynamicTableRow, column: DynamicTableColumn, summary?: DynamicRowValidationSummary): boolean;
}

View File

@@ -1,57 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import moment from 'moment';
import { CellValidator } from './cell-validator.model';
import { DynamicRowValidationSummary } from './dynamic-row-validation-summary.model';
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
export class DateCellValidator implements CellValidator {
private supportedTypes: string[] = [
'Date'
];
isSupported(column: DynamicTableColumn): boolean {
return column && column.editable && this.supportedTypes.indexOf(column.type) > -1;
}
validate(row: DynamicTableRow, column: DynamicTableColumn, summary?: DynamicRowValidationSummary): boolean {
if (this.isSupported(column)) {
const value = row.value[column.id];
if (!value && !column.required) {
return true;
}
const dateValue = moment(value, 'YYYY-MM-DDTHH:mm:ss.SSSSZ', true);
if (!dateValue.isValid()) {
if (summary) {
summary.isValid = false;
summary.message = `Invalid '${column.name}' format.`;
}
return false;
}
}
return true;
}
}

View File

@@ -1,30 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { ErrorMessageModel } from '../core/error-message.model';
/* eslint-disable @angular-eslint/component-selector */
export class DynamicRowValidationSummary extends ErrorMessageModel {
isValid: boolean;
constructor(json?: any) {
super(json);
this.isValid = json.isValid;
}
}

View File

@@ -1,24 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
// maps to: com.activiti.model.editor.form.OptionRepresentation
export interface DynamicTableColumnOption {
id: string;
name: string;
}

View File

@@ -1,46 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { DynamicTableColumnOption } from './dynamic-table-column-option.model';
// maps to: com.activiti.model.editor.form.ColumnDefinitionRepresentation
export interface DynamicTableColumn {
id: string;
name: string;
type: string;
value: any;
optionType: string;
options: DynamicTableColumnOption[];
restResponsePath: string;
restUrl: string;
restIdProperty: string;
restLabelProperty: string;
amountCurrency: string;
amountEnableFractions: boolean;
required: boolean;
editable: boolean;
sortable: boolean;
visible: boolean;
// TODO: com.activiti.domain.idm.EndpointConfiguration.EndpointConfigurationRepresentation
endpoint: any;
// TODO: com.activiti.model.editor.form.RequestHeaderRepresentation
requestHeaders: any;
}

View File

@@ -1,24 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
export interface DynamicTableRow {
isNew: boolean;
selected: boolean;
value: any;
}

View File

@@ -1,71 +0,0 @@
<div class="adf-dynamic-table-scrolling {{field.className}}"
[class.adf-invalid]="!isValid()">
<div class="adf-label">{{content.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></div>
<div *ngIf="!editMode">
<div class="adf-table-container">
<table class="adf-full-width adf-dynamic-table" id="dynamic-table-{{content.id}}">
<thead>
<tr>
<th *ngFor="let column of content.visibleColumns">
{{column.name}}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of content.rows; let idx = index" tabindex="0" id="{{content.id}}-row-{{idx}}"
[class.adf-dynamic-table-widget__row-selected]="row.selected" (keyup)="onKeyPressed($event, row)">
<td *ngFor="let column of content.visibleColumns"
(click)="onRowClicked(row)">
<span *ngIf="column.type !== 'Boolean' else checkbox">
{{ getCellValue(row, column) }}
</span>
<ng-template #checkbox>
<mat-checkbox disabled [checked]="getCellValue(row, column)">
</mat-checkbox>
</ng-template>
</td>
</tr>
</tbody>
</table>
</div>
<div *ngIf="!readOnly">
<button mat-button
[disabled]="!hasSelection()"
(click)="moveSelectionUp()">
<mat-icon>arrow_upward</mat-icon>
</button>
<button mat-button
[disabled]="!hasSelection()"
(click)="moveSelectionDown()">
<mat-icon>arrow_downward</mat-icon>
</button>
<button mat-button
[disabled]="field.readOnly"
id="{{content.id}}-add-row"
(click)="addNewRow()">
<mat-icon>add_circle_outline</mat-icon>
</button>
<button mat-button
[disabled]="!hasSelection()"
(click)="deleteSelection()">
<mat-icon>remove_circle_outline</mat-icon>
</button>
<button mat-button
[disabled]="!hasSelection()"
(click)="editSelection()">
<mat-icon>edit</mat-icon>
</button>
</div>
</div>
<row-editor *ngIf="editMode"
[table]="content"
[row]="editRow"
(save)="onSaveChanges()"
(cancel)="onCancelChanges()">
</row-editor>
<error-widget [error]="field.validationSummary" ></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -1,203 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import moment from 'moment';
import { ValidateDynamicTableRowEvent } from '../../../events/validate-dynamic-table-row.event';
import { FormService } from '../../../services/form.service';
import { FormFieldModel } from '../core/form-field.model';
import { FormWidgetModel } from '../core/form-widget.model';
import { CellValidator } from './cell-validator.model';
import { DateCellValidator } from './date-cell-validator-model';
import { DynamicRowValidationSummary } from './dynamic-row-validation-summary.model';
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
import { NumberCellValidator } from './number-cell-validator.model';
import { RequiredCellValidator } from './required-cell-validator.model';
export class DynamicTableModel extends FormWidgetModel {
field: FormFieldModel;
columns: DynamicTableColumn[] = [];
visibleColumns: DynamicTableColumn[] = [];
rows: DynamicTableRow[] = [];
private _selectedRow: DynamicTableRow;
private readonly _validators: CellValidator[] = [];
get selectedRow(): DynamicTableRow {
return this._selectedRow;
}
set selectedRow(value: DynamicTableRow) {
if (this._selectedRow && this._selectedRow === value) {
this._selectedRow.selected = false;
this._selectedRow = null;
return;
}
this.rows.forEach((row) => row.selected = false);
this._selectedRow = value;
if (value) {
this._selectedRow.selected = true;
}
}
constructor(field: FormFieldModel, private formService: FormService) {
super(field.form, field.json);
this.field = field;
if (field.json) {
const columns = this.getColumns(field);
if (columns) {
this.columns = columns;
this.visibleColumns = this.columns.filter((col) => col.visible);
}
if (field.json.value) {
this.rows = field.json.value.map((obj) => ({ selected: false, value: obj } as DynamicTableRow));
}
}
this._validators = [
new RequiredCellValidator(),
new DateCellValidator(),
new NumberCellValidator()
];
}
private getColumns(field: FormFieldModel): DynamicTableColumn[] {
if (field && field.json) {
let definitions = field.json.columnDefinitions;
if (!definitions && field.json.params && field.json.params.field) {
definitions = field.json.params.field.columnDefinitions;
}
if (definitions) {
return definitions.map((obj) => obj as DynamicTableColumn);
}
}
return null;
}
flushValue() {
if (this.field) {
this.field.value = this.rows.map((r) => r.value);
this.field.updateForm();
}
}
moveRow(row: DynamicTableRow, offset: number) {
const oldIndex = this.rows.indexOf(row);
if (oldIndex > -1) {
let newIndex = (oldIndex + offset);
if (newIndex < 0) {
newIndex = 0;
} else if (newIndex >= this.rows.length) {
newIndex = this.rows.length;
}
const arr = this.rows.slice();
arr.splice(oldIndex, 1);
arr.splice(newIndex, 0, row);
this.rows = arr;
this.flushValue();
}
}
deleteRow(row: DynamicTableRow) {
if (row) {
if (this.selectedRow === row) {
this.selectedRow = null;
}
const idx = this.rows.indexOf(row);
if (idx > -1) {
this.rows.splice(idx, 1);
this.flushValue();
}
}
}
addRow(row: DynamicTableRow) {
if (row) {
this.rows.push(row);
// this.selectedRow = row;
}
}
validateRow(row: DynamicTableRow): DynamicRowValidationSummary {
const summary = new DynamicRowValidationSummary( {
isValid: true,
message: null
});
const event = new ValidateDynamicTableRowEvent(this.form, this.field, row, summary);
this.formService.validateDynamicTableRow.next(event);
if (event.defaultPrevented || !summary.isValid) {
return summary;
}
if (row) {
for (const col of this.columns) {
for (const validator of this._validators) {
if (!validator.validate(row, col, summary)) {
return summary;
}
}
}
}
return summary;
}
getCellValue(row: DynamicTableRow, column: DynamicTableColumn): any {
const rowValue = row.value[column.id];
if (column.type === 'Dropdown') {
if (rowValue) {
return rowValue.name;
}
}
if (column.type === 'Boolean') {
return !!rowValue;
}
if (column.type === 'Date') {
if (rowValue) {
return moment(rowValue.split('T')[0], 'YYYY-MM-DD').format('DD-MM-YYYY');
}
}
return rowValue || '';
}
getDisplayText(column: DynamicTableColumn): string {
let columnName = column.name;
if (column.type === 'Amount') {
const currency = column.amountCurrency || '$';
columnName = `${column.name} (${currency})`;
}
return columnName;
}
}

View File

@@ -1,172 +0,0 @@
/* stylelint-disable no-descending-specificity */
@import '../../../../styles/mixins';
$dynamic-table-font-size: var(--theme-body-1-font-size) !default;
$dynamic-table-header-font-size: var(--theme-caption-font-size) !default;
$dynamic-table-header-sort-icon-size: 16px !default;
$dynamic-table-hover-color: #eee !default;
$dynamic-table-selection-color: #e0f7fa !default;
$dynamic-table-row-height: 56px !default;
$dynamic-table-column-spacing: 36px !default;
$dynamic-table-column-padding: 18px !default;
$dynamic-table-card-padding: 24px !default;
$dynamic-table-cell-top: 12px !default;
$dynamic-table-drag-border: 1px dashed rgb(68, 138, 255);
dynamic-table-widget .adf-label {
width: auto;
height: auto;
}
.adf {
&-dynamic-table-scrolling {
overflow: auto;
}
&-dynamic-table {
width: 100%;
position: relative;
border: 1px solid var(--theme-border-color);
white-space: nowrap;
font-size: $dynamic-table-font-size;
/* Firefox fixes */
border-collapse: unset;
border-spacing: 0;
thead {
padding-bottom: 3px;
}
tbody {
tr {
position: relative;
height: $dynamic-table-row-height;
@include material-animation-default(0.28s);
transition-property: background-color;
&:hover {
background-color: $dynamic-table-hover-color;
}
&.adf-is-selected,
&.adf-is-selected:hover {
background-color: $dynamic-table-selection-color;
}
&:focus {
outline-offset: -1px;
outline: rgb(68, 138, 255) solid 1px;
}
}
}
td,
th {
padding: 0 $dynamic-table-column-padding 12px $dynamic-table-column-padding;
text-align: center;
&:first-of-type {
padding-left: 24px;
}
&:last-of-type {
padding-right: 24px;
}
}
td {
color: var(--theme-text-fg-color);
position: relative;
vertical-align: middle;
height: $dynamic-table-row-height;
border-top: 1px solid var(--theme-border-color);
border-bottom: 1px solid var(--theme-border-color);
padding-top: $dynamic-table-cell-top;
box-sizing: border-box;
@include adf-no-select;
cursor: default;
}
th {
@include adf-no-select;
cursor: pointer;
position: relative;
vertical-align: bottom;
text-overflow: ellipsis;
font-weight: bold;
line-height: 24px;
letter-spacing: 0;
height: $dynamic-table-row-height;
font-size: $dynamic-table-header-font-size;
color: var(--theme-text-fg-color);
padding-bottom: 8px;
box-sizing: border-box;
&.adf-sortable {
@include adf-no-select;
&:hover {
cursor: pointer;
}
}
&.adf-dynamic-table__header--sorted-asc,
&.adf-dynamic-table__header--sorted-desc {
color: var(--theme-text-fg-color);
&::before {
@include typo-icon;
font-size: $dynamic-table-header-sort-icon-size;
content: '\e5d8';
margin-right: 5px;
vertical-align: sub;
}
&:hover {
cursor: pointer;
&::before {
color: var(--theme-disabled-text-color);
}
}
}
&.adf-dynamic-table__header--sorted-desc::before {
content: '\e5db';
}
}
.adf-dynamic-table-cell {
text-align: left;
cursor: default;
&--text {
text-align: left;
}
&--number {
text-align: right;
}
&--image {
text-align: left;
img {
width: 24px;
height: 24px;
}
}
}
.adf-full-width {
width: 100%;
}
}
}

View File

@@ -1,374 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { LogService } from '../../../../services';
import { FormService } from '../../../services/form.service';
import { FormFieldModel, FormFieldTypes, FormModel } from '../core';
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
import { DynamicTableWidgetComponent } from './dynamic-table.widget';
import { DynamicTableModel } from './dynamic-table.widget.model';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { CoreTestingModule } from '../../../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
const fakeFormField = {
id: 'fake-dynamic-table',
name: 'fake-label',
value: [{1: 1, 2: 2, 3: 4}],
required: false,
readOnly: false,
overrideId: false,
colspan: 1,
placeholder: null,
minLength: 0,
maxLength: 0,
params: {
existingColspan: 1,
maxColspan: 1
},
sizeX: 2,
sizeY: 2,
row: -1,
col: -1,
columnDefinitions: [
{
id: 1,
name: 1,
type: 'String',
visible: true
},
{
id: 2,
name: 2,
type: 'String',
visible: true
},
{
id: 3,
name: 3,
type: 'String',
visible: true
}
]
};
describe('DynamicTableWidgetComponent', () => {
let widget: DynamicTableWidgetComponent;
let fixture: ComponentFixture<DynamicTableWidgetComponent>;
let element: HTMLElement;
let table: DynamicTableModel;
let logService: LogService;
let formService: FormService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
const field = new FormFieldModel(new FormModel());
logService = TestBed.inject(LogService);
formService = TestBed.inject(FormService);
table = new DynamicTableModel(field, formService);
const changeDetectorSpy = jasmine.createSpyObj('cd', ['detectChanges']);
const nativeElementSpy = jasmine.createSpyObj('nativeElement', ['querySelector']);
changeDetectorSpy.nativeElement = nativeElementSpy;
const elementRefSpy = jasmine.createSpyObj('elementRef', ['']);
elementRefSpy.nativeElement = nativeElementSpy;
fixture = TestBed.createComponent(DynamicTableWidgetComponent);
element = fixture.nativeElement;
widget = fixture.componentInstance;
widget.content = table;
widget.field = field;
});
afterEach(() => {
fixture.destroy();
});
it('should select row on click', () => {
const row = {selected: false} as DynamicTableRow;
widget.onRowClicked(row);
expect(row.selected).toBeTruthy();
expect(widget.content.selectedRow).toBe(row);
});
it('should require table to select clicked row', () => {
const row = {selected: false} as DynamicTableRow;
widget.content = null;
widget.onRowClicked(row);
expect(row.selected).toBeFalsy();
});
it('should reset selected row', () => {
const row = {selected: false} as DynamicTableRow;
widget.content.rows.push(row);
widget.content.selectedRow = row;
expect(widget.content.selectedRow).toBe(row);
expect(row.selected).toBeTruthy();
widget.onRowClicked(null);
expect(widget.content.selectedRow).toBeNull();
expect(row.selected).toBeFalsy();
});
it('should check selection', () => {
const row = {selected: false} as DynamicTableRow;
widget.content.rows.push(row);
widget.content.selectedRow = row;
expect(widget.hasSelection()).toBeTruthy();
widget.content.selectedRow = null;
expect(widget.hasSelection()).toBeFalsy();
widget.content = null;
expect(widget.hasSelection()).toBeFalsy();
});
it('should require table to move selection up', () => {
widget.content = null;
expect(widget.moveSelectionUp()).toBeFalsy();
});
it('should move selection up', () => {
const row1 = {} as DynamicTableRow;
const row2 = {} as DynamicTableRow;
widget.content.rows.push(...[row1, row2]);
widget.content.selectedRow = row2;
expect(widget.moveSelectionUp()).toBeTruthy();
expect(widget.content.rows.indexOf(row2)).toBe(0);
});
it('should require table to move selection down', () => {
widget.content = null;
expect(widget.moveSelectionDown()).toBeFalsy();
});
it('should move selection down', () => {
const row1 = {} as DynamicTableRow;
const row2 = {} as DynamicTableRow;
widget.content.rows.push(...[row1, row2]);
widget.content.selectedRow = row1;
expect(widget.moveSelectionDown()).toBeTruthy();
expect(widget.content.rows.indexOf(row1)).toBe(1);
});
it('should require table to delete selection', () => {
widget.content = null;
expect(widget.deleteSelection()).toBeFalsy();
});
it('should delete selected row', () => {
const row = {} as DynamicTableRow;
widget.content.rows.push(row);
widget.content.selectedRow = row;
widget.deleteSelection();
expect(widget.content.rows.length).toBe(0);
});
it('should require table to add new row', () => {
widget.content = null;
expect(widget.addNewRow()).toBeFalsy();
});
it('should start editing new row', () => {
expect(widget.editMode).toBeFalsy();
expect(widget.editRow).toBeNull();
expect(widget.addNewRow()).toBeTruthy();
expect(widget.editRow).not.toBeNull();
expect(widget.editMode).toBeTruthy();
});
it('should require table to edit selected row', () => {
widget.content = null;
expect(widget.editSelection()).toBeFalsy();
});
it('should start editing selected row', () => {
expect(widget.editMode).toBeFalsy();
expect(widget.editRow).toBeFalsy();
const row = {value: true} as DynamicTableRow;
widget.content.selectedRow = row;
expect(widget.editSelection()).toBeTruthy();
expect(widget.editMode).toBeTruthy();
expect(widget.editRow).not.toBeNull();
expect(widget.editRow.value).toEqual(row.value);
});
it('should copy row', () => {
const row = {value: {opt: {key: '1', value: 1}}} as DynamicTableRow;
const copy = widget.copyRow(row);
expect(copy.value).toEqual(row.value);
});
it('should require table to retrieve cell value', () => {
widget.content = null;
expect(widget.getCellValue(null, null)).toBeNull();
});
it('should retrieve cell value', () => {
const value = '<value>';
const row = {value: {key: value}} as DynamicTableRow;
const column = {id: 'key'} as DynamicTableColumn;
expect(widget.getCellValue(row, column)).toBe(value);
});
it('should save changes and add new row', () => {
const row = {isNew: true, value: {key: 'value'}} as DynamicTableRow;
widget.editMode = true;
widget.editRow = row;
widget.onSaveChanges();
expect(row.isNew).toBeFalsy();
expect(widget.content.selectedRow).toBeNull();
expect(widget.content.rows.length).toBe(1);
expect(widget.content.rows[0].value).toEqual(row.value);
});
it('should save changes and update row', () => {
const row = {isNew: false, value: {key: 'value'}} as DynamicTableRow;
widget.editMode = true;
widget.editRow = row;
widget.content.selectedRow = row;
widget.onSaveChanges();
expect(widget.content.selectedRow.value).toEqual(row.value);
});
it('should require table to save changes', () => {
spyOn(logService, 'error').and.stub();
widget.editMode = true;
widget.content = null;
widget.onSaveChanges();
expect(widget.editMode).toBeFalsy();
});
it('should cancel changes', () => {
widget.editMode = true;
widget.editRow = {} as DynamicTableRow;
widget.onCancelChanges();
expect(widget.editMode).toBeFalsy();
expect(widget.editRow).toBeNull();
});
it('should be valid by default', () => {
widget.content.field = null;
expect(widget.isValid()).toBeTruthy();
widget.content = null;
expect(widget.isValid()).toBeTruthy();
});
it('should take validation state from underlying field', () => {
const form = new FormModel();
const field = new FormFieldModel(form, {
type: FormFieldTypes.DYNAMIC_TABLE,
required: true,
value: null
});
widget.content = new DynamicTableModel(field, formService);
expect(widget.content.field.validate()).toBeFalsy();
expect(widget.isValid()).toBe(widget.content.field.isValid);
expect(widget.content.field.isValid).toBeFalsy();
widget.content.field.value = [{}];
expect(widget.content.field.validate()).toBeTruthy();
expect(widget.isValid()).toBe(widget.content.field.isValid);
expect(widget.content.field.isValid).toBeTruthy();
});
it('should prepend default currency for amount columns', () => {
const row = {value: {key: '100'}} as DynamicTableRow;
const column = {id: 'key', type: 'Amount'} as DynamicTableColumn;
const actual = widget.getCellValue(row, column);
expect(actual).toBe('$ 100');
});
it('should prepend custom currency for amount columns', () => {
const row = {value: {key: '100'}} as DynamicTableRow;
const column = {id: 'key', type: 'Amount', amountCurrency: 'GBP'} as DynamicTableColumn;
const actual = widget.getCellValue(row, column);
expect(actual).toBe('GBP 100');
});
describe('when template is ready', () => {
beforeEach(() => {
widget.field = new FormFieldModel(new FormModel({taskId: 'fake-task-id'}), fakeFormField);
widget.field.type = FormFieldTypes.DYNAMIC_TABLE;
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
it('should select a row when press space bar', async () => {
const rowElement = element.querySelector('#fake-dynamic-table-row-0');
expect(element.querySelector('#dynamic-table-fake-dynamic-table')).not.toBeNull();
expect(rowElement).not.toBeNull();
expect(rowElement.className).not.toContain('adf-dynamic-table-widget__row-selected');
const event: any = new Event('keyup');
event.keyCode = 32;
rowElement.dispatchEvent(event);
fixture.detectChanges();
await fixture.whenStable();
const selectedRow = element.querySelector('#fake-dynamic-table-row-0');
expect(selectedRow.className).toContain('adf-dynamic-table-widget__row-selected');
});
it('should focus on add button when a new row is saved', async () => {
const addNewRowButton = element.querySelector<HTMLButtonElement>('#fake-dynamic-table-add-row');
expect(element.querySelector('#dynamic-table-fake-dynamic-table')).not.toBeNull();
expect(addNewRowButton).not.toBeNull();
widget.addNewRow();
widget.onSaveChanges();
fixture.detectChanges();
await fixture.whenStable();
expect(document.activeElement.id).toBe('fake-dynamic-table-add-row');
});
});
});

View File

@@ -1,215 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { LogService } from '../../../../services/log.service';
import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewEncapsulation } from '@angular/core';
import { WidgetVisibilityService } from '../../../services/widget-visibility.service';
import { FormService } from '../../../services/form.service';
import { WidgetComponent } from '../widget.component';
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
import { DynamicTableModel } from './dynamic-table.widget.model';
@Component({
selector: 'dynamic-table-widget',
templateUrl: './dynamic-table.widget.html',
styleUrls: ['./dynamic-table.widget.scss'],
host: {
'(click)': 'event($event)',
'(blur)': 'event($event)',
'(change)': 'event($event)',
'(focus)': 'event($event)',
'(focusin)': 'event($event)',
'(focusout)': 'event($event)',
'(input)': 'event($event)',
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
encapsulation: ViewEncapsulation.None
})
export class DynamicTableWidgetComponent extends WidgetComponent implements OnInit {
ERROR_MODEL_NOT_FOUND = 'Table model not found';
content: DynamicTableModel;
editMode: boolean = false;
editRow: DynamicTableRow = null;
private selectArrayCode = [32, 0, 13];
constructor(public formService: FormService,
public elementRef: ElementRef,
private visibilityService: WidgetVisibilityService,
private logService: LogService,
private cd: ChangeDetectorRef) {
super(formService);
}
ngOnInit() {
if (this.field) {
this.content = new DynamicTableModel(this.field, this.formService);
this.visibilityService.refreshVisibility(this.field.form);
}
}
forceFocusOnAddButton() {
if (this.content) {
this.cd.detectChanges();
const buttonAddRow = this.elementRef.nativeElement.querySelector('#' + this.content.id + '-add-row');
if (this.isDynamicTableReady(buttonAddRow)) {
buttonAddRow.focus();
}
}
}
private isDynamicTableReady(buttonAddRow) {
return this.field && !this.editMode && buttonAddRow;
}
isValid() {
let valid = true;
if (this.content && this.content.field) {
valid = this.content.field.isValid;
}
return valid;
}
onRowClicked(row: DynamicTableRow) {
if (this.content) {
this.content.selectedRow = row;
}
}
onKeyPressed($event: KeyboardEvent, row: DynamicTableRow) {
if (this.content && this.isEnterOrSpacePressed($event.keyCode)) {
this.content.selectedRow = row;
}
}
private isEnterOrSpacePressed(keyCode) {
return this.selectArrayCode.indexOf(keyCode) !== -1;
}
hasSelection(): boolean {
return !!(this.content && this.content.selectedRow);
}
moveSelectionUp(): boolean {
if (this.content && !this.readOnly) {
this.content.moveRow(this.content.selectedRow, -1);
return true;
}
return false;
}
moveSelectionDown(): boolean {
if (this.content && !this.readOnly) {
this.content.moveRow(this.content.selectedRow, 1);
return true;
}
return false;
}
deleteSelection(): boolean {
if (this.content && !this.readOnly) {
this.content.deleteRow(this.content.selectedRow);
return true;
}
return false;
}
addNewRow(): boolean {
if (this.content && !this.readOnly) {
this.editRow = {
isNew: true,
selected: false,
value: {}
};
this.editMode = true;
return true;
}
return false;
}
editSelection(): boolean {
if (this.content && !this.readOnly) {
this.editRow = this.copyRow(this.content.selectedRow);
this.editMode = true;
return true;
}
return false;
}
getCellValue(row: DynamicTableRow, column: DynamicTableColumn): any {
if (this.content) {
const cellValue = this.content.getCellValue(row, column);
if (column.type === 'Amount') {
return (column.amountCurrency || '$') + ' ' + (cellValue || 0);
}
return cellValue;
}
return null;
}
onSaveChanges() {
if (this.content) {
if (this.editRow.isNew) {
const row = this.copyRow(this.editRow);
this.content.selectedRow = null;
this.content.addRow(row);
this.editRow.isNew = false;
} else {
this.content.selectedRow.value = this.copyObject(this.editRow.value);
}
this.content.flushValue();
} else {
this.logService.error(this.ERROR_MODEL_NOT_FOUND);
}
this.editMode = false;
this.forceFocusOnAddButton();
}
onCancelChanges() {
this.editMode = false;
this.editRow = null;
this.forceFocusOnAddButton();
}
copyRow(row: DynamicTableRow): DynamicTableRow {
return { value: this.copyObject(row.value) } as DynamicTableRow;
}
private copyObject(obj: any): any {
let result = obj;
if (typeof obj === 'object' && obj !== null && obj !== undefined) {
result = Object.assign({}, obj);
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'object') {
result[key] = this.copyObject(obj[key]);
}
});
}
return result;
}
}

View File

@@ -1,12 +0,0 @@
<div class="adf-amount-editor">
<mat-form-field>
<label [attr.for]="column.id">{{displayName}}</label>
<input matInput
type="number"
[value]="table.getCellValue(row, column)"
(keyup)="onValueChanged(row, column, $event)"
[required]="column.required"
[disabled]="!column.editable"
[id]="column.id">
</mat-form-field>
</div>

View File

@@ -1,5 +0,0 @@
.adf {
&-text-editor {
width: 100%;
}
}

View File

@@ -1,40 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { DynamicTableColumn } from '../../dynamic-table-column.model';
import { DynamicTableRow } from '../../dynamic-table-row.model';
import { AmountEditorComponent } from './amount.editor';
describe('AmountEditorComponent', () => {
let editor: AmountEditorComponent;
beforeEach(() => {
editor = new AmountEditorComponent();
});
it('should update row value on change', () => {
const row = { value: {} } as DynamicTableRow;
const column = { id: 'key' } as DynamicTableColumn;
const value = 100;
const event = { target: { value } };
editor.onValueChanged(row, column, event);
expect(row.value[column.id]).toBe(value);
});
});

View File

@@ -1,52 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { Component, Input, OnInit } from '@angular/core';
import { DynamicTableColumn } from '../../dynamic-table-column.model';
import { DynamicTableRow } from '../../dynamic-table-row.model';
import { DynamicTableModel } from '../../dynamic-table.widget.model';
@Component({
selector: 'adf-amount-editor',
templateUrl: './amount.editor.html',
styleUrls: ['./amount.editor.scss']
})
export class AmountEditorComponent implements OnInit {
@Input()
table: DynamicTableModel;
@Input()
row: DynamicTableRow;
@Input()
column: DynamicTableColumn;
displayName: string;
ngOnInit() {
this.displayName = this.table.getDisplayText(this.column);
}
onValueChanged(row: DynamicTableRow, column: DynamicTableColumn, event: any) {
const value: number = Number(event.target.value);
row.value[column.id] = value;
}
}

View File

@@ -1,11 +0,0 @@
<label [attr.for]="column.id">
<mat-checkbox
color="primary"
[id]="column.id"
[checked]="table.getCellValue(row, column)"
[required]="column.required"
[disabled]="!column.editable"
(change)="onValueChanged(row, column, $event)">
<span class="adf-checkbox-label">{{column.name}}</span>
</mat-checkbox>
</label>

View File

@@ -1,9 +0,0 @@
.adf {
&-checkbox-label {
position: relative;
cursor: pointer;
font-size: var(--theme-subheading-2-font-size);
line-height: 24px;
margin: 0;
}
}

View File

@@ -1,39 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { DynamicTableColumn } from '../../dynamic-table-column.model';
import { DynamicTableRow } from '../../dynamic-table-row.model';
import { BooleanEditorComponent } from './boolean.editor';
describe('BooleanEditorComponent', () => {
let component: BooleanEditorComponent;
beforeEach(() => {
component = new BooleanEditorComponent();
});
it('should update row value on change', () => {
const row = { value: {} } as DynamicTableRow;
const column = { id: 'key' } as DynamicTableColumn;
const event = { checked: true } ;
component.onValueChanged(row, column, event);
expect(row.value[column.id]).toBeTruthy();
});
});

View File

@@ -1,46 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { Component, Input } from '@angular/core';
import { DynamicTableColumn } from '../../dynamic-table-column.model';
import { DynamicTableRow } from '../../dynamic-table-row.model';
import { DynamicTableModel } from '../../dynamic-table.widget.model';
@Component({
selector: 'adf-boolean-editor',
templateUrl: './boolean.editor.html',
styleUrls: ['./boolean.editor.scss']
})
export class BooleanEditorComponent {
@Input()
table: DynamicTableModel;
@Input()
row: DynamicTableRow;
@Input()
column: DynamicTableColumn;
onValueChanged(row: DynamicTableRow, column: DynamicTableColumn, event: any) {
const value: boolean = event.checked;
row.value[column.id] = value;
}
}

View File

@@ -1,17 +0,0 @@
<div>
<mat-form-field class="adf-date-editor">
<label [attr.for]="column.id">{{column.name}} ({{DATE_FORMAT}})</label>
<input matInput
id="dateInput"
type="text"
[matDatepicker]="datePicker"
[value]="value"
[id]="column.id"
[required]="column.required"
[disabled]="!column.editable"
(focusout)="onDateChanged($any($event).srcElement)"
(dateChange)="onDateChanged($event)">
<mat-datepicker-toggle *ngIf="column.editable" matSuffix [for]="datePicker" class="adf-date-editor-button" ></mat-datepicker-toggle>
</mat-form-field>
<mat-datepicker #datePicker [touchUi]="true"></mat-datepicker>
</div>

View File

@@ -1,10 +0,0 @@
.adf {
&-date-editor {
width: 100%;
}
&-date-editor-button {
position: relative;
top: 25px;
}
}

View File

@@ -1,143 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { FormFieldModel, FormModel } from '../../../index';
import { DynamicTableColumn } from '../../dynamic-table-column.model';
import { DynamicTableRow } from '../../dynamic-table-row.model';
import { DynamicTableModel } from '../../dynamic-table.widget.model';
import { DateEditorComponent } from './date.editor';
import { setupTestBed } from '../../../../../../testing/setup-test-bed';
import { By } from '@angular/platform-browser';
import { MatDatepickerInputEvent } from '@angular/material/datepicker';
import { CoreTestingModule } from '../../../../../../testing';
import { TranslateModule } from '@ngx-translate/core';
describe('DateEditorComponent', () => {
let component: DateEditorComponent;
let fixture: ComponentFixture<DateEditorComponent>;
let row: DynamicTableRow;
let column: DynamicTableColumn;
let table: DynamicTableModel;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
fixture = TestBed.createComponent(DateEditorComponent);
component = fixture.componentInstance;
row = { value: { date: '1879-03-14T00:00:00.000Z' } } as DynamicTableRow;
column = { id: 'date', type: 'Date' } as DynamicTableColumn;
const field = new FormFieldModel(new FormModel());
table = new DynamicTableModel(field, null);
table.rows.push(row);
table.columns.push(column);
component.table = table;
component.row = row;
component.column = column;
});
describe('using Date Piker', () => {
it('should update row value on change', () => {
const input = {value: '14-03-2016' } as MatDatepickerInputEvent<any>;
component.ngOnInit();
component.onDateChanged(input);
const actual = row.value[column.id];
expect(actual).toBe('2016-03-14T00:00:00.000Z');
});
it('should flush value on user input', () => {
spyOn(table, 'flushValue').and.callThrough();
const input = {value: '14-03-2016' } as MatDatepickerInputEvent<any>;
component.ngOnInit();
component.onDateChanged(input);
expect(table.flushValue).toHaveBeenCalled();
});
});
describe('user manual input', () => {
beforeEach(() => {
spyOn(component, 'onDateChanged').and.callThrough();
spyOn(table, 'flushValue').and.callThrough();
});
it('should update row value upon user input', () => {
const inputElement = fixture.debugElement.query(By.css('input'));
inputElement.nativeElement.value = '14-03-1879';
inputElement.nativeElement.dispatchEvent(new Event('focusout'));
fixture.detectChanges();
expect(component.onDateChanged).toHaveBeenCalled();
const actual = row.value[column.id];
expect(actual).toBe('1879-03-14T00:00:00.000Z');
});
it('should flush value on user input', () => {
const inputElement = fixture.debugElement.query(By.css('input'));
inputElement.nativeElement.value = '14-03-1879';
inputElement.nativeElement.dispatchEvent(new Event('focusout'));
fixture.detectChanges();
expect(table.flushValue).toHaveBeenCalled();
});
it('should not flush value when user input is wrong', () => {
const inputElement = fixture.debugElement.query(By.css('input'));
inputElement.nativeElement.value = 'ab-bc-de';
inputElement.nativeElement.dispatchEvent(new Event('focusout'));
fixture.detectChanges();
expect(table.flushValue).not.toHaveBeenCalled();
inputElement.nativeElement.value = '12';
inputElement.nativeElement.dispatchEvent(new Event('focusout'));
fixture.detectChanges();
expect(table.flushValue).not.toHaveBeenCalled();
inputElement.nativeElement.value = '12-11';
inputElement.nativeElement.dispatchEvent(new Event('focusout'));
fixture.detectChanges();
expect(table.flushValue).not.toHaveBeenCalled();
inputElement.nativeElement.value = '12-13-12';
inputElement.nativeElement.dispatchEvent(new Event('focusout'));
fixture.detectChanges();
expect(table.flushValue).not.toHaveBeenCalled();
});
it('should remove the date when user removes manually', () => {
const inputElement = fixture.debugElement.query(By.css('input'));
inputElement.nativeElement.value = '';
inputElement.nativeElement.dispatchEvent(new Event('focusout'));
fixture.detectChanges();
expect(component.onDateChanged).toHaveBeenCalled();
const actual = row.value[column.id];
expect(actual).toBe('');
});
});
});

View File

@@ -1,100 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { UserPreferencesService, UserPreferenceValues } from '../../../../../../services/user-preferences.service';
import { MomentDateAdapter } from '../../../../../../utils/moment-date-adapter';
import { MOMENT_DATE_FORMATS } from '../../../../../../utils/moment-date-formats.model';
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { MatDatepickerInputEvent } from '@angular/material/datepicker';
import moment, { Moment } from 'moment';
import { DynamicTableColumn } from '../../dynamic-table-column.model';
import { DynamicTableRow } from '../../dynamic-table-row.model';
import { DynamicTableModel } from '../../dynamic-table.widget.model';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'adf-date-editor',
templateUrl: './date.editor.html',
providers: [
{provide: DateAdapter, useClass: MomentDateAdapter},
{provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS}],
styleUrls: ['./date.editor.scss']
})
export class DateEditorComponent implements OnInit, OnDestroy {
DATE_FORMAT: string = 'DD-MM-YYYY';
value: any;
@Input()
table: DynamicTableModel;
@Input()
row: DynamicTableRow;
@Input()
column: DynamicTableColumn;
minDate: Moment;
maxDate: Moment;
private onDestroy$ = new Subject<boolean>();
constructor(private dateAdapter: DateAdapter<Moment>,
private userPreferencesService: UserPreferencesService) {
}
ngOnInit() {
this.userPreferencesService
.select(UserPreferenceValues.Locale)
.pipe(takeUntil(this.onDestroy$))
.subscribe(locale => this.dateAdapter.setLocale(locale));
const momentDateAdapter = this.dateAdapter as MomentDateAdapter;
momentDateAdapter.overrideDisplayFormat = this.DATE_FORMAT;
this.value = moment(this.table.getCellValue(this.row, this.column), this.DATE_FORMAT);
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
onDateChanged(newDateValue: MatDatepickerInputEvent<any> | HTMLInputElement) {
if (newDateValue && newDateValue.value) {
/* validates the user inputs */
const momentDate = moment(newDateValue.value, this.DATE_FORMAT, true);
if (!momentDate.isValid()) {
this.row.value[this.column.id] = newDateValue.value;
} else {
this.row.value[this.column.id] = `${momentDate.format('YYYY-MM-DD')}T00:00:00.000Z`;
this.table.flushValue();
}
} else {
/* removes the date */
this.row.value[this.column.id] = '';
}
}
}

View File

@@ -1,24 +0,0 @@
<div>
<mat-form-field class="adf-date-editor">
<label [attr.for]="column.id">{{column.name}} {{DATE_TIME_FORMAT}}</label>
<input matInput
[matDatetimepicker]="datetimePicker"
[(ngModel)]="value"
[id]="column.id"
[required]="column.required"
[disabled]="!column.editable"
(focusout)="onDateChanged($any($event).srcElement.value)"
(dateChange)="onDateChanged($event)">
<mat-datetimepicker-toggle
matSuffix
[for]="datetimePicker"
class="adf-date-editor-button">
</mat-datetimepicker-toggle>
</mat-form-field>
<mat-datetimepicker
#datetimePicker
type="datetime"
[openOnFocus]="true"
[timeInterval]="5">
</mat-datetimepicker>
</div>

View File

@@ -1,10 +0,0 @@
.adf {
&-date-editor {
width: 100%;
}
&-date-editor-button {
position: relative;
top: 25px;
}
}

View File

@@ -1,84 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 moment from 'moment';
import { FormFieldModel, FormModel } from '../../../index';
import { DynamicTableColumn } from '../../dynamic-table-column.model';
import { DynamicTableRow } from '../../dynamic-table-row.model';
import { DynamicTableModel } from '../../dynamic-table.widget.model';
import { DateTimeEditorComponent } from './datetime.editor';
import { setupTestBed } from '../../../../../../testing/setup-test-bed';
import { CoreTestingModule } from '../../../../../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
describe('DateTimeEditorComponent', () => {
let component: DateTimeEditorComponent;
let fixture: ComponentFixture<DateTimeEditorComponent>;
let row: DynamicTableRow;
let column: DynamicTableColumn;
let table: DynamicTableModel;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
fixture = TestBed.createComponent(DateTimeEditorComponent);
component = fixture.componentInstance;
row = { value: { date: '1879-03-14T00:00:00.000Z' } } as DynamicTableRow;
column = { id: 'datetime', type: 'Datetime' } as DynamicTableColumn;
const field = new FormFieldModel(new FormModel());
table = new DynamicTableModel(field, null);
table.rows.push(row);
table.columns.push(column);
component.table = table;
component.row = row;
component.column = column;
});
it('should update fow value on change', () => {
component.ngOnInit();
const newDate = moment('22-6-2018 04:20 AM', 'D-M-YYYY hh:mm A');
component.onDateChanged(newDate);
expect(moment(row.value[column.id]).isSame(newDate)).toBeTruthy();
});
it('should update row value upon user input', () => {
const input = '22-6-2018 04:20 AM';
component.ngOnInit();
component.onDateChanged(input);
const actual = row.value[column.id];
expect(actual).toBe('22-6-2018 04:20 AM');
});
it('should flush value on user input', () => {
spyOn(table, 'flushValue').and.callThrough();
const input = '22-6-2018 04:20 AM';
component.ngOnInit();
component.onDateChanged(input);
expect(table.flushValue).toHaveBeenCalled();
});
});

View File

@@ -1,102 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { UserPreferencesService, UserPreferenceValues } from '../../../../../../services/user-preferences.service';
import { MomentDateAdapter } from '../../../../../../utils/moment-date-adapter';
import { MOMENT_DATE_FORMATS } from '../../../../../../utils/moment-date-formats.model';
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import moment, { Moment } from 'moment';
import { DynamicTableColumn } from '../../dynamic-table-column.model';
import { DynamicTableRow } from '../../dynamic-table-row.model';
import { DynamicTableModel } from '../../dynamic-table.widget.model';
import { DatetimeAdapter, MAT_DATETIME_FORMATS } from '@mat-datetimepicker/core';
import { MomentDatetimeAdapter, MAT_MOMENT_DATETIME_FORMATS } from '@mat-datetimepicker/moment';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'adf-datetime-editor',
templateUrl: './datetime.editor.html',
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter },
{ provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS },
{ provide: DatetimeAdapter, useClass: MomentDatetimeAdapter },
{ provide: MAT_DATETIME_FORMATS, useValue: MAT_MOMENT_DATETIME_FORMATS }
],
styleUrls: ['./datetime.editor.scss']
})
export class DateTimeEditorComponent implements OnInit, OnDestroy {
DATE_TIME_FORMAT: string = 'DD/MM/YYYY HH:mm';
value: any;
@Input()
table: DynamicTableModel;
@Input()
row: DynamicTableRow;
@Input()
column: DynamicTableColumn;
minDate: Moment;
maxDate: Moment;
private onDestroy$ = new Subject<boolean>();
constructor(private dateAdapter: DateAdapter<Moment>,
private userPreferencesService: UserPreferencesService) {
}
ngOnInit() {
this.userPreferencesService
.select(UserPreferenceValues.Locale)
.pipe(takeUntil(this.onDestroy$))
.subscribe(locale => this.dateAdapter.setLocale(locale));
const momentDateAdapter = this.dateAdapter as MomentDateAdapter;
momentDateAdapter.overrideDisplayFormat = this.DATE_TIME_FORMAT;
this.value = moment(this.table.getCellValue(this.row, this.column), this.DATE_TIME_FORMAT);
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
onDateChanged(newDateValue) {
if (newDateValue && newDateValue.value) {
const newValue = moment(newDateValue.value, this.DATE_TIME_FORMAT);
this.row.value[this.column.id] = newDateValue.value.format(this.DATE_TIME_FORMAT);
this.value = newValue;
this.table.flushValue();
} else if (newDateValue) {
const newValue = moment(newDateValue, this.DATE_TIME_FORMAT);
this.value = newValue;
this.row.value[this.column.id] = newDateValue;
this.table.flushValue();
} else {
this.row.value[this.column.id] = '';
}
}
}

View File

@@ -1,16 +0,0 @@
<div class="dropdown-editor">
<label [attr.for]="column.id">{{column.name}}</label>
<mat-form-field>
<mat-select
floatPlaceholder="never"
class="adf-dropdown-editor-select"
[id]="column.id"
[(ngModel)]="value"
[required]="column.required"
[disabled]="!column.editable"
(selectionChange)="onValueChanged(row, column, $event)">
<mat-option></mat-option>
<mat-option *ngFor="let opt of options" [value]="opt.name" [id]="opt.id">{{opt.name}}</mat-option>
</mat-select>
</mat-form-field>
</div>

View File

@@ -1,5 +0,0 @@
.adf {
&-dropdown-editor-select {
width: 100%;
}
}

View File

@@ -1,307 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { By } from '@angular/platform-browser';
import { Observable, of, throwError } from 'rxjs';
import { FormService } from '../../../../../services/form.service';
import { FormFieldModel, FormModel } from '../../../core';
import { DynamicTableColumnOption } from '../../dynamic-table-column-option.model';
import { DynamicTableColumn } from '../../dynamic-table-column.model';
import { DynamicTableRow } from '../../dynamic-table-row.model';
import { DynamicTableModel } from '../../dynamic-table.widget.model';
import { DropdownEditorComponent } from './dropdown.editor';
import { setupTestBed } from '../../../../../../testing/setup-test-bed';
import { CoreTestingModule } from '../../../../../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { AlfrescoApiService } from '../../../../../../services';
describe('DropdownEditorComponent', () => {
let component: DropdownEditorComponent;
let formService: FormService;
let form: FormModel;
let table: DynamicTableModel;
let column: DynamicTableColumn;
let row: DynamicTableRow;
let alfrescoApiService: AlfrescoApiService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
alfrescoApiService = TestBed.inject(AlfrescoApiService);
formService = new FormService(null, alfrescoApiService, null);
row = {value: {dropdown: 'one'}} as DynamicTableRow;
column = {
id: 'dropdown',
options: [
{id: '1', name: 'one'},
{id: '2', name: 'two'}
]
} as DynamicTableColumn;
form = new FormModel({taskId: '<task-id>'});
table = new DynamicTableModel(new FormFieldModel(form, {id: '<field-id>'}), formService);
table.rows.push(row);
table.columns.push(column);
component = new DropdownEditorComponent(formService, null);
component.table = table;
component.row = row;
component.column = column;
});
it('should require table field to setup', () => {
table.field = null;
component.ngOnInit();
expect(component.value).toBeNull();
expect(component.options).toEqual([]);
});
it('should setup with manual mode', () => {
row.value[column.id] = 'two';
component.ngOnInit();
expect(component.options).toEqual(column.options);
expect(component.value).toBe(row.value[column.id]);
});
it('should setup empty columns for manual mode', () => {
column.options = null;
component.ngOnInit();
expect(component.options).toEqual([]);
});
it('should setup with REST mode', () => {
column.optionType = 'rest';
row.value[column.id] = 'twelve';
const restResults: DynamicTableColumnOption[] = [
{id: '11', name: 'eleven'},
{id: '12', name: 'twelve'}
];
spyOn(formService, 'getRestFieldValuesColumn').and.returnValue(
new Observable((observer) => {
observer.next(restResults);
observer.complete();
})
);
component.ngOnInit();
expect(formService.getRestFieldValuesColumn).toHaveBeenCalledWith(
form.taskId,
table.field.id,
column.id
);
expect(column.options).toEqual(restResults);
expect(component.options).toEqual(restResults);
expect(component.value).toBe(row.value[column.id]);
});
it('should create empty options array on REST response', () => {
column.optionType = 'rest';
spyOn(formService, 'getRestFieldValuesColumn').and.returnValue(
new Observable((observer) => {
observer.next(null);
observer.complete();
})
);
component.ngOnInit();
expect(formService.getRestFieldValuesColumn).toHaveBeenCalledWith(
form.taskId,
table.field.id,
column.id
);
expect(column.options).toEqual([]);
expect(component.options).toEqual([]);
expect(component.value).toBe(row.value[column.id]);
});
it('should handle REST error getting options with task id', () => {
column.optionType = 'rest';
const error = 'error';
spyOn(formService, 'getRestFieldValuesColumn').and.returnValue(
throwError(error)
);
spyOn(component, 'handleError').and.stub();
component.ngOnInit();
expect(component.handleError).toHaveBeenCalledWith(error);
});
it('should handle REST error getting option with processDefinitionId', () => {
column.optionType = 'rest';
const procForm = new FormModel({processDefinitionId: '<process-definition-id>'});
const procTable = new DynamicTableModel(new FormFieldModel(procForm, {id: '<field-id>'}), formService);
component.table = procTable;
const error = 'error';
spyOn(formService, 'getRestFieldValuesColumnByProcessId').and.returnValue(
throwError(error)
);
spyOn(component, 'handleError').and.stub();
component.ngOnInit();
expect(component.handleError).toHaveBeenCalledWith(error);
});
it('should update row on value change', () => {
const event = {value: 'two'};
component.onValueChanged(row, column, event);
expect(row.value[column.id]).toBe(column.options[1]);
});
describe('when template is ready', () => {
let dropDownEditorComponent: DropdownEditorComponent;
let fixture: ComponentFixture<DropdownEditorComponent>;
let element: HTMLElement;
let stubFormService;
const fakeOptionList: DynamicTableColumnOption[] = [{
id: 'opt_1',
name: 'option_1'
}, {
id: 'opt_2',
name: 'option_2'
}, {id: 'opt_3', name: 'option_3'}];
let dynamicTable: DynamicTableModel;
const openSelect = () => {
const dropdown = fixture.debugElement.query(By.css('.mat-select-trigger'));
dropdown.triggerEventHandler('click', null);
fixture.detectChanges();
};
beforeEach(() => {
fixture = TestBed.createComponent(DropdownEditorComponent);
dropDownEditorComponent = fixture.componentInstance;
element = fixture.nativeElement;
});
afterEach(() => {
fixture.destroy();
});
describe('and dropdown is populated via taskId', () => {
beforeEach(() => {
stubFormService = fixture.debugElement.injector.get(FormService);
spyOn(stubFormService, 'getRestFieldValuesColumn').and.returnValue(of(fakeOptionList));
row = {value: {dropdown: 'one'}} as DynamicTableRow;
column = {
id: 'column-id',
optionType: 'rest',
options: [
{id: '1', name: 'one'},
{id: '2', name: 'two'}
]
} as DynamicTableColumn;
form = new FormModel({taskId: '<task-id>'});
dynamicTable = new DynamicTableModel(new FormFieldModel(form, {id: '<field-id>'}), formService);
dynamicTable.rows.push(row);
dynamicTable.columns.push(column);
dropDownEditorComponent.table = dynamicTable;
dropDownEditorComponent.column = column;
dropDownEditorComponent.row = row;
dropDownEditorComponent.table.field = new FormFieldModel(form, {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
restUrl: 'fake-rest-url'
});
dropDownEditorComponent.table.field.isVisible = true;
fixture.detectChanges();
});
it('should show visible dropdown widget', () => {
expect(element.querySelector('#column-id')).toBeDefined();
expect(element.querySelector('#column-id')).not.toBeNull();
openSelect();
const optOne = fixture.debugElement.queryAll(By.css('[id="mat-option-1"]'));
const optTwo = fixture.debugElement.queryAll(By.css('[id="mat-option-2"]'));
const optThree = fixture.debugElement.queryAll(By.css('[id="mat-option-3"]'));
expect(optOne).not.toBeNull();
expect(optTwo).not.toBeNull();
expect(optThree).not.toBeNull();
});
});
describe('and dropdown is populated via processDefinitionId', () => {
beforeEach(() => {
stubFormService = fixture.debugElement.injector.get(FormService);
spyOn(stubFormService, 'getRestFieldValuesColumnByProcessId').and.returnValue(of(fakeOptionList));
row = {value: {dropdown: 'one'}} as DynamicTableRow;
column = {
id: 'column-id',
optionType: 'rest',
options: [
{id: '1', name: 'one'},
{id: '2', name: 'two'}
]
} as DynamicTableColumn;
form = new FormModel({processDefinitionId: '<proc-id>'});
dynamicTable = new DynamicTableModel(new FormFieldModel(form, {id: '<field-id>'}), formService);
dynamicTable.rows.push(row);
dynamicTable.columns.push(column);
dropDownEditorComponent.table = dynamicTable;
dropDownEditorComponent.column = column;
dropDownEditorComponent.row = row;
dropDownEditorComponent.table.field = new FormFieldModel(form, {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
restUrl: 'fake-rest-url'
});
dropDownEditorComponent.table.field.isVisible = true;
fixture.detectChanges();
});
it('should show visible dropdown widget', () => {
expect(element.querySelector('#column-id')).toBeDefined();
expect(element.querySelector('#column-id')).not.toBeNull();
openSelect();
const optOne = fixture.debugElement.queryAll(By.css('[id="mat-option-1"]'));
const optTwo = fixture.debugElement.queryAll(By.css('[id="mat-option-2"]'));
const optThree = fixture.debugElement.queryAll(By.css('[id="mat-option-3"]'));
expect(optOne).not.toBeNull();
expect(optTwo).not.toBeNull();
expect(optThree).not.toBeNull();
});
});
});
});

View File

@@ -1,110 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { LogService } from '../../../../../../services/log.service';
import { Component, Input, OnInit } from '@angular/core';
import { FormService } from '../../../../../services/form.service';
import { DynamicTableColumnOption } from '../../dynamic-table-column-option.model';
import { DynamicTableColumn } from '../../dynamic-table-column.model';
import { DynamicTableRow } from '../../dynamic-table-row.model';
import { DynamicTableModel } from '../../dynamic-table.widget.model';
@Component({
selector: 'adf-dropdown-editor',
templateUrl: './dropdown.editor.html',
styleUrls: ['./dropdown.editor.scss']
})
export class DropdownEditorComponent implements OnInit {
value: any = null;
options: DynamicTableColumnOption[] = [];
@Input()
table: DynamicTableModel;
@Input()
row: DynamicTableRow;
@Input()
column: DynamicTableColumn;
constructor(public formService: FormService,
private logService: LogService) {
}
ngOnInit() {
const field = this.table.field;
if (field) {
if (this.column.optionType === 'rest') {
if (this.table.form && this.table.form.taskId) {
this.getValuesByTaskId(field);
} else {
this.getValuesByProcessDefinitionId(field);
}
} else {
this.options = this.column.options || [];
this.value = this.table.getCellValue(this.row, this.column);
}
}
}
getValuesByTaskId(field) {
this.formService
.getRestFieldValuesColumn(
field.form.taskId,
field.id,
this.column.id
)
.subscribe(
(dynamicTableColumnOption: DynamicTableColumnOption[]) => {
this.column.options = dynamicTableColumnOption || [];
this.options = this.column.options;
this.value = this.table.getCellValue(this.row, this.column);
},
(err) => this.handleError(err)
);
}
getValuesByProcessDefinitionId(field) {
this.formService
.getRestFieldValuesColumnByProcessId(
field.form.processDefinitionId,
field.id,
this.column.id
)
.subscribe(
(dynamicTableColumnOption: DynamicTableColumnOption[]) => {
this.column.options = dynamicTableColumnOption || [];
this.options = this.column.options;
this.value = this.table.getCellValue(this.row, this.column);
},
(err) => this.handleError(err)
);
}
onValueChanged(row: DynamicTableRow, column: DynamicTableColumn, event: any) {
let value: any = event.value;
value = column.options.find((opt) => opt.name === value);
row.value[column.id] = value;
}
handleError(error: any) {
this.logService.error(error);
}
}

View File

@@ -1,13 +0,0 @@
.row-editor {
padding: 8px;
}
.row-editor__validation-summary {
visibility: hidden;
}
.row-editor__invalid .row-editor__validation-summary {
padding: 8px 16px;
color: #d50000;
visibility: visible;
}

View File

@@ -1,54 +0,0 @@
<div class="row-editor mdl-shadow--2dp"
[class.row-editor__invalid]="!validationSummary.isValid">
<div class="mdl-grid" *ngFor="let column of table.columns">
<div class="mdl-cell mdl-cell--6-col" [ngSwitch]="column.type">
<div *ngSwitchCase="'Dropdown'">
<adf-dropdown-editor
[table]="table"
[row]="row"
[column]="column">
</adf-dropdown-editor>
</div>
<div *ngSwitchCase="'Date'">
<adf-date-editor
[table]="table"
[row]="row"
[column]="column">
</adf-date-editor>
</div>
<div *ngSwitchCase="'Datetime'">
<adf-datetime-editor
[table]="table"
[row]="row"
[column]="column">
</adf-datetime-editor>
</div>
<div *ngSwitchCase="'Boolean'">
<adf-boolean-editor
[table]="table"
[row]="row"
[column]="column">
</adf-boolean-editor>
</div>
<div *ngSwitchCase="'Amount'">
<adf-amount-editor
[table]="table"
[row]="row"
[column]="column">
</adf-amount-editor>
</div>
<div *ngSwitchDefault>
<adf-text-editor
[table]="table"
[row]="row"
[column]="column">
</adf-text-editor>
</div>
</div>
</div>
<error-widget [error]="validationSummary"></error-widget>
<div>
<button mat-button (click)="onCancelChanges()">Cancel</button>
<button mat-button (click)="onSaveChanges()">Save</button>
</div>
</div>

View File

@@ -1,95 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { FormFieldModel, FormModel } from '../../core';
import { FormService } from '../../../../services/form.service';
import { DynamicTableColumn } from '../dynamic-table-column.model';
import { DynamicTableRow } from '../dynamic-table-row.model';
import { DynamicTableModel } from '../dynamic-table.widget.model';
import { RowEditorComponent } from './row.editor';
import { AlfrescoApiService } from '../../../../../services';
import { TestBed } from '@angular/core/testing';
import { CoreTestingModule, setupTestBed } from '../../../../../testing';
import { TranslateModule } from '@ngx-translate/core';
import { DynamicRowValidationSummary } from '../dynamic-row-validation-summary.model';
describe('RowEditorComponent', () => {
let component: RowEditorComponent;
let alfrescoApiService: AlfrescoApiService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
alfrescoApiService = TestBed.inject(AlfrescoApiService);
component = new RowEditorComponent();
const field = new FormFieldModel(new FormModel());
component.table = new DynamicTableModel(field, new FormService(null, alfrescoApiService, null));
component.row = {} as DynamicTableRow;
component.column = {} as DynamicTableColumn;
});
it('should be valid upon init', () => {
expect(component.validationSummary.isValid).toBeTruthy();
expect(component.validationSummary.message).toBe('');
});
it('should emit [cancel] event', (done) => {
component.cancel.subscribe((e) => {
expect(e.table).toBe(component.table);
expect(e.row).toBe(component.row);
expect(e.column).toBe(component.column);
done();
});
component.onCancelChanges();
});
it('should validate row on save', () => {
spyOn(component.table, 'validateRow').and.callThrough();
component.onSaveChanges();
expect(component.table.validateRow).toHaveBeenCalledWith(component.row);
});
it('should emit [save] event', (done) => {
spyOn(component.table, 'validateRow').and.returnValue(
new DynamicRowValidationSummary({ isValid: true, message: null })
);
component.save.subscribe((event) => {
expect(event.table).toBe(component.table);
expect(event.row).toBe(component.row);
expect(event.column).toBe(component.column);
done();
});
component.onSaveChanges();
});
it('should not emit [save] event for invalid row', () => {
spyOn(component.table, 'validateRow').and.returnValue(
new DynamicRowValidationSummary({ isValid: false, message: 'error' })
);
let raised = false;
component.save.subscribe(() => raised = true);
component.onSaveChanges();
expect(raised).toBeFalsy();
});
});

View File

@@ -1,81 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { DynamicRowValidationSummary } from '../dynamic-row-validation-summary.model';
import { DynamicTableColumn } from '../dynamic-table-column.model';
import { DynamicTableRow } from '../dynamic-table-row.model';
import { DynamicTableModel } from '../dynamic-table.widget.model';
@Component({
selector: 'row-editor',
templateUrl: './row.editor.html',
styleUrls: ['./row.editor.css']
})
export class RowEditorComponent {
@Input()
table: DynamicTableModel;
@Input()
row: DynamicTableRow;
@Input()
column: DynamicTableColumn;
@Output()
save: EventEmitter<any> = new EventEmitter<any>();
@Output()
cancel: EventEmitter<any> = new EventEmitter<any>();
validationSummary: DynamicRowValidationSummary;
constructor() {
this.validationSummary = new DynamicRowValidationSummary({ isValid: true, message: '' });
}
onCancelChanges() {
this.cancel.emit({
table: this.table,
row: this.row,
column: this.column
});
}
onSaveChanges() {
this.validate();
if (this.isValid()) {
this.save.emit({
table: this.table,
row: this.row,
column: this.column
});
}
}
private isValid(): boolean {
return this.validationSummary && this.validationSummary.isValid;
}
private validate() {
this.validationSummary = this.table.validateRow(this.row);
}
}

View File

@@ -1,12 +0,0 @@
<div class="adf-text-editor">
<mat-form-field>
<label [attr.for]="column.id">{{displayName}}</label>
<input matInput
type="text"
[value]="table.getCellValue(row, column)"
(keyup)="onValueChanged(row, column, $event)"
[required]="column.required"
[disabled]="!column.editable"
[id]="column.id">
</mat-form-field>
</div>

View File

@@ -1,5 +0,0 @@
.adf {
&-text-editor {
width: 100%;
}
}

View File

@@ -1,40 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { DynamicTableColumn } from '../../dynamic-table-column.model';
import { DynamicTableRow } from '../../dynamic-table-row.model';
import { TextEditorComponent } from './text.editor';
describe('TextEditorComponent', () => {
let editor: TextEditorComponent;
beforeEach(() => {
editor = new TextEditorComponent();
});
it('should update row value on change', () => {
const row = { value: {} } as DynamicTableRow;
const column = { id: 'key' } as DynamicTableColumn;
const value = '<value>';
const event = { target: { value } };
editor.onValueChanged(row, column, event);
expect(row.value[column.id]).toBe(value);
});
});

View File

@@ -1,52 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { Component, Input, OnInit } from '@angular/core';
import { DynamicTableColumn } from '../../dynamic-table-column.model';
import { DynamicTableRow } from '../../dynamic-table-row.model';
import { DynamicTableModel } from '../../dynamic-table.widget.model';
@Component({
selector: 'adf-text-editor',
templateUrl: './text.editor.html',
styleUrls: ['./text.editor.scss']
})
export class TextEditorComponent implements OnInit {
@Input()
table: DynamicTableModel;
@Input()
row: DynamicTableRow;
@Input()
column: DynamicTableColumn;
displayName: string;
ngOnInit() {
this.displayName = this.table.getDisplayText(this.column);
}
onValueChanged(row: DynamicTableRow, column: DynamicTableColumn, event: any) {
const value: any = event.target.value;
row.value[column.id] = value;
}
}

View File

@@ -1,63 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { CellValidator } from './cell-validator.model';
import { DynamicRowValidationSummary } from './dynamic-row-validation-summary.model';
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
export class NumberCellValidator implements CellValidator {
private supportedTypes: string[] = [
'Number',
'Amount'
];
isSupported(column: DynamicTableColumn): boolean {
return column && column.required && this.supportedTypes.indexOf(column.type) > -1;
}
isNumber(value: any): boolean {
if (value === null || value === undefined || value === '') {
return false;
}
return !isNaN(+value);
}
validate(row: DynamicTableRow, column: DynamicTableColumn, summary?: DynamicRowValidationSummary): boolean {
if (this.isSupported(column)) {
const value = row.value[column.id];
if (value === null ||
value === undefined ||
value === '' ||
this.isNumber(value)) {
return true;
}
if (summary) {
summary.isValid = false;
summary.message = `Field '${column.name}' must be a number.`;
}
return false;
}
return true;
}
}

View File

@@ -1,55 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { CellValidator } from './cell-validator.model';
import { DynamicRowValidationSummary } from './dynamic-row-validation-summary.model';
import { DynamicTableColumn } from './dynamic-table-column.model';
import { DynamicTableRow } from './dynamic-table-row.model';
export class RequiredCellValidator implements CellValidator {
private supportedTypes: string[] = [
'String',
'Number',
'Amount',
'Date',
'Dropdown'
];
isSupported(column: DynamicTableColumn): boolean {
return column && column.required && this.supportedTypes.indexOf(column.type) > -1;
}
validate(row: DynamicTableRow, column: DynamicTableColumn, summary?: DynamicRowValidationSummary): boolean {
if (this.isSupported(column)) {
const value = row.value[column.id];
if (column.required) {
if (value === null || value === undefined || value === '') {
if (summary) {
summary.isValid = false;
summary.message = `Field '${column.name}' is required.`;
}
return false;
}
}
}
return true;
}
}

View File

@@ -1,30 +0,0 @@
<div class="adf-group-widget {{field.className}}"
[class.is-dirty]="!!field.value"
[class.adf-invalid]="!field.isValid && isTouched()"
[class.adf-readonly]="field.readOnly"
id="functional-group-div">
<mat-form-field>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<input matInput
class="adf-input"
type="text"
data-automation-id="adf-group-search-input"
[id]="field.id"
[formControl]="searchTerm"
[placeholder]="field.placeholder"
(blur)="markAsTouched()"
[matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="updateOption($event.option.value)" [displayWith]="getDisplayName">
<mat-option *ngFor="let item of groups$ | async; let i = index"
id="adf-group-widget-user-{{i}}"
[id]="field.id +'-'+item.id"
[value]="item">
<span id="adf-group-label-name">{{item.name}}</span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -1,5 +0,0 @@
.adf {
&-group-widget {
width: 100%;
}
}

View File

@@ -1,186 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { of, timer } from 'rxjs';
import { FormService } from '../../../services/form.service';
import { FormFieldModel } from '../core/form-field.model';
import { FormModel } from '../core/form.model';
import { GroupModel } from '../core/group.model';
import { FunctionalGroupWidgetComponent } from './functional-group.widget';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CoreTestingModule, setupTestBed } from '../../../../testing';
import { TranslateModule } from '@ngx-translate/core';
import { FormFieldTypes } from '../core/form-field-types';
describe('FunctionalGroupWidgetComponent', () => {
let fixture: ComponentFixture<FunctionalGroupWidgetComponent>;
let component: FunctionalGroupWidgetComponent;
let formService: FormService;
let getWorkflowGroupsSpy: jasmine.Spy;
let element: HTMLElement;
const groups: GroupModel[] = [
{ id: '1', name: 'group 1' },
{ id: '2', name: 'group 2' }
];
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
formService = TestBed.inject(FormService);
getWorkflowGroupsSpy = spyOn(formService, 'getWorkflowGroups').and.returnValue(of([]));
fixture = TestBed.createComponent(FunctionalGroupWidgetComponent);
component = fixture.componentInstance;
component.field = new FormFieldModel(new FormModel());
element = fixture.nativeElement;
fixture.detectChanges();
});
afterEach(() => {
getWorkflowGroupsSpy.calls.reset();
fixture.destroy();
});
const typeIntoInput = async (text: string) => {
component.searchTerm.setValue(text);
fixture.detectChanges();
await timer(300).toPromise();
await fixture.whenStable();
fixture.detectChanges();
const input = fixture.nativeElement.querySelector('input');
input.focus();
input.dispatchEvent(new Event('focusin'));
input.dispatchEvent(new Event('input'));
await fixture.whenStable();
fixture.detectChanges();
};
it('should setup text from underlying field on init', async () => {
const group: GroupModel = { name: 'group-1'};
component.field.value = group;
component.ngOnInit();
expect(component.searchTerm.value).toEqual(group.name);
});
it('should not setup text on init', () => {
component.field.value = null;
component.ngOnInit();
expect(component.searchTerm.value).toBeNull();
});
it('should setup group restriction', () => {
component.ngOnInit();
expect(component.groupId).toBeUndefined();
component.field.params = { restrictWithGroup: { id: '<id>' } };
component.ngOnInit();
expect(component.groupId).toBe('<id>');
});
it('should update form on value flush', () => {
spyOn(component.field, 'updateForm').and.callThrough();
component.updateOption();
expect(component.field.updateForm).toHaveBeenCalled();
});
it('should flush selected value', () => {
getWorkflowGroupsSpy.and.returnValue(of(groups));
component.updateOption(groups[1]);
expect(component.field.value).toBe(groups[1]);
});
it('should fetch groups and show popup on key up', async () => {
component.groupId = 'parentGroup';
getWorkflowGroupsSpy.and.returnValue(of(groups));
await typeIntoInput('group');
const options: HTMLElement[] = Array.from(document.querySelectorAll('[id="adf-group-label-name"]'));
expect(options.map(option => option.innerText)).toEqual(['group 1', 'group 2']);
expect(getWorkflowGroupsSpy).toHaveBeenCalledWith('group', 'parentGroup');
});
it('should hide popup when fetching empty group list', async () => {
component.groupId = 'parentGroup';
getWorkflowGroupsSpy.and.returnValues(of(groups), of([]));
await typeIntoInput('group');
let options: HTMLElement[] = Array.from(document.querySelectorAll('[id="adf-group-label-name"]'));
expect(options.map(option => option.innerText)).toEqual(['group 1', 'group 2']);
await typeIntoInput('unknown-group');
options = Array.from(document.querySelectorAll('[id="adf-group-label-name"]'));
expect(options).toEqual([]);
expect(getWorkflowGroupsSpy).toHaveBeenCalledTimes(2);
});
it('should not fetch groups when value is missing', async () => {
await typeIntoInput('');
expect(getWorkflowGroupsSpy).not.toHaveBeenCalled();
});
it('should not fetch groups when value violates constraints', async () => {
component.minTermLength = 4;
await typeIntoInput('123');
expect(getWorkflowGroupsSpy).not.toHaveBeenCalled();
});
describe('when is required', () => {
beforeEach(() => {
component.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
type: FormFieldTypes.FUNCTIONAL_GROUP,
required: true
});
});
it('should be marked as invalid after interaction', async () => {
const functionalGroupInput = fixture.nativeElement.querySelector('input');
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeFalsy();
functionalGroupInput.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeTruthy();
});
it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
});
});

View File

@@ -1,118 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { Component, ElementRef, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService } from '../../../services/form.service';
import { GroupModel } from '../core/group.model';
import { WidgetComponent } from '../widget.component';
import { catchError, debounceTime, filter, switchMap, tap } from 'rxjs/operators';
import { merge, of } from 'rxjs';
import { UntypedFormControl } from '@angular/forms';
@Component({
selector: 'functional-group-widget',
templateUrl: './functional-group.widget.html',
styleUrls: ['./functional-group.widget.scss'],
host: {
'(click)': 'event($event)',
'(blur)': 'event($event)',
'(change)': 'event($event)',
'(focus)': 'event($event)',
'(focusin)': 'event($event)',
'(focusout)': 'event($event)',
'(input)': 'event($event)',
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
encapsulation: ViewEncapsulation.None
})
export class FunctionalGroupWidgetComponent extends WidgetComponent implements OnInit {
minTermLength: number = 1;
groupId: string;
searchTerm = new UntypedFormControl();
groups$ = merge(this.searchTerm.valueChanges).pipe(
tap((search: GroupModel | string) => {
const isValid = typeof search !== 'string';
const empty = search === '';
this.updateOption( isValid ? search as GroupModel : null );
this.validateGroup(isValid, empty);
}),
filter((group: string | GroupModel) => typeof group === 'string' && group.length >= this.minTermLength),
debounceTime(300),
switchMap((searchTerm: string) => this.formService.getWorkflowGroups(searchTerm, this.groupId)
.pipe(catchError(() => of([]))))
);
constructor(public formService: FormService,
public elementRef: ElementRef) {
super(formService);
}
ngOnInit() {
if (this.field) {
if (this.field.readOnly) {
this.searchTerm.disable();
}
const params = this.field.params;
if (params && params.restrictWithGroup) {
const restrictWithGroup = params.restrictWithGroup;
this.groupId = restrictWithGroup.id;
}
if (this.field.value?.name) {
this.searchTerm.setValue(this.field.value.name);
}
}
}
updateOption(option?: GroupModel) {
if (option) {
this.field.value = option;
} else {
this.field.value = null;
}
this.field.updateForm();
}
validateGroup(valid: boolean, empty: boolean) {
const isEmpty = !this.field.required && (empty || valid);
const hasValue = this.field.required && valid;
if (hasValue || isEmpty) {
this.field.validationSummary.message = '';
this.field.validate();
this.field.form.validateForm();
} else {
this.field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_VALUE';
this.field.markAsInvalid();
this.field.form.markAsInvalid();
}
}
getDisplayName(model: GroupModel | string) {
if (model) {
return typeof model === 'string' ? model : model.name;
}
return '';
}
}

View File

@@ -21,30 +21,14 @@ import { AmountWidgetComponent } from './amount/amount.widget';
import { CheckboxWidgetComponent } from './checkbox/checkbox.widget';
import { DateWidgetComponent } from './date/date.widget';
import { DisplayTextWidgetComponent } from './display-text/display-text.widget';
import { DocumentWidgetComponent } from './document/document.widget';
import { DropdownWidgetComponent } from './dropdown/dropdown.widget';
import { DynamicTableWidgetComponent } from './dynamic-table/dynamic-table.widget';
import { BooleanEditorComponent } from './dynamic-table/editors/boolean/boolean.editor';
import { DateEditorComponent } from './dynamic-table/editors/date/date.editor';
import { DateTimeEditorComponent } from './dynamic-table/editors/datetime/datetime.editor';
import { DropdownEditorComponent } from './dynamic-table/editors/dropdown/dropdown.editor';
import { RowEditorComponent } from './dynamic-table/editors/row.editor';
import { TextEditorComponent } from './dynamic-table/editors/text/text.editor';
import { AmountEditorComponent } from './dynamic-table/editors/amount/amount.editor';
import { ErrorWidgetComponent } from './error/error.component';
import { FunctionalGroupWidgetComponent } from './functional-group/functional-group.widget';
import { HyperlinkWidgetComponent } from './hyperlink/hyperlink.widget';
import { MultilineTextWidgetComponentComponent } from './multiline-text/multiline-text.widget';
import { NumberWidgetComponent } from './number/number.widget';
import { PeopleWidgetComponent } from './people/people.widget';
import { RadioButtonsWidgetComponent } from './radio-buttons/radio-buttons.widget';
import { InputMaskDirective } from './text/text-mask.component';
import { TextWidgetComponent } from './text/text.widget';
import { TypeaheadWidgetComponent } from './typeahead/typeahead.widget';
import { UploadWidgetComponent } from './upload/upload.widget';
import { DateTimeWidgetComponent } from './date-time/date-time.widget';
import { JsonWidgetComponent } from './json/json.widget';
import { UploadFolderWidgetComponent } from './upload-folder/upload-folder.widget';
import { FileViewerWidgetComponent } from './file-viewer/file-viewer.widget';
import { DisplayRichTextWidgetComponent } from './display-rich-text/display-rich-text.widget';
@@ -52,41 +36,21 @@ import { DisplayRichTextWidgetComponent } from './display-rich-text/display-rich
export * from './widget.component';
export * from './core';
// primitives
export * from './unknown/unknown.widget';
export * from './text/text.widget';
export * from './number/number.widget';
export * from './checkbox/checkbox.widget';
export * from './multiline-text/multiline-text.widget';
export * from './dropdown/dropdown.widget';
export * from './hyperlink/hyperlink.widget';
export * from './radio-buttons/radio-buttons.widget';
export * from './display-text/display-text.widget';
export * from './upload/upload.widget';
export * from './typeahead/typeahead.widget';
export * from './functional-group/functional-group.widget';
export * from './people/people.widget';
export * from './date/date.widget';
export * from './amount/amount.widget';
export * from './dynamic-table/dynamic-table.widget';
export * from './error/error.component';
export * from './document/document.widget';
export * from './date-time/date-time.widget';
export * from './json/json.widget';
export * from './upload-folder/upload-folder.widget';
export * from './file-viewer/file-viewer.widget';
export * from './display-rich-text/display-rich-text.widget';
// editors (dynamic table)
export * from './dynamic-table/dynamic-table.widget.model';
export * from './dynamic-table/editors/row.editor';
export * from './dynamic-table/editors/date/date.editor';
export * from './dynamic-table/editors/dropdown/dropdown.editor';
export * from './dynamic-table/editors/boolean/boolean.editor';
export * from './dynamic-table/editors/text/text.editor';
export * from './dynamic-table/editors/datetime/datetime.editor';
export * from './dynamic-table/editors/amount/amount.editor';
export * from './text/text-mask.component';
export const WIDGET_DIRECTIVES: any[] = [
@@ -95,29 +59,13 @@ export const WIDGET_DIRECTIVES: any[] = [
NumberWidgetComponent,
CheckboxWidgetComponent,
MultilineTextWidgetComponentComponent,
DropdownWidgetComponent,
HyperlinkWidgetComponent,
RadioButtonsWidgetComponent,
DisplayTextWidgetComponent,
UploadWidgetComponent,
TypeaheadWidgetComponent,
FunctionalGroupWidgetComponent,
PeopleWidgetComponent,
DateWidgetComponent,
AmountWidgetComponent,
DynamicTableWidgetComponent,
DateEditorComponent,
DropdownEditorComponent,
BooleanEditorComponent,
TextEditorComponent,
RowEditorComponent,
ErrorWidgetComponent,
DocumentWidgetComponent,
DateTimeWidgetComponent,
DateTimeEditorComponent,
JsonWidgetComponent,
AmountEditorComponent,
UploadFolderWidgetComponent,
FileViewerWidgetComponent,
DisplayRichTextWidgetComponent
];

View File

@@ -1,38 +0,0 @@
<div class="adf-people-widget {{field.className}}"
[class.adf-invalid]="!field.isValid && isTouched()"
[class.adf-readonly]="field.readOnly"
id="people-widget-content">
<mat-form-field>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<input #inputValue
matInput
class="adf-input"
data-automation-id="adf-people-search-input"
type="text"
[id]="field.id"
[formControl]="searchTerm"
[placeholder]="field.placeholder"
[matAutocomplete]="auto"
(blur)="markAsTouched()"
[matTooltip]="field.tooltip"
matTooltipPosition="above"
matTooltipShowDelay="1000">
<mat-autocomplete class="adf-people-widget-list"
#auto="matAutocomplete"
(optionSelected)="onItemSelect($event.option.value)"
[displayWith]="getDisplayName">
<mat-option *ngFor="let user of users$ | async; let i = index" [value]="user">
<div class="adf-people-widget-row" id="adf-people-widget-user-{{i}}">
<div [outerHTML]="user | usernameInitials:'adf-people-widget-pic'"></div>
<div *ngIf="user.pictureId" class="adf-people-widget-image-row">
<img id="adf-people-widget-pic-{{i}}" class="adf-people-widget-image"
[alt]="getDisplayName(user)" [src]="peopleProcessService.getUserImage(user)"/>
</div>
<span class="adf-people-label-name">{{getDisplayName(user)}}</span>
</div>
</mat-option>
</mat-autocomplete>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -1,53 +0,0 @@
.adf {
&-people-widget {
width: 100%;
.mat-form-field-label-wrapper {
top: 10px;
}
}
&-people-widget-list {
margin: 5px 0;
padding: 10px 0;
}
&-people-widget-row {
display: flex;
align-items: center;
}
&-people-widget-pic {
background: var(--theme-primary-color);
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
border-radius: 100px;
color: var(--theme-text-fg-color);
font-weight: bolder;
font-size: var(--theme-adf-picture-1-font-size);
text-transform: uppercase;
}
&-people-widget-image {
margin-left: -44px;
left: 21px;
background: var(--theme-dialog-bg-color);
border-radius: 100px;
width: 40px;
height: 40px;
vertical-align: middle;
display: inline-block;
padding: 0;
}
&-people-widget-image-row {
display: inline-block;
}
&-people-label-name {
padding-left: 10px;
}
}

View File

@@ -1,309 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { By } from '@angular/platform-browser';
import { UserProcessModel } from '../../../../models';
import { Observable, of } from 'rxjs';
import { FormService } from '../../../services/form.service';
import { FormFieldTypes } from '../core/form-field-types';
import { FormFieldModel } from '../core/form-field.model';
import { FormModel } from '../core/form.model';
import { PeopleWidgetComponent } from './people.widget';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { CoreTestingModule } from '../../../../testing/core.testing.module';
describe('PeopleWidgetComponent', () => {
let widget: PeopleWidgetComponent;
let fixture: ComponentFixture<PeopleWidgetComponent>;
let element: HTMLElement;
let formService: FormService;
let translationService: TranslateService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
fixture = TestBed.createComponent(PeopleWidgetComponent);
formService = TestBed.inject(FormService);
translationService = TestBed.inject(TranslateService);
spyOn(translationService, 'instant').and.callFake((key) => key);
spyOn(translationService, 'get').and.callFake((key) => of(key));
element = fixture.nativeElement;
widget = fixture.componentInstance;
widget.field = new FormFieldModel(new FormModel());
fixture.detectChanges();
});
it('should return empty display name for missing model', () => {
expect(widget.getDisplayName(null)).toBe('');
});
it('should return full name for a given model', () => {
const model = new UserProcessModel({
firstName: 'John',
lastName: 'Doe'
});
expect(widget.getDisplayName(model)).toBe('John Doe');
});
it('should skip first name for display name', () => {
const model = new UserProcessModel({ firstName: null, lastName: 'Doe' });
expect(widget.getDisplayName(model)).toBe('Doe');
});
it('should skip last name for display name', () => {
const model = new UserProcessModel({ firstName: 'John', lastName: null });
expect(widget.getDisplayName(model)).toBe('John');
});
it('should init value from the field', async () => {
widget.field.value = new UserProcessModel({
id: 'people-id',
firstName: 'John',
lastName: 'Doe'
});
spyOn(formService, 'getWorkflowUsers').and.returnValue(of(null));
widget.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
expect((element.querySelector('input') as HTMLInputElement).value).toBe('John Doe');
});
it('should show the readonly value when the form is readonly', async () => {
widget.field.value = new UserProcessModel({
id: 'people-id',
firstName: 'John',
lastName: 'Doe'
});
widget.field.readOnly = true;
widget.field.form.readOnly = true;
spyOn(formService, 'getWorkflowUsers').and.returnValue(of(null));
widget.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
expect((element.querySelector('input') as HTMLInputElement).value).toBe('John Doe');
expect((element.querySelector('input') as HTMLInputElement).disabled).toBeTruthy();
});
it('should require form field to setup values on init', () => {
widget.field.value = null;
widget.ngOnInit();
fixture.detectChanges();
const input = widget.input;
expect(input.nativeElement.value).toBe('');
expect(widget.groupId).toBeUndefined();
});
it('should setup group restriction', () => {
widget.ngOnInit();
expect(widget.groupId).toBeUndefined();
widget.field.params = { restrictWithGroup: { id: '<id>' } };
widget.ngOnInit();
expect(widget.groupId).toBe('<id>');
});
it('should display involved user in task form', async () => {
spyOn(formService, 'getWorkflowUsers').and.returnValue(
new Observable((observer) => {
observer.next(null);
observer.complete();
})
);
widget.field.value = new UserProcessModel({
id: 'people-id',
firstName: 'John',
lastName: 'Doe',
email: 'john@test.com'
});
widget.ngOnInit();
const involvedUser = fixture.debugElement.nativeElement.querySelector('input[data-automation-id="adf-people-search-input"]');
fixture.detectChanges();
await fixture.whenStable();
expect(involvedUser.value).toBe('John Doe');
});
describe('when is required', () => {
beforeEach(() => {
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
type: FormFieldTypes.PEOPLE,
required: true
});
});
it('should be marked as invalid after interaction', async () => {
const peopleInput = fixture.nativeElement.querySelector('input');
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeFalsy();
peopleInput.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeTruthy();
});
it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
});
describe('when template is ready', () => {
const fakeUserResult = [
{ id: 1001, firstName: 'Test01', lastName: 'Test01', email: 'test' },
{ id: 1002, firstName: 'Test02', lastName: 'Test02', email: 'test2' }];
beforeEach(() => {
spyOn(formService, 'getWorkflowUsers').and.returnValue(new Observable((observer) => {
observer.next(fakeUserResult);
observer.complete();
}));
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id' }), {
id: 'people-id',
name: 'people-name',
type: FormFieldTypes.PEOPLE,
readOnly: false
});
fixture.detectChanges();
element = fixture.nativeElement;
});
afterEach(() => {
fixture.destroy();
});
afterAll(() => {
TestBed.resetTestingModule();
});
it('should render the people component', () => {
expect(element.querySelector('#people-widget-content')).not.toBeNull();
});
it('should show an error message if the user is invalid', async () => {
const peopleHTMLElement = element.querySelector<HTMLInputElement>('input');
peopleHTMLElement.focus();
peopleHTMLElement.value = 'K';
peopleHTMLElement.dispatchEvent(new Event('keyup'));
peopleHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-error-text')).not.toBeNull();
expect(element.querySelector('.adf-error-text').textContent).toContain('FORM.FIELD.VALIDATOR.INVALID_VALUE');
});
it('should show the people if the typed result match', async () => {
const peopleHTMLElement = element.querySelector<HTMLInputElement>('input');
peopleHTMLElement.focus();
peopleHTMLElement.value = 'T';
peopleHTMLElement.dispatchEvent(new Event('keyup'));
peopleHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.debugElement.query(By.css('#adf-people-widget-user-0'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('#adf-people-widget-user-1'))).not.toBeNull();
});
it('should hide result list if input is empty', async () => {
const peopleHTMLElement = element.querySelector<HTMLInputElement>('input');
peopleHTMLElement.focus();
peopleHTMLElement.value = '';
peopleHTMLElement.dispatchEvent(new Event('keyup'));
peopleHTMLElement.dispatchEvent(new Event('focusin'));
peopleHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.debugElement.query(By.css('#adf-people-widget-user-0'))).toBeNull();
});
it('should display two options if we tap one letter', async () => {
fixture.detectChanges();
await fixture.whenStable();
const peopleHTMLElement = element.querySelector<HTMLInputElement>('input');
peopleHTMLElement.focus();
peopleHTMLElement.value = 'T';
peopleHTMLElement.dispatchEvent(new Event('keyup'));
peopleHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.debugElement.query(By.css('#adf-people-widget-user-0'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('#adf-people-widget-user-1'))).not.toBeNull();
});
it('should emit peopleSelected if option is valid', async () => {
const selectEmitSpy = spyOn(widget.peopleSelected, 'emit');
const peopleHTMLElement = element.querySelector<HTMLInputElement>('input');
peopleHTMLElement.focus();
peopleHTMLElement.value = 'Test01 Test01';
peopleHTMLElement.dispatchEvent(new Event('keyup'));
peopleHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
expect(selectEmitSpy).toHaveBeenCalledWith(1001);
});
it('should display tooltip when tooltip is set', async () => {
widget.field.tooltip = 'people widget';
fixture.detectChanges();
await fixture.whenStable();
const radioButtonsElement: any = element.querySelector('#people-id');
const tooltip = radioButtonsElement.getAttribute('ng-reflect-message');
expect(tooltip).toEqual(widget.field.tooltip);
});
});
});

View File

@@ -1,146 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { PeopleProcessService } from '../../../../services/people-process.service';
import { UserProcessModel } from '../../../../models';
import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core';
import { FormService } from '../../../services/form.service';
import { WidgetComponent } from '../widget.component';
import { UntypedFormControl } from '@angular/forms';
import { Observable, of } from 'rxjs';
import {
catchError,
distinctUntilChanged,
map,
switchMap,
tap
} from 'rxjs/operators';
@Component({
selector: 'people-widget',
templateUrl: './people.widget.html',
styleUrls: ['./people.widget.scss'],
host: {
'(click)': 'event($event)',
'(blur)': 'event($event)',
'(change)': 'event($event)',
'(focus)': 'event($event)',
'(focusin)': 'event($event)',
'(focusout)': 'event($event)',
'(input)': 'event($event)',
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
encapsulation: ViewEncapsulation.None
})
export class PeopleWidgetComponent extends WidgetComponent implements OnInit {
@ViewChild('inputValue', { static: true })
input: ElementRef;
@Output()
peopleSelected: EventEmitter<number> = new EventEmitter();
groupId: string;
value: any;
searchTerm = new UntypedFormControl();
searchTerms$: Observable<any> = this.searchTerm.valueChanges;
users$ = this.searchTerms$.pipe(
tap((searchInput) => {
if (typeof searchInput === 'string') {
this.onItemSelect();
}
}),
distinctUntilChanged(),
switchMap((searchTerm) => {
const value = searchTerm.email ? this.getDisplayName(searchTerm) : searchTerm;
return this.formService.getWorkflowUsers(value, this.groupId)
.pipe(catchError(() => of([])));
}),
map((list: UserProcessModel[]) => {
const value = this.searchTerm.value.email ? this.getDisplayName(this.searchTerm.value) : this.searchTerm.value;
this.checkUserAndValidateForm(list, value);
return list;
})
);
constructor(public formService: FormService, public peopleProcessService: PeopleProcessService) {
super(formService);
}
ngOnInit() {
if (this.field) {
if (this.field.value) {
this.searchTerm.setValue(this.field.value);
}
if (this.field.readOnly) {
this.searchTerm.disable();
}
const params = this.field.params;
if (params && params.restrictWithGroup) {
const restrictWithGroup = params.restrictWithGroup;
this.groupId = restrictWithGroup.id;
}
}
}
checkUserAndValidateForm(list: UserProcessModel[], value: string): void {
const isValidUser = this.isValidUser(list, value);
if (isValidUser || value === '') {
this.field.validationSummary.message = '';
this.field.validate();
this.field.form.validateForm();
} else {
this.field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_VALUE';
this.field.markAsInvalid();
this.field.form.markAsInvalid();
}
}
isValidUser(users: UserProcessModel[], name: string): boolean {
if (users) {
return !!users.find((user) => {
const selectedUser = this.getDisplayName(user).toLocaleLowerCase() === name.toLocaleLowerCase();
if (selectedUser) {
this.peopleSelected.emit(user && user.id || undefined);
}
return selectedUser;
});
}
return false;
}
getDisplayName(model: UserProcessModel) {
if (model) {
const displayName = `${model.firstName || ''} ${model.lastName || ''}`;
return displayName.trim();
}
return '';
}
onItemSelect(item?: UserProcessModel) {
if (item) {
this.field.value = item;
} else {
this.field.value = null;
}
}
}

View File

@@ -1,22 +0,0 @@
<div class="adf-radio-buttons-widget {{field.className}}"
[class.adf-readonly]="field.readOnly" [id]="field.id">
<div class="adf-radio-button-container">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<mat-radio-group class="adf-radio-group" [(ngModel)]="field.value" [disabled]="field.readOnly">
<mat-radio-button
[matTooltip]="field.tooltip"
matTooltipPosition="above"
matTooltipShowDelay="1000"
[id]="field.id + '-' + opt.id"
[name]="field.id"
[value]="opt.id"
[checked]="field.value === opt.id"
(change)="onOptionClick(opt.id)"
color="primary"
class="adf-radio-button" *ngFor="let opt of field.options" >
{{opt.name}}
</mat-radio-button>
</mat-radio-group>
</div>
<error-widget [error]="field.validationSummary" ></error-widget>
</div>

View File

@@ -1,18 +0,0 @@
.adf {
&-radio-button-container {
margin-bottom: 15px;
display: flex;
flex-direction: column;
}
&-radio-group {
margin-top: 15px;
margin-left: 5px;
display: inline-flex;
flex-direction: column;
}
&-radio-button {
margin: 5px;
}
}

View File

@@ -1,345 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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, fakeAsync, TestBed } from '@angular/core/testing';
import { Observable, of } from 'rxjs';
import { FormService } from '../../../services/form.service';
import { ContainerModel } from '../core/container.model';
import { FormFieldTypes } from '../core/form-field-types';
import { FormFieldOption } from '../core/form-field-option';
import { FormFieldModel } from '../core/form-field.model';
import { FormModel } from '../core/form.model';
import { RadioButtonsWidgetComponent } from './radio-buttons.widget';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { MatIconModule } from '@angular/material/icon';
import { MatRadioModule } from '@angular/material/radio';
import { FormsModule } from '@angular/forms';
import { CoreTestingModule } from '../../../../testing';
import { TranslateModule } from '@ngx-translate/core';
import { AlfrescoApiService } from '../../../../services';
describe('RadioButtonsWidgetComponent', () => {
let formService: FormService;
let widget: RadioButtonsWidgetComponent;
let alfrescoApiService: AlfrescoApiService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule,
MatRadioModule,
FormsModule,
MatIconModule
]
});
beforeEach(() => {
alfrescoApiService = TestBed.inject(AlfrescoApiService);
formService = new FormService(null, alfrescoApiService, null);
widget = new RadioButtonsWidgetComponent(formService, null);
widget.field = new FormFieldModel(new FormModel(), { restUrl: '<url>' });
});
it('should request field values from service', () => {
const taskId = '<form-id>';
const fieldId = '<field-id>';
const form = new FormModel({
taskId
});
widget.field = new FormFieldModel(form, {
id: fieldId,
restUrl: '<url>'
});
spyOn(formService, 'getRestFieldValues').and.returnValue(new Observable((observer) => {
observer.next(null);
observer.complete();
}));
widget.ngOnInit();
expect(formService.getRestFieldValues).toHaveBeenCalledWith(taskId, fieldId);
});
it('should update form on values fetched', () => {
const taskId = '<form-id>';
const fieldId = '<field-id>';
const form = new FormModel({
taskId
});
widget.field = new FormFieldModel(form, {
id: fieldId,
restUrl: '<url>'
});
const field = widget.field;
spyOn(field, 'updateForm').and.stub();
spyOn(formService, 'getRestFieldValues').and.returnValue(new Observable((observer) => {
observer.next(null);
observer.complete();
}));
widget.ngOnInit();
expect(field.updateForm).toHaveBeenCalled();
});
it('should require field with rest URL to fetch data', () => {
const taskId = '<form-id>';
const fieldId = '<field-id>';
const form = new FormModel({
taskId
});
widget.field = new FormFieldModel(form, {
id: fieldId,
restUrl: '<url>'
});
spyOn(formService, 'getRestFieldValues').and.returnValue(new Observable((observer) => {
observer.next(null);
observer.complete();
}));
const field = widget.field;
widget.field = null;
widget.ngOnInit();
expect(formService.getRestFieldValues).not.toHaveBeenCalled();
widget.field = field;
widget.field.restUrl = null;
widget.ngOnInit();
expect(formService.getRestFieldValues).not.toHaveBeenCalled();
widget.field.restUrl = '<url>';
widget.ngOnInit();
expect(formService.getRestFieldValues).toHaveBeenCalled();
});
it('should update the field value when an option is selected', () => {
spyOn(widget, 'onFieldChanged').and.stub();
widget.onOptionClick('fake-opt');
expect(widget.field.value).toEqual('fake-opt');
});
describe('when template is ready', () => {
let radioButtonWidget: RadioButtonsWidgetComponent;
let fixture: ComponentFixture<RadioButtonsWidgetComponent>;
let element: HTMLElement;
let stubFormService: FormService;
const restOption: FormFieldOption[] = [
{
id: 'opt-1',
name: 'opt-name-1'
},
{
id: 'opt-2',
name: 'opt-name-2'
}];
beforeEach(() => {
fixture = TestBed.createComponent(RadioButtonsWidgetComponent);
radioButtonWidget = fixture.componentInstance;
element = fixture.nativeElement;
stubFormService = fixture.debugElement.injector.get(FormService);
});
it('should show radio buttons as text when is readonly', async () => {
radioButtonWidget.field = new FormFieldModel(new FormModel({}), {
id: 'radio-id',
name: 'radio-name',
type: FormFieldTypes.RADIO_BUTTONS,
readOnly: true
});
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
expect(element.querySelector('display-text-widget')).toBeDefined();
});
it('should be able to set label property for Radio Button widget', () => {
radioButtonWidget.field = new FormFieldModel(new FormModel({}), {
id: 'radio-id',
name: 'radio-name-label',
type: FormFieldTypes.RADIO_BUTTONS,
readOnly: true
});
fixture.detectChanges();
expect(element.querySelector('label').innerText).toBe('radio-name-label');
});
it('should be able to set a Radio Button widget as required', async () => {
radioButtonWidget.field = new FormFieldModel(new FormModel({}), {
id: 'radio-id',
name: 'radio-name-label',
type: FormFieldTypes.RADIO_BUTTONS,
readOnly: false,
required: true,
optionType: 'manual',
options: restOption,
restUrl: null
});
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const widgetLabel = element.querySelector('label');
expect(widgetLabel.innerText).toBe('radio-name-label*');
expect(radioButtonWidget.field.isValid).toBe(false);
const option = element.querySelector<HTMLElement>('#radio-id-opt-1 label');
option.click();
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const selectedOption = element.querySelector<HTMLElement>('[class*="mat-radio-checked"]');
expect(selectedOption.innerText).toBe('opt-name-1');
expect(radioButtonWidget.field.isValid).toBe(true);
});
it('should be able to set a Radio Button widget as required', () => {
radioButtonWidget.field = new FormFieldModel(new FormModel({}), {
id: 'radio-id',
name: 'radio-name-label',
type: FormFieldTypes.RADIO_BUTTONS,
readOnly: false,
required: true,
optionType: 'manual',
options: restOption,
restUrl: null,
value: 'opt-name-2'
});
fixture.detectChanges();
const selectedOption = element.querySelector<HTMLElement>('[class*="mat-radio-checked"]');
expect(selectedOption.innerText).toBe('opt-name-2');
expect(radioButtonWidget.field.isValid).toBe(true);
});
it('should display tooltip when tooltip is set', async () => {
radioButtonWidget.field = new FormFieldModel(new FormModel(), {
id: 'radio-id',
name: 'radio-name-label',
type: FormFieldTypes.RADIO_BUTTONS,
readOnly: false,
required: true,
optionType: 'manual',
options: restOption,
value: 'opt-name-2',
tooltip: 'radio widget'
});
fixture.detectChanges();
await fixture.whenStable();
const radioButtonsElement: any = element.querySelector('#radio-id-opt-1');
const tooltip = radioButtonsElement.getAttribute('ng-reflect-message');
expect(tooltip).toEqual(radioButtonWidget.field.tooltip);
});
describe('and radioButton is populated via taskId', () => {
beforeEach(() => {
spyOn(stubFormService, 'getRestFieldValues').and.returnValue(of(restOption));
radioButtonWidget.field = new FormFieldModel(new FormModel({ taskId: 'task-id' }), {
id: 'radio-id',
name: 'radio-name',
type: FormFieldTypes.RADIO_BUTTONS,
restUrl: 'rest-url'
});
radioButtonWidget.field.isVisible = true;
const fakeContainer = new ContainerModel(radioButtonWidget.field);
radioButtonWidget.field.form.fields.push(fakeContainer);
fixture.detectChanges();
});
it('should show radio buttons', () => {
expect(element.querySelector('#radio-id')).toBeDefined();
expect(element.querySelector('#radio-id-opt-1-input')).not.toBeNull();
expect(element.querySelector('#radio-id-opt-1')).not.toBeNull();
expect(element.querySelector('#radio-id-opt-2-input')).not.toBeNull();
expect(element.querySelector('#radio-id-opt-2')).not.toBeNull();
});
it('should trigger field changed event on click', fakeAsync(() => {
const option = element.querySelector<HTMLElement>('#radio-id-opt-1-input');
expect(element.querySelector('#radio-id')).not.toBeNull();
expect(option).not.toBeNull();
option.click();
widget.fieldChanged.subscribe(() => {
expect(element.querySelector('#radio-id')).toBeNull();
expect(element.querySelector('#radio-id-opt-1-input')).toBeNull();
});
}));
describe('and radioButton is readonly', () => {
beforeEach(() => {
radioButtonWidget.field.readOnly = true;
fixture.detectChanges();
});
it('should show radio buttons disabled', () => {
expect(element.querySelector('.mat-radio-disabled[ng-reflect-id="radio-id-opt-1"]')).toBeDefined();
expect(element.querySelector('.mat-radio-disabled[ng-reflect-id="radio-id-opt-1"]')).not.toBeNull();
expect(element.querySelector('.mat-radio-disabled[ng-reflect-id="radio-id-opt-2"]')).toBeDefined();
expect(element.querySelector('.mat-radio-disabled[ng-reflect-id="radio-id-opt-2"]')).not.toBeNull();
});
describe('and a value is selected', () => {
beforeEach(() => {
radioButtonWidget.field.value = restOption[0].id;
fixture.detectChanges();
});
it('should check the selected value', () => {
expect(element.querySelector('.mat-radio-checked')).toBe(element.querySelector('mat-radio-button[ng-reflect-id="radio-id-opt-1"]'));
});
});
});
});
describe('and radioButton is populated via processDefinitionId', () => {
beforeEach(() => {
radioButtonWidget.field = new FormFieldModel(new FormModel({ processDefinitionId: 'proc-id' }), {
id: 'radio-id',
name: 'radio-name',
type: FormFieldTypes.RADIO_BUTTONS,
restUrl: 'rest-url'
});
spyOn(stubFormService, 'getRestFieldValuesByProcessId').and.returnValue(of(restOption));
radioButtonWidget.field.isVisible = true;
fixture.detectChanges();
});
it('should show visible radio buttons', () => {
expect(element.querySelector('#radio-id')).toBeDefined();
expect(element.querySelector('#radio-id-opt-1-input')).not.toBeNull();
expect(element.querySelector('#radio-id-opt-1')).not.toBeNull();
expect(element.querySelector('#radio-id-opt-2-input')).not.toBeNull();
expect(element.querySelector('#radio-id-opt-2')).not.toBeNull();
});
});
});
});

View File

@@ -1,99 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { LogService } from '../../../../services/log.service';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService } from '../../../services/form.service';
import { FormFieldOption } from '../core/form-field-option';
import { WidgetComponent } from '../widget.component';
@Component({
selector: 'radio-buttons-widget',
templateUrl: './radio-buttons.widget.html',
styleUrls: ['./radio-buttons.widget.scss'],
host: {
'(click)': 'event($event)',
'(blur)': 'event($event)',
'(change)': 'event($event)',
'(focus)': 'event($event)',
'(focusin)': 'event($event)',
'(focusout)': 'event($event)',
'(input)': 'event($event)',
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
encapsulation: ViewEncapsulation.None
})
export class RadioButtonsWidgetComponent extends WidgetComponent implements OnInit {
constructor(public formService: FormService,
private logService: LogService) {
super(formService);
}
ngOnInit() {
if (this.field && this.field.restUrl) {
if (this.field.form.taskId) {
this.getOptionsByTaskId();
} else {
this.getOptionsByProcessDefinitionId();
}
}
}
getOptionsByTaskId() {
this.formService
.getRestFieldValues(
this.field.form.taskId,
this.field.id
)
.subscribe(
(formFieldOption: FormFieldOption[]) => {
this.field.options = formFieldOption || [];
this.field.updateForm();
},
(err) => this.handleError(err)
);
}
getOptionsByProcessDefinitionId() {
this.formService
.getRestFieldValuesByProcessId(
this.field.form.processDefinitionId,
this.field.id
)
.subscribe(
(formFieldOption: FormFieldOption[]) => {
this.field.options = formFieldOption || [];
this.field.updateForm();
},
(err) => this.handleError(err)
);
}
onOptionClick(optionSelected: any) {
this.field.value = optionSelected;
this.fieldChanged.emit(this.field);
}
handleError(error: any) {
this.logService.error(error);
}
}

View File

@@ -1,29 +0,0 @@
<div class="adf-typeahead-widget-container">
<div class="adf-typeahead-widget {{field.className}}"
[class.is-dirty]="value"
[class.adf-invalid]="!field.isValid"
[class.adf-readonly]="field.readOnly"
id="typehead-div">
<mat-form-field>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}</label>
<input matInput class="adf-input"
type="text"
[id]="field.id"
[(ngModel)]="value"
(ngModelChange)="validate()"
(keyup)="onKeyUp($event)"
[disabled]="field.readOnly"
data-automation-id="adf-typeahed-search-input"
placeholder="{{field.placeholder}}"
[matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="onItemSelect($event.option.value)">
<mat-option *ngFor="let item of options; let i = index" id="adf-typeahed-widget-user-{{i}}" [value]="item">
<span id="adf-typeahed-label-name">{{item.name}}</span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>
</div>

View File

@@ -1,10 +0,0 @@
.adf {
&-typeahead-widget-container {
position: relative;
display: block;
}
&-typeahead-widget {
width: 100%;
}
}

View File

@@ -1,390 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { Observable, of, throwError } from 'rxjs';
import { By } from '@angular/platform-browser';
import { FormService } from '../../../services/form.service';
import { FormFieldOption } from '../core/form-field-option';
import { FormFieldTypes } from '../core/form-field-types';
import { FormFieldModel } from '../core/form-field.model';
import { FormModel } from '../core/form.model';
import { TypeaheadWidgetComponent } from './typeahead.widget';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { CoreTestingModule } from '../../../../testing/core.testing.module';
import { AlfrescoApiService } from '../../../../services';
describe('TypeaheadWidgetComponent', () => {
let formService: FormService;
let widget: TypeaheadWidgetComponent;
let translationService: TranslateService;
let alfrescoApiService: AlfrescoApiService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
alfrescoApiService = TestBed.inject(AlfrescoApiService);
translationService = TestBed.inject(TranslateService);
spyOn(translationService, 'instant').and.callFake((key) => key);
spyOn(translationService, 'get').and.callFake((key) => of(key));
formService = new FormService(null, alfrescoApiService, null);
widget = new TypeaheadWidgetComponent(formService, null);
widget.field = new FormFieldModel(new FormModel({ taskId: 'task-id' }));
widget.field.restUrl = 'whateverURL';
});
it('should request field values from service', () => {
const taskId = '<form-id>';
const fieldId = '<field-id>';
const form = new FormModel({
taskId
});
widget.field = new FormFieldModel(form, {
id: fieldId,
restUrl: 'whateverURL'
});
spyOn(formService, 'getRestFieldValues').and.returnValue(new Observable((observer) => {
observer.next(null);
observer.complete();
}));
widget.ngOnInit();
expect(formService.getRestFieldValues).toHaveBeenCalledWith(taskId, fieldId);
});
it('should not perform any request if restUrl is not present', () => {
const taskId = '<form-id>';
const fieldId = '<field-id>';
const form = new FormModel({
taskId
});
widget.field = new FormFieldModel(form, {
id: fieldId
});
spyOn(formService, 'getRestFieldValues');
widget.ngOnInit();
expect(formService.getRestFieldValues).not.toHaveBeenCalled();
});
it('should handle error when requesting fields with task id', () => {
const taskId = '<form-id>';
const fieldId = '<field-id>';
const form = new FormModel({
taskId
});
widget.field = new FormFieldModel(form, {
id: fieldId,
restUrl: 'whateverURL'
});
const err = 'Error';
spyOn(formService, 'getRestFieldValues').and.returnValue(throwError(err));
spyOn(widget, 'handleError').and.stub();
widget.ngOnInit();
expect(formService.getRestFieldValues).toHaveBeenCalled();
expect(widget.handleError).toHaveBeenCalledWith(err);
});
it('should handle error when requesting fields with process id', () => {
const processDefinitionId = '<process-id>';
const fieldId = '<field-id>';
const form = new FormModel({
processDefinitionId
});
widget.field = new FormFieldModel(form, {
id: fieldId,
restUrl: 'whateverURL'
});
const err = 'Error';
spyOn(formService, 'getRestFieldValuesByProcessId').and.returnValue(throwError(err));
spyOn(widget, 'handleError').and.stub();
widget.ngOnInit();
expect(formService.getRestFieldValuesByProcessId).toHaveBeenCalled();
expect(widget.handleError).toHaveBeenCalledWith(err);
});
it('should setup initial value', () => {
spyOn(formService, 'getRestFieldValues').and.returnValue(new Observable((observer) => {
observer.next([
{ id: '1', name: 'One' },
{ id: '2', name: 'Two' }
]);
observer.complete();
}));
widget.field.value = '2';
widget.field.restUrl = 'whateverURL';
widget.ngOnInit();
expect(formService.getRestFieldValues).toHaveBeenCalled();
expect(widget.value).toBe('Two');
});
it('should not setup initial value due to missing option', () => {
spyOn(formService, 'getRestFieldValues').and.returnValue(new Observable((observer) => {
observer.next([
{ id: '1', name: 'One' },
{ id: '2', name: 'Two' }
]);
observer.complete();
}));
widget.field.value = '3';
widget.field.restUrl = 'whateverURL';
widget.ngOnInit();
expect(formService.getRestFieldValues).toHaveBeenCalled();
expect(widget.value).toBeUndefined();
});
it('should setup field options on load', () => {
const options: FormFieldOption[] = [
{ id: '1', name: 'One' },
{ id: '2', name: 'Two' }
];
spyOn(formService, 'getRestFieldValues').and.returnValue(new Observable((observer) => {
observer.next(options);
observer.complete();
}));
widget.ngOnInit();
expect(widget.field.options).toEqual(options);
});
it('should update form upon options setup', () => {
spyOn(formService, 'getRestFieldValues').and.returnValue(new Observable((observer) => {
observer.next([]);
observer.complete();
}));
widget.field.restUrl = 'whateverURL';
spyOn(widget.field, 'updateForm').and.callThrough();
widget.ngOnInit();
expect(widget.field.updateForm).toHaveBeenCalled();
});
it('should get filtered options', () => {
const options: FormFieldOption[] = [
{ id: '1', name: 'Item one' },
{ id: '2', name: 'Item two' }
];
widget.field.options = options;
widget.value = 'tw';
const filtered = widget.getOptions();
expect(filtered.length).toBe(1);
expect(filtered[0]).toEqual(options[1]);
});
it('should be case insensitive when filtering options', () => {
const options: FormFieldOption[] = [
{ id: '1', name: 'Item one' },
{ id: '2', name: 'iTEM TWo' }
];
widget.field.options = options;
widget.value = 'tW';
const filtered = widget.getOptions();
expect(filtered.length).toBe(1);
expect(filtered[0]).toEqual(options[1]);
});
describe('when template is ready', () => {
let typeaheadWidgetComponent: TypeaheadWidgetComponent;
let fixture: ComponentFixture<TypeaheadWidgetComponent>;
let element: HTMLElement;
let stubFormService;
const fakeOptionList: FormFieldOption[] = [{
id: '1',
name: 'Fake Name 1 '
}, {
id: '2',
name: 'Fake Name 2'
}, { id: '3', name: 'Fake Name 3' }];
beforeEach(() => {
fixture = TestBed.createComponent(TypeaheadWidgetComponent);
typeaheadWidgetComponent = fixture.componentInstance;
element = fixture.nativeElement;
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
describe ('and typeahead is in readonly mode', () => {
it('should show typeahead value with input disabled', async () => {
typeaheadWidgetComponent.field = new FormFieldModel(
new FormModel({ processVariables: [{ name: 'typeahead-id_LABEL', value: 'FakeProcessValue' }] }), {
id: 'typeahead-id',
name: 'typeahead-name',
type: 'readonly',
params: { field: { id: 'typeahead-id', name: 'typeahead-name', type: 'typeahead' } }
});
fixture.detectChanges();
await fixture.whenStable();
const readonlyInput = element.querySelector<HTMLInputElement>('#typeahead-id');
expect(readonlyInput.disabled).toBeTruthy();
expect(readonlyInput).not.toBeNull();
expect(readonlyInput.value).toBe('FakeProcessValue');
});
afterEach(() => {
fixture.destroy();
});
});
describe('and typeahead is populated via taskId', () => {
beforeEach(() => {
stubFormService = fixture.debugElement.injector.get(FormService);
spyOn(stubFormService, 'getRestFieldValues').and.returnValue(of(fakeOptionList));
typeaheadWidgetComponent.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id' }), {
id: 'typeahead-id',
name: 'typeahead-name',
type: FormFieldTypes.TYPEAHEAD,
readOnly: false,
restUrl: 'whateverURL'
});
typeaheadWidgetComponent.field.isVisible = true;
fixture.detectChanges();
});
it('should show visible typeahead widget', () => {
expect(element.querySelector('#typeahead-id')).toBeDefined();
expect(element.querySelector('#typeahead-id')).not.toBeNull();
});
it('should show typeahead options', async () => {
const typeaheadElement = fixture.debugElement.query(By.css('#typeahead-id'));
const typeaheadHTMLElement = typeaheadElement.nativeElement as HTMLInputElement;
typeaheadHTMLElement.focus();
typeaheadWidgetComponent.value = 'F';
typeaheadHTMLElement.value = 'F';
typeaheadHTMLElement.dispatchEvent(new Event('keyup'));
typeaheadHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.debugElement.query(By.css('[id="adf-typeahed-widget-user-0"] span'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('[id="adf-typeahed-widget-user-1"] span'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('[id="adf-typeahed-widget-user-2"] span'))).not.toBeNull();
});
it('should hide the option when the value is empty', async () => {
const typeaheadElement = fixture.debugElement.query(By.css('#typeahead-id'));
const typeaheadHTMLElement = typeaheadElement.nativeElement as HTMLInputElement;
typeaheadHTMLElement.focus();
typeaheadWidgetComponent.value = 'F';
typeaheadHTMLElement.value = 'F';
typeaheadHTMLElement.dispatchEvent(new Event('keyup'));
typeaheadHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.debugElement.query(By.css('[id="adf-typeahed-widget-user-0"] span'))).not.toBeNull();
typeaheadHTMLElement.focus();
typeaheadWidgetComponent.value = '';
typeaheadHTMLElement.dispatchEvent(new Event('keyup'));
typeaheadHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.debugElement.query(By.css('[id="adf-typeahed-widget-user-0"] span'))).toBeNull();
});
it('should show error message when the value is not valid', async () => {
typeaheadWidgetComponent.value = 'Fake Name';
typeaheadWidgetComponent.field.value = 'Fake Name';
typeaheadWidgetComponent.field.options = fakeOptionList;
expect(element.querySelector('.adf-error-text')).toBeNull();
const keyboardEvent = new KeyboardEvent('keypress');
typeaheadWidgetComponent.onKeyUp(keyboardEvent);
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-error-text')).not.toBeNull();
expect(element.querySelector('.adf-error-text').textContent).toContain('FORM.FIELD.VALIDATOR.INVALID_VALUE');
});
});
describe('and typeahead is populated via processDefinitionId', () => {
beforeEach(() => {
stubFormService = fixture.debugElement.injector.get(FormService);
spyOn(stubFormService, 'getRestFieldValuesByProcessId').and.returnValue(of(fakeOptionList));
typeaheadWidgetComponent.field = new FormFieldModel(new FormModel({ processDefinitionId: 'fake-process-id' }), {
id: 'typeahead-id',
name: 'typeahead-name',
type: FormFieldTypes.TYPEAHEAD,
readOnly: 'false'
});
typeaheadWidgetComponent.field.emptyOption = { id: 'empty', name: 'Choose one...' };
typeaheadWidgetComponent.field.isVisible = true;
fixture.detectChanges();
});
it('should show visible typeahead widget', () => {
expect(element.querySelector('#typeahead-id')).toBeDefined();
expect(element.querySelector('#typeahead-id')).not.toBeNull();
});
it('should show typeahead options', async () => {
const keyboardEvent = new KeyboardEvent('keypress');
typeaheadWidgetComponent.value = 'F';
typeaheadWidgetComponent.onKeyUp(keyboardEvent);
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.debugElement.queryAll(By.css('[id="adf-typeahed-widget-user-0"] span'))).toBeDefined();
expect(fixture.debugElement.queryAll(By.css('[id="adf-typeahed-widget-user-1"] span'))).toBeDefined();
expect(fixture.debugElement.queryAll(By.css('[id="adf-typeahed-widget-user-2"] span'))).toBeDefined();
});
});
});
});

View File

@@ -1,172 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { LogService } from '../../../../services/log.service';
import { ENTER, ESCAPE } from '@angular/cdk/keycodes';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService } from '../../../services/form.service';
import { FormFieldOption } from '../core/form-field-option';
import { WidgetComponent } from '../widget.component';
@Component({
selector: 'typeahead-widget',
templateUrl: './typeahead.widget.html',
styleUrls: ['./typeahead.widget.scss'],
host: {
'(click)': 'event($event)',
'(blur)': 'event($event)',
'(change)': 'event($event)',
'(focus)': 'event($event)',
'(focusin)': 'event($event)',
'(focusout)': 'event($event)',
'(input)': 'event($event)',
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
encapsulation: ViewEncapsulation.None
})
export class TypeaheadWidgetComponent extends WidgetComponent implements OnInit {
minTermLength: number = 1;
value: string;
oldValue: string;
options: FormFieldOption[] = [];
constructor(public formService: FormService,
private logService: LogService) {
super(formService);
}
ngOnInit() {
if (this.field.form.taskId && this.field.restUrl) {
this.getValuesByTaskId();
} else if (this.field.form.processDefinitionId && this.field.restUrl) {
this.getValuesByProcessDefinitionId();
}
if (this.isReadOnlyType()) {
this.value = this.field.value;
}
}
getValuesByTaskId() {
this.formService
.getRestFieldValues(
this.field.form.taskId,
this.field.id
)
.subscribe(
(formFieldOption: FormFieldOption[]) => {
const options = formFieldOption || [];
this.field.options = options;
const fieldValue = this.field.value;
if (fieldValue) {
const toSelect = options.find((item) => item.id === fieldValue || item.name.toLocaleLowerCase() === fieldValue.toLocaleLowerCase());
if (toSelect) {
this.value = toSelect.name;
}
}
this.onFieldChanged(this.field);
this.field.updateForm();
},
(err) => this.handleError(err)
);
}
getValuesByProcessDefinitionId() {
this.formService
.getRestFieldValuesByProcessId(
this.field.form.processDefinitionId,
this.field.id
)
.subscribe(
(formFieldOption: FormFieldOption[]) => {
const options = formFieldOption || [];
this.field.options = options;
const fieldValue = this.field.value;
if (fieldValue) {
const toSelect = options.find((item) => item.id === fieldValue);
if (toSelect) {
this.value = toSelect.name;
}
}
this.onFieldChanged(this.field);
this.field.updateForm();
},
(err) => this.handleError(err)
);
}
getOptions(): FormFieldOption[] {
const val = this.value.trim().toLocaleLowerCase();
return this.field.options.filter((item) => {
const name = item.name.toLocaleLowerCase();
return name.indexOf(val) > -1;
});
}
isValidOptionName(optionName: string): boolean {
const option = this.field.options.find((item) => item.name && item.name.toLocaleLowerCase() === optionName.toLocaleLowerCase());
return option ? true : false;
}
onKeyUp(event: KeyboardEvent) {
if (this.value && this.value.trim().length >= this.minTermLength && this.oldValue !== this.value) {
if (event.keyCode !== ESCAPE && event.keyCode !== ENTER) {
if (this.value.length >= this.minTermLength) {
this.options = this.getOptions();
this.oldValue = this.value;
if (this.isValidOptionName(this.value)) {
this.field.value = this.options[0].id;
}
}
}
}
if (this.isValueDefined() && this.value.trim().length === 0) {
this.oldValue = this.value;
this.options = [];
}
}
onItemSelect(item: FormFieldOption) {
if (item) {
this.field.value = item.id;
this.value = item.name;
this.onFieldChanged(this.field);
}
}
validate() {
this.field.value = this.value;
}
isValueDefined() {
return this.value !== null && this.value !== undefined;
}
handleError(error: any) {
this.logService.error(error);
}
isReadOnlyType(): boolean {
return this.field.type === 'readonly' ? true : false;
}
}

View File

@@ -1,7 +0,0 @@
<div class="adf-upload-folder-widget {{field.className}}"
[class.adf-invalid]="!field.isValid"
[class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<div class="adf-upload-widget-container">
</div>
</div>

View File

@@ -1,8 +0,0 @@
.adf {
&-upload-folder-widget {
width: 100%;
word-break: break-all;
padding: 0.4375em 0;
border-top: 0.8438em solid transparent;
}
}

View File

@@ -1,61 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { setupTestBed } from '../../../../testing/setup-test-bed';
import { CoreTestingModule } from '../../../../testing';
import { UploadFolderWidgetComponent } from './upload-folder.widget';
import { FormFieldModel } from '../core/form-field.model';
import { FormModel } from '../core/form.model';
import { FormFieldTypes } from '../core/form-field-types';
describe('UploadFolderWidgetComponent', () => {
let widget: UploadFolderWidgetComponent;
let fixture: ComponentFixture<UploadFolderWidgetComponent>;
let element: HTMLElement;
setupTestBed({
imports: [
CoreTestingModule
]
});
beforeEach(() => {
fixture = TestBed.createComponent(UploadFolderWidgetComponent);
widget = fixture.componentInstance;
element = fixture.nativeElement;
});
describe('when is required', () => {
it('should be able to display label with asterisk', async () => {
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
type: FormFieldTypes.UPLOAD,
required: true
});
fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
});
});

View File

@@ -1,165 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { LogService } from '../../../../services/log.service';
import { ThumbnailService } from '../../../../services/thumbnail.service';
import { Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Observable, from } from 'rxjs';
import { FormService } from '../../../services/form.service';
import { ProcessContentService } from '../../../services/process-content.service';
import { ContentLinkModel } from '../core/content-link.model';
import { WidgetComponent } from '../widget.component';
import { mergeMap, map } from 'rxjs/operators';
@Component({
selector: 'upload-folder-widget',
templateUrl: './upload-folder.widget.html',
styleUrls: ['./upload-folder.widget.scss'],
host: {
'(click)': 'event($event)',
'(blur)': 'event($event)',
'(change)': 'event($event)',
'(focus)': 'event($event)',
'(focusin)': 'event($event)',
'(focusout)': 'event($event)',
'(input)': 'event($event)',
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
encapsulation: ViewEncapsulation.None
})
export class UploadFolderWidgetComponent extends WidgetComponent implements OnInit {
hasFile: boolean;
displayText: string;
multipleOption: string = '';
mimeTypeIcon: string;
@ViewChild('uploadFiles')
fileInput: ElementRef;
constructor(public formService: FormService,
private logService: LogService,
private thumbnailService: ThumbnailService,
public processContentService: ProcessContentService) {
super(formService);
}
ngOnInit() {
if (this.field &&
this.field.value &&
this.field.value.length > 0) {
this.hasFile = true;
}
this.getMultipleFileParam();
}
removeFile(file: any) {
if (this.field) {
this.removeElementFromList(file);
}
}
onFileChanged(event: any) {
const files = event.target.files;
let filesSaved = [];
if (this.field.json.value) {
filesSaved = [...this.field.json.value];
}
if (files && files.length > 0) {
from(files)
.pipe(mergeMap((file) => this.uploadRawContent(file)))
.subscribe(
(res) => {
filesSaved.push(res);
},
() => {
this.logService.error('Error uploading file. See console output for more details.');
},
() => {
this.field.value = filesSaved;
this.field.json.value = filesSaved;
}
);
this.hasFile = true;
}
}
private uploadRawContent(file): Observable<any> {
return this.processContentService.createTemporaryRawRelatedContent(file).pipe(
map((response: any) => {
this.logService.info(response);
return response;
})
);
}
private getMultipleFileParam() {
if (this.field &&
this.field.params &&
this.field.params.multiple) {
this.multipleOption = this.field.params.multiple ? 'multiple' : '';
}
}
private removeElementFromList(file) {
const index = this.field.value.indexOf(file);
if (index !== -1) {
this.field.value.splice(index, 1);
this.field.json.value = this.field.value;
this.field.updateForm();
}
this.hasFile = this.field.value.length > 0;
this.resetFormValueWithNoFiles();
}
private resetFormValueWithNoFiles() {
if (this.field.value.length === 0) {
this.field.value = [];
this.field.json.value = [];
}
}
getIcon(mimeType) {
return this.thumbnailService.getMimeTypeIcon(mimeType);
}
fileClicked(contentLinkModel: any): void {
const file = new ContentLinkModel(contentLinkModel);
let fetch = this.processContentService.getContentPreview(file.id);
if (file.isTypeImage() || file.isTypePdf()) {
fetch = this.processContentService.getFileRawContent(file.id);
}
fetch.subscribe(
(blob: Blob) => {
file.contentBlob = blob;
this.formService.formContentClicked.next(file);
},
() => {
this.logService.error('Unable to send event for file ' + file.name);
}
);
}
}

View File

@@ -1,40 +0,0 @@
<div class="adf-upload-widget {{field.className}}"
[class.adf-invalid]="!field.isValid"
[class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<div class="adf-upload-widget-container">
<div>
<mat-list *ngIf="hasFile">
<mat-list-item class="adf-upload-files-row" *ngFor="let file of field.value">
<img mat-list-icon class="adf-upload-widget__icon"
[id]="'file-'+file.id+'-icon'"
[src]="getIcon(file.mimeType)"
[alt]="mimeTypeIcon"
(click)="fileClicked(file)"
(keyup.enter)="fileClicked(file)"
role="button"
tabindex="0"/>
<span matLine id="{{'file-'+file.id}}" (click)="fileClicked(file)" (keyup.enter)="fileClicked(file)"
role="button" tabindex="0" class="adf-file">{{file.name}}</span>
<button *ngIf="!field.readOnly" mat-icon-button [id]="'file-'+file.id+'-remove'"
(click)="removeFile(file);" (keyup.enter)="removeFile(file);">
<mat-icon class="mat-24">highlight_off</mat-icon>
</button>
</mat-list-item>
</mat-list>
</div>
<div *ngIf="(!hasFile || multipleOption) && !field.readOnly">
<button mat-raised-button color="primary" (click)="uploadFiles.click()">
{{ 'FORM.FIELD.UPLOAD' | translate }}<mat-icon>file_upload</mat-icon>
<input #uploadFiles
[multiple]="multipleOption"
type="file"
[id]="field.id"
(change)="onFileChanged($event)"/>
</button>
</div>
</div>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -1,32 +0,0 @@
.adf {
&-upload-widget-container {
margin-bottom: 15px;
input {
display: none;
}
}
&-upload-widget {
width: 100%;
word-break: break-all;
padding: 0.4375em 0;
border-top: 0.8438em solid transparent;
}
&-upload-widget__icon {
padding: 6px;
float: left;
cursor: pointer;
}
&-upload-widget__reset {
margin-top: -2px;
}
&-upload-files-row {
.mat-line {
margin-bottom: 0;
}
}
}

View File

@@ -1,411 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs';
import { FormService } from '../../../services/form.service';
import { ProcessContentService } from '../../../services/process-content.service';
import { FormFieldTypes } from '../core/form-field-types';
import { FormModel } from '../core/form.model';
import { FormFieldModel } from '../core/form-field.model';
import { UploadWidgetComponent } from './upload.widget';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { CoreTestingModule } from '../../../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { RelatedContentRepresentation } from '@alfresco/js-api';
const fakePngAnswer = new RelatedContentRepresentation({
id: 1155,
name: 'a_png_file.png',
created: '2017-07-25T17:17:37.099Z',
createdBy: { id: 1001, firstName: 'Admin', lastName: 'admin', email: 'admin' },
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'image/png',
simpleType: 'image',
previewStatus: 'queued',
thumbnailStatus: 'queued'
});
const fakeJpgAnswer = {
id: 1156,
name: 'a_jpg_file.jpg',
created: '2017-07-25T17:17:37.118Z',
createdBy: { id: 1001, firstName: 'Admin', lastName: 'admin', email: 'admin' },
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'image/jpeg',
simpleType: 'image',
previewStatus: 'queued',
thumbnailStatus: 'queued'
};
describe('UploadWidgetComponent', () => {
const fakeCreationFile = (name: string, id: string | number) => ({
id,
name,
created: '2017-07-25T17:17:37.118Z',
createdBy: { id: 1001, firstName: 'Admin', lastName: 'admin', email: 'admin' },
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'image/jpeg',
simpleType: 'image',
previewStatus: 'queued',
thumbnailStatus: 'queued'
});
let contentService: ProcessContentService;
const filePngFake = new File(['fakePng'], 'file-fake.png', { type: 'image/png' });
const filJpgFake = new File(['fakeJpg'], 'file-fake.jpg', { type: 'image/jpg' });
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
describe('when template is ready', () => {
let uploadWidgetComponent: UploadWidgetComponent;
let fixture: ComponentFixture<UploadWidgetComponent>;
let element: HTMLInputElement;
let debugElement: DebugElement;
let inputElement: HTMLInputElement;
let formServiceInstance: FormService;
beforeEach(() => {
fixture = TestBed.createComponent(UploadWidgetComponent);
uploadWidgetComponent = fixture.componentInstance;
element = fixture.nativeElement;
debugElement = fixture.debugElement;
contentService = TestBed.inject(ProcessContentService);
});
it('should setup with field data', () => {
const fileName = 'hello world';
const encodedFileName = encodeURI(fileName);
uploadWidgetComponent.field = new FormFieldModel(null, {
type: FormFieldTypes.UPLOAD,
value: [
{ name: encodedFileName }
]
});
uploadWidgetComponent.ngOnInit();
expect(uploadWidgetComponent.hasFile).toBeTruthy();
});
it('should require form field to setup', () => {
uploadWidgetComponent.field = null;
uploadWidgetComponent.ngOnInit();
expect(uploadWidgetComponent.hasFile).toBeFalsy();
});
it('should reset field value', () => {
uploadWidgetComponent.field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.UPLOAD,
value: [
{ name: 'filename' }
]
});
uploadWidgetComponent.removeFile(uploadWidgetComponent.field.value[0]);
expect(uploadWidgetComponent.field.value).toBeNull();
expect(uploadWidgetComponent.field.json.value).toBeNull();
expect(uploadWidgetComponent.hasFile).toBeFalsy();
});
beforeEach(() => {
uploadWidgetComponent.field = new FormFieldModel(new FormModel({ taskId: 'fake-upload-id' }), {
id: 'upload-id',
name: 'upload-name',
value: '',
type: FormFieldTypes.UPLOAD,
readOnly: false
});
formServiceInstance = TestBed.inject(FormService);
uploadWidgetComponent.field.value = [];
});
it('should be not present in readonly forms', async () => {
uploadWidgetComponent.field.form.readOnly = true;
fixture.detectChanges();
inputElement = element.querySelector<HTMLInputElement>('#upload-id');
fixture.detectChanges();
await fixture.whenStable();
expect(inputElement).toBeNull();
});
it('should have the multiple attribute when is selected in parameters', async () => {
uploadWidgetComponent.field.params.multiple = true;
fixture.detectChanges();
inputElement = element.querySelector<HTMLInputElement>('#upload-id');
fixture.detectChanges();
await fixture.whenStable();
expect(inputElement).toBeDefined();
expect(inputElement).not.toBeNull();
expect(inputElement.getAttributeNode('multiple')).toBeTruthy();
});
it('should not have the multiple attribute if multiple is false', async () => {
uploadWidgetComponent.field.params.multiple = false;
fixture.detectChanges();
inputElement = element.querySelector<HTMLInputElement>('#upload-id');
fixture.detectChanges();
await fixture.whenStable();
expect(inputElement).toBeDefined();
expect(inputElement).not.toBeNull();
expect(inputElement.getAttributeNode('multiple')).toBeFalsy();
});
it('should show the list file after upload a new content', async () => {
spyOn(contentService, 'createTemporaryRawRelatedContent').and.returnValue(of(fakePngAnswer));
uploadWidgetComponent.field.params.multiple = false;
fixture.detectChanges();
await fixture.whenStable();
const inputDebugElement = fixture.debugElement.query(By.css('#upload-id'));
inputDebugElement.triggerEventHandler('change', { target: { files: [filJpgFake] } });
const filesList = fixture.debugElement.query(By.css('#file-1156'));
expect(filesList).toBeDefined();
});
it('should update the form after deleted a file', async () => {
spyOn(contentService, 'createTemporaryRawRelatedContent').and.callFake((file: any) => {
if (file.name === 'file-fake.png') {
return of(fakePngAnswer);
}
if (file.name === 'file-fake.jpg') {
return of(fakeJpgAnswer);
}
return of(null);
});
uploadWidgetComponent.field.params.multiple = true;
spyOn(uploadWidgetComponent.field, 'updateForm');
fixture.detectChanges();
await fixture.whenStable();
const inputDebugElement = fixture.debugElement.query(By.css('#upload-id'));
inputDebugElement.triggerEventHandler('change', { target: { files: [filePngFake, filJpgFake] } });
fixture.detectChanges();
await fixture.whenStable();
const deleteButton = element.querySelector<HTMLInputElement>('#file-1155-remove');
deleteButton.click();
expect(uploadWidgetComponent.field.updateForm).toHaveBeenCalled();
});
it('should set has field value all the files uploaded', async () => {
spyOn(contentService, 'createTemporaryRawRelatedContent').and.callFake((file: any) => {
if (file.name === 'file-fake.png') {
return of(fakePngAnswer);
}
if (file.name === 'file-fake.jpg') {
return of(fakeJpgAnswer);
}
return of(null);
});
uploadWidgetComponent.field.params.multiple = true;
fixture.detectChanges();
await fixture.whenStable();
const inputDebugElement = fixture.debugElement.query(By.css('#upload-id'));
inputDebugElement.triggerEventHandler('change', { target: { files: [filePngFake, filJpgFake] } });
fixture.detectChanges();
await fixture.whenStable();
inputElement = element.querySelector<HTMLInputElement>('#upload-id');
expect(inputElement).toBeDefined();
expect(inputElement).not.toBeNull();
expect(uploadWidgetComponent.field.value).not.toBeNull();
expect(uploadWidgetComponent.field.value.length).toBe(2);
expect(uploadWidgetComponent.field.value[0].id).toBe(1155);
expect(uploadWidgetComponent.field.value[1].id).toBe(1156);
expect(uploadWidgetComponent.field.json.value.length).toBe(2);
});
it('should show all the file uploaded on multiple field', async () => {
uploadWidgetComponent.field.params.multiple = true;
uploadWidgetComponent.field.value.push(fakeJpgAnswer);
uploadWidgetComponent.field.value.push(fakePngAnswer);
fixture.detectChanges();
await fixture.whenStable();
const jpegElement = element.querySelector('#file-1156');
const pngElement = element.querySelector('#file-1155');
expect(jpegElement).not.toBeNull();
expect(pngElement).not.toBeNull();
expect(jpegElement.textContent).toBe('a_jpg_file.jpg');
expect(pngElement.textContent).toBe('a_png_file.png');
});
it('should show correctly the file name when is formed with special characters', async () => {
uploadWidgetComponent.field.value.push(fakeCreationFile('±!@#$%^&*()_+{}:”|<>?§™£-=[];\\,./.jpg', 10));
fixture.detectChanges();
await fixture.whenStable();
const jpegElement = element.querySelector('#file-10');
expect(jpegElement).not.toBeNull();
expect(jpegElement.textContent).toBe(`±!@#$%^&*()_+{}:”|<>?§™£-=[];\\,./.jpg`);
});
it('should show correctly the file name when is formed with Arabic characters', async () => {
const name = 'غ ظ ض ذ خ ث ت ش ر ق ص ف ع س ن م ل ك ي ط ح ز و ه د ج ب ا.jpg';
uploadWidgetComponent.field.value.push(fakeCreationFile(name, 11));
fixture.detectChanges();
await fixture.whenStable();
const jpegElement = element.querySelector('#file-11');
expect(jpegElement).not.toBeNull();
expect(jpegElement.textContent).toBe('غ ظ ض ذ خ ث ت ش ر ق ص ف ع س ن م ل ك ي ط ح ز و ه د ج ب ا.jpg');
});
it('should show correctly the file name when is formed with French characters', async () => {
// cspell: disable-next
uploadWidgetComponent.field.value.push(fakeCreationFile('Àâæçéèêëïîôœùûüÿ.jpg', 12));
fixture.detectChanges();
await fixture.whenStable();
const jpegElement = element.querySelector('#file-12');
expect(jpegElement).not.toBeNull();
// cspell: disable-next
expect(jpegElement.textContent).toBe('Àâæçéèêëïîôœùûüÿ.jpg');
});
it('should show correctly the file name when is formed with Greek characters', async () => {
// cspell: disable-next
uploadWidgetComponent.field.value.push(fakeCreationFile('άέήίϊϊΐόύϋΰώθωερτψυιοπασδφγηςκλζχξωβνμ.jpg', 13));
fixture.detectChanges();
await fixture.whenStable();
const jpegElement = element.querySelector('#file-13');
expect(jpegElement).not.toBeNull();
// cspell: disable-next
expect(jpegElement.textContent).toBe('άέήίϊϊΐόύϋΰώθωερτψυιοπασδφγηςκλζχξωβνμ.jpg');
});
it('should show correctly the file name when is formed with Polish accented characters', async () => {
uploadWidgetComponent.field.value.push(fakeCreationFile('Ą Ć Ę Ł Ń Ó Ś Ź Żą ć ę ł ń ó ś ź ż.jpg', 14));
fixture.detectChanges();
await fixture.whenStable();
const jpegElement = element.querySelector('#file-14');
expect(jpegElement).not.toBeNull();
expect(jpegElement.textContent).toBe('Ą Ć Ę Ł Ń Ó Ś Ź Żą ć ę ł ń ó ś ź ż.jpg');
});
it('should show correctly the file name when is formed with Spanish accented characters', async () => {
uploadWidgetComponent.field.value.push(fakeCreationFile('á, é, í, ó, ú, ñ, Ñ, ü, Ü, ¿, ¡. Á, É, Í, Ó, Ú.jpg', 15));
fixture.detectChanges();
await fixture.whenStable();
const jpegElement = element.querySelector('#file-15');
expect(jpegElement).not.toBeNull();
expect(jpegElement.textContent).toBe('á, é, í, ó, ú, ñ, Ñ, ü, Ü, ¿, ¡. Á, É, Í, Ó, Ú.jpg');
});
it('should show correctly the file name when is formed with Swedish characters', async () => {
// cspell: disable-next
uploadWidgetComponent.field.value.push(fakeCreationFile('Äåéö.jpg', 16));
fixture.detectChanges();
await fixture.whenStable();
const jpegElement = element.querySelector('#file-16');
expect(jpegElement).not.toBeNull();
// cspell: disable-next
expect(jpegElement.textContent).toBe('Äåéö.jpg');
});
it('should remove file from field value', async () => {
uploadWidgetComponent.field.params.multiple = true;
uploadWidgetComponent.field.value.push(fakeJpgAnswer);
uploadWidgetComponent.field.value.push(fakePngAnswer);
fixture.detectChanges();
await fixture.whenStable();
const buttonElement = element.querySelector<HTMLButtonElement>('#file-1156-remove');
buttonElement.click();
fixture.detectChanges();
const jpegElement = element.querySelector('#file-1156');
expect(jpegElement).toBeNull();
expect(uploadWidgetComponent.field.value.length).toBe(1);
});
it('should emit form content clicked event on icon click', (done) => {
spyOn(contentService, 'getContentPreview').and.returnValue(of(new Blob()));
spyOn(contentService, 'getFileRawContent').and.returnValue(of(new Blob()));
formServiceInstance.formContentClicked.subscribe((content: any) => {
expect(content.name).toBe(fakeJpgAnswer.name);
expect(content.id).toBe(fakeJpgAnswer.id);
expect(content.contentBlob).not.toBeNull();
done();
});
uploadWidgetComponent.field.params.multiple = true;
uploadWidgetComponent.field.value.push(fakeJpgAnswer);
uploadWidgetComponent.field.value.push(fakePngAnswer);
fixture.detectChanges();
fixture.whenStable().then(() => {
const fileJpegIcon = debugElement.query(By.css('#file-1156-icon'));
fileJpegIcon.nativeElement.dispatchEvent(new MouseEvent('click'));
});
});
});
});

View File

@@ -1,158 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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.
*/
/* eslint-disable @angular-eslint/component-selector */
import { LogService } from '../../../../services/log.service';
import { ThumbnailService } from '../../../../services/thumbnail.service';
import { Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Observable, from } from 'rxjs';
import { FormService } from '../../../services/form.service';
import { ProcessContentService } from '../../../services/process-content.service';
import { ContentLinkModel } from '../core/content-link.model';
import { WidgetComponent } from '../widget.component';
import { mergeMap, map } from 'rxjs/operators';
@Component({
selector: 'upload-widget',
templateUrl: './upload.widget.html',
styleUrls: ['./upload.widget.scss'],
host: {
'(click)': 'event($event)',
'(blur)': 'event($event)',
'(change)': 'event($event)',
'(focus)': 'event($event)',
'(focusin)': 'event($event)',
'(focusout)': 'event($event)',
'(input)': 'event($event)',
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
encapsulation: ViewEncapsulation.None
})
export class UploadWidgetComponent extends WidgetComponent implements OnInit {
hasFile: boolean;
displayText: string;
multipleOption: string = '';
mimeTypeIcon: string;
@ViewChild('uploadFiles')
fileInput: ElementRef;
constructor(public formService: FormService,
private logService: LogService,
private thumbnailService: ThumbnailService,
public processContentService: ProcessContentService) {
super(formService);
}
ngOnInit() {
if (this.field &&
this.field.value &&
this.field.value.length > 0) {
this.hasFile = true;
}
this.getMultipleFileParam();
}
removeFile(file: any) {
if (this.field) {
this.removeElementFromList(file);
}
}
onFileChanged(event: any) {
const files = event.target.files;
let filesSaved = [];
if (this.field.json.value) {
filesSaved = [...this.field.json.value];
}
if (files && files.length > 0) {
from(files)
.pipe(mergeMap((file) => this.uploadRawContent(file)))
.subscribe(
(res) => filesSaved.push(res),
() => this.logService.error('Error uploading file. See console output for more details.'),
() => {
this.field.value = filesSaved;
this.field.json.value = filesSaved;
this.hasFile = true;
}
);
}
}
private uploadRawContent(file): Observable<any> {
return this.processContentService.createTemporaryRawRelatedContent(file)
.pipe(
map((response: any) => {
this.logService.info(response);
response.contentBlob = file;
return response;
})
);
}
getMultipleFileParam() {
if (this.field &&
this.field.params &&
this.field.params.multiple) {
this.multipleOption = this.field.params.multiple ? 'multiple' : '';
}
}
private removeElementFromList(file: any) {
const index = this.field.value.indexOf(file);
if (index !== -1) {
this.field.value.splice(index, 1);
this.field.json.value = this.field.value;
this.field.updateForm();
}
this.hasFile = this.field.value.length > 0;
if (!this.hasFile) {
this.field.value = null;
this.field.json.value = null;
}
}
getIcon(mimeType: string): string {
return this.thumbnailService.getMimeTypeIcon(mimeType);
}
fileClicked(contentLinkModel: any): void {
const file = new ContentLinkModel(contentLinkModel);
let fetch = this.processContentService.getContentPreview(file.id);
if (file.isTypeImage() || file.isTypePdf()) {
fetch = this.processContentService.getFileRawContent(file.id);
}
fetch.subscribe(
(blob: Blob) => {
file.contentBlob = blob;
this.formService.formContentClicked.next(file);
},
() => {
this.logService.error('Unable to send event for file ' + file.name);
}
);
}
}

View File

@@ -20,5 +20,4 @@ export * from './form-error.event';
export * from './form-field.event';
export * from './validate-form-field.event';
export * from './validate-form.event';
export * from './validate-dynamic-table-row.event';
export * from './form-rules.event';

View File

@@ -1,35 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { FormFieldModel } from '../components/widgets/core/form-field.model';
import { FormModel } from '../components/widgets/core/form.model';
import { DynamicRowValidationSummary } from '../components/widgets/dynamic-table/dynamic-row-validation-summary.model';
import { DynamicTableRow } from '../components/widgets/dynamic-table/dynamic-table-row.model';
import { FormFieldEvent } from './form-field.event';
export class ValidateDynamicTableRowEvent extends FormFieldEvent {
isValid = true;
constructor(form: FormModel,
field: FormFieldModel,
public row: DynamicTableRow,
public summary: DynamicRowValidationSummary) {
super(form, field);
}
}

View File

@@ -31,8 +31,6 @@ import { MASK_DIRECTIVE, WIDGET_DIRECTIVES } from './components/widgets';
import { StartFormCustomButtonDirective } from './components/form-custom-button.directive';
import { FormFieldComponent } from './components/form-field/form-field.component';
import { FormListComponent } from './components/form-list.component';
import { ContentWidgetComponent } from './components/widgets/content/content.widget';
import { WidgetComponent } from './components/widgets/widget.component';
import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core';
import { FormRendererComponent } from './components/form-renderer.component';
@@ -61,9 +59,7 @@ import { InplaceFormInputComponent } from './components/inplace-form-input/inpla
ViewerModule
],
declarations: [
ContentWidgetComponent,
FormFieldComponent,
FormListComponent,
FormRendererComponent,
StartFormCustomButtonDirective,
...WIDGET_DIRECTIVES,
@@ -72,9 +68,7 @@ import { InplaceFormInputComponent } from './components/inplace-form-input/inpla
InplaceFormInputComponent
],
exports: [
ContentWidgetComponent,
FormFieldComponent,
FormListComponent,
FormRendererComponent,
StartFormCustomButtonDirective,
...WIDGET_DIRECTIVES,

View File

@@ -17,21 +17,14 @@
export * from './components/form-field/form-field.component';
export * from './components/form-base.component';
export * from './components/form-list.component';
export * from './components/inplace-form-input/inplace-form-input.component';
export * from './components/widgets/content/content.widget';
export * from './components/form-custom-button.directive';
export * from './components/form-renderer.component';
export * from './components/widgets';
export * from './components/widgets/dynamic-table/dynamic-table-row.model';
export * from './services/activiti-alfresco.service';
export * from './services/ecm-model.service';
export * from './services/form-rendering.service';
export * from './services/form.service';
export * from './services/form-validation-service.interface';
export * from './services/node.service';
export * from './services/process-content.service';
export * from './services/widget-visibility.service';
export * from './events';
@@ -39,3 +32,5 @@ export * from './events';
export * from './form-base.module';
export * from './models/form-rules.model';
export * from './models/form-definition.model';
export * from './models/task-process-variable.model';

View File

@@ -1,151 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { AlfrescoApiService } from '../../services/alfresco-api.service';
import { LogService } from '../../services/log.service';
import { SitesService } from '../../services/sites.service';
import { Injectable } from '@angular/core';
import {
IntegrationAlfrescoOnPremiseApi,
MinimalNode,
RelatedContentRepresentation,
ActivitiContentApi
} from '@alfresco/js-api';
import { Observable, from, throwError } from 'rxjs';
import { ExternalContent } from '../components/widgets/core/external-content';
import { ExternalContentLink } from '../components/widgets/core/external-content-link';
import { map, catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ActivitiContentService {
static UNKNOWN_ERROR_MESSAGE: string = 'Unknown error';
static GENERIC_ERROR_MESSAGE: string = 'Server error';
_integrationAlfrescoOnPremiseApi: IntegrationAlfrescoOnPremiseApi;
get integrationAlfrescoOnPremiseApi(): IntegrationAlfrescoOnPremiseApi {
this._integrationAlfrescoOnPremiseApi = this._integrationAlfrescoOnPremiseApi ?? new IntegrationAlfrescoOnPremiseApi(this.apiService.getInstance());
return this._integrationAlfrescoOnPremiseApi;
}
_contentApi: ActivitiContentApi;
get contentApi(): ActivitiContentApi {
this._contentApi = this._contentApi ?? new ActivitiContentApi(this.apiService.getInstance());
return this._contentApi;
}
constructor(private apiService: AlfrescoApiService,
private logService: LogService,
private sitesService: SitesService) {
}
/**
* Returns a list of child nodes below the specified folder
*
* @param accountId
* @param folderId
*/
getAlfrescoNodes(accountId: string, folderId: string): Observable<[ExternalContent]> {
const accountShortId = accountId.replace('alfresco-', '');
return from(this.integrationAlfrescoOnPremiseApi.getContentInFolder(accountShortId, folderId))
.pipe(
map(this.toJsonArray),
catchError((err) => this.handleError(err))
);
}
/**
* Returns a list of all the repositories configured
*
* @param tenantId
* @param includeAccount
*/
getAlfrescoRepositories(tenantId?: number, includeAccount?: boolean): Observable<any> {
const opts = {
tenantId,
includeAccounts: includeAccount ? includeAccount : true
};
return from(this.integrationAlfrescoOnPremiseApi.getRepositories(opts))
.pipe(
map(this.toJsonArray),
catchError((err) => this.handleError(err))
);
}
/**
* Returns a list of child nodes below the specified folder
*
* @param accountId
* @param node
* @param siteId
*/
linkAlfrescoNode(accountId: string, node: ExternalContent, siteId: string): Observable<ExternalContentLink> {
return from(this.contentApi.createTemporaryRelatedContent({
link: true,
name: node.title,
simpleType: node.simpleType,
source: accountId,
sourceId: node.id + '@' + siteId
}))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
applyAlfrescoNode(node: MinimalNode, siteId: string, accountId: string) {
const currentSideId = siteId ? siteId : this.sitesService.getSiteNameFromNodePath(node);
const params: RelatedContentRepresentation = {
source: accountId,
mimeType: node?.content?.mimeType,
sourceId: node.id + ';' + node.properties['cm:versionLabel'] + '@' + currentSideId,
name: node.name,
link: node.isLink
};
return from(this.contentApi.createTemporaryRelatedContent(params))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
toJson(res: any) {
if (res) {
return res || {};
}
return {};
}
toJsonArray(res: any) {
if (res) {
return res.data || [];
}
return [];
}
handleError(error: any): Observable<any> {
let errMsg = ActivitiContentService.UNKNOWN_ERROR_MESSAGE;
if (error) {
errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : ActivitiContentService.GENERIC_ERROR_MESSAGE;
}
this.logService.error(errMsg);
return throwError(errMsg);
}
}

View File

@@ -1,314 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { Observable } from 'rxjs';
import { FormModel } from '../components/widgets/core/form.model';
import { EcmModelService } from './ecm-model.service';
import { setupTestBed } from '../../testing/setup-test-bed';
import { TestBed } from '@angular/core/testing';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
declare let jasmine: any;
describe('EcmModelService', () => {
let service: EcmModelService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
service = TestBed.inject(EcmModelService);
jasmine.Ajax.install();
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
it('Should fetch ECM models', (done) => {
service.getEcmModels().subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('alfresco/versions/1/cmm')).toBeTruthy();
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('Should fetch ECM types', (done) => {
const modelName = 'modelTest';
service.getEcmType(modelName).subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('versions/1/cmm/' + modelName + '/types')).toBeTruthy();
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('Should create ECM types', (done) => {
const typeName = 'typeTest';
service.createEcmType(typeName, EcmModelService.MODEL_NAME, EcmModelService.TYPE_MODEL).subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('versions/1/cmm/' + EcmModelService.MODEL_NAME + '/types')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).name).toEqual(typeName);
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).title).toEqual(typeName);
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).parentName).toEqual(EcmModelService.TYPE_MODEL);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('Should create ECM types with a clean and preserve real name in the title', (done) => {
const typeName = 'typeTest:testName@#$*!';
const cleanName = 'testName';
service.createEcmType(typeName, EcmModelService.MODEL_NAME, EcmModelService.TYPE_MODEL).subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('versions/1/cmm/' + EcmModelService.MODEL_NAME + '/types')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).name).toEqual(cleanName);
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).title).toEqual(typeName);
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).parentName).toEqual(EcmModelService.TYPE_MODEL);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('Should add property to a type', (done) => {
const typeName = 'typeTest';
const formFields = {
values: {
test: 'test',
test2: 'test2'
}
};
service.addPropertyToAType(EcmModelService.MODEL_NAME, typeName, formFields).subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('1/cmm/' + EcmModelService.MODEL_NAME + '/types/' + typeName + '?select=props')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).properties).toEqual([{
name: 'test',
title: 'test',
description: 'test',
dataType: 'd:text',
multiValued: false,
mandatory: false,
mandatoryEnforced: false
}, {
name: 'test2',
title: 'test2',
description: 'test2',
dataType: 'd:text',
multiValued: false,
mandatory: false,
mandatoryEnforced: false
}]);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('Should add property to a type and clean name type', (done) => {
const typeName = 'typeTest:testName@#$*!';
const cleanName = 'testName';
const formFields = {
values: {
test: 'test',
test2: 'test2'
}
};
service.addPropertyToAType(EcmModelService.MODEL_NAME, typeName, formFields).subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('1/cmm/' + EcmModelService.MODEL_NAME + '/types/' + cleanName + '?select=props')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).properties).toEqual([{
name: 'test',
title: 'test',
description: 'test',
dataType: 'd:text',
multiValued: false,
mandatory: false,
mandatoryEnforced: false
}, {
name: 'test2',
title: 'test2',
description: 'test2',
dataType: 'd:text',
multiValued: false,
mandatory: false,
mandatoryEnforced: false
}]);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('Should create ECM model', (done) => {
service.createEcmModel(EcmModelService.MODEL_NAME, EcmModelService.MODEL_NAMESPACE).subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('alfresco/versions/1/cmm')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).status).toEqual('DRAFT');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('Should activate ECM model', (done) => {
service.activeEcmModel(EcmModelService.MODEL_NAME).subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('alfresco/versions/1/cmm/' + EcmModelService.MODEL_NAME + '?select=status')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).status).toEqual('ACTIVE');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('Should create an ECM type with properties', (done) => {
spyOn(service, 'createEcmType').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
spyOn(service, 'addPropertyToAType').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
service.createEcmTypeWithProperties('nameType', new FormModel()).subscribe(() => {
expect(service.createEcmType).toHaveBeenCalled();
expect(service.addPropertyToAType).toHaveBeenCalled();
done();
});
});
it('Should return the already existing type', (done) => {
spyOn(service, 'searchEcmType').and.callFake(() => new Observable((observer) => {
observer.next({test: 'I-EXIST'});
observer.complete();
}));
spyOn(service, 'createEcmTypeWithProperties').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
service.saveFomType('nameType', new FormModel()).subscribe(() => {
expect(service.searchEcmType).toHaveBeenCalled();
expect(service.createEcmTypeWithProperties).not.toHaveBeenCalled();
done();
});
});
it('Should create an ECM type with properties if the ecm Type is not defined already', (done) => {
spyOn(service, 'searchEcmType').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
spyOn(service, 'createEcmTypeWithProperties').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
service.saveFomType('nameType', new FormModel()).subscribe(() => {
expect(service.searchEcmType).toHaveBeenCalled();
expect(service.createEcmTypeWithProperties).toHaveBeenCalled();
done();
});
});
it('Should create an ECM model for the activiti if not defined already', (done) => {
spyOn(service, 'searchActivitiEcmModel').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
spyOn(service, 'createActivitiEcmModel').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
service.createEcmTypeForActivitiForm('nameType', new FormModel()).subscribe(() => {
expect(service.searchActivitiEcmModel).toHaveBeenCalled();
expect(service.createActivitiEcmModel).toHaveBeenCalled();
done();
});
});
it('If a model for the activiti is already define has to save the new type', (done) => {
spyOn(service, 'searchActivitiEcmModel').and.callFake(() => new Observable((observer) => {
observer.next({test: 'I-EXIST'});
observer.complete();
}));
spyOn(service, 'saveFomType').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
service.createEcmTypeForActivitiForm('nameType', new FormModel()).subscribe(() => {
expect(service.searchActivitiEcmModel).toHaveBeenCalled();
expect(service.saveFomType).toHaveBeenCalled();
done();
});
});
});

View File

@@ -1,222 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { LogService } from '../../services/log.service';
import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { Injectable } from '@angular/core';
import { Observable, from } from 'rxjs';
import { FormModel } from '../components/widgets/core/form.model';
import { map, catchError } from 'rxjs/operators';
import { CustomModelApi } from '@alfresco/js-api';
@Injectable({
providedIn: 'root'
})
export class EcmModelService {
public static MODEL_NAMESPACE: string = 'activitiForms';
public static MODEL_NAME: string = 'activitiFormsModel';
public static TYPE_MODEL: string = 'cm:folder';
_customModelApi: CustomModelApi;
get customModelApi(): CustomModelApi {
this._customModelApi = this._customModelApi ?? new CustomModelApi(this.apiService.getInstance());
return this._customModelApi;
}
constructor(private apiService: AlfrescoApiService,
private logService: LogService) {
}
public createEcmTypeForActivitiForm(formName: string, form: FormModel): Observable<any> {
return new Observable((observer) => {
this.searchActivitiEcmModel().subscribe(
(model) => {
if (!model) {
this.createActivitiEcmModel(formName, form).subscribe((typeForm) => {
observer.next(typeForm);
observer.complete();
});
} else {
this.saveFomType(formName, form).subscribe((typeForm) => {
observer.next(typeForm);
observer.complete();
});
}
},
(err) => this.handleError(err)
);
});
}
searchActivitiEcmModel() {
return this.getEcmModels().pipe(map((ecmModels: any) => ecmModels.list.entries.find((model) => model.entry.name === EcmModelService.MODEL_NAME)));
}
createActivitiEcmModel(formName: string, form: FormModel): Observable<any> {
return new Observable((observer) => {
this.createEcmModel(EcmModelService.MODEL_NAME, EcmModelService.MODEL_NAMESPACE).subscribe(
(model) => {
this.logService.info('model created', model);
this.activeEcmModel(EcmModelService.MODEL_NAME).subscribe(
(modelActive) => {
this.logService.info('model active', modelActive);
this.createEcmTypeWithProperties(formName, form).subscribe((typeCreated) => {
observer.next(typeCreated);
observer.complete();
});
},
(err) => this.handleError(err)
);
},
(err) => this.handleError(err)
);
});
}
saveFomType(formName: string, form: FormModel): Observable<any> {
return new Observable((observer) => {
this.searchEcmType(formName, EcmModelService.MODEL_NAME).subscribe(
(ecmType) => {
this.logService.info('custom types', ecmType);
if (!ecmType) {
this.createEcmTypeWithProperties(formName, form).subscribe((typeCreated) => {
observer.next(typeCreated);
observer.complete();
});
} else {
observer.next(ecmType);
observer.complete();
}
},
(err) => this.handleError(err)
);
});
}
public createEcmTypeWithProperties(formName: string, form: FormModel): Observable<any> {
return new Observable((observer) => {
this.createEcmType(formName, EcmModelService.MODEL_NAME, EcmModelService.TYPE_MODEL).subscribe(
(typeCreated) => {
this.logService.info('type Created', typeCreated);
this.addPropertyToAType(EcmModelService.MODEL_NAME, formName, form).subscribe(
(propertyAdded) => {
this.logService.info('property Added', propertyAdded);
observer.next(typeCreated);
observer.complete();
},
(err) => this.handleError(err));
},
(err) => this.handleError(err));
});
}
public searchEcmType(typeName: string, modelName: string): Observable<any> {
return this.getEcmType(modelName).pipe(map((customTypes: any) =>
customTypes.list.entries.find((type) => type.entry.prefixedName === typeName || type.entry.title === typeName)));
}
public activeEcmModel(modelName: string): Observable<any> {
return from(this.customModelApi.activateCustomModel(modelName))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
public createEcmModel(modelName: string, nameSpace: string): Observable<any> {
return from(this.customModelApi.createCustomModel('DRAFT', '', modelName, modelName, nameSpace))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
public getEcmModels(): Observable<any> {
return from(this.customModelApi.getAllCustomModel())
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
public getEcmType(modelName: string): Observable<any> {
return from(this.customModelApi.getAllCustomType(modelName))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
public createEcmType(typeName: string, modelName: string, parentType: string): Observable<any> {
const name = this.cleanNameType(typeName);
return from(this.customModelApi.createCustomType(modelName, name, parentType, typeName, ''))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
public addPropertyToAType(modelName: string, typeName: string, formFields: any) {
const name = this.cleanNameType(typeName);
const properties = [];
if (formFields && formFields.values) {
for (const key in formFields.values) {
if (key) {
properties.push({
name: key,
title: key,
description: key,
dataType: 'd:text',
multiValued: false,
mandatory: false,
mandatoryEnforced: false
});
}
}
}
return from(this.customModelApi.addPropertyToType(modelName, name, properties))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
cleanNameType(name: string): string {
let cleanName = name;
if (name.indexOf(':') !== -1) {
cleanName = name.split(':')[1];
}
return cleanName.replace(/[^a-zA-Z ]/g, '');
}
toJson(res: any) {
if (res) {
return res || {};
}
return {};
}
handleError(err: any): any {
this.logService.error(err);
}
}

View File

@@ -17,10 +17,8 @@
import { DynamicComponentResolver } from '../../../../index';
import {
FormFieldModel,
FormFieldTypes,
UnknownWidgetComponent,
UploadWidgetComponent,
TextWidgetComponent,
JsonWidgetComponent,
DisplayRichTextWidgetComponent
@@ -35,23 +33,6 @@ describe('FormRenderingService', () => {
service = new FormRenderingService();
});
it('should resolve Upload field as Upload widget', () => {
const field = new FormFieldModel(null, {
type: FormFieldTypes.UPLOAD,
params: {
link: null
}
});
const type = service.resolveComponentType(field);
expect(type).toBe(UploadWidgetComponent);
});
it('should resolve Upload widget for Upload field', () => {
const resolver = service.getComponentTypeResolver(FormFieldTypes.UPLOAD);
const type = resolver(null);
expect(type).toBe(UploadWidgetComponent);
});
it('should resolve Unknown widget for unknown field type', () => {
const resolver = service.getComponentTypeResolver('missing-type');
const type = resolver(null);
@@ -70,12 +51,6 @@ describe('FormRenderingService', () => {
expect(type).toBe(UnknownWidgetComponent);
});
it('should fallback to custom resolver when field type missing', () => {
const resolver = service.getComponentTypeResolver(null, UploadWidgetComponent);
const type = resolver(null);
expect(type).toBe(UploadWidgetComponent);
});
it('should require field type to set resolver for type', () => {
expect(
() => service.setComponentTypeResolver(
@@ -120,10 +95,6 @@ describe('FormRenderingService', () => {
expect(service.resolveComponentType(null)).toBe(UnknownWidgetComponent);
});
it('should return custom value when resolving with no field', () => {
expect(service.resolveComponentType(null, UploadWidgetComponent)).toBe(UploadWidgetComponent);
});
it('should resolve Display Text Widget for JSON field type', () => {
const resolver = service.getComponentTypeResolver('json');
const type = resolver(null);

View File

@@ -32,20 +32,12 @@ export class FormRenderingService extends DynamicComponentMapper {
integer: DynamicComponentResolver.fromType(widgets.NumberWidgetComponent),
'multi-line-text': DynamicComponentResolver.fromType(widgets.MultilineTextWidgetComponentComponent),
boolean: DynamicComponentResolver.fromType(widgets.CheckboxWidgetComponent),
dropdown: DynamicComponentResolver.fromType(widgets.DropdownWidgetComponent),
date: DynamicComponentResolver.fromType(widgets.DateWidgetComponent),
amount: DynamicComponentResolver.fromType(widgets.AmountWidgetComponent),
'radio-buttons': DynamicComponentResolver.fromType(widgets.RadioButtonsWidgetComponent),
hyperlink: DynamicComponentResolver.fromType(widgets.HyperlinkWidgetComponent),
'readonly-text': DynamicComponentResolver.fromType(widgets.DisplayTextWidgetComponent),
json: DynamicComponentResolver.fromType(widgets.JsonWidgetComponent),
readonly: DynamicComponentResolver.fromType(widgets.TextWidgetComponent),
typeahead: DynamicComponentResolver.fromType(widgets.TypeaheadWidgetComponent),
people: DynamicComponentResolver.fromType(widgets.PeopleWidgetComponent),
'functional-group': DynamicComponentResolver.fromType(widgets.FunctionalGroupWidgetComponent),
'dynamic-table': DynamicComponentResolver.fromType(widgets.DynamicTableWidgetComponent),
document: DynamicComponentResolver.fromType(widgets.DocumentWidgetComponent),
upload: DynamicComponentResolver.fromType(widgets.UploadWidgetComponent),
datetime: DynamicComponentResolver.fromType(widgets.DateTimeWidgetComponent),
'file-viewer': DynamicComponentResolver.fromType(widgets.FileViewerWidgetComponent),
'display-rich-text': DynamicComponentResolver.fromType(widgets.DisplayRichTextWidgetComponent)

View File

@@ -15,40 +15,13 @@
* limitations under the License.
*/
import { fakeAsync, TestBed } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import { formModelTabs } from '../../mock';
import { FormService } from './form.service';
import { setupTestBed } from '../../testing/setup-test-bed';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
declare let jasmine: any;
const fakeGroupResponse = {
size: 2,
total: 2,
start: 0,
data: [{
id: '2004',
name: 'PEOPLE_GROUP',
externalId: null,
status: 'active',
groups: null
}, { id: 2005, name: 'PEOPLE_GROUP_2', externalId: null, status: 'active', groups: null }]
};
const fakePeopleResponse = {
size: 3,
total: 3,
start: 0,
data: [{ id: 2002, firstName: 'Peo', lastName: 'Ple', email: 'people' }, {
id: 2003,
firstName: 'Peo02',
lastName: 'Ple02',
email: 'people02'
}, { id: 2004, firstName: 'Peo03', lastName: 'Ple03', email: 'people03' }]
};
describe('Form service', () => {
let service: FormService;
@@ -62,289 +35,12 @@ describe('Form service', () => {
beforeEach(() => {
service = TestBed.inject(FormService);
jasmine.Ajax.install();
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
describe('Content tests', () => {
const responseBody = {
data: [
{ id: '1' },
{ id: '2' }
]
};
const values = {
field1: 'one',
field2: 'two'
};
const simpleResponseBody = { id: 1, modelType: 'test' };
it('should fetch and parse process definitions', (done) => {
service.getProcessDefinitions().subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/process-definitions')).toBeTruthy();
expect([{ id: '1' }, { id: '2' }]).toEqual(JSON.parse(jasmine.Ajax.requests.mostRecent().response).data);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(responseBody)
});
});
it('should fetch and parse tasks', (done) => {
service.getTasks().subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/tasks/query')).toBeTruthy();
expect([{ id: '1' }, { id: '2' }]).toEqual(JSON.parse(jasmine.Ajax.requests.mostRecent().response).data);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(responseBody)
});
});
it('should fetch and parse the task by id', (done) => {
service.getTask('1').subscribe((result) => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/tasks/1')).toBeTruthy();
expect(result.id).toEqual('1');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({ id: '1' })
});
});
it('should save task form', (done) => {
service.saveTaskForm('1', values).subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/task-forms/1/save-form')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).values.field1).toEqual(values.field1);
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).values.field2).toEqual(values.field2);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(responseBody)
});
});
it('should complete task form', (done) => {
service.completeTaskForm('1', values).subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/task-forms/1')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).values.field1).toEqual(values.field1);
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).values.field2).toEqual(values.field2);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(responseBody)
});
});
it('should complete task form with a specific outcome', (done) => {
service.completeTaskForm('1', values, 'custom').subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/task-forms/1')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).values.field2).toEqual(values.field2);
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).outcome).toEqual('custom');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(responseBody)
});
});
it('should get task form by id', (done) => {
service.getTaskForm('1').subscribe((result) => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/task-forms/1')).toBeTruthy();
expect(result.id).toEqual(1);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({ id: 1 })
});
});
it('should get form definition by id', (done) => {
service.getFormDefinitionById(1).subscribe((result) => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/form-models/1')).toBeTruthy();
expect(result.id).toEqual(1);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({ id: 1 })
});
});
it('should get form definition id by name', (done) => {
const formName = 'form1';
const formId = 1;
const response = {
data: [
{ id: formId }
]
};
service.getFormDefinitionByName(formName).subscribe((result) => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith(`models?filter=myReusableForms&filterText=${formName}&modelType=2`)).toBeTruthy();
expect(result).toEqual(formId);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(response)
});
});
it('should handle error with generic message', () => {
service.handleError(null).subscribe(() => {
}, (error) => {
expect(error).toBe(FormService.UNKNOWN_ERROR_MESSAGE);
});
});
it('should handle error with error message', () => {
const message = '<error>';
service.handleError({ message }).subscribe(() => {
}, (error) => {
expect(error).toBe(message);
});
});
it('should handle error with detailed message', () => {
service.handleError({
status: '400',
statusText: 'Bad request'
}).subscribe(
() => {
},
(error) => {
expect(error).toBe('400 - Bad request');
});
});
it('should handle error with generic message', () => {
service.handleError({}).subscribe(() => {
}, (error) => {
expect(error).toBe(FormService.GENERIC_ERROR_MESSAGE);
});
});
it('should get all the forms with modelType=2', (done) => {
service.getForms().subscribe((result) => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('models?modelType=2')).toBeTruthy();
expect(result.length).toEqual(2);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({
data: [
{ name: 'FakeName-1', lastUpdatedByFullName: 'FakeUser-1', lastUpdated: '2017-01-02' },
{ name: 'FakeName-2', lastUpdatedByFullName: 'FakeUser-2', lastUpdated: '2017-01-03' }
]
})
});
});
it('should search for Form with modelType=2', (done) => {
const response = { data: [{ id: 1, name: 'findMe' }, { id: 2, name: 'testForm' }] };
service.searchFrom('findMe').subscribe((result) => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('models?modelType=2')).toBeTruthy();
expect(result.name).toEqual('findMe');
expect(result.id).toEqual(1);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(response)
});
});
it('should create a Form with modelType=2', (done) => {
service.createForm('testName').subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/models')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).modelType).toEqual(2);
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).name).toEqual('testName');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(simpleResponseBody)
});
});
it('should return list of people', (done) => {
spyOn(service, 'getUserProfileImageApi').and.returnValue('/app/rest/users/2002/picture');
const fakeFilter: string = 'whatever';
service.getWorkflowUsers(fakeFilter).subscribe((result) => {
expect(result).toBeDefined();
expect(result.length).toBe(3);
expect(result[0].id).toBe(2002);
expect(result[0].firstName).toBe('Peo');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(fakePeopleResponse)
});
});
it('should return list of groups', (done) => {
const fakeFilter: string = 'whatever';
service.getWorkflowGroups(fakeFilter).subscribe((result) => {
expect(result).toBeDefined();
expect(result.length).toBe(2);
expect(result[0].id).toBe('2004');
expect(result[0].name).toBe('PEOPLE_GROUP');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGroupResponse)
});
});
describe('parseForm', () => {
it('should parse a Form Definition with tabs', () => {
expect(formModelTabs.formRepresentation.formDefinition).toBeDefined();
@@ -352,60 +48,5 @@ describe('Form service', () => {
expect(formParsed).toBeDefined();
});
it('should create a Form form a Node', fakeAsync(() => {
const nameForm = 'testNode';
const formId = 100;
const stubCreateForm = () => {
jasmine.Ajax.stubRequest(
'http://localhost:9876/bpm/activiti-app/api/enterprise/models'
).andReturn({
status: 200,
statusText: 'HTTP/1.1 200 OK',
contentType: 'text/xml;charset=UTF-8',
responseText: { id: formId, name: 'test', lastUpdatedByFullName: 'uset', lastUpdated: '12-12-2016' }
});
};
const stubGetEcmModel = () => {
jasmine.Ajax.stubRequest(
'http://localhost:9876/ecm/alfresco/api/-default-/private/alfresco/versions/1/cmm/activitiFormsModel/types'
).andReturn({
status: 200,
statusText: 'HTTP/1.1 200 OK',
contentType: 'text/xml;charset=UTF-8',
responseText: {
list: {
entries: [{
entry: {
prefixedName: nameForm,
title: nameForm,
properties: [{ name: 'name' }, { name: 'email' }]
}
}, { entry: { prefixedName: 'notme', title: 'notme' } }]
}
}
});
};
const stubAddFieldsToAForm = () => {
jasmine.Ajax.stubRequest(
'http://localhost:9876/bpm/activiti-app/api/enterprise/editor/form-models/' + formId
).andReturn({
status: 200,
statusText: 'HTTP/1.1 200 OK',
contentType: 'text/xml;charset=UTF-8',
responseText: { id: formId, name: 'test', lastUpdatedByFullName: 'user', lastUpdated: '12-12-2016' }
});
};
stubCreateForm();
stubGetEcmModel();
stubAddFieldsToAForm();
service.createFormFromANode(nameForm).subscribe((result) => {
expect(result.id).toEqual(formId);
});
}));
});
});

View File

@@ -15,29 +15,9 @@
* limitations under the License.
*/
import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { LogService } from '../../services/log.service';
import { UserProcessModel } from '../../models';
import { Injectable } from '@angular/core';
import { Observable, Subject, from, of, throwError } from 'rxjs';
import { FormDefinitionModel } from '../models/form-definition.model';
import { Subject } from 'rxjs';
import { ContentLinkModel } from '../components/widgets/core/content-link.model';
import { GroupModel } from '../components/widgets/core/group.model';
import { EcmModelService } from './ecm-model.service';
import { map, catchError, switchMap, combineAll, defaultIfEmpty } from 'rxjs/operators';
import {
CompleteFormRepresentation,
ModelsApi,
ProcessInstanceVariablesApi,
SaveFormRepresentation,
TasksApi,
TaskFormsApi,
ProcessInstancesApi,
FormModelsApi,
ProcessDefinitionsApi,
UsersApi,
ActivitiGroupsApi
} from '@alfresco/js-api';
import { FormOutcomeEvent } from '../components/widgets/core/form-outcome-event.model';
import { FormValues } from '../components/widgets/core/form-values';
import { FormModel } from '../components/widgets/core/form.model';
@@ -47,7 +27,6 @@ import { FormFieldEvent } from '../events/form-field.event';
import { FormErrorEvent } from '../events/form-error.event';
import { ValidateFormEvent } from '../events/validate-form.event';
import { ValidateFormFieldEvent } from '../events/validate-form-field.event';
import { ValidateDynamicTableRowEvent } from '../events/validate-dynamic-table-row.event';
import { FormValidationService } from './form-validation-service.interface';
import { FormRulesEvent } from '../events/form-rules.event';
@@ -56,63 +35,6 @@ import { FormRulesEvent } from '../events/form-rules.event';
})
export class FormService implements FormValidationService {
static UNKNOWN_ERROR_MESSAGE: string = 'Unknown error';
static GENERIC_ERROR_MESSAGE: string = 'Server error';
_taskFormsApi: TaskFormsApi;
get taskFormsApi(): TaskFormsApi {
this._taskFormsApi = this._taskFormsApi ?? new TaskFormsApi(this.apiService.getInstance());
return this._taskFormsApi;
}
_taskApi: TasksApi;
get taskApi(): TasksApi {
this._taskApi = this._taskApi ?? new TasksApi(this.apiService.getInstance());
return this._taskApi;
}
_modelsApi: ModelsApi;
get modelsApi(): ModelsApi {
this._modelsApi = this._modelsApi ?? new ModelsApi(this.apiService.getInstance());
return this._modelsApi;
}
_editorApi: FormModelsApi;
get editorApi(): FormModelsApi {
this._editorApi = this._editorApi ?? new FormModelsApi(this.apiService.getInstance());
return this._editorApi;
}
_processDefinitionsApi: ProcessDefinitionsApi;
get processDefinitionsApi(): ProcessDefinitionsApi {
this._processDefinitionsApi = this._processDefinitionsApi ?? new ProcessDefinitionsApi(this.apiService.getInstance());
return this._processDefinitionsApi;
}
_processInstanceVariablesApi: ProcessInstanceVariablesApi;
get processInstanceVariablesApi(): ProcessInstanceVariablesApi {
this._processInstanceVariablesApi = this._processInstanceVariablesApi ?? new ProcessInstanceVariablesApi(this.apiService.getInstance());
return this._processInstanceVariablesApi;
}
_processInstancesApi: ProcessInstancesApi;
get processInstancesApi(): ProcessInstancesApi {
this._processInstancesApi = this._processInstancesApi ?? new ProcessInstancesApi(this.apiService.getInstance());
return this._processInstancesApi;
}
_groupsApi: ActivitiGroupsApi;
get groupsApi(): ActivitiGroupsApi {
this._groupsApi = this._groupsApi ?? new ActivitiGroupsApi(this.apiService.getInstance());
return this._groupsApi;
}
_usersApi: UsersApi;
get usersApi(): UsersApi {
this._usersApi = this._usersApi ?? new UsersApi(this.apiService.getInstance());
return this._usersApi;
}
formLoaded = new Subject<FormEvent>();
formDataRefreshed = new Subject<FormEvent>();
formFieldValueChanged = new Subject<FormFieldEvent>();
@@ -125,7 +47,7 @@ export class FormService implements FormValidationService {
validateForm = new Subject<ValidateFormEvent>();
validateFormField = new Subject<ValidateFormFieldEvent>();
validateDynamicTableRow = new Subject<ValidateDynamicTableRowEvent>();
validateDynamicTableRow = new Subject<FormFieldEvent>();
executeOutcome = new Subject<FormOutcomeEvent>();
@@ -133,9 +55,7 @@ export class FormService implements FormValidationService {
formRulesEvent = new Subject<FormRulesEvent>();
constructor(private ecmModelService: EcmModelService,
private apiService: AlfrescoApiService,
protected logService: LogService) {
constructor() {
}
/**
@@ -163,449 +83,4 @@ export class FormService implements FormValidationService {
}
return null;
}
/**
* Creates a Form with a field for each metadata property.
*
* @param formName Name of the new form
* @returns The new form
*/
createFormFromANode(formName: string): Observable<any> {
return new Observable((observer) => {
this.createForm(formName).subscribe(
(form) => {
this.ecmModelService.searchEcmType(formName, EcmModelService.MODEL_NAME).subscribe(
(customType) => {
const formDefinitionModel = new FormDefinitionModel(form.id, form.name, form.lastUpdatedByFullName, form.lastUpdated, customType.entry.properties);
from(
this.editorApi.saveForm(form.id, formDefinitionModel)
).subscribe((formData) => {
observer.next(formData);
observer.complete();
}, (err) => this.handleError(err));
},
(err) => this.handleError(err));
},
(err) => this.handleError(err));
});
}
/**
* Create a Form.
*
* @param formName Name of the new form
* @returns The new form
*/
createForm(formName: string): Observable<any> {
const dataModel = {
name: formName,
description: '',
modelType: 2,
stencilSet: 0
};
return from(
this.modelsApi.createModel(dataModel)
);
}
/**
* Saves a form.
*
* @param formId ID of the form to save
* @param formModel Model data for the form
* @returns Data for the saved form
*/
saveForm(formId: number, formModel: FormDefinitionModel): Observable<any> {
return from(
this.editorApi.saveForm(formId, formModel)
);
}
/**
* Searches for a form by name.
*
* @param name The form name to search for
* @returns Form model(s) matching the search name
*/
searchFrom(name: string): Observable<any> {
const opts = {
modelType: 2
};
return from(
this.modelsApi.getModels(opts)
)
.pipe(
map((forms: any) => forms.data.find((formData) => formData.name === name)),
catchError((err) => this.handleError(err))
);
}
/**
* Gets all the forms.
*
* @returns List of form models
*/
getForms(): Observable<any> {
const opts = {
modelType: 2
};
return from(this.modelsApi.getModels(opts))
.pipe(
map(this.toJsonArray),
catchError((err) => this.handleError(err))
);
}
/**
* Gets process definitions.
*
* @returns List of process definitions
*/
getProcessDefinitions(): Observable<any> {
return from(this.processDefinitionsApi.getProcessDefinitions({}))
.pipe(
map(this.toJsonArray),
catchError((err) => this.handleError(err))
);
}
/**
* Gets instance variables for a process.
*
* @param processInstanceId ID of the target process
* @returns List of instance variable information
*/
getProcessVariablesById(processInstanceId: string): Observable<any[]> {
return from(this.processInstanceVariablesApi.getProcessInstanceVariables(processInstanceId))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
/**
* Gets all the tasks.
*
* @returns List of tasks
*/
getTasks(): Observable<any> {
return from(this.taskApi.listTasks({}))
.pipe(
map(this.toJsonArray),
catchError((err) => this.handleError(err))
);
}
/**
* Gets a task.
*
* @param taskId Task Id
* @returns Task info
*/
getTask(taskId: string): Observable<any> {
return from(this.taskApi.getTask(taskId))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
/**
* Saves a task form.
*
* @param taskId Task Id
* @param formValues Form Values
* @returns Null response when the operation is complete
*/
saveTaskForm(taskId: string, formValues: FormValues): Observable<any> {
const saveFormRepresentation = { values: formValues } as SaveFormRepresentation;
return from(this.taskFormsApi.saveTaskForm(taskId, saveFormRepresentation))
.pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Completes a Task Form.
*
* @param taskId Task Id
* @param formValues Form Values
* @param outcome Form Outcome
* @returns Null response when the operation is complete
*/
completeTaskForm(taskId: string, formValues: FormValues, outcome?: string): Observable<any> {
const completeFormRepresentation = { values: formValues } as CompleteFormRepresentation;
if (outcome) {
completeFormRepresentation.outcome = outcome;
}
return from(this.taskFormsApi.completeTaskForm(taskId, completeFormRepresentation))
.pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Gets a form related to a task.
*
* @param taskId ID of the target task
* @returns Form definition
*/
getTaskForm(taskId: string): Observable<any> {
return from(this.taskFormsApi.getTaskForm(taskId))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
/**
* Gets a form definition.
*
* @param formId ID of the target form
* @returns Form definition
*/
getFormDefinitionById(formId: number): Observable<any> {
return from(this.editorApi.getForm(formId))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
/**
* Gets the form definition with a given name.
*
* @param name The form name
* @returns Form definition
*/
getFormDefinitionByName(name: string): Observable<any> {
const opts = {
filter: 'myReusableForms',
filterText: name,
modelType: 2
};
return from(this.modelsApi.getModels(opts))
.pipe(
map(this.getFormId),
catchError((err) => this.handleError(err))
);
}
/**
* Gets the start form instance for a given process.
*
* @param processId Process definition ID
* @returns Form definition
*/
getStartFormInstance(processId: string): Observable<any> {
return from(this.processInstancesApi.getProcessInstanceStartForm(processId))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
/**
* Gets a process instance.
*
* @param processId ID of the process to get
* @returns Process instance
*/
getProcessInstance(processId: string): Observable<any> {
return from(this.processInstancesApi.getProcessInstance(processId))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
/**
* Gets the start form definition for a given process.
*
* @param processId Process definition ID
* @returns Form definition
*/
getStartFormDefinition(processId: string): Observable<any> {
return from(this.processDefinitionsApi.getProcessDefinitionStartForm(processId))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
/**
* Gets values of fields populated by a REST backend.
*
* @param taskId Task identifier
* @param field Field identifier
* @returns Field values
*/
getRestFieldValues(taskId: string, field: string): Observable<any> {
return from(this.taskFormsApi.getRestFieldValues(taskId, field))
.pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Gets values of fields populated by a REST backend using a process ID.
*
* @param processDefinitionId Process identifier
* @param field Field identifier
* @returns Field values
*/
getRestFieldValuesByProcessId(processDefinitionId: string, field: string): Observable<any> {
return from(this.processDefinitionsApi.getRestFieldValues(processDefinitionId, field))
.pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Gets column values of fields populated by a REST backend using a process ID.
*
* @param processDefinitionId Process identifier
* @param field Field identifier
* @param column Column identifier
* @returns Field values
*/
getRestFieldValuesColumnByProcessId(processDefinitionId: string, field: string, column?: string): Observable<any> {
return from(this.processDefinitionsApi.getRestTableFieldValues(processDefinitionId, field, column))
.pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Gets column values of fields populated by a REST backend.
*
* @param taskId Task identifier
* @param field Field identifier
* @param column Column identifier
* @returns Field values
*/
getRestFieldValuesColumn(taskId: string, field: string, column?: string): Observable<any> {
return from(this.taskFormsApi.getRestFieldColumnValues(taskId, field, column))
.pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Returns a URL for the profile picture of a user.
*
* @param userId ID of the target user
* @returns URL string
*/
getUserProfileImageApi(userId: string): string {
return this.usersApi.getUserProfilePictureUrl(userId);
}
/**
* Gets a list of workflow users.
*
* @param filter Filter to select specific users
* @param groupId Group ID for the search
* @returns Array of users
*/
getWorkflowUsers(filter: string, groupId?: string): Observable<UserProcessModel[]> {
const option: any = { filter };
if (groupId) {
option.groupId = groupId;
}
return from(this.usersApi.getUsers(option))
.pipe(
switchMap(response => response.data as UserProcessModel[] || []),
map((user) => {
user.userImage = this.getUserProfileImageApi(user.id.toString());
return of(user);
}),
combineAll(),
defaultIfEmpty([]),
catchError((err) => this.handleError(err))
);
}
/**
* Gets a list of groups in a workflow.
*
* @param filter Filter to select specific groups
* @param groupId Group ID for the search
* @returns Array of groups
*/
getWorkflowGroups(filter: string, groupId?: string): Observable<GroupModel[]> {
const option: any = { filter };
if (groupId) {
option.groupId = groupId;
}
return from(this.groupsApi.getGroups(option))
.pipe(
map((response: any) => response.data || []),
catchError((err) => this.handleError(err))
);
}
/**
* Gets the ID of a form.
*
* @param form Object representing a form
* @returns ID string
*/
getFormId(form: any): string {
let result = null;
if (form && form.data && form.data.length > 0) {
result = form.data[0].id;
}
return result;
}
/**
* Creates a JSON representation of form data.
*
* @param res Object representing form data
* @returns JSON data
*/
toJson(res: any) {
if (res) {
return res || {};
}
return {};
}
/**
* Creates a JSON array representation of form data.
*
* @param res Object representing form data
* @returns JSON data
*/
toJsonArray(res: any) {
if (res) {
return res.data || [];
}
return [];
}
/**
* Reports an error message.
*
* @param error Data object with optional `message` and `status` fields for the error
* @returns Error message
*/
handleError(error: any): Observable<any> {
let errMsg = FormService.UNKNOWN_ERROR_MESSAGE;
if (error) {
errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : FormService.GENERIC_ERROR_MESSAGE;
}
this.logService.error(errMsg);
return throwError(errMsg);
}
}

View File

@@ -1,171 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { TestBed } from '@angular/core/testing';
import { NodeMetadata } from '../../models/node-metadata.model';
import { EcmModelService } from './ecm-model.service';
import { NodeService } from './node.service';
import { setupTestBed } from '../../testing/setup-test-bed';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
declare let jasmine: any;
describe('NodeService', () => {
let service: NodeService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
service = TestBed.inject(NodeService);
});
beforeEach(() => {
jasmine.Ajax.install();
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
it('Should fetch and node metadata', (done) => {
const responseBody = {
entry: {
id: '111-222-33-44-1123',
nodeType: 'typeTest',
properties: {
test: 'test',
testdata: 'testdata'
}
}
};
service.getNodeMetadata('-nodeid-').subscribe((result) => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('nodes/-nodeid-')).toBeTruthy();
const node = new NodeMetadata({
test: 'test',
testdata: 'testdata'
}, 'typeTest');
expect(result).toEqual(node);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(responseBody)
});
});
it('Should clean the metadata from :', (done) => {
const responseBody = {
entry: {
id: '111-222-33-44-1123',
nodeType: 'typeTest',
properties: {
'metadata:test': 'test',
'metadata:testdata': 'testdata'
}
}
};
service.getNodeMetadata('-nodeid-').subscribe((result) => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('nodes/-nodeid-')).toBeTruthy();
const node = new NodeMetadata({
test: 'test',
testdata: 'testdata'
}, 'typeTest');
expect(result).toEqual(node);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(responseBody)
});
});
it('Should create a node with metadata', (done) => {
const data = {
test: 'test',
testdata: 'testdata'
};
const responseBody = {
id: 'a74d91fb-ea8a-4812-ad98-ad878366b5be',
isFile: false,
isFolder: true
};
service.createNodeMetadata('typeTest', EcmModelService.MODEL_NAMESPACE, data, '/Sites/swsdp/documentLibrary', 'testNode').subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('-root-/children')).toBeTruthy();
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(responseBody)
});
});
it('Should add activitiForms suffix to the metadata properties', (done) => {
const data = {
test: 'test',
testdata: 'testdata'
};
service.createNodeMetadata('typeTest', EcmModelService.MODEL_NAMESPACE, data, '/Sites/swsdp/documentLibrary').subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('-root-/children')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).properties[EcmModelService.MODEL_NAMESPACE + ':test']).toBeDefined();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).properties[EcmModelService.MODEL_NAMESPACE + ':testdata']).toBeDefined();
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('Should assign an UUID to the name when name not passed', (done) => {
const data = {
test: 'test',
testdata: 'testdata'
};
service.createNodeMetadata('typeTest', EcmModelService.MODEL_NAMESPACE, data, '/Sites/swsdp/documentLibrary').subscribe(() => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('-root-/children')).toBeTruthy();
expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params).name).toBeDefined();
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
});

View File

@@ -1,70 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { NodeEntry } from '@alfresco/js-api';
import { NodeMetadata } from '../../models/node-metadata.model';
import { NodesApiService } from '../../services/nodes-api.service';
@Injectable({
providedIn: 'root'
})
/**
* @deprecated in 3.8.0, use NodesApiService instead.
*/
export class NodeService {
constructor(private nodesApiService: NodesApiService) {}
/**
* @deprecated in 3.8.0, use NodesApiService instead.
* Get the metadata and the nodeType for a nodeId cleaned by the prefix.
* @param nodeId ID of the target node
* @returns Node metadata
*/
public getNodeMetadata(nodeId: string): Observable<NodeMetadata> {
return this.nodesApiService.getNodeMetadata(nodeId);
}
/**
* @deprecated in 3.8.0, use NodesApiService instead.
* Create a new Node from form metadata.
* @param path Path to the node
* @param nodeType Node type
* @param name Node name
* @param nameSpace Namespace for properties
* @param data Property data to store in the node under namespace
* @returns The created node
*/
public createNodeMetadata(nodeType: string, nameSpace: any, data: any, path: string, name?: string): Observable<NodeEntry> {
return this.nodesApiService.createNodeMetadata(nodeType, nameSpace, data, path, name);
}
/**
* @deprecated in 3.8.0, use `createNodeInsideRoot` method from NodesApiService instead.
* Create a new Node from form metadata
* @param name Node name
* @param nodeType Node type
* @param properties Node body properties
* @param path Path to the node
* @returns The created node
*/
public createNode(name: string, nodeType: string, properties: any, path: string): Observable<NodeEntry> {
return this.nodesApiService.createNodeInsideRoot(name, nodeType, properties, path);
}
}

View File

@@ -1,194 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { ProcessContentService } from './process-content.service';
import { setupTestBed } from '../../testing/setup-test-bed';
import { CoreTestingModule } from '../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
declare let jasmine: any;
const fileContentPdfResponseBody = {
id: 999,
name: 'fake-name.pdf',
created: '2017-01-23T12:12:53.219+0000',
createdBy: { id: 2, firstName: 'fake-admin', lastName: 'fake-last', email: 'fake-admin' },
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'application/pdf',
simpleType: 'pdf',
previewStatus: 'created',
thumbnailStatus: 'created'
};
const fileContentJpgResponseBody = {
id: 888,
name: 'fake-name.jpg',
created: '2017-01-23T12:12:53.219+0000',
createdBy: { id: 2, firstName: 'fake-admin', lastName: 'fake-last', email: 'fake-admin' },
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'image/jpeg',
simpleType: 'image',
previewStatus: 'unsupported',
thumbnailStatus: 'unsupported'
};
const createFakeBlob = () => {
const data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
const bytes = new Uint8Array(data.length / 2);
for (let i = 0; i < data.length; i += 2) {
bytes[i / 2] = parseInt(data.substring(i, i + 2), /* base = */ 16);
}
return new Blob([bytes], { type: 'image/png' });
};
describe('ProcessContentService', () => {
let service: ProcessContentService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
service = TestBed.inject(ProcessContentService);
});
beforeEach(() => {
jasmine.Ajax.install();
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
it('Should fetch the attachments', (done) => {
service.getTaskRelatedContent('1234').subscribe((res) => {
expect(res.data).toBeDefined();
expect(res.data.length).toBe(2);
expect(res.data[0].name).toBe('fake.zip');
expect(res.data[0].mimeType).toBe('application/zip');
expect(res.data[0].relatedContent).toBeTruthy();
expect(res.data[1].name).toBe('fake.jpg');
expect(res.data[1].mimeType).toBe('image/jpeg');
expect(res.data[1].relatedContent).toBeTruthy();
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify({
size: 2,
total: 2,
start: 0,
data: [
{
id: 8,
name: 'fake.zip',
created: 1494595697381,
createdBy: {id: 2, firstName: 'user', lastName: 'user', email: 'user@user.com'},
relatedContent: true,
contentAvailable: true,
link: false,
mimeType: 'application/zip',
simpleType: 'content',
previewStatus: 'unsupported',
thumbnailStatus: 'unsupported'
},
{
id: 9,
name: 'fake.jpg',
created: 1494595655381,
createdBy: {id: 2, firstName: 'user', lastName: 'user', email: 'user@user.com'},
relatedContent: true,
contentAvailable: true,
link: false,
mimeType: 'image/jpeg',
simpleType: 'image',
previewStatus: 'unsupported',
thumbnailStatus: 'unsupported'
}
]
})
});
});
it('should return the unsupported content when the file is an image', (done) => {
const contentId: number = 888;
service.getFileContent(contentId).subscribe((result) => {
expect(result.id).toEqual(contentId);
expect(result.name).toEqual('fake-name.jpg');
expect(result.simpleType).toEqual('image');
expect(result.thumbnailStatus).toEqual('unsupported');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(fileContentJpgResponseBody)
});
});
it('should return the supported content when the file is a pdf', (done) => {
const contentId: number = 999;
service.getFileContent(contentId).subscribe((result) => {
expect(result.id).toEqual(contentId);
expect(result.name).toEqual('fake-name.pdf');
expect(result.simpleType).toEqual('pdf');
expect(result.thumbnailStatus).toEqual('created');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'application/json',
responseText: JSON.stringify(fileContentPdfResponseBody)
});
});
it('should return the raw content URL', () => {
const contentId: number = 999;
const contentUrl = service.getFileRawContentUrl(contentId);
expect(contentUrl).toContain(`/api/enterprise/content/${contentId}/raw`);
});
it('should return a Blob as thumbnail', (done) => {
const contentId: number = 999;
const blob = createFakeBlob();
spyOn(service, 'getContentThumbnail').and.returnValue(of(blob));
service.getContentThumbnail(contentId).subscribe((result) => {
expect(result).toEqual(jasmine.any(Blob));
expect(result.size).toEqual(48);
expect(result.type).toEqual('image/png');
done();
});
});
});

View File

@@ -1,229 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* 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 { AlfrescoApiService } from '../../services/alfresco-api.service';
import { LogService } from '../../services/log.service';
import { Injectable } from '@angular/core';
import { ActivitiContentApi, RelatedContentRepresentation } from '@alfresco/js-api';
import { Observable, from, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ProcessContentService {
static UNKNOWN_ERROR_MESSAGE: string = 'Unknown error';
static GENERIC_ERROR_MESSAGE: string = 'Server error';
_contentApi: ActivitiContentApi;
get contentApi(): ActivitiContentApi {
this._contentApi = this._contentApi ?? new ActivitiContentApi(this.apiService.getInstance());
return this._contentApi;
}
constructor(private apiService: AlfrescoApiService,
private logService: LogService) {
}
/**
* Create temporary related content from an uploaded file.
*
* @param file File to use for content
* @returns The created content data
*/
createTemporaryRawRelatedContent(file: any): Observable<RelatedContentRepresentation> {
return from(this.contentApi.createTemporaryRawRelatedContent(file))
.pipe(catchError((err) => this.handleError(err)));
}
/**
* Gets the metadata for a related content item.
*
* @param contentId ID of the content item
* @returns Metadata for the content
*/
getFileContent(contentId: number): Observable<RelatedContentRepresentation> {
return from(this.contentApi.getContent(contentId))
.pipe(catchError((err) => this.handleError(err)));
}
/**
* Gets raw binary content data for a related content file.
*
* @param contentId ID of the related content
* @returns Binary data of the related content
*/
getFileRawContent(contentId: number): Observable<Blob> {
return from(this.contentApi.getRawContent(contentId))
.pipe(catchError((err) => this.handleError(err)));
}
/**
* Gets the preview for a related content file.
*
* @param contentId ID of the related content
* @returns Binary data of the content preview
*/
getContentPreview(contentId: number): Observable<Blob> {
return new Observable((observer) => {
this.contentApi.getRawContent(contentId).then(
(result) => {
observer.next(result);
observer.complete();
},
() => {
this.contentApi.getRawContent(contentId).then(
(data) => {
observer.next(data);
observer.complete();
},
(err) => {
observer.error(err);
observer.complete();
}
);
}
);
});
}
/**
* Gets a URL for direct access to a related content file.
*
* @param contentId ID of the related content
* @returns URL to access the content
*/
getFileRawContentUrl(contentId: number): string {
return this.contentApi.getRawContentUrl(contentId);
}
/**
* Gets the thumbnail for a related content file.
*
* @param contentId ID of the related content
* @returns Binary data of the thumbnail image
*/
getContentThumbnail(contentId: number): Observable<Blob> {
return from(this.contentApi.getRawContent(contentId, 'thumbnail'))
.pipe(catchError((err) => this.handleError(err)));
}
/**
* Gets related content items for a task instance.
*
* @param taskId ID of the target task
* @param opts Options supported by JS-API
* @returns Metadata for the content
*/
getTaskRelatedContent(taskId: string, opts?: any): Observable<any> {
return from(this.contentApi.getRelatedContentForTask(taskId, opts))
.pipe(catchError((err) => this.handleError(err)));
}
/**
* Gets related content items for a process instance.
*
* @param processId ID of the target process
* @param opts Options supported by JS-API
* @returns Metadata for the content
*/
getProcessRelatedContent(processId: string, opts?: any): Observable<any> {
return from(this.contentApi.getRelatedContentForProcessInstance(processId, opts))
.pipe(catchError((err) => this.handleError(err)));
}
/**
* Deletes related content.
*
* @param contentId Identifier of the content to delete
* @returns Null response that notifies when the deletion is complete
*/
deleteRelatedContent(contentId: number): Observable<any> {
return from(this.contentApi.deleteContent(contentId))
.pipe(catchError((err) => this.handleError(err)));
}
/**
* Associates an uploaded file with a process instance.
*
* @param processInstanceId ID of the target process instance
* @param content File to associate
* @param opts Options supported by JS-API
* @returns Details of created content
*/
createProcessRelatedContent(processInstanceId: string, content: any, opts?: any): Observable<any> {
return from(this.contentApi.createRelatedContentOnProcessInstance(processInstanceId, content, opts))
.pipe(catchError((err) => this.handleError(err)));
}
/**
* Associates an uploaded file with a task instance.
*
* @param taskId ID of the target task
* @param file File to associate
* @param opts Options supported by JS-API
* @returns Details of created content
*/
createTaskRelatedContent(taskId: string, file: any, opts?: any) {
return from(this.contentApi.createRelatedContentOnTask(taskId, file, opts))
.pipe(catchError((err) => this.handleError(err)));
}
/**
* Creates a JSON representation of data.
*
* @param res Object representing data
* @returns JSON object
*/
toJson(res: any) {
if (res) {
return res || {};
}
return {};
}
/**
* Creates a JSON array representation of data.
*
* @param res Object representing data
* @returns JSON array object
*/
toJsonArray(res: any) {
if (res) {
return res.data || [];
}
return [];
}
/**
* Reports an error message.
*
* @param error Data object with optional `message` and `status` fields for the error
* @returns Callback when an error occurs
*/
handleError(error: any): Observable<any> {
let errMsg = ProcessContentService.UNKNOWN_ERROR_MESSAGE;
if (error) {
errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : ProcessContentService.GENERIC_ERROR_MESSAGE;
}
this.logService.error(errMsg);
return throwError(errMsg);
}
}

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { fakeAsync, TestBed } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import {
ContainerModel,
FormFieldModel,
@@ -24,12 +24,11 @@ import {
TabModel,
FormOutcomeModel
} from '../components/widgets/core';
import { TaskProcessVariableModel } from '../models/task-process-variable.model';
import { WidgetVisibilityModel, WidgetTypeEnum } from '../models/widget-visibility.model';
import { WidgetVisibilityService } from './widget-visibility.service';
import { setupTestBed } from '../../testing/setup-test-bed';
import {
fakeFormJson, fakeTaskProcessVariableModels,
fakeFormJson,
formTest, formValues, complexVisibilityJsonVisible,
nextConditionForm, complexVisibilityJsonNotVisible,
headerVisibilityCond
@@ -156,160 +155,6 @@ describe('WidgetVisibilityCloudService', () => {
});
});
describe('should retrieve the process variables', () => {
const fakeFormWithField = new FormModel(fakeFormJson);
let visibilityObjTest: WidgetVisibilityModel;
const chainedVisibilityObj = new WidgetVisibilityModel({});
beforeEach(() => {
visibilityObjTest = new WidgetVisibilityModel({});
});
it('should return the process variables for task', (done) => {
service.getTaskProcessVariable('9999').subscribe(
(res) => {
expect(res).toBeDefined();
expect(res.length).toEqual(3);
expect(res[0].id).toEqual('TEST_VAR_1');
expect(res[0].type).toEqual('string');
expect(res[0].value).toEqual('test_value_1');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should be able to retrieve the value of a process variable', (done) => {
service.getTaskProcessVariable('9999').subscribe(
(res: TaskProcessVariableModel[]) => {
expect(res).toBeDefined();
const varValue = service.getVariableValue(formTest, 'TEST_VAR_1', res);
expect(varValue).not.toBeUndefined();
expect(varValue).toBe('test_value_1');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should return undefined if the variable does not exist', (done) => {
service.getTaskProcessVariable('9999').subscribe(
(res: TaskProcessVariableModel[]) => {
const varValue = service.getVariableValue(formTest, 'TEST_MYSTERY_VAR', res);
expect(varValue).toBeUndefined();
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should retrieve the value for the right field when it is a process variable', (done) => {
service.getTaskProcessVariable('9999').subscribe(
() => {
visibilityObjTest.rightValue = 'test_value_2';
spyOn(service, 'isFormFieldValid').and.returnValue(true);
const rightValue = service.getRightValue(formTest, visibilityObjTest);
expect(rightValue).not.toBeNull();
expect(rightValue).toBe('test_value_2');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should retrieve the value for the left field when it is a process variable', (done) => {
service.getTaskProcessVariable('9999').subscribe(
() => {
visibilityObjTest.leftValue = 'TEST_VAR_2';
visibilityObjTest.leftType = WidgetTypeEnum.field;
const leftValue = service.getLeftValue(formTest, visibilityObjTest);
expect(leftValue).not.toBeNull();
expect(leftValue).toBe('test_value_2');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should evaluate the visibility for the field between form value and process var', (done) => {
service.getTaskProcessVariable('9999').subscribe(
() => {
visibilityObjTest.leftType = 'LEFT_FORM_FIELD_ID';
visibilityObjTest.operator = '!=';
visibilityObjTest.rightValue = 'TEST_VAR_2';
const isVisible = service.isFieldVisible(fakeFormWithField, visibilityObjTest);
expect(isVisible).toBeTruthy();
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should evaluate visibility with multiple conditions', (done) => {
service.getTaskProcessVariable('9999').subscribe(
() => {
visibilityObjTest.leftType = 'field';
visibilityObjTest.leftValue = 'TEST_VAR_2';
visibilityObjTest.operator = '!=';
visibilityObjTest.rightValue = 'TEST_VAR_2';
visibilityObjTest.nextConditionOperator = 'and';
chainedVisibilityObj.leftType = 'field';
chainedVisibilityObj.leftValue = 'TEST_VAR_2';
chainedVisibilityObj.operator = '!empty';
visibilityObjTest.nextCondition = chainedVisibilityObj;
const isVisible = service.isFieldVisible(fakeFormWithField, visibilityObjTest);
expect(isVisible).toBeTruthy();
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should catch error on 403 response', fakeAsync(() => {
service.getTaskProcessVariable('9999').subscribe(() => {
}, (errorMessage) => {
expect(errorMessage).toEqual('Error while performing a call - Server error');
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 403
});
}));
});
describe('should return the value of the field', () => {
let visibilityObjTest: WidgetVisibilityModel;
let fakeFormWithField = new FormModel(fakeFormJson);
@@ -644,143 +489,6 @@ describe('WidgetVisibilityCloudService', () => {
expect(fakeFormWithField.outcomes[outcomeIndex].isVisible).toBeFalsy();
});
it('should use the form value to evaluate the visibility condition if the form value is defined', (done) => {
service.getTaskProcessVariable('9999').subscribe(
(res: TaskProcessVariableModel[]) => {
expect(res).toBeDefined();
const varValue = service.getVariableValue(formTest, 'FIELD_FORM_EMPTY', res);
expect(varValue).not.toBeUndefined();
expect(varValue).toBe('PROCESS_RIGHT_FORM_FIELD_VALUE');
visibilityObjTest.leftType = WidgetTypeEnum.field;
visibilityObjTest.leftValue = 'FIELD_FORM_EMPTY';
visibilityObjTest.operator = '==';
visibilityObjTest.rightValue = 'RIGHT_FORM_FIELD_VALUE';
const myForm = new FormModel({
id: '9999',
name: 'FORM_PROCESS_VARIABLE_VISIBILITY',
processDefinitionId: 'PROCESS_TEST:9:9999',
processDefinitionName: 'PROCESS_TEST',
processDefinitionKey: 'PROCESS_TEST',
taskId: '999',
taskName: 'TEST',
fields: [
{
fieldType: 'ContainerRepresentation',
id: '000000000000000000',
name: 'Label',
type: 'container',
value: null,
numberOfColumns: 2,
fields: {
1: [
{
fieldType: 'FormFieldRepresentation',
id: 'FIELD_FORM_EMPTY',
name: 'FIELD_FORM_EMPTY',
type: 'text',
value: 'RIGHT_FORM_FIELD_VALUE',
visibilityCondition: null,
isVisible: true
},
{
fieldType: 'FormFieldRepresentation',
id: 'FIELD_FORM_WITH_CONDITION',
name: 'FIELD_FORM_WITH_CONDITION',
type: 'text',
value: 'field_form_with_condition_value',
visibilityCondition: visibilityObjTest,
isVisible: false
}
]
}
}
]
});
service.refreshVisibility(myForm);
const fieldWithVisibilityAttached = myForm.getFieldById('FIELD_FORM_WITH_CONDITION');
expect(fieldWithVisibilityAttached.isVisible).toBeTruthy();
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: [{ id: 'FIELD_FORM_EMPTY', type: 'string', value: 'PROCESS_RIGHT_FORM_FIELD_VALUE' }]
});
});
it('should use the process value to evaluate the True visibility condition if the form value is empty', (done) => {
service.getTaskProcessVariable('9999').subscribe(
(res: TaskProcessVariableModel[]) => {
expect(res).toBeDefined();
visibilityObjTest.leftType = WidgetTypeEnum.field;
visibilityObjTest.leftValue = 'FIELD_FORM_EMPTY';
visibilityObjTest.operator = '==';
visibilityObjTest.rightType = WidgetTypeEnum.value;
visibilityObjTest.rightValue = 'PROCESS_RIGHT_FORM_FIELD_VALUE';
const myForm = new FormModel({
id: '9999',
name: 'FORM_PROCESS_VARIABLE_VISIBILITY',
processDefinitionId: 'PROCESS_TEST:9:9999',
processDefinitionName: 'PROCESS_TEST',
processDefinitionKey: 'PROCESS_TEST',
taskId: '999',
taskName: 'TEST',
fields: [
{
fieldType: 'ContainerRepresentation',
id: '000000000000000000',
name: 'Label',
type: 'container',
value: null,
numberOfColumns: 2,
fields: {
1: [
{
fieldType: 'FormFieldRepresentation',
id: 'FIELD_FORM_EMPTY',
name: 'FIELD_FORM_EMPTY',
type: 'text',
value: '',
visibilityCondition: null,
isVisible: true
},
{
fieldType: 'FormFieldRepresentation',
id: 'FIELD_FORM_WITH_CONDITION',
name: 'FIELD_FORM_WITH_CONDITION',
type: 'text',
value: 'field_form_with_condition_value',
visibilityCondition: visibilityObjTest,
isVisible: false
}
]
}
}
]
});
service.refreshVisibility(myForm);
const fieldWithVisibilityAttached = myForm.getFieldById('FIELD_FORM_WITH_CONDITION');
expect(fieldWithVisibilityAttached.isVisible).toBeTruthy();
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: [{ id: 'FIELD_FORM_EMPTY', type: 'string', value: 'PROCESS_RIGHT_FORM_FIELD_VALUE' }]
});
});
it('should use the process variables when they are passed to check the visibility', () => {
visibilityObjTest.leftType = WidgetTypeEnum.field;
@@ -837,72 +545,6 @@ describe('WidgetVisibilityCloudService', () => {
expect(fieldWithVisibilityAttached.isVisible).toBeTruthy();
});
it('should use the process value to evaluate the False visibility condition if the form value is empty', (done) => {
service.getTaskProcessVariable('9999').subscribe(
(res: TaskProcessVariableModel[]) => {
expect(res).toBeDefined();
visibilityObjTest.leftType = 'FIELD_FORM_EMPTY';
visibilityObjTest.operator = '==';
visibilityObjTest.rightValue = 'RIGHT_FORM_FIELD_VALUE';
const myForm = new FormModel({
id: '9999',
name: 'FORM_PROCESS_VARIABLE_VISIBILITY',
processDefinitionId: 'PROCESS_TEST:9:9999',
processDefinitionName: 'PROCESS_TEST',
processDefinitionKey: 'PROCESS_TEST',
taskId: '999',
taskName: 'TEST',
fields: [
{
fieldType: 'ContainerRepresentation',
id: '000000000000000000',
name: 'Label',
type: 'container',
value: null,
numberOfColumns: 2,
fields: {
1: [
{
fieldType: 'FormFieldRepresentation',
id: 'FIELD_FORM_EMPTY',
name: 'FIELD_FORM_EMPTY',
type: 'text',
value: '',
visibilityCondition: null,
isVisible: true
},
{
fieldType: 'FormFieldRepresentation',
id: 'FIELD_FORM_WITH_CONDITION',
name: 'FIELD_FORM_WITH_CONDITION',
type: 'text',
value: 'field_form_with_condition_value',
visibilityCondition: visibilityObjTest,
isVisible: true
}
]
}
}
]
});
service.refreshVisibility(myForm);
const fieldWithVisibilityAttached = myForm.getFieldById('FIELD_FORM_WITH_CONDITION');
expect(fieldWithVisibilityAttached.isVisible).toBeFalsy();
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: [{ id: 'FIELD_FORM_EMPTY', type: 'string', value: 'PROCESS_RIGHT_FORM_FIELD_VALUE' }]
});
});
it('should refresh the visibility for single tab', () => {
visibilityObjTest.leftType = WidgetTypeEnum.field;
visibilityObjTest.leftValue = 'FIELD_TEST';

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { fakeAsync, TestBed } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import {
ContainerModel,
FormFieldModel,
@@ -23,12 +23,10 @@ import {
FormModel,
TabModel
} from '../components/widgets/core';
import { TaskProcessVariableModel } from '../models/task-process-variable.model';
import { WidgetVisibilityModel } from '../models/widget-visibility.model';
import { WidgetVisibilityService } from './widget-visibility.service';
import { setupTestBed } from '../../testing/setup-test-bed';
import {
fakeTaskProcessVariableModels,
fakeFormJson, formTest,
formValues, complexVisibilityJsonVisible,
complexVisibilityJsonNotVisible, tabVisibilityJsonMock,
@@ -39,8 +37,6 @@ import {
import { CoreTestingModule } from '../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
declare let jasmine: any;
describe('WidgetVisibilityService', () => {
let service: WidgetVisibilityService;
@@ -56,11 +52,6 @@ describe('WidgetVisibilityService', () => {
beforeEach(() => {
service = TestBed.inject(WidgetVisibilityService);
jasmine.Ajax.install();
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
describe('should be able to evaluate next condition operations', () => {
@@ -161,158 +152,6 @@ describe('WidgetVisibilityService', () => {
});
});
describe('should retrieve the process variables', () => {
let fakeFormWithField: FormModel;
let visibilityObjTest: WidgetVisibilityModel;
const chainedVisibilityObj = new WidgetVisibilityModel({});
beforeEach(() => {
fakeFormWithField = new FormModel(fakeFormJson);
visibilityObjTest = new WidgetVisibilityModel({});
fakeFormWithField = new FormModel(fakeFormJson);
});
it('should return the process variables for task', (done) => {
service.getTaskProcessVariable('9999').subscribe(
(res) => {
expect(res).toBeDefined();
expect(res.length).toEqual(3);
expect(res[0].id).toEqual('TEST_VAR_1');
expect(res[0].type).toEqual('string');
expect(res[0].value).toEqual('test_value_1');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should be able to retrieve the value of a process variable', (done) => {
service.getTaskProcessVariable('9999').subscribe(
(res: TaskProcessVariableModel[]) => {
expect(res).toBeDefined();
const varValue = service.getVariableValue(formTest, 'TEST_VAR_1', res);
expect(varValue).not.toBeUndefined();
expect(varValue).toBe('test_value_1');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should return undefined if the variable does not exist', (done) => {
service.getTaskProcessVariable('9999').subscribe(
(res: TaskProcessVariableModel[]) => {
const varValue = service.getVariableValue(formTest, 'TEST_MYSTERY_VAR', res);
expect(varValue).toBeUndefined();
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should retrieve the value for the right field when it is a process variable', (done) => {
service.getTaskProcessVariable('9999').subscribe(
() => {
visibilityObjTest.rightRestResponseId = 'TEST_VAR_2';
const rightValue = service.getRightValue(formTest, visibilityObjTest);
expect(rightValue).not.toBeNull();
expect(rightValue).toBe('test_value_2');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should retrieve the value for the left field when it is a process variable', (done) => {
service.getTaskProcessVariable('9999').subscribe(
() => {
visibilityObjTest.leftRestResponseId = 'TEST_VAR_2';
const leftValue = service.getLeftValue(formTest, visibilityObjTest);
expect(leftValue).not.toBeNull();
expect(leftValue).toBe('test_value_2');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should evaluate the visibility for the field between form value and process var', (done) => {
service.getTaskProcessVariable('9999').subscribe(
() => {
visibilityObjTest.leftFormFieldId = 'LEFT_FORM_FIELD_ID';
visibilityObjTest.operator = '!=';
visibilityObjTest.rightRestResponseId = 'TEST_VAR_2';
const isVisible = service.isFieldVisible(fakeFormWithField, new WidgetVisibilityModel(visibilityObjTest));
expect(isVisible).toBeTruthy();
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should evaluate visibility with multiple conditions', (done) => {
service.getTaskProcessVariable('9999').subscribe(
() => {
visibilityObjTest.leftFormFieldId = 'LEFT_FORM_FIELD_ID';
visibilityObjTest.operator = '!=';
visibilityObjTest.rightRestResponseId = 'TEST_VAR_2';
visibilityObjTest.nextConditionOperator = 'and';
chainedVisibilityObj.leftRestResponseId = 'TEST_VAR_2';
chainedVisibilityObj.operator = '!empty';
visibilityObjTest.nextCondition = chainedVisibilityObj;
const isVisible = service.isFieldVisible(fakeFormWithField, visibilityObjTest);
expect(isVisible).toBeTruthy();
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeTaskProcessVariableModels
});
});
it('should catch error on 403 response', fakeAsync(() => {
service.getTaskProcessVariable('9999').subscribe(() => {
}, (errorMessage) => {
expect(errorMessage).toEqual('Error while performing a call - Server error');
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 403
});
}));
});
describe('should return the value of the field', () => {
let visibilityObjTest: WidgetVisibilityModel;
let fakeFormWithField: FormModel;
@@ -650,206 +489,6 @@ describe('WidgetVisibilityService', () => {
expect(fakeFormWithField.tabs[0].isVisible).toBeFalsy();
});
it('should use the form value to evaluate the visibility condition if the form value is defined', (done) => {
service.getTaskProcessVariable('9999').subscribe(
(res: TaskProcessVariableModel[]) => {
expect(res).toBeDefined();
const varValue = service.getVariableValue(formTest, 'FIELD_FORM_EMPTY', res);
expect(varValue).not.toBeUndefined();
expect(varValue).toBe('PROCESS_RIGHT_FORM_FIELD_VALUE');
visibilityObjTest.leftFormFieldId = 'FIELD_FORM_EMPTY';
visibilityObjTest.operator = '==';
visibilityObjTest.rightValue = 'RIGHT_FORM_FIELD_VALUE';
const myForm = new FormModel({
id: '9999',
name: 'FORM_PROCESS_VARIABLE_VISIBILITY',
processDefinitionId: 'PROCESS_TEST:9:9999',
processDefinitionName: 'PROCESS_TEST',
processDefinitionKey: 'PROCESS_TEST',
taskId: '999',
taskName: 'TEST',
fields: [
{
fieldType: 'ContainerRepresentation',
id: '000000000000000000',
name: 'Label',
type: 'container',
value: null,
numberOfColumns: 2,
fields: {
1: [
{
fieldType: 'FormFieldRepresentation',
id: 'FIELD_FORM_EMPTY',
name: 'FIELD_FORM_EMPTY',
type: 'text',
value: 'RIGHT_FORM_FIELD_VALUE',
visibilityCondition: null,
isVisible: true
},
{
fieldType: 'FormFieldRepresentation',
id: 'FIELD_FORM_WITH_CONDITION',
name: 'FIELD_FORM_WITH_CONDITION',
type: 'text',
value: 'field_form_with_condition_value',
visibilityCondition: visibilityObjTest,
isVisible: false
}
]
}
}
]
});
service.refreshVisibility(myForm);
const fieldWithVisibilityAttached = myForm.getFieldById('FIELD_FORM_WITH_CONDITION');
expect(fieldWithVisibilityAttached.isVisible).toBeTruthy();
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: [{ id: 'FIELD_FORM_EMPTY', type: 'string', value: 'PROCESS_RIGHT_FORM_FIELD_VALUE' }]
});
});
it('should use the process value to evaluate the True visibility condition if the form value is empty', (done) => {
service.getTaskProcessVariable('9999').subscribe(
(res: TaskProcessVariableModel[]) => {
expect(res).toBeDefined();
visibilityObjTest.leftFormFieldId = 'FIELD_FORM_EMPTY';
visibilityObjTest.operator = '==';
visibilityObjTest.rightValue = 'PROCESS_RIGHT_FORM_FIELD_VALUE';
const myForm = new FormModel({
id: '9999',
name: 'FORM_PROCESS_VARIABLE_VISIBILITY',
processDefinitionId: 'PROCESS_TEST:9:9999',
processDefinitionName: 'PROCESS_TEST',
processDefinitionKey: 'PROCESS_TEST',
taskId: '999',
taskName: 'TEST',
fields: [
{
fieldType: 'ContainerRepresentation',
id: '000000000000000000',
name: 'Label',
type: 'container',
value: null,
numberOfColumns: 2,
fields: {
1: [
{
fieldType: 'FormFieldRepresentation',
id: 'FIELD_FORM_EMPTY',
name: 'FIELD_FORM_EMPTY',
type: 'text',
value: '',
visibilityCondition: null,
isVisible: true
},
{
fieldType: 'FormFieldRepresentation',
id: 'FIELD_FORM_WITH_CONDITION',
name: 'FIELD_FORM_WITH_CONDITION',
type: 'text',
value: 'field_form_with_condition_value',
visibilityCondition: visibilityObjTest,
isVisible: false
}
]
}
}
]
});
service.refreshVisibility(myForm);
const fieldWithVisibilityAttached = myForm.getFieldById('FIELD_FORM_WITH_CONDITION');
expect(fieldWithVisibilityAttached.isVisible).toBeTruthy();
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: [{ id: 'FIELD_FORM_EMPTY', type: 'string', value: 'PROCESS_RIGHT_FORM_FIELD_VALUE' }]
});
});
it('should use the process value to evaluate the False visibility condition if the form value is empty', (done) => {
service.getTaskProcessVariable('9999').subscribe(
(res: TaskProcessVariableModel[]) => {
expect(res).toBeDefined();
visibilityObjTest.leftFormFieldId = 'FIELD_FORM_EMPTY';
visibilityObjTest.operator = '==';
visibilityObjTest.rightValue = 'RIGHT_FORM_FIELD_VALUE';
const myForm = new FormModel({
id: '9999',
name: 'FORM_PROCESS_VARIABLE_VISIBILITY',
processDefinitionId: 'PROCESS_TEST:9:9999',
processDefinitionName: 'PROCESS_TEST',
processDefinitionKey: 'PROCESS_TEST',
taskId: '999',
taskName: 'TEST',
fields: [
{
fieldType: 'ContainerRepresentation',
id: '000000000000000000',
name: 'Label',
type: 'container',
value: null,
numberOfColumns: 2,
fields: {
1: [
{
fieldType: 'FormFieldRepresentation',
id: 'FIELD_FORM_EMPTY',
name: 'FIELD_FORM_EMPTY',
type: 'text',
value: '',
visibilityCondition: null,
isVisible: true
},
{
fieldType: 'FormFieldRepresentation',
id: 'FIELD_FORM_WITH_CONDITION',
name: 'FIELD_FORM_WITH_CONDITION',
type: 'text',
value: 'field_form_with_condition_value',
visibilityCondition: visibilityObjTest,
isVisible: true
}
]
}
}
]
});
service.refreshVisibility(myForm);
const fieldWithVisibilityAttached = myForm.getFieldById('FIELD_FORM_WITH_CONDITION');
expect(fieldWithVisibilityAttached.isVisible).toBeFalsy();
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: [{ id: 'FIELD_FORM_EMPTY', type: 'string', value: 'PROCESS_RIGHT_FORM_FIELD_VALUE' }]
});
});
it('should refresh the visibility for single tab', () => {
visibilityObjTest.leftFormFieldId = 'FIELD_TEST';
visibilityObjTest.operator = '!=';

View File

@@ -15,11 +15,9 @@
* limitations under the License.
*/
import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { LogService } from '../../services/log.service';
import { Injectable } from '@angular/core';
import moment from 'moment';
import { Observable, from, throwError } from 'rxjs';
import {
FormFieldModel,
FormModel,
@@ -29,25 +27,16 @@ import {
} from '../components/widgets/core';
import { TaskProcessVariableModel } from '../models/task-process-variable.model';
import { WidgetVisibilityModel, WidgetTypeEnum } from '../models/widget-visibility.model';
import { map, catchError } from 'rxjs/operators';
import { TaskFormsApi } from '@alfresco/js-api';
@Injectable({
providedIn: 'root'
})
export class WidgetVisibilityService {
_taskFormsApi: TaskFormsApi;
get taskFormsApi(): TaskFormsApi {
this._taskFormsApi = this._taskFormsApi ?? new TaskFormsApi(this.apiService.getInstance());
return this._taskFormsApi;
}
private processVarList: TaskProcessVariableModel[];
private form: FormModel;
constructor(private apiService: AlfrescoApiService,
private logService: LogService) {
constructor(private logService: LogService) {
}
public refreshVisibility(form: FormModel, processVarList?: TaskProcessVariableModel[]) {
@@ -68,15 +57,15 @@ export class WidgetVisibilityService {
}
}
refreshEntityVisibility(element: FormFieldModel | TabModel) {
public refreshEntityVisibility(element: FormFieldModel | TabModel) {
element.isVisible = this.isParentTabVisible(this.form, element) && this.evaluateVisibility(element.form, element.visibilityCondition);
}
refreshOutcomeVisibility(element: FormOutcomeModel) {
private refreshOutcomeVisibility(element: FormOutcomeModel) {
element.isVisible = this.evaluateVisibility(element.form, element.visibilityCondition);
}
evaluateVisibility(form: FormModel, visibilityObj: WidgetVisibilityModel): boolean {
public evaluateVisibility(form: FormModel, visibilityObj: WidgetVisibilityModel): boolean {
const isLeftFieldPresent = visibilityObj && (visibilityObj.leftType || visibilityObj.leftValue);
if (!isLeftFieldPresent || isLeftFieldPresent === 'null') {
return true;
@@ -85,12 +74,12 @@ export class WidgetVisibilityService {
}
}
isFieldVisible(form: FormModel, visibilityObj: WidgetVisibilityModel, accumulator: any[] = [], result: boolean = false): boolean {
public isFieldVisible(form: FormModel, visibilityObj: WidgetVisibilityModel, accumulator: any[] = [], result: boolean = false): boolean {
const leftValue = this.getLeftValue(form, visibilityObj);
const rightValue = this.getRightValue(form, visibilityObj);
const actualResult = this.evaluateCondition(leftValue, rightValue, visibilityObj.operator);
accumulator.push({ value: actualResult, operator: visibilityObj.nextConditionOperator });
accumulator.push({value: actualResult, operator: visibilityObj.nextConditionOperator});
if (this.isValidCondition(visibilityObj.nextCondition)) {
result = this.isFieldVisible(form, visibilityObj.nextCondition, accumulator);
@@ -124,7 +113,7 @@ export class WidgetVisibilityService {
}
}
getLeftValue(form: FormModel, visibilityObj: WidgetVisibilityModel): string {
public getLeftValue(form: FormModel, visibilityObj: WidgetVisibilityModel): string {
let leftValue = '';
if (visibilityObj.leftType && visibilityObj.leftType === WidgetTypeEnum.variable) {
leftValue = this.getVariableValue(form, visibilityObj.leftValue, this.processVarList);
@@ -138,7 +127,7 @@ export class WidgetVisibilityService {
return leftValue;
}
getRightValue(form: FormModel, visibilityObj: WidgetVisibilityModel): string {
public getRightValue(form: FormModel, visibilityObj: WidgetVisibilityModel): string {
let valueFound = '';
if (visibilityObj.rightType === WidgetTypeEnum.variable) {
valueFound = this.getVariableValue(form, visibilityObj.rightValue, this.processVarList);
@@ -154,7 +143,7 @@ export class WidgetVisibilityService {
return valueFound;
}
getFormValue(form: FormModel, fieldId: string): any {
public getFormValue(form: FormModel, fieldId: string): any {
const formField = this.getFormFieldById(form, fieldId);
let value;
@@ -168,18 +157,18 @@ export class WidgetVisibilityService {
return value;
}
isFormFieldValid(formField: FormFieldModel): boolean {
public isFormFieldValid(formField: FormFieldModel): boolean {
return formField && formField.isValid;
}
getFieldValue(valueList: any, fieldId: string): any {
public getFieldValue(valueList: any, fieldId: string): any {
let labelFilterByName;
let valueFound;
if (fieldId && fieldId.indexOf('_LABEL') > 0) {
labelFilterByName = fieldId.substring(0, fieldId.length - 6);
if (valueList[labelFilterByName]) {
if (Array.isArray(valueList[labelFilterByName])) {
valueFound = valueList[labelFilterByName].map(({ name }) => name);
valueFound = valueList[labelFilterByName].map(({name}) => name);
} else {
valueFound = valueList[labelFilterByName].name;
}
@@ -187,7 +176,7 @@ export class WidgetVisibilityService {
} else if (valueList[fieldId] && valueList[fieldId].id) {
valueFound = valueList[fieldId].id;
} else if (valueList[fieldId] && Array.isArray(valueList[fieldId])) {
valueFound = valueList[fieldId].map(({ id }) => id);
valueFound = valueList[fieldId].map(({id}) => id);
} else {
valueFound = valueList[fieldId];
}
@@ -198,11 +187,11 @@ export class WidgetVisibilityService {
return value === undefined || value === null;
}
getFormFieldById(form: FormModel, fieldId: string): FormFieldModel {
public getFormFieldById(form: FormModel, fieldId: string): FormFieldModel {
return form.getFormFields().find((formField: FormFieldModel) => this.isSearchedField(formField, fieldId));
}
searchValueInForm(formField: FormFieldModel, fieldId: string): string {
public searchValueInForm(formField: FormFieldModel, fieldId: string): string {
let fieldValue = '';
if (formField) {
@@ -219,7 +208,7 @@ export class WidgetVisibilityService {
return fieldValue;
}
isParentTabVisible(form: FormModel, currentFormField: FormFieldModel | TabModel): boolean {
private isParentTabVisible(form: FormModel, currentFormField: FormFieldModel | TabModel): boolean {
const containers = this.getFormTabContainers(form);
let isVisible: boolean = true;
containers.map((container: ContainerModel) => {
@@ -281,7 +270,7 @@ export class WidgetVisibilityService {
return (field.id && fieldToFind) ? field.id.toUpperCase() === fieldToFind.toUpperCase() : false;
}
getVariableValue(form: FormModel, name: string, processVarList: TaskProcessVariableModel[]): string {
public getVariableValue(form: FormModel, name: string, processVarList: TaskProcessVariableModel[]): string {
const processVariableValue = this.getProcessVariableValue(name, processVarList);
const variableDefaultValue = form.getDefaultFormVariableValue(name);
@@ -303,7 +292,7 @@ export class WidgetVisibilityService {
return undefined;
}
evaluateCondition(leftValue: any, rightValue: any, operator: string): boolean | undefined {
public evaluateCondition(leftValue: any, rightValue: any, operator: string): boolean | undefined {
switch (operator) {
case '==':
return leftValue + '' === rightValue + '';
@@ -339,28 +328,7 @@ export class WidgetVisibilityService {
this.processVarList = [];
}
getTaskProcessVariable(taskId: string): Observable<TaskProcessVariableModel[]> {
return from(this.taskFormsApi.getTaskFormVariables(taskId))
.pipe(
map((res) => {
const jsonRes = this.toJson(res);
this.processVarList = jsonRes;
return jsonRes;
}),
catchError(() => this.handleError())
);
}
toJson(res: any): any {
return res || {};
}
private isValidCondition(condition: WidgetVisibilityModel): boolean {
return !!(condition && condition.operator);
}
private handleError() {
this.logService.error('Error while performing a call');
return throwError('Error while performing a call - Server error');
}
}

View File

@@ -16,16 +16,17 @@
*/
import { Injectable } from '@angular/core';
import { Observable, from, throwError } from 'rxjs';
import { Observable, from, throwError, of } from 'rxjs';
import { UserProcessModel } from '../models/user-process.model';
import { AlfrescoApiService } from './alfresco-api.service';
import { LogService } from './log.service';
import { catchError, map } from 'rxjs/operators';
import { catchError, combineAll, defaultIfEmpty, map, switchMap } from 'rxjs/operators';
import {
TaskActionsApi,
UsersApi,
ResultListDataRepresentationLightUserRepresentation
ResultListDataRepresentationLightUserRepresentation, ActivitiGroupsApi
} from '@alfresco/js-api';
import { GroupModel } from '../form';
@Injectable({
providedIn: 'root'
@@ -44,10 +45,35 @@ export class PeopleProcessService {
return this._userApi;
}
_groupsApi: ActivitiGroupsApi;
get groupsApi(): ActivitiGroupsApi {
this._groupsApi = this._groupsApi ?? new ActivitiGroupsApi(this.apiService.getInstance());
return this._groupsApi;
}
constructor(private apiService: AlfrescoApiService,
private logService: LogService) {
}
/**
* Gets a list of groups in a workflow.
*
* @param filter Filter to select specific groups
* @param groupId Group ID for the search
* @returns Array of groups
*/
getWorkflowGroups(filter: string, groupId?: string): Observable<GroupModel[]> {
const option: any = { filter };
if (groupId) {
option.groupId = groupId;
}
return from(this.groupsApi.getGroups(option))
.pipe(
map((response: any) => response.data || []),
catchError((err) => this.handleError(err))
);
}
/**
* Gets information about users across all tasks.
*
@@ -55,15 +81,21 @@ export class PeopleProcessService {
* @param searchWord Filter text to search for
* @returns Array of user information objects
*/
getWorkflowUsers(taskId?: string, searchWord?: string): Observable<UserProcessModel[]> {
const option = { excludeTaskId: taskId, filter: searchWord };
getWorkflowUsers(taskId?: string, searchWord?: string, groupId?: string): Observable<UserProcessModel[]> {
const option = { excludeTaskId: taskId, filter: searchWord, groupId };
return from(this.getWorkflowUserApi(option))
.pipe(
map((response: any) => response.data || []),
switchMap(response => response.data as UserProcessModel[] || []),
map((user) => {
user.userImage = this.getUserProfileImageApi(user.id.toString());
return of(user);
}),
combineAll(),
defaultIfEmpty([]),
catchError((err) => this.handleError(err))
);
}
/**
* Gets the profile picture URL for the specified user.
*