[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

@@ -80,4 +80,5 @@ module.exports = function (config) {
browsers: ['Chrome'],
singleRun: true
});
process.env.TZ = 'UTC';
};

View File

@@ -16,7 +16,7 @@
*/
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { ProcessContentService } from '@alfresco/adf-core';
import { ProcessContentService } from '../form/services/process-content.service';
@Component({
selector: 'adf-create-process-attachment',

View File

@@ -19,10 +19,11 @@ import { SimpleChange } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { ProcessContentService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { AttachmentComponent } from './create-task-attachment.component';
import { ProcessTestingModule } from '../testing/process.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { ProcessContentService } from '../form/services/process-content.service';
describe('AttachmentComponent', () => {

View File

@@ -16,7 +16,7 @@
*/
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { ProcessContentService } from '@alfresco/adf-core';
import { ProcessContentService } from '../form/services/process-content.service';
@Component({
selector: 'adf-create-task-attachment',

View File

@@ -18,12 +18,13 @@
import { SimpleChange, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ProcessContentService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { of, throwError } from 'rxjs';
import { ProcessAttachmentListComponent } from './process-attachment-list.component';
import { ProcessTestingModule } from '../testing/process.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { mockEmittedProcessAttachments, mockProcessAttachments } from '../mock/process/process-attachments.mock';
import { ProcessContentService } from '../form/services/process-content.service';
describe('ProcessAttachmentListComponent', () => {

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { ContentService, EmptyListComponent, ThumbnailService, ProcessContentService } from '@alfresco/adf-core';
import { ContentService, EmptyListComponent, ThumbnailService } from '@alfresco/adf-core';
import {
AfterContentInit,
ContentChild,
@@ -28,6 +28,7 @@ import {
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import { ProcessContentService } from '../form/services/process-content.service';
@Component({
selector: 'adf-process-attachment-list',

View File

@@ -20,10 +20,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { of, throwError } from 'rxjs';
import { TaskAttachmentListComponent } from './task-attachment-list.component';
import { ProcessContentService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { ProcessTestingModule } from '../testing/process.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { mockEmittedTaskAttachments, mockTaskAttachments } from '../mock/task/task-attachments.mock';
import { ProcessContentService } from '../form/services/process-content.service';
describe('TaskAttachmentList', () => {

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { ContentService, ThumbnailService, EmptyListComponent, ProcessContentService } from '@alfresco/adf-core';
import { ContentService, ThumbnailService, EmptyListComponent } from '@alfresco/adf-core';
import {
AfterContentInit,
ContentChild,
@@ -28,6 +28,7 @@ import {
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import { ProcessContentService } from '../form/services/process-content.service';
@Component({
selector: 'adf-task-attachment-list',

View File

@@ -0,0 +1,33 @@
/*!
* @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, FormFieldEvent, FormModel } from '@alfresco/adf-core';
import { DynamicRowValidationSummary } from '../widgets/dynamic-table/editors/models/dynamic-row-validation-summary.model';
import { DynamicTableRow } from '../widgets/dynamic-table/editors/models/dynamic-table-row.model';
export class ValidateDynamicTableRowEvent extends FormFieldEvent {
isValid = true;
constructor(form: FormModel,
field: FormFieldModel,
public row: DynamicTableRow,
public summary: DynamicRowValidationSummary) {
super(form, field);
}
}

View File

@@ -0,0 +1,8 @@
<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

@@ -0,0 +1,57 @@
/*!
* @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 { setupTestBed, CoreTestingModule } from '@alfresco/adf-core';
import { FormListComponent } from './form-list.component';
import { ModelService } from '../services/model.service';
describe('TaskAttachmentList', () => {
let component: FormListComponent;
let fixture: ComponentFixture<FormListComponent>;
let modelService: ModelService;
let element: HTMLElement;
setupTestBed({
imports: [
CoreTestingModule
]
});
beforeEach(() => {
fixture = TestBed.createComponent(FormListComponent);
component = fixture.componentInstance;
element = fixture.debugElement.nativeElement;
modelService = TestBed.inject(ModelService);
});
it('should show the forms as a list', async () => {
spyOn(modelService, '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

@@ -0,0 +1,49 @@
/*!
* @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 { ModelService } from '../services/model.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 modelService: ModelService) {
}
ngOnChanges() {
this.getForms();
}
isEmpty(): boolean {
return this.forms && this.forms.length === 0;
}
getForms() {
this.modelService.getForms().subscribe((forms) => {
this.forms.push(...forms);
});
}
}

View File

@@ -25,18 +25,23 @@ import {
DebugElement
} from '@angular/core';
import { By } from '@angular/platform-browser';
import { TaskRepresentation } from '@alfresco/js-api';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { Observable, of, throwError } from 'rxjs';
import {
FormFieldModel, FormFieldTypes, FormModel, FormOutcomeEvent, FormOutcomeModel,
FormService, WidgetVisibilityService, NodeService, ContainerModel, fakeForm,
FormService, WidgetVisibilityService, ContainerModel, fakeForm,
setupTestBed,
NodeMetadata
NodeMetadata, NodesApiService
} from '@alfresco/adf-core';
import { FormComponent } from './form.component';
import { ProcessFormRenderingService } from './process-form-rendering.service';
import { ProcessTestingModule } from '../testing/process.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { TaskFormService } from './services/task-form.service';
import { TaskService } from './services/task.service';
import { EditorService } from './services/editor.service';
import { ModelService } from './services/model.service';
describe('FormComponent', () => {
@@ -45,7 +50,11 @@ describe('FormComponent', () => {
let formService: FormService;
let visibilityService: WidgetVisibilityService;
let nodeService: NodeService;
let taskFormService: TaskFormService;
let taskService: TaskService;
let editorService: EditorService;
let modelService: ModelService;
let nodeService: NodesApiService;
let formRenderingService: ProcessFormRenderingService;
@Component({
@@ -87,8 +96,12 @@ describe('FormComponent', () => {
visibilityService = TestBed.inject(WidgetVisibilityService);
spyOn(visibilityService, 'refreshVisibility').and.stub();
nodeService = TestBed.inject(NodesApiService);
formService = TestBed.inject(FormService);
nodeService = TestBed.inject(NodeService);
taskService = TestBed.inject(TaskService);
taskFormService = TestBed.inject(TaskFormService);
editorService = TestBed.inject(EditorService);
modelService = TestBed.inject(ModelService);
formRenderingService = TestBed.inject(ProcessFormRenderingService);
fixture = TestBed.createComponent(FormComponent);
@@ -159,14 +172,14 @@ describe('FormComponent', () => {
it('should enable custom outcome buttons', () => {
const formModel = new FormModel();
formComponent.form = formModel;
const outcome = new FormOutcomeModel(formModel, { id: 'action1', name: 'Action 1' });
const outcome = new FormOutcomeModel(formModel, {id: 'action1', name: 'Action 1'});
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
});
it('should allow controlling [complete] button visibility', () => {
const formModel = new FormModel();
formComponent.form = formModel;
const outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.SAVE_ACTION });
const outcome = new FormOutcomeModel(formModel, {id: '$save', name: FormOutcomeModel.SAVE_ACTION});
formComponent.showSaveButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
@@ -179,7 +192,7 @@ describe('FormComponent', () => {
const formModel = new FormModel();
formModel.readOnly = true;
formComponent.form = formModel;
const outcome = new FormOutcomeModel(formModel, { id: '$complete', name: FormOutcomeModel.COMPLETE_ACTION });
const outcome = new FormOutcomeModel(formModel, {id: '$complete', name: FormOutcomeModel.COMPLETE_ACTION});
formComponent.showCompleteButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
@@ -189,23 +202,23 @@ describe('FormComponent', () => {
const formModel = new FormModel();
formModel.readOnly = true;
formComponent.form = formModel;
const outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.SAVE_ACTION });
const outcome = new FormOutcomeModel(formModel, {id: '$save', name: FormOutcomeModel.SAVE_ACTION});
formComponent.showSaveButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeFalsy();
});
it('should show [custom-outcome] button with readOnly form and selected custom-outcome', () => {
const formModel = new FormModel({ selectedOutcome: 'custom-outcome' });
const formModel = new FormModel({selectedOutcome: 'custom-outcome'});
formModel.readOnly = true;
formComponent.form = formModel;
let outcome = new FormOutcomeModel(formModel, { id: '$customoutome', name: 'custom-outcome' });
let outcome = new FormOutcomeModel(formModel, {id: '$customoutome', name: 'custom-outcome'});
formComponent.showCompleteButton = true;
formComponent.showSaveButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
outcome = new FormOutcomeModel(formModel, { id: '$customoutome2', name: 'custom-outcome2' });
outcome = new FormOutcomeModel(formModel, {id: '$customoutome2', name: 'custom-outcome2'});
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeFalsy();
});
@@ -213,7 +226,7 @@ describe('FormComponent', () => {
const formModel = new FormModel();
formModel.readOnly = false;
formComponent.form = formModel;
const outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.COMPLETE_ACTION });
const outcome = new FormOutcomeModel(formModel, {id: '$save', name: FormOutcomeModel.COMPLETE_ACTION});
formComponent.showCompleteButton = true;
expect(formComponent.isOutcomeButtonVisible(outcome, formComponent.form.readOnly)).toBeTruthy();
@@ -240,44 +253,6 @@ describe('FormComponent', () => {
expect(formComponent.getFormByTaskId).toHaveBeenCalledWith(taskId);
});
it('should get process variable if is a process task', () => {
spyOn(formService, 'getTaskForm').and.callFake((currentTaskId) => new Observable((observer) => {
observer.next({ taskId: currentTaskId });
observer.complete();
}));
spyOn(visibilityService, 'getTaskProcessVariable').and.returnValue(of(null));
spyOn(formService, 'getTask').and.callFake((currentTaskId) => new Observable((observer) => {
observer.next({ taskId: currentTaskId, processDefinitionId: '10201' });
observer.complete();
}));
const taskId = '123';
formComponent.taskId = taskId;
formComponent.loadForm();
expect(visibilityService.getTaskProcessVariable).toHaveBeenCalledWith(taskId);
});
it('should not get process variable if is not a process task', () => {
spyOn(formService, 'getTaskForm').and.callFake((currentTaskId) => new Observable((observer) => {
observer.next({ taskId: currentTaskId });
observer.complete();
}));
spyOn(visibilityService, 'getTaskProcessVariable').and.returnValue(of(null));
spyOn(formService, 'getTask').and.callFake((currentTaskId) => new Observable((observer) => {
observer.next({ taskId: currentTaskId, processDefinitionId: 'null' });
observer.complete();
}));
const taskId = '123';
formComponent.taskId = taskId;
formComponent.loadForm();
expect(visibilityService.getTaskProcessVariable).toHaveBeenCalledWith(taskId);
});
it('should get form definition by form id on load', () => {
spyOn(formComponent, 'getFormDefinitionByFormId').and.stub();
const formId = 123;
@@ -289,13 +264,13 @@ describe('FormComponent', () => {
});
it('should refresh visibility when the form is loaded', () => {
spyOn(formService, 'getFormDefinitionById').and.returnValue(of(JSON.parse(JSON.stringify(fakeForm))));
spyOn(editorService, 'getFormDefinitionById').and.returnValue(of(JSON.parse(JSON.stringify(fakeForm))));
const formId = 123;
formComponent.formId = formId;
formComponent.loadForm();
expect(formService.getFormDefinitionById).toHaveBeenCalledWith(formId);
expect(editorService.getFormDefinitionById).toHaveBeenCalledWith(formId);
expect(visibilityService.refreshVisibility).toHaveBeenCalled();
});
@@ -314,7 +289,7 @@ describe('FormComponent', () => {
const taskId = '<task id>';
const change = new SimpleChange(null, taskId, true);
formComponent.ngOnChanges({ taskId: change });
formComponent.ngOnChanges({taskId: change});
expect(formComponent.getFormByTaskId).toHaveBeenCalledWith(taskId);
});
@@ -324,7 +299,7 @@ describe('FormComponent', () => {
const formId = '123';
const change = new SimpleChange(null, formId, true);
formComponent.ngOnChanges({ formId: change });
formComponent.ngOnChanges({formId: change});
expect(formComponent.getFormDefinitionByFormId).toHaveBeenCalledWith(formId);
});
@@ -334,7 +309,7 @@ describe('FormComponent', () => {
const formName = '<form>';
const change = new SimpleChange(null, formName, true);
formComponent.ngOnChanges({ formName: change });
formComponent.ngOnChanges({formName: change});
expect(formComponent.getFormDefinitionByFormName).toHaveBeenCalledWith(formName);
});
@@ -359,7 +334,7 @@ describe('FormComponent', () => {
spyOn(formComponent, 'getFormDefinitionByFormId').and.stub();
spyOn(formComponent, 'getFormDefinitionByFormName').and.stub();
formComponent.ngOnChanges({ tag: new SimpleChange(null, 'hello world', true) });
formComponent.ngOnChanges({tag: new SimpleChange(null, 'hello world', true)});
expect(formComponent.getFormByTaskId).not.toHaveBeenCalled();
expect(formComponent.getFormDefinitionByFormId).not.toHaveBeenCalled();
@@ -369,7 +344,7 @@ describe('FormComponent', () => {
it('should complete form on custom outcome click', () => {
const formModel = new FormModel();
const outcomeName = 'Custom Action';
const outcome = new FormOutcomeModel(formModel, { id: 'custom1', name: outcomeName });
const outcome = new FormOutcomeModel(formModel, {id: 'custom1', name: outcomeName});
let saved = false;
formComponent.form = formModel;
@@ -434,7 +409,7 @@ describe('FormComponent', () => {
it('should do nothing when clicking outcome for readonly form', () => {
const formModel = new FormModel();
const outcomeName = 'Custom Action';
const outcome = new FormOutcomeModel(formModel, { id: 'custom1', name: outcomeName });
const outcome = new FormOutcomeModel(formModel, {id: 'custom1', name: outcomeName});
formComponent.form = formModel;
spyOn(formComponent, 'completeTaskForm').and.stub();
@@ -453,7 +428,7 @@ describe('FormComponent', () => {
it('should require loaded form when clicking outcome', () => {
const formModel = new FormModel();
const outcomeName = 'Custom Action';
const outcome = new FormOutcomeModel(formModel, { id: 'custom1', name: outcomeName });
const outcome = new FormOutcomeModel(formModel, {id: 'custom1', name: outcomeName});
formComponent.readOnly = false;
formComponent.form = null;
@@ -462,7 +437,7 @@ describe('FormComponent', () => {
it('should not execute unknown system outcome', () => {
const formModel = new FormModel();
const outcome = new FormOutcomeModel(formModel, { id: 'unknown', name: 'Unknown', isSystem: true });
const outcome = new FormOutcomeModel(formModel, {id: 'unknown', name: 'Unknown', isSystem: true});
formComponent.form = formModel;
expect(formComponent.onOutcomeClicked(outcome)).toBeFalsy();
@@ -470,26 +445,26 @@ describe('FormComponent', () => {
it('should require custom action name to complete form', () => {
const formModel = new FormModel();
let outcome = new FormOutcomeModel(formModel, { id: 'custom' });
let outcome = new FormOutcomeModel(formModel, {id: 'custom'});
formComponent.form = formModel;
expect(formComponent.onOutcomeClicked(outcome)).toBeFalsy();
outcome = new FormOutcomeModel(formModel, { id: 'custom', name: 'Custom' });
outcome = new FormOutcomeModel(formModel, {id: 'custom', name: 'Custom'});
spyOn(formComponent, 'completeTaskForm').and.stub();
expect(formComponent.onOutcomeClicked(outcome)).toBeTruthy();
});
it('should fetch and parse form by task id', (done) => {
spyOn(formService, 'getTask').and.returnValue(of({}));
spyOn(formService, 'getTaskForm').and.callFake((currentTaskId) => new Observable((observer) => {
observer.next({ taskId: currentTaskId });
spyOn(taskService, 'getTask').and.returnValue(of(<TaskRepresentation>{}));
spyOn(taskFormService, 'getTaskForm').and.callFake((currentTaskId) => new Observable((observer) => {
observer.next({taskId: currentTaskId});
observer.complete();
}));
const taskId = '456';
formComponent.formLoaded.subscribe(() => {
expect(formService.getTaskForm).toHaveBeenCalledWith(taskId);
expect(taskFormService.getTaskForm).toHaveBeenCalledWith(taskId);
expect(formComponent.form).toBeDefined();
expect(formComponent.form.taskId).toBe(taskId);
done();
@@ -502,9 +477,9 @@ describe('FormComponent', () => {
it('should handle error when getting form by task id', (done) => {
const error = 'Some error';
spyOn(formService, 'getTask').and.returnValue(of({}));
spyOn(taskService, 'getTask').and.returnValue(of(<TaskRepresentation>{}));
spyOn(formComponent, 'handleError').and.stub();
spyOn(formService, 'getTaskForm').and.callFake(() => throwError(error));
spyOn(taskFormService, 'getTaskForm').and.callFake(() => throwError(error));
formComponent.getFormByTaskId('123').then((_) => {
expect(formComponent.handleError).toHaveBeenCalledWith(error);
@@ -513,9 +488,9 @@ describe('FormComponent', () => {
});
it('should apply readonly state when getting form by task id', (done) => {
spyOn(formService, 'getTask').and.returnValue(of({}));
spyOn(formService, 'getTaskForm').and.callFake((taskId) => new Observable((observer) => {
observer.next({ taskId });
spyOn(taskService, 'getTask').and.returnValue(of(<TaskRepresentation>{}));
spyOn(taskFormService, 'getTaskForm').and.callFake((taskId) => new Observable((observer) => {
observer.next({taskId});
observer.complete();
}));
@@ -528,8 +503,8 @@ describe('FormComponent', () => {
});
it('should fetch and parse form definition by id', () => {
spyOn(formService, 'getFormDefinitionById').and.callFake((currentFormId) => new Observable((observer) => {
observer.next({ id: currentFormId });
spyOn(editorService, 'getFormDefinitionById').and.callFake((currentFormId) => new Observable((observer) => {
observer.next({id: currentFormId});
observer.complete();
}));
@@ -549,20 +524,20 @@ describe('FormComponent', () => {
const error = 'Some error';
spyOn(formComponent, 'handleError').and.stub();
spyOn(formService, 'getFormDefinitionById').and.callFake(() => throwError(error));
spyOn(editorService, 'getFormDefinitionById').and.callFake(() => throwError(error));
formComponent.getFormDefinitionByFormId(123);
expect(formComponent.handleError).toHaveBeenCalledWith(error);
});
it('should fetch and parse form definition by form name', () => {
spyOn(formService, 'getFormDefinitionByName').and.callFake((currentFormName) => new Observable((observer) => {
spyOn(modelService, 'getFormDefinitionByName').and.callFake((currentFormName) => new Observable((observer) => {
observer.next(currentFormName);
observer.complete();
}));
spyOn(formService, 'getFormDefinitionById').and.callFake((currentFormName) => new Observable((observer) => {
observer.next({ name: currentFormName });
spyOn(editorService, 'getFormDefinitionById').and.callFake((currentFormName) => new Observable((observer) => {
observer.next({name: currentFormName});
observer.complete();
}));
@@ -574,13 +549,13 @@ describe('FormComponent', () => {
formComponent.getFormDefinitionByFormName(formName);
expect(loaded).toBeTruthy();
expect(formService.getFormDefinitionByName).toHaveBeenCalledWith(formName);
expect(modelService.getFormDefinitionByName).toHaveBeenCalledWith(formName);
expect(formComponent.form).toBeDefined();
expect(formComponent.form.name).toBe(formName);
});
it('should save task form and raise corresponding event', () => {
spyOn(formService, 'saveTaskForm').and.callFake(() => new Observable((observer) => {
spyOn(taskFormService, 'saveTaskForm').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
@@ -595,31 +570,31 @@ describe('FormComponent', () => {
const formModel = new FormModel({
taskId: '123',
fields: [
{ id: 'field1' },
{ id: 'field2' }
{id: 'field1'},
{id: 'field2'}
]
});
formComponent.form = formModel;
formComponent.saveTaskForm();
expect(formService.saveTaskForm).toHaveBeenCalledWith(formModel.taskId, formModel.values);
expect(taskFormService.saveTaskForm).toHaveBeenCalledWith(formModel.taskId, formModel.values);
expect(saved).toBeTruthy();
expect(savedForm).toEqual(formModel);
});
it('should handle error during form save', () => {
const error = 'Error';
spyOn(formService, 'saveTaskForm').and.callFake(() => throwError(error));
spyOn(taskFormService, 'saveTaskForm').and.callFake(() => throwError(error));
spyOn(formComponent, 'handleError').and.stub();
formComponent.form = new FormModel({ taskId: '123' });
formComponent.form = new FormModel({taskId: '123'});
formComponent.saveTaskForm();
expect(formComponent.handleError).toHaveBeenCalledWith(error);
});
it('should require form with task id to save', () => {
spyOn(formService, 'saveTaskForm').and.stub();
spyOn(taskFormService, 'saveTaskForm').and.stub();
formComponent.form = null;
formComponent.saveTaskForm();
@@ -627,11 +602,11 @@ describe('FormComponent', () => {
formComponent.form = new FormModel();
formComponent.saveTaskForm();
expect(formService.saveTaskForm).not.toHaveBeenCalled();
expect(taskFormService.saveTaskForm).not.toHaveBeenCalled();
});
it('should require form with task id to complete', () => {
spyOn(formService, 'completeTaskForm').and.stub();
spyOn(taskFormService, 'completeTaskForm').and.stub();
formComponent.form = null;
formComponent.completeTaskForm('save');
@@ -639,11 +614,11 @@ describe('FormComponent', () => {
formComponent.form = new FormModel();
formComponent.completeTaskForm('complete');
expect(formService.completeTaskForm).not.toHaveBeenCalled();
expect(taskFormService.completeTaskForm).not.toHaveBeenCalled();
});
it('should complete form and raise corresponding event', () => {
spyOn(formService, 'completeTaskForm').and.callFake(() => new Observable((observer) => {
spyOn(taskFormService, 'completeTaskForm').and.callFake(() => new Observable((observer) => {
observer.next();
observer.complete();
}));
@@ -655,15 +630,15 @@ describe('FormComponent', () => {
const formModel = new FormModel({
taskId: '123',
fields: [
{ id: 'field1' },
{ id: 'field2' }
{id: 'field1'},
{id: 'field2'}
]
});
formComponent.form = formModel;
formComponent.completeTaskForm(outcome);
expect(formService.completeTaskForm).toHaveBeenCalledWith(formModel.taskId, formModel.values, outcome);
expect(taskFormService.completeTaskForm).toHaveBeenCalledWith(formModel.taskId, formModel.values, outcome);
expect(completed).toBeTruthy();
});
@@ -675,7 +650,7 @@ describe('FormComponent', () => {
const form = formComponent.parseForm({
id: 1,
fields: [
{ id: 'field1', type: FormFieldTypes.CONTAINER }
{id: 'field1', type: FormFieldTypes.CONTAINER}
]
});
@@ -688,7 +663,7 @@ describe('FormComponent', () => {
it('should provide outcomes for form definition', () => {
spyOn(formComponent, 'getFormDefinitionOutcomes').and.callThrough();
const form = formComponent.parseForm({ id: 1 });
const form = formComponent.parseForm({id: 1});
expect(formComponent.getFormDefinitionOutcomes).toHaveBeenCalledWith(form);
});
@@ -753,7 +728,7 @@ describe('FormComponent', () => {
const nodeId = '<id>';
const change = new SimpleChange(null, nodeId, false);
formComponent.ngOnChanges({ nodeId: change });
formComponent.ngOnChanges({nodeId: change});
expect(nodeService.getNodeMetadata).toHaveBeenCalledWith(nodeId);
expect(formComponent.loadFormFromActiviti).toHaveBeenCalled();
@@ -973,10 +948,10 @@ describe('FormComponent', () => {
id: 'option_2',
name: 'test2'
};
formValues.radio = { id: 'option_2', name: 'Option 2' };
formValues.radio = {id: 'option_2', name: 'Option 2'};
const change = new SimpleChange(null, formValues, false);
formComponent.data = formValues;
formComponent.ngOnChanges({ data: change });
formComponent.ngOnChanges({data: change});
formFields = formComponent.form.getFormFields();
dropdownField = formFields.find((field) => field.id === 'dropdownId');
@@ -996,7 +971,7 @@ describe('FormComponent', () => {
formValues.radio = 'option_3';
const change = new SimpleChange(null, formValues, false);
formComponent.data = formValues;
formComponent.ngOnChanges({ data: change });
formComponent.ngOnChanges({data: change});
formFields = formComponent.form.getFormFields();
radioFieldById = formFields.find((field) => field.id === 'radio');
@@ -1021,7 +996,7 @@ describe('FormComponent', () => {
class FormWithCustomOutComesComponent {
@ViewChild('adfForm', { static: true })
@ViewChild('adfForm', {static: true})
adfForm: FormComponent;
onCustomButtonOneClick() {
@@ -1051,10 +1026,10 @@ describe('FormWithCustomOutComesComponent', () => {
debugElement = fixture.debugElement;
const formRepresentation = {
fields: [
{ id: 'container1' }
{id: 'container1'}
],
outcomes: [
{ id: 'outcome-1', name: 'outcome 1' }
{id: 'outcome-1', name: 'outcome 1'}
]
};

View File

@@ -15,13 +15,41 @@
* limitations under the License.
*/
import { Component, EventEmitter, Input, Output, ViewEncapsulation, SimpleChanges, OnInit, OnDestroy, OnChanges } from '@angular/core';
import { EcmModelService, NodeService, WidgetVisibilityService,
FormService, FormBaseComponent, FormOutcomeModel,
FormEvent, FormErrorEvent, FormFieldModel,
FormModel, FormOutcomeEvent, FormValues, ContentLinkModel } from '@alfresco/adf-core';
import { Observable, of, Subject } from 'rxjs';
import {
Component,
EventEmitter,
Input,
Output,
ViewEncapsulation,
SimpleChanges,
OnInit,
OnDestroy,
OnChanges
} from '@angular/core';
import {
WidgetVisibilityService,
FormService,
FormBaseComponent,
FormOutcomeModel,
FormEvent,
FormErrorEvent,
FormFieldModel,
FormModel,
FormOutcomeEvent,
FormValues,
ContentLinkModel,
NodesApiService,
FormDefinitionModel,
TaskProcessVariableModel
} from '@alfresco/adf-core';
import { from, Observable, of, Subject } from 'rxjs';
import { switchMap, takeUntil } from 'rxjs/operators';
import { EcmModelService } from './services/ecm-model.service';
import { ModelService } from './services/model.service';
import { EditorService } from './services/editor.service';
import { TaskService } from './services/task.service';
import { TaskFormService } from './services/task-form.service';
import { TaskRepresentation } from '@alfresco/js-api';
@Component({
selector: 'adf-form',
@@ -59,7 +87,7 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
data: FormValues;
/** The form will set a prefixed space for invisible fields. */
@Input()
@Input()
enableFixedSpacedForm: boolean = true;
/** Emitted when the form is submitted with the `Save` or custom outcomes. */
@@ -87,9 +115,13 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
protected onDestroy$ = new Subject<boolean>();
constructor(protected formService: FormService,
protected taskFormService: TaskFormService,
protected taskService: TaskService,
protected editorService: EditorService,
protected modelService: ModelService,
protected visibilityService: WidgetVisibilityService,
protected ecmModelService: EcmModelService,
protected nodeService: NodeService) {
protected nodeService: NodesApiService) {
super();
}
@@ -168,13 +200,13 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
}
}
findProcessVariablesByTaskId(taskId: string): Observable<any> {
return this.formService.getTask(taskId).pipe(
switchMap((task: any) => {
findProcessVariablesByTaskId(taskId: string): Observable<TaskProcessVariableModel[]> {
return this.taskService.getTask(taskId).pipe(
switchMap((task: TaskRepresentation) => {
if (this.isAProcessTask(task)) {
return this.visibilityService.getTaskProcessVariable(taskId);
return this.taskFormService.getTaskProcessVariable(taskId);
} else {
return of({});
return of([]);
}
})
);
@@ -186,13 +218,13 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
getFormByTaskId(taskId: string): Promise<FormModel> {
return new Promise<FormModel>(resolve => {
this.findProcessVariablesByTaskId(taskId).subscribe(() => {
this.formService
this.findProcessVariablesByTaskId(taskId).subscribe((taskProcessVariables) => {
this.taskFormService
.getTaskForm(taskId)
.subscribe(
(form) => {
const parsedForm = this.parseForm(form);
this.visibilityService.refreshVisibility(parsedForm);
this.visibilityService.refreshVisibility(parsedForm, taskProcessVariables);
parsedForm.validateForm();
this.form = parsedForm;
this.onFormLoaded(this.form);
@@ -208,7 +240,7 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
}
getFormDefinitionByFormId(formId: number) {
this.formService
this.editorService
.getFormDefinitionById(formId)
.subscribe(
(form) => {
@@ -225,11 +257,11 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
}
getFormDefinitionByFormName(formName: string) {
this.formService
this.modelService
.getFormDefinitionByName(formName)
.subscribe(
(id) => {
this.formService.getFormDefinitionById(id).subscribe(
this.editorService.getFormDefinitionById(id).subscribe(
(form) => {
this.form = this.parseForm(form);
this.visibilityService.refreshVisibility(this.form);
@@ -249,7 +281,7 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
saveTaskForm() {
if (this.form && this.form.taskId) {
this.formService
this.taskFormService
.saveTaskForm(this.form.taskId, this.form.values)
.subscribe(
() => {
@@ -263,7 +295,7 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
completeTaskForm(outcome?: string) {
if (this.form && this.form.taskId) {
this.formService
this.taskFormService
.completeTaskForm(this.form.taskId, this.form.values, outcome)
.subscribe(
() => {
@@ -300,7 +332,7 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
*/
getFormDefinitionOutcomes(form: FormModel): FormOutcomeModel[] {
return [
new FormOutcomeModel(form, { id: '$save', name: FormOutcomeModel.SAVE_ACTION, isSystem: true })
new FormOutcomeModel(form, {id: '$save', name: FormOutcomeModel.SAVE_ACTION, isSystem: true})
];
}
@@ -311,10 +343,10 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
}
loadFormFromActiviti(nodeType: string): any {
this.formService.searchFrom(nodeType).subscribe(
this.modelService.searchFrom(nodeType).subscribe(
(form) => {
if (!form) {
this.formService.createFormFromANode(nodeType).subscribe((formMetadata) => {
this.createFormFromANode(nodeType).subscribe((formMetadata) => {
this.loadFormFromFormId(formMetadata.id);
});
} else {
@@ -327,6 +359,33 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
);
}
/**
* 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.modelService.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.editorService.saveForm(form.id, formDefinitionModel)
).subscribe((formData) => {
observer.next(formData);
observer.complete();
}, (err) => this.handleError(err));
},
(err) => this.handleError(err));
},
(err) => this.handleError(err));
});
}
protected storeFormAsMetadata() {
if (this.saveMetadata) {
this.ecmModelService.createEcmTypeForActivitiForm(this.formName, this.form).subscribe((type) => {

View File

@@ -21,17 +21,23 @@ import { of } from 'rxjs';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { formDefinitionDropdownField, formDefinitionTwoTextFields,
formDefinitionRequiredField, FormService, setupTestBed,
import {
formDefinitionDropdownField, formDefinitionTwoTextFields,
formDefinitionRequiredField, setupTestBed,
formDefVisibilitiFieldDependsOnNextOne, formDefVisibilitiFieldDependsOnPreviousOne,
formReadonlyTwoTextFields } from '@alfresco/adf-core';
formReadonlyTwoTextFields
} from '@alfresco/adf-core';
import { FormComponent } from './form.component';
import { ProcessTestingModule } from '../testing/process.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { TaskService } from './services/task.service';
import { TaskFormService } from './services/task-form.service';
import { TaskRepresentation } from '@alfresco/js-api';
describe('FormComponent UI and visibility', () => {
let component: FormComponent;
let service: FormService;
let taskService: TaskService;
let taskFormService: TaskFormService;
let fixture: ComponentFixture<FormComponent>;
const openSelect = () => {
@@ -50,7 +56,8 @@ describe('FormComponent UI and visibility', () => {
beforeEach(() => {
fixture = TestBed.createComponent(FormComponent);
component = fixture.componentInstance;
service = TestBed.inject(FormService);
taskService = TestBed.inject(TaskService);
taskFormService = TestBed.inject(TaskFormService);
});
afterEach(() => {
@@ -60,11 +67,11 @@ describe('FormComponent UI and visibility', () => {
describe('Validation icon', () => {
it('should display valid icon for valid form', () => {
spyOn(service, 'getTask').and.returnValue(of({}));
spyOn(service, 'getTaskForm').and.returnValue(of(formDefinitionTwoTextFields));
spyOn(taskService, 'getTask').and.returnValue(of(<TaskRepresentation>{}));
spyOn(taskFormService, 'getTaskForm').and.returnValue(of(formDefinitionTwoTextFields));
const change = new SimpleChange(null, 1, true);
component.ngOnChanges({ taskId: change });
component.ngOnChanges({taskId: change});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('#adf-valid-form-icon'))).toBeDefined();
expect(fixture.debugElement.query(By.css('#adf-valid-form-icon'))).not.toBeNull();
@@ -72,11 +79,11 @@ describe('FormComponent UI and visibility', () => {
});
it('should display invalid icon for valid form', () => {
spyOn(service, 'getTask').and.returnValue(of({}));
spyOn(service, 'getTaskForm').and.returnValue(of(formDefinitionRequiredField));
spyOn(taskService, 'getTask').and.returnValue(of(<TaskRepresentation>{}));
spyOn(taskFormService, 'getTaskForm').and.returnValue(of(formDefinitionRequiredField));
const change = new SimpleChange(null, 1, true);
component.ngOnChanges({ taskId: change });
component.ngOnChanges({taskId: change});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('#adf-valid-form-icon'))).toBeNull();
expect(fixture.debugElement.query(By.css('#adf-invalid-form-icon'))).toBeDefined();
@@ -84,11 +91,11 @@ describe('FormComponent UI and visibility', () => {
});
it('should NOT display validation icon when [showValidationIcon] is false', () => {
spyOn(service, 'getTask').and.returnValue(of({}));
spyOn(service, 'getTaskForm').and.returnValue(of(formDefinitionTwoTextFields));
spyOn(taskService, 'getTask').and.returnValue(of(<TaskRepresentation>{}));
spyOn(taskFormService, 'getTaskForm').and.returnValue(of(formDefinitionTwoTextFields));
const change = new SimpleChange(null, 1, true);
component.ngOnChanges({ taskId: change });
component.ngOnChanges({taskId: change});
component.showValidationIcon = false;
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('#adf-valid-form-icon'))).toBeNull();
@@ -99,11 +106,11 @@ describe('FormComponent UI and visibility', () => {
describe('form definition', () => {
it('should display two text fields form definition', () => {
spyOn(service, 'getTask').and.returnValue(of({}));
spyOn(service, 'getTaskForm').and.returnValue(of(formDefinitionTwoTextFields));
spyOn(taskService, 'getTask').and.returnValue(of(<TaskRepresentation>{}));
spyOn(taskFormService, 'getTaskForm').and.returnValue(of(formDefinitionTwoTextFields));
const change = new SimpleChange(null, 1, true);
component.ngOnChanges({ taskId: change });
component.ngOnChanges({taskId: change});
fixture.detectChanges();
const firstNameEl = fixture.debugElement.query(By.css('#firstname'));
@@ -116,11 +123,11 @@ describe('FormComponent UI and visibility', () => {
});
it('should display dropdown field', async () => {
spyOn(service, 'getTask').and.returnValue(of({}));
spyOn(service, 'getTaskForm').and.returnValue(of(formDefinitionDropdownField));
spyOn(taskService, 'getTask').and.returnValue(of(<TaskRepresentation>{}));
spyOn(taskFormService, 'getTaskForm').and.returnValue(of(formDefinitionDropdownField));
const change = new SimpleChange(null, 1, true);
component.ngOnChanges({ taskId: change });
component.ngOnChanges({taskId: change});
fixture.detectChanges();
await fixture.whenStable();
@@ -150,11 +157,11 @@ describe('FormComponent UI and visibility', () => {
describe('Visibility conditions', () => {
it('should hide the field based on the next one', () => {
spyOn(service, 'getTask').and.returnValue(of({}));
spyOn(service, 'getTaskForm').and.returnValue(of(formDefVisibilitiFieldDependsOnNextOne));
spyOn(taskService, 'getTask').and.returnValue(of(<TaskRepresentation>{}));
spyOn(taskFormService, 'getTaskForm').and.returnValue(of(formDefVisibilitiFieldDependsOnNextOne));
const change = new SimpleChange(null, 1, true);
component.ngOnChanges({ taskId: change });
component.ngOnChanges({taskId: change});
fixture.detectChanges();
const firstEl = fixture.debugElement.query(By.css('#field-country-container'));
@@ -167,11 +174,11 @@ describe('FormComponent UI and visibility', () => {
});
it('should hide the field based on the previous one', () => {
spyOn(service, 'getTask').and.returnValue(of({}));
spyOn(service, 'getTaskForm').and.returnValue(of(formDefVisibilitiFieldDependsOnPreviousOne));
spyOn(taskService, 'getTask').and.returnValue(of(<TaskRepresentation>{}));
spyOn(taskFormService, 'getTaskForm').and.returnValue(of(formDefVisibilitiFieldDependsOnPreviousOne));
const change = new SimpleChange(null, 1, true);
component.ngOnChanges({ taskId: change });
component.ngOnChanges({taskId: change});
fixture.detectChanges();
const firstEl = fixture.debugElement.query(By.css('#name'));
@@ -184,11 +191,11 @@ describe('FormComponent UI and visibility', () => {
});
it('should show the hidden field when the visibility condition change to true', () => {
spyOn(service, 'getTask').and.returnValue(of({}));
spyOn(service, 'getTaskForm').and.returnValue(of(formDefVisibilitiFieldDependsOnNextOne));
spyOn(taskService, 'getTask').and.returnValue(of(<TaskRepresentation>{}));
spyOn(taskFormService, 'getTaskForm').and.returnValue(of(formDefVisibilitiFieldDependsOnNextOne));
const change = new SimpleChange(null, 1, true);
component.ngOnChanges({ taskId: change });
component.ngOnChanges({taskId: change});
fixture.detectChanges();
let firstEl = fixture.debugElement.query(By.css('#field-country-container'));
@@ -209,11 +216,11 @@ describe('FormComponent UI and visibility', () => {
describe('Readonly Form', () => {
it('should display two text fields readonly', async () => {
spyOn(service, 'getTask').and.returnValue(of({}));
spyOn(service, 'getTaskForm').and.returnValue(of(formReadonlyTwoTextFields));
spyOn(taskService, 'getTask').and.returnValue(of(<TaskRepresentation>{}));
spyOn(taskFormService, 'getTaskForm').and.returnValue(of(formReadonlyTwoTextFields));
const change = new SimpleChange(null, 1, true);
component.ngOnChanges({ taskId: change });
component.ngOnChanges({taskId: change});
fixture.detectChanges();
await fixture.whenStable();

View File

@@ -21,21 +21,48 @@ import { CoreModule } from '@alfresco/adf-core';
import { FormComponent } from './form.component';
import { StartFormComponent } from './start-form.component';
import { FormCustomOutcomesComponent } from './form-custom-outcomes.component';
import { DocumentWidgetComponent } from './widgets/document/document.widget';
import { ContentWidgetComponent } from './widgets/document/content.widget';
import { UploadWidgetComponent } from './widgets/upload/upload.widget';
import { FormListComponent } from './form-list/form-list.component';
import { FunctionalGroupWidgetComponent } from './widgets/functional-group/functional-group.widget';
import { PeopleWidgetComponent } from './widgets/people/people.widget';
import { RadioButtonsWidgetComponent } from './widgets/radio-buttons/radio-buttons.widget';
import { TypeaheadWidgetComponent } from './widgets/typeahead/typeahead.widget';
import { DropdownWidgetComponent } from './widgets/dropdown/dropdown.widget';
import { DynamicTableModule } from './widgets/dynamic-table/dynamic-table.module';
@NgModule({
imports: [
DynamicTableModule,
CoreModule,
MaterialModule
],
declarations: [
UploadWidgetComponent,
FormComponent,
StartFormComponent,
FormCustomOutcomesComponent
FormCustomOutcomesComponent,
DocumentWidgetComponent,
ContentWidgetComponent,
PeopleWidgetComponent,
FunctionalGroupWidgetComponent,
FormListComponent,
RadioButtonsWidgetComponent,
DropdownWidgetComponent,
TypeaheadWidgetComponent
],
exports: [
FormComponent,
StartFormComponent,
FormCustomOutcomesComponent
FormCustomOutcomesComponent,
PeopleWidgetComponent,
FunctionalGroupWidgetComponent,
RadioButtonsWidgetComponent,
TypeaheadWidgetComponent,
DropdownWidgetComponent,
FormListComponent
]
})
export class FormModule {}
export class FormModule {
}

View File

@@ -0,0 +1,103 @@
/*!
* @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 { ProcessFormRenderingService } from './process-form-rendering.service';
import { FormFieldModel, FormFieldTypes } from '@alfresco/adf-core';
import { AttachFolderWidgetComponent } from './widgets/content-widget/attach-folder-widget.component';
import { DropdownWidgetComponent } from './widgets/dropdown/dropdown.widget';
import { DynamicTableWidgetComponent } from './widgets/dynamic-table/dynamic-table.widget';
import { FunctionalGroupWidgetComponent } from './widgets/functional-group/functional-group.widget';
import { PeopleWidgetComponent } from './widgets/people/people.widget';
import { RadioButtonsWidgetComponent } from './widgets/radio-buttons/radio-buttons.widget';
import { TypeaheadWidgetComponent } from './widgets/typeahead/typeahead.widget';
import { DocumentWidgetComponent } from './widgets/document/document.widget';
import { AttachFileWidgetComponent } from './widgets/content-widget/attach-file-widget.component';
describe('ProcessFormRenderingService', () => {
let service: ProcessFormRenderingService;
beforeEach(() => {
service = new ProcessFormRenderingService();
});
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(AttachFileWidgetComponent);
});
it('should resolve Upload widget for Upload', () => {
const resolver = service.getComponentTypeResolver(FormFieldTypes.UPLOAD);
const type = resolver(null);
expect(type).toBe(AttachFileWidgetComponent);
});
it('should resolve Upload widget for dropdown', () => {
const resolver = service.getComponentTypeResolver(FormFieldTypes.DROPDOWN);
const type = resolver(null);
expect(type).toBe(DropdownWidgetComponent);
});
it('should resolve Upload widget for typeahead', () => {
const resolver = service.getComponentTypeResolver(FormFieldTypes.TYPEAHEAD);
const type = resolver(null);
expect(type).toBe(TypeaheadWidgetComponent);
});
it('should resolve Upload widget for radio button', () => {
const resolver = service.getComponentTypeResolver(FormFieldTypes.RADIO_BUTTONS);
const type = resolver(null);
expect(type).toBe(RadioButtonsWidgetComponent);
});
it('should resolve Upload widget for select folder', () => {
const resolver = service.getComponentTypeResolver(FormFieldTypes.ATTACH_FOLDER);
const type = resolver(null);
expect(type).toBe(AttachFolderWidgetComponent);
});
it('should resolve Upload widget for document', () => {
const resolver = service.getComponentTypeResolver(FormFieldTypes.DOCUMENT);
const type = resolver(null);
expect(type).toBe(DocumentWidgetComponent);
});
it('should resolve Upload widget for people', () => {
const resolver = service.getComponentTypeResolver(FormFieldTypes.PEOPLE);
const type = resolver(null);
expect(type).toBe(PeopleWidgetComponent);
});
it('should resolve Upload widget for group', () => {
const resolver = service.getComponentTypeResolver(FormFieldTypes.FUNCTIONAL_GROUP);
const type = resolver(null);
expect(type).toBe(FunctionalGroupWidgetComponent);
});
it('should resolve Upload widget for dynamic table', () => {
const resolver = service.getComponentTypeResolver(FormFieldTypes.DYNAMIC_TABLE);
const type = resolver(null);
expect(type).toBe(DynamicTableWidgetComponent);
});
});

View File

@@ -16,9 +16,19 @@
*/
import { Injectable } from '@angular/core';
import { FormRenderingService } from '@alfresco/adf-core';
import { AttachFileWidgetComponent } from '../content-widget/attach-file-widget.component';
import { AttachFolderWidgetComponent } from '../content-widget/attach-folder-widget.component';
import {
FormFieldTypes,
FormRenderingService
} from '@alfresco/adf-core';
import { AttachFileWidgetComponent } from './widgets/content-widget/attach-file-widget.component';
import { AttachFolderWidgetComponent } from './widgets/content-widget/attach-folder-widget.component';
import { DocumentWidgetComponent } from './widgets/document/document.widget';
import { PeopleWidgetComponent } from './widgets/people/people.widget';
import { FunctionalGroupWidgetComponent } from './widgets/functional-group/functional-group.widget';
import { RadioButtonsWidgetComponent } from './widgets/radio-buttons/radio-buttons.widget';
import { TypeaheadWidgetComponent } from './widgets/typeahead/typeahead.widget';
import { DynamicTableWidgetComponent } from './widgets/dynamic-table/dynamic-table.widget';
import { DropdownWidgetComponent } from './widgets/dropdown/dropdown.widget';
@Injectable({
providedIn: 'root'
@@ -28,8 +38,15 @@ export class ProcessFormRenderingService extends FormRenderingService {
super();
this.register({
upload: () => AttachFileWidgetComponent,
'select-folder': () => AttachFolderWidgetComponent
[FormFieldTypes.DROPDOWN]: () => DropdownWidgetComponent,
[FormFieldTypes.TYPEAHEAD]: () => TypeaheadWidgetComponent,
[FormFieldTypes.RADIO_BUTTONS]: () => RadioButtonsWidgetComponent,
[FormFieldTypes.UPLOAD]: () => AttachFileWidgetComponent,
[FormFieldTypes.ATTACH_FOLDER]: () => AttachFolderWidgetComponent,
[FormFieldTypes.DOCUMENT]: () => DocumentWidgetComponent,
[FormFieldTypes.PEOPLE]: () => PeopleWidgetComponent,
[FormFieldTypes.FUNCTIONAL_GROUP]: () => FunctionalGroupWidgetComponent,
[FormFieldTypes.DYNAMIC_TABLE]: () => DynamicTableWidgetComponent
}, true);
}
}

View File

@@ -15,8 +15,23 @@
* limitations under the License.
*/
export * from './widgets/index';
export * from './services/ecm-model.service';
export * from './services/editor.service';
export * from './services/process-content.service';
export * from './services/task.service';
export * from './services/task-form.service';
export * from './services/process-definition.service';
export * from './services/activiti-alfresco.service';
export * from './process-form-rendering.service';
export * from './events/validate-dynamic-table-row.event';
export * from './form-list/form-list.component';
export * from './form.component';
export * from './start-form.component';
export * from './process-form-rendering.service';
export * from './form-custom-outcomes.component';
export * from './form.module';

View File

@@ -0,0 +1,147 @@
/*!
* @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, LogService, SitesService, ExternalContent, ExternalContentLink } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import {
IntegrationAlfrescoOnPremiseApi,
MinimalNode,
RelatedContentRepresentation,
ActivitiContentApi
} from '@alfresco/js-api';
import { Observable, from, throwError } from 'rxjs';
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

@@ -0,0 +1,312 @@
/*!
* @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, setupTestBed, CoreTestingModule } from '@alfresco/adf-core';
import { EcmModelService } from './ecm-model.service';
import { TestBed } from '@angular/core/testing';
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

@@ -0,0 +1,220 @@
/*!
* @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, LogService, FormModel } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import { Observable, from } from 'rxjs';
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 {};
}
private handleError(err: any): any {
this.logService.error(err);
}
}

View File

@@ -0,0 +1,97 @@
/*!
* @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, FormDefinitionModel, LogService } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import { Observable, from, throwError } from 'rxjs';
import { FormModelsApi } from '@alfresco/js-api';
import { catchError, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class EditorService {
static UNKNOWN_ERROR_MESSAGE: string = 'Unknown error';
static GENERIC_ERROR_MESSAGE: string = 'Server error';
_editorApi: FormModelsApi;
get editorApi(): FormModelsApi {
this._editorApi = this._editorApi ?? new FormModelsApi(this.apiService.getInstance());
return this._editorApi;
}
constructor(private apiService: AlfrescoApiService, private logService: LogService) {
}
/**
* 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)
);
}
/**
* 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))
);
}
/**
* Creates a JSON representation of form data.
*
* @param res Object representing form data
* @returns JSON data
*/
toJson(res: any) {
if (res) {
return res || {};
}
return {};
}
/**
* Reports an error message.
*
* @param error Data object with optional `message` and `status` fields for the error
* @returns Error message
*/
private handleError(error: any): Observable<any> {
let errMsg = EditorService.UNKNOWN_ERROR_MESSAGE;
if (error) {
errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : EditorService.GENERIC_ERROR_MESSAGE;
}
this.logService.error(errMsg);
return throwError(errMsg);
}
}

View File

@@ -0,0 +1,162 @@
/*!
* @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, LogService } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import { Observable, from, throwError } from 'rxjs';
import { ModelsApi } from '@alfresco/js-api';
import { catchError, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ModelService {
static UNKNOWN_ERROR_MESSAGE: string = 'Unknown error';
static GENERIC_ERROR_MESSAGE: string = 'Server error';
_modelsApi: ModelsApi;
get modelsApi(): ModelsApi {
this._modelsApi = this._modelsApi ?? new ModelsApi(this.apiService.getInstance());
return this._modelsApi;
}
constructor(private apiService: AlfrescoApiService, private logService: LogService) {
}
/**
* 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)
);
}
/**
* 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))
);
}
/**
* 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 [];
}
/**
* 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 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 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;
}
/**
* Reports an error message.
*
* @param error Data object with optional `message` and `status` fields for the error
* @returns Error message
*/
private handleError(error: any): Observable<any> {
let errMsg = ModelService.UNKNOWN_ERROR_MESSAGE;
if (error) {
errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : ModelService.GENERIC_ERROR_MESSAGE;
}
this.logService.error(errMsg);
return throwError(errMsg);
}
}

View File

@@ -0,0 +1,193 @@
/*!
* @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 { TranslateModule } from '@ngx-translate/core';
import { setupTestBed, CoreTestingModule } from '@alfresco/adf-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

@@ -0,0 +1,228 @@
/*!
* @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, LogService } from '@alfresco/adf-core';
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

@@ -0,0 +1,100 @@
/*!
* @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, LogService } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import { Observable, from, throwError } from 'rxjs';
import { ProcessDefinitionsApi } from '@alfresco/js-api';
import { catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ProcessDefinitionService {
static UNKNOWN_ERROR_MESSAGE: string = 'Unknown error';
static GENERIC_ERROR_MESSAGE: string = 'Server error';
_processDefinitionsApi: ProcessDefinitionsApi;
get processDefinitionsApi(): ProcessDefinitionsApi {
this._processDefinitionsApi = this._processDefinitionsApi ?? new ProcessDefinitionsApi(this.apiService.getInstance());
return this._processDefinitionsApi;
}
constructor(private apiService: AlfrescoApiService, private logService: LogService) {
}
/**
* 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))
);
}
/**
* Creates a JSON representation of form data.
*
* @param res Object representing form data
* @returns JSON data
*/
toJson(res: any) {
if (res) {
return res || {};
}
return {};
}
/**
* Reports an error message.
*
* @param error Data object with optional `message` and `status` fields for the error
* @returns Error message
*/
private handleError(error: any): Observable<any> {
let errMsg = ProcessDefinitionService.UNKNOWN_ERROR_MESSAGE;
if (error) {
errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : ProcessDefinitionService.GENERIC_ERROR_MESSAGE;
}
this.logService.error(errMsg);
return throwError(errMsg);
}
}

View File

@@ -0,0 +1,157 @@
/*!
* @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, FormValues, LogService, TaskProcessVariableModel } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import { from, Observable, throwError } from 'rxjs';
import { CompleteFormRepresentation, SaveFormRepresentation, TaskFormsApi } from '@alfresco/js-api';
import { catchError, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class TaskFormService {
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;
}
constructor(private apiService: AlfrescoApiService, private logService: LogService) {
}
/**
* 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 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 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))
);
}
getTaskProcessVariable(taskId: string): Observable<TaskProcessVariableModel[]> {
return from(this.taskFormsApi.getTaskFormVariables(taskId))
.pipe(
map((res) => this.toJson(res)),
catchError((err) => this.handleError(err))
);
}
/**
* Creates a JSON representation of form data.
*
* @param res Object representing form data
* @returns JSON data
*/
toJson(res: any) {
if (res) {
return res || {};
}
return {};
}
/**
* Reports an error message.
*
* @param error Data object with optional `message` and `status` fields for the error
* @returns Error message
*/
private handleError(error: any): Observable<any> {
let errMsg = TaskFormService.UNKNOWN_ERROR_MESSAGE;
if (error) {
errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : TaskFormService.GENERIC_ERROR_MESSAGE;
}
this.logService.error(errMsg);
return throwError(errMsg);
}
}

View File

@@ -0,0 +1,85 @@
/*!
* @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, LogService } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import { Observable, from, throwError } from 'rxjs';
import { TaskRepresentation, TasksApi } from '@alfresco/js-api';
import { catchError, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class TaskService {
static UNKNOWN_ERROR_MESSAGE: string = 'Unknown error';
static GENERIC_ERROR_MESSAGE: string = 'Server error';
_taskApi: TasksApi;
get taskApi(): TasksApi {
this._taskApi = this._taskApi ?? new TasksApi(this.apiService.getInstance());
return this._taskApi;
}
constructor(private apiService: AlfrescoApiService, private logService: LogService) {
}
/**
* Gets a task.
*
* @param taskId Task Id
* @returns Task info
*/
getTask(taskId: string): Observable<TaskRepresentation> {
return from(this.taskApi.getTask(taskId))
.pipe(
map(this.toJson),
catchError((err) => this.handleError(err))
);
}
/**
* Creates a JSON representation of form data.
*
* @param res Object representing form data
* @returns JSON data
*/
toJson(res: any) {
if (res) {
return res || {};
}
return {};
}
/**
* Reports an error message.
*
* @param error Data object with optional `message` and `status` fields for the error
* @returns Error message
*/
private handleError(error: any): Observable<any> {
let errMsg = TaskService.UNKNOWN_ERROR_MESSAGE;
if (error) {
errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : TaskService.GENERIC_ERROR_MESSAGE;
}
this.logService.error(errMsg);
return throwError(errMsg);
}
}

View File

@@ -25,18 +25,19 @@ import {
taskFormSingleUploadMock, taskFormMultipleUploadMock, preselectedSingleNode, preselectedMultipleeNode
} from './start-form.component.mock';
import { StartFormComponent } from './start-form.component';
import { FormService, WidgetVisibilityService, setupTestBed, FormModel, FormOutcomeModel } from '@alfresco/adf-core';
import { WidgetVisibilityService, setupTestBed, FormModel, FormOutcomeModel } from '@alfresco/adf-core';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { ProcessTestingModule } from '../testing/process.testing.module';
import { ProcessService } from '../process-list/services/process.service';
describe('StartFormComponent', () => {
let formService: FormService;
let component: StartFormComponent;
let fixture: ComponentFixture<StartFormComponent>;
let getStartFormSpy: jasmine.Spy;
let visibilityService: WidgetVisibilityService;
let translate: TranslateService;
let processService: ProcessService;
const exampleId1 = 'my:process1';
const exampleId2 = 'my:process2';
@@ -52,11 +53,11 @@ describe('StartFormComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(StartFormComponent);
component = fixture.componentInstance;
formService = TestBed.inject(FormService);
processService = TestBed.inject(ProcessService);
visibilityService = TestBed.inject(WidgetVisibilityService);
translate = TestBed.inject(TranslateService);
getStartFormSpy = spyOn(formService, 'getStartFormDefinition').and.returnValue(of({
getStartFormSpy = spyOn(processService, 'getStartFormDefinition').and.returnValue(of({
processDefinitionName: 'my:process'
}));
@@ -71,27 +72,27 @@ describe('StartFormComponent', () => {
it('should load start form on change if processDefinitionId defined', () => {
component.processDefinitionId = exampleId1;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
expect(formService.getStartFormDefinition).toHaveBeenCalled();
expect(processService.getStartFormDefinition).toHaveBeenCalled();
});
it('should load start form when processDefinitionId changed', () => {
component.processDefinitionId = exampleId1;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
expect(formService.getStartFormDefinition).toHaveBeenCalled();
expect(processService.getStartFormDefinition).toHaveBeenCalled();
});
it('should check visibility when the start form is loaded', () => {
spyOn(visibilityService, 'refreshVisibility');
component.processDefinitionId = exampleId1;
component.ngOnChanges({ processDefinitionId: new SimpleChange(exampleId1, exampleId2, true) });
expect(formService.getStartFormDefinition).toHaveBeenCalled();
expect(processService.getStartFormDefinition).toHaveBeenCalled();
expect(visibilityService.refreshVisibility).toHaveBeenCalled();
});
it('should not load start form when changes notified but no change to processDefinitionId', () => {
component.processDefinitionId = undefined;
component.ngOnChanges({ otherProp: new SimpleChange(exampleId1, exampleId2, true) });
expect(formService.getStartFormDefinition).not.toHaveBeenCalled();
expect(processService.getStartFormDefinition).not.toHaveBeenCalled();
});
it('should be able to inject sigle file as value into the form with an upload single widget', () => {

View File

@@ -30,6 +30,11 @@ import {
} from '@angular/core';
import { FormComponent } from './form.component';
import { ContentLinkModel, FormService, WidgetVisibilityService, FormOutcomeModel } from '@alfresco/adf-core';
import { ProcessService } from '../process-list/services/process.service';
import { EditorService } from './services/editor.service';
import { ModelService } from './services/model.service';
import { TaskFormService } from './services/task-form.service';
import { TaskService } from './services/task.service';
@Component({
selector: 'adf-start-form',
@@ -70,8 +75,13 @@ export class StartFormComponent extends FormComponent implements OnChanges, OnIn
@ViewChild('outcomesContainer')
outcomesContainer: ElementRef = null;
constructor(formService: FormService, visibilityService: WidgetVisibilityService) {
super(formService, visibilityService, null, null);
constructor(public processService: ProcessService,
taskFormService: TaskFormService,
taskService: TaskService,
editorService: EditorService,
modelService: ModelService,
formService: FormService, visibilityService: WidgetVisibilityService) {
super(formService, taskFormService, taskService, editorService, modelService, visibilityService, null, null);
this.showTitle = false;
}
@@ -99,9 +109,9 @@ export class StartFormComponent extends FormComponent implements OnChanges, OnIn
}
loadStartForm(processId: string) {
this.formService.getProcessInstance(processId)
this.processService.getProcess(processId)
.subscribe((instance: any) => {
this.formService
this.processService
.getStartFormInstance(processId)
.subscribe(
(form) => {
@@ -117,7 +127,7 @@ export class StartFormComponent extends FormComponent implements OnChanges, OnIn
}
getStartFormDefinition(processId: string) {
this.formService
this.processService
.getStartFormDefinition(processId)
.subscribe(
(form) => {

View File

@@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ContentModule, ContentNodeSelectorPanelComponent, DocumentListService } from '@alfresco/adf-content-services';
import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
import { ProcessTestingModule } from '../testing/process.testing.module';
import { ProcessTestingModule } from '../../../testing/process.testing.module';
import { AttachFileWidgetDialogComponent } from './attach-file-widget-dialog.component';
import { setupTestBed, AuthenticationService, SitesService, AlfrescoApiService, NodesApiService } from '@alfresco/adf-core';
import { AttachFileWidgetDialogComponentData } from './attach-file-widget-dialog-component.interface';

View File

@@ -20,7 +20,7 @@ import { MatDialog } from '@angular/material/dialog';
import { AttachFileWidgetDialogService } from './attach-file-widget-dialog.service';
import { Subject, of } from 'rxjs';
import { setupTestBed } from '@alfresco/adf-core';
import { ProcessTestingModule } from '../testing/process.testing.module';
import { ProcessTestingModule } from '../../../testing/process.testing.module';
import { TranslateModule } from '@ngx-translate/core';
describe('AttachFileWidgetDialogService', () => {

View File

@@ -40,7 +40,7 @@
(click)="openSelectDialogFromFileSource()">
{{field.params?.fileSource?.name}}
<mat-icon>
<img alt="alfresco" class="adf-attach-widget__image-logo" src="../assets/images/alfresco-flower.svg">
<img alt="alfresco" class="adf-attach-widget__image-logo" src="../../../assets/images/alfresco-flower.svg">
</mat-icon>
</button>
<div *ngIf="!isDefinedSourceFolder()">
@@ -49,7 +49,7 @@
(click)="openSelectDialog(repo)">
{{repo.name}}
<mat-icon>
<img alt="alfresco" class="adf-attach-widget__image-logo" src="../assets/images/alfresco-flower.svg">
<img alt="alfresco" class="adf-attach-widget__image-logo" src="../../../assets/images/alfresco-flower.svg">
</mat-icon>
</button>
</div>

View File

@@ -23,8 +23,6 @@ import {
FormModel,
FormFieldTypes,
FormService,
ProcessContentService,
ActivitiContentService,
FormFieldMetadata,
setupTestBed,
DownloadService
@@ -32,9 +30,11 @@ import {
import { ContentNodeDialogService, ContentModule } from '@alfresco/adf-content-services';
import { of } from 'rxjs';
import { Node } from '@alfresco/js-api';
import { ProcessTestingModule } from '../testing/process.testing.module';
import { ProcessTestingModule } from '../../../testing/process.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { AttachFileWidgetDialogService } from './attach-file-widget-dialog.service';
import { ActivitiContentService } from '../../services/activiti-alfresco.service';
import { ProcessContentService } from '../../services/process-content.service';
const fakeRepositoryListAnswer = [
{

View File

@@ -19,16 +19,13 @@
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import {
ActivitiContentService,
AppConfigService,
AppConfigValues,
ContentService,
DownloadService,
FormService,
LogService,
ProcessContentService,
ThumbnailService,
UploadWidgetComponent
ThumbnailService
} from '@alfresco/adf-core';
import { ContentNodeDialogService } from '@alfresco/adf-content-services';
import {
@@ -40,6 +37,9 @@ import {
import { from, of, Subject, zip } from 'rxjs';
import { mergeMap, takeUntil } from 'rxjs/operators';
import { AttachFileWidgetDialogService } from './attach-file-widget-dialog.service';
import { UploadWidgetComponent } from '../upload/upload.widget';
import { ProcessContentService } from '../../services/process-content.service';
import { ActivitiContentService } from '../../services/activiti-alfresco.service';
@Component({
selector: 'attach-widget',

View File

@@ -27,7 +27,7 @@ import {
import { ContentNodeDialogService } from '@alfresco/adf-content-services';
import { of } from 'rxjs';
import { Node } from '@alfresco/js-api';
import { ProcessTestingModule } from '../testing/process.testing.module';
import { ProcessTestingModule } from '../../../testing/process.testing.module';
import { TranslateModule } from '@ngx-translate/core';
const fakeMinimalNode: Node = {

View File

@@ -16,7 +16,7 @@
*/
import { NgModule } from '@angular/core';
import { MaterialModule } from '../material.module';
import { MaterialModule } from '../../../material.module';
import { CoreModule } from '@alfresco/adf-core';
import { ContentNodeSelectorModule } from '@alfresco/adf-content-services';

View File

@@ -0,0 +1,22 @@
<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

@@ -0,0 +1,15 @@
.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

@@ -0,0 +1,284 @@
/*!
* @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,
ContentLinkModel,
CoreTestingModule,
setupTestBed
} from '@alfresco/adf-core';
import { of } from 'rxjs';
import { ContentWidgetComponent } from './content.widget';
import { TranslateModule } from '@ngx-translate/core';
import { ProcessContentService } from '../../services/process-content.service';
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

@@ -0,0 +1,139 @@
/*!
* @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, LogService, ContentLinkModel, FormService } from '@alfresco/adf-core';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Observable } from 'rxjs';
import { ProcessContentService } from '../../services/process-content.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

@@ -0,0 +1,5 @@
<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

@@ -0,0 +1,59 @@
/*!
* @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, WidgetComponent } from '@alfresco/adf-core';
@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

@@ -0,0 +1,21 @@
<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

@@ -0,0 +1,18 @@
.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

@@ -0,0 +1,334 @@
/*!
* @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 {
WidgetVisibilityService,
FormFieldOption,
FormFieldModel,
FormModel,
FormFieldTypes,
CoreTestingModule,
setupTestBed
} from '@alfresco/adf-core';
import { DropdownWidgetComponent } from './dropdown.widget';
import { TranslateModule } from '@ngx-translate/core';
import { TaskFormService } from '../../services/task-form.service';
import { ProcessDefinitionService } from '../../services/process-definition.service';
describe('DropdownWidgetComponent', () => {
let taskFormService: TaskFormService;
let processDefinitionService: ProcessDefinitionService;
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;
taskFormService = TestBed.inject(TaskFormService);
visibilityService = TestBed.inject(WidgetVisibilityService);
processDefinitionService = TestBed.inject(ProcessDefinitionService);
widget.field = new FormFieldModel(new FormModel());
});
it('should require field with restUrl', () => {
spyOn(taskFormService, 'getRestFieldValues').and.stub();
widget.field = null;
widget.ngOnInit();
expect(taskFormService.getRestFieldValues).not.toHaveBeenCalled();
widget.field = new FormFieldModel(null, {restUrl: null});
widget.ngOnInit();
expect(taskFormService.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(taskFormService, 'getRestFieldValues').and.returnValue(
new Observable((observer) => {
observer.next(null);
observer.complete();
})
);
widget.ngOnInit();
expect(taskFormService.getRestFieldValues).toHaveBeenCalledWith(taskId, fieldId);
});
it('should preserve empty option when loading fields', () => {
const restFieldValue: FormFieldOption = {id: '1', name: 'Option1'} as FormFieldOption;
spyOn(taskFormService, '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(taskFormService.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(taskFormService, '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(processDefinitionService, '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

@@ -0,0 +1,125 @@
/*!
* @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, OnInit, ViewEncapsulation } from '@angular/core';
import {
FormService,
FormFieldOption,
WidgetComponent,
LogService
} from '@alfresco/adf-core';
import { ProcessDefinitionService } from '../../services/process-definition.service';
import { TaskFormService } from '../../services/task-form.service';
@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,
public taskFormService: TaskFormService,
public processDefinitionService: ProcessDefinitionService,
private logService: LogService) {
super(formService);
}
ngOnInit() {
if (this.field && this.field.restUrl) {
if (this.field.form.taskId) {
this.getValuesByTaskId();
} else {
this.getValuesByProcessDefinitionId();
}
}
}
getValuesByTaskId() {
this.taskFormService
.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.processDefinitionService
.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

@@ -0,0 +1,51 @@
/*!
* @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 { NgModule } from '@angular/core';
import { MaterialModule } from '../../../material.module';
import { CoreModule } from '@alfresco/adf-core';
import { RowEditorComponent } from './editors/row-editor/row.editor';
import { DynamicTableWidgetComponent } from './dynamic-table.widget';
import { DropdownEditorComponent } from './editors/dropdown/dropdown.editor';
import { DateTimeEditorComponent } from './editors/datetime/datetime.editor';
import { DateEditorComponent } from './editors/date/date.editor';
import { BooleanEditorComponent } from './editors/boolean/boolean.editor';
import { AmountEditorComponent } from './editors/amount/amount.editor';
import { TextEditorComponent } from './editors/text/text.editor';
@NgModule({
imports: [
CoreModule,
MaterialModule
],
declarations: [
AmountEditorComponent,
BooleanEditorComponent,
DateEditorComponent,
DateTimeEditorComponent,
DropdownEditorComponent,
RowEditorComponent,
DynamicTableWidgetComponent,
TextEditorComponent
],
exports: [
DynamicTableWidgetComponent,
RowEditorComponent
]
})
export class DynamicTableModule {
}

View File

@@ -0,0 +1,71 @@
<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

@@ -0,0 +1,172 @@
/* stylelint-disable no-descending-specificity */
@import '../../../../../../core/src/lib/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

@@ -0,0 +1,378 @@
/*!
* @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,
FormFieldTypes,
FormModel,
LogService,
FormService,
setupTestBed,
CoreTestingModule
} from '@alfresco/adf-core';
import { DynamicTableColumn } from './editors/models/dynamic-table-column.model';
import { DynamicTableRow } from './editors/models/dynamic-table-row.model';
import { DynamicTableWidgetComponent } from './dynamic-table.widget';
import { DynamicTableModel } from './editors/models/dynamic-table.widget.model';
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

@@ -0,0 +1,212 @@
/*!
* @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, WidgetVisibilityService, WidgetComponent, FormService } from '@alfresco/adf-core';
import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewEncapsulation } from '@angular/core';
import { DynamicTableColumn } from './editors/models/dynamic-table-column.model';
import { DynamicTableRow } from './editors/models/dynamic-table-row.model';
import { DynamicTableModel } from './editors/models/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

@@ -0,0 +1,12 @@
<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

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

View File

@@ -0,0 +1,40 @@
/*!
* @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 '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/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

@@ -0,0 +1,52 @@
/*!
* @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 '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/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

@@ -0,0 +1,11 @@
<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

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

View File

@@ -0,0 +1,39 @@
/*!
* @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 '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/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

@@ -0,0 +1,46 @@
/*!
* @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 '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/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

@@ -0,0 +1,17 @@
<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

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

View File

@@ -0,0 +1,141 @@
/*!
* @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, setupTestBed, CoreTestingModule } from '@alfresco/adf-core';
import { DynamicTableColumn } from '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/dynamic-table.widget.model';
import { DateEditorComponent } from './date.editor';
import { By } from '@angular/platform-browser';
import { MatDatepickerInputEvent } from '@angular/material/datepicker';
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

@@ -0,0 +1,102 @@
/*!
* @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,
MomentDateAdapter,
MOMENT_DATE_FORMATS
} from '@alfresco/adf-core';
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 '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/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

@@ -0,0 +1,24 @@
<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

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

View File

@@ -0,0 +1,82 @@
/*!
* @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, setupTestBed, CoreTestingModule } from '@alfresco/adf-core';
import { DynamicTableColumn } from '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/dynamic-table.widget.model';
import { DateTimeEditorComponent } from './datetime.editor';
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

@@ -0,0 +1,105 @@
/*!
* @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_DATE_FORMATS,
MomentDateAdapter,
UserPreferencesService,
UserPreferenceValues
} from '@alfresco/adf-core';
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 '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/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

@@ -0,0 +1,16 @@
<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

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

View File

@@ -0,0 +1,305 @@
/*!
* @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, throwError } from 'rxjs';
import {
AlfrescoApiService,
setupTestBed,
CoreTestingModule,
FormFieldModel,
FormModel,
FormService
} from '@alfresco/adf-core';
import { DynamicTableColumnOption } from '../models/dynamic-table-column-option.model';
import { DynamicTableColumn } from '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/dynamic-table.widget.model';
import { DropdownEditorComponent } from './dropdown.editor';
import { TranslateModule } from '@ngx-translate/core';
import { TaskFormService } from '../../../../services/task-form.service';
import { ProcessDefinitionService } from '../../../../services/process-definition.service';
describe('DropdownEditorComponent', () => {
let component: DropdownEditorComponent;
let formService: FormService;
let taskFormService: TaskFormService;
let processDefinitionService: ProcessDefinitionService;
let alfrescoApiService: AlfrescoApiService;
let form: FormModel;
let table: DynamicTableModel;
let column: DynamicTableColumn;
let row: DynamicTableRow;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
alfrescoApiService = TestBed.inject(AlfrescoApiService);
formService = new FormService();
taskFormService = new TaskFormService(alfrescoApiService, null);
processDefinitionService = new ProcessDefinitionService(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, taskFormService, processDefinitionService, 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(taskFormService, 'getRestFieldValuesColumn').and.returnValue(
new Observable((observer) => {
observer.next(restResults);
observer.complete();
})
);
component.ngOnInit();
expect(taskFormService.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(taskFormService, 'getRestFieldValuesColumn').and.returnValue(
new Observable((observer) => {
observer.next(null);
observer.complete();
})
);
component.ngOnInit();
expect(taskFormService.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(taskFormService, '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(processDefinitionService, '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 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(() => {
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(() => {
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

@@ -0,0 +1,113 @@
/*!
* @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, FormService } from '@alfresco/adf-core';
import { Component, Input, OnInit } from '@angular/core';
import { DynamicTableColumnOption } from '../models/dynamic-table-column-option.model';
import { DynamicTableColumn } from '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/dynamic-table.widget.model';
import { ProcessDefinitionService } from '../../../../services/process-definition.service';
import { TaskFormService } from '../../../../services/task-form.service';
@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 taskFormService: TaskFormService,
private processDefinitionService: ProcessDefinitionService,
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.taskFormService
.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.processDefinitionService
.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

@@ -0,0 +1,29 @@
/*!
* @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

@@ -0,0 +1,57 @@
/*!
* @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

@@ -0,0 +1,30 @@
/*!
* @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 '@alfresco/adf-core';
/* eslint-disable @angular-eslint/component-selector */
export class DynamicRowValidationSummary extends ErrorMessageModel {
isValid: boolean;
constructor(json?: any) {
super(json);
this.isValid = json.isValid;
}
}

View File

@@ -0,0 +1,24 @@
/*!
* @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

@@ -0,0 +1,46 @@
/*!
* @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

@@ -0,0 +1,24 @@
/*!
* @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

@@ -0,0 +1,201 @@
/*!
* @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, FormFieldModel, FormWidgetModel } from '@alfresco/adf-core';
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

@@ -0,0 +1,63 @@
/*!
* @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

@@ -0,0 +1,55 @@
/*!
* @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

@@ -0,0 +1,13 @@
.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

@@ -0,0 +1,54 @@
<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

@@ -0,0 +1,94 @@
/*!
* @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,
FormService,
CoreTestingModule,
setupTestBed
} from '@alfresco/adf-core';
import { DynamicTableColumn } from '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/dynamic-table.widget.model';
import { RowEditorComponent } from './row.editor';
import { TranslateModule } from '@ngx-translate/core';
import { DynamicRowValidationSummary } from '../models/dynamic-row-validation-summary.model';
describe('RowEditorComponent', () => {
let component: RowEditorComponent;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
component = new RowEditorComponent();
const field = new FormFieldModel(new FormModel());
component.table = new DynamicTableModel(field, new FormService());
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

@@ -0,0 +1,81 @@
/*!
* @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 '../models/dynamic-row-validation-summary.model';
import { DynamicTableColumn } from '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/dynamic-table-row.model';
import { DynamicTableModel } from '../models/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

@@ -0,0 +1,12 @@
<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

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

View File

@@ -0,0 +1,40 @@
/*!
* @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 '../models/dynamic-table-column.model';
import { DynamicTableRow } from '../models/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);
});
});

Some files were not shown because too many files have changed in this diff Show More