mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[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:
@@ -80,4 +80,5 @@ module.exports = function (config) {
|
||||
browsers: ['Chrome'],
|
||||
singleRun: true
|
||||
});
|
||||
process.env.TZ = 'UTC';
|
||||
};
|
||||
|
@@ -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',
|
||||
|
@@ -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', () => {
|
||||
|
||||
|
@@ -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',
|
||||
|
@@ -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', () => {
|
||||
|
||||
|
@@ -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',
|
||||
|
@@ -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', () => {
|
||||
|
||||
|
@@ -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',
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -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);
|
||||
});
|
||||
});
|
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -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'}
|
||||
]
|
||||
};
|
||||
|
||||
|
@@ -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) => {
|
||||
|
@@ -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();
|
||||
|
@@ -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 {
|
||||
}
|
||||
|
@@ -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);
|
||||
});
|
||||
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
220
lib/process-services/src/lib/form/services/ecm-model.service.ts
Normal file
220
lib/process-services/src/lib/form/services/ecm-model.service.ts
Normal 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);
|
||||
}
|
||||
}
|
97
lib/process-services/src/lib/form/services/editor.service.ts
Normal file
97
lib/process-services/src/lib/form/services/editor.service.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
162
lib/process-services/src/lib/form/services/model.service.ts
Normal file
162
lib/process-services/src/lib/form/services/model.service.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
157
lib/process-services/src/lib/form/services/task-form.service.ts
Normal file
157
lib/process-services/src/lib/form/services/task-form.service.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
85
lib/process-services/src/lib/form/services/task.service.ts
Normal file
85
lib/process-services/src/lib/form/services/task.service.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@@ -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', () => {
|
||||
|
@@ -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) => {
|
||||
|
@@ -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';
|
@@ -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', () => {
|
@@ -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>
|
@@ -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 = [
|
||||
{
|
@@ -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',
|
@@ -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 = {
|
@@ -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';
|
||||
|
@@ -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>
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
}
|
@@ -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>
|
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,5 @@
|
||||
.adf {
|
||||
&-text-editor {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
});
|
||||
});
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,9 @@
|
||||
.adf {
|
||||
&-checkbox-label {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
font-size: var(--theme-subheading-2-font-size);
|
||||
line-height: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,10 @@
|
||||
.adf {
|
||||
&-date-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-date-editor-button {
|
||||
position: relative;
|
||||
top: 25px;
|
||||
}
|
||||
}
|
@@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
@@ -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] = '';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,10 @@
|
||||
.adf {
|
||||
&-date-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-date-editor-button {
|
||||
position: relative;
|
||||
top: 25px;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
@@ -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] = '';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,5 @@
|
||||
.adf {
|
||||
&-dropdown-editor-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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>
|
@@ -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();
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,5 @@
|
||||
.adf {
|
||||
&-text-editor {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@@ -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
Reference in New Issue
Block a user