diff --git a/lib/core/form/components/widgets/core/form-field-types.ts b/lib/core/form/components/widgets/core/form-field-types.ts index fd3e70979f..43050c20e2 100644 --- a/lib/core/form/components/widgets/core/form-field-types.ts +++ b/lib/core/form/components/widgets/core/form-field-types.ts @@ -39,6 +39,7 @@ export class FormFieldTypes { static DOCUMENT: string = 'document'; static DATETIME: string = 'datetime'; static ATTACH_FOLDER: string = 'select-folder'; + static FILE_VIEWER: string = 'file-viewer'; static READONLY_TYPES: string[] = [ FormFieldTypes.HYPERLINK, diff --git a/lib/core/form/components/widgets/core/form.model.spec.ts b/lib/core/form/components/widgets/core/form.model.spec.ts index f03eee4bf9..6a736e7012 100644 --- a/lib/core/form/components/widgets/core/form.model.spec.ts +++ b/lib/core/form/components/widgets/core/form.model.spec.ts @@ -24,7 +24,9 @@ import { FormFieldModel } from './form-field.model'; import { FormOutcomeModel } from './form-outcome.model'; import { FormModel } from './form.model'; import { TabModel } from './tab.model'; -import { fakeMetadataForm } from 'process-services-cloud/src/lib/form/mocks/cloud-form.mock'; +import { fakeMetadataForm, fakeViewerForm } from 'process-services-cloud/src/lib/form/mocks/cloud-form.mock'; +import { Node } from '@alfresco/js-api'; +import { UploadWidgetContentLinkModel } from './upload-widget-content-link.model'; describe('FormModel', () => { let formService: FormService; @@ -568,25 +570,65 @@ describe('FormModel', () => { form.values['pfx_property_three'] = {}; form.values['pfx_property_four'] = 'empty'; form.values['pfx_property_five'] = 'green'; + form.values['pfx_property_six'] = 'text-value'; + form.values['pfx_property_seven'] = null; }); - it('should not find a process variable', () => { + it('should add values to form that are not already present', () => { const values = { pfx_property_one: 'testValue', pfx_property_two: true, pfx_property_three: 'opt_1', pfx_property_four: 'option_2', pfx_property_five: 'orange', + pfx_property_six: 'other-value', pfx_property_none: 'no_form_field' }; - const data = form.addValuesNotPresent(values); + form.addValuesNotPresent(values); - expect(data).toContain({ name: 'pfx_property_one', value: 'testValue' }); - expect(data).toContain({ name: 'pfx_property_two', value: true }); - expect(data).toContain({ name: 'pfx_property_three', value: 'opt_1' }); - expect(data).toContain({ name: 'pfx_property_four', value: 'option_2' }); - expect(data).toContain({ name: 'pfx_property_five', value: 'green' }); + expect(form.values['pfx_property_one']).toBe('testValue'); + expect(form.values['pfx_property_two']).toBe(true); + expect(form.values['pfx_property_three']).toEqual({ id: 'opt_1', name: 'Option 1'}); + expect(form.values['pfx_property_four']).toEqual({ id: 'option_2', name: 'Option: 2'}); + expect(form.values['pfx_property_five']).toEqual('green'); + expect(form.values['pfx_property_six']).toEqual('text-value'); + expect(form.values['pfx_property_seven']).toBeNull(); + expect(form.values['pfx_property_eight']).toBeNull(); + + }); + }); + + describe('setNodeIdValueForViewersLinkedToUploadWidget', () => { + const fakeNodeWithProperties: Node = { + id: 'fake-properties', + name: 'fake-properties-name', + content: { + mimeType: 'application/pdf' + }, + properties: { + 'pfx:property_one': 'testValue', + 'pfx:property_two': true + } + }; + let form: FormModel; + + it('should set the node id to the viewers linked to the upload widget in the event', () => { + form = new FormModel(fakeMetadataForm); + const uploadWidgetContentLinkModel = new UploadWidgetContentLinkModel(fakeNodeWithProperties, 'content_form_nodes'); + + form.setNodeIdValueForViewersLinkedToUploadWidget(uploadWidgetContentLinkModel); + + expect(form.values['cmfb85b2a7295ba41209750bca176ccaf9a']).toBe(fakeNodeWithProperties.id); + }); + + it('should not set the node id to the viewers when they are not linked', () => { + form = new FormModel(fakeViewerForm); + const uploadWidgetContentLinkModel = new UploadWidgetContentLinkModel(fakeNodeWithProperties, 'upload_widget'); + + form.setNodeIdValueForViewersLinkedToUploadWidget(uploadWidgetContentLinkModel); + + expect(form.values['cmfb85b2a7295ba41209750bca176ccaf9a']).toBeNull(); }); }); }); diff --git a/lib/core/form/components/widgets/core/form.model.ts b/lib/core/form/components/widgets/core/form.model.ts index e47934a5e2..98e9b23ef7 100644 --- a/lib/core/form/components/widgets/core/form.model.ts +++ b/lib/core/form/components/widgets/core/form.model.ts @@ -31,6 +31,7 @@ import { ProcessVariableModel } from './process-variable.model'; import { FormOutcomeModel } from './form-outcome.model'; import { FormFieldValidator, FORM_FIELD_VALIDATORS } from './form-field-validator'; import { FormFieldTemplates } from './form-field-templates'; +import { UploadWidgetContentLinkModel } from './upload-widget-content-link.model'; export interface FormRepresentationModel { [key: string]: any; @@ -376,17 +377,14 @@ export class FormModel { } } - addValuesNotPresent(valuesToSetIfNotPresent: FormValues): { name: string; value: any }[] { - const keys = Object.keys(valuesToSetIfNotPresent); - keys.forEach(key => { - if (!this.values[key] || this.isEmptyDropdownOption(key)) { - this.values[key] = valuesToSetIfNotPresent[key]; + addValuesNotPresent(valuesToSetIfNotPresent: FormValues) { + this.getFormFields().forEach(field => { + if (valuesToSetIfNotPresent[field.id] && (!this.values[field.id] || this.isEmptyDropdownOption(field.id))) { + this.values[field.id] = valuesToSetIfNotPresent[field.id]; + field.json.value = this.values[field.id]; + field.value = field.parseValue(field.json); } }); - const data = []; - const fields = Object.keys(this.values); - fields.forEach(field => data.push({ name: field, value: this.values[field] })); - return data; } private isEmptyDropdownOption(key: string): boolean { @@ -395,4 +393,16 @@ export class FormModel { } return false; } + + setNodeIdValueForViewersLinkedToUploadWidget(linkedUploadWidgetContentSelected: UploadWidgetContentLinkModel) { + const subscribedViewers = this.getFormFields().filter(field => + field.type === FormFieldTypes.FILE_VIEWER && linkedUploadWidgetContentSelected.uploadWidgetId === field.params['uploadWidget'] + ); + + subscribedViewers.forEach(viewer => { + this.values[viewer.id] = linkedUploadWidgetContentSelected.id; + viewer.json.value = this.values[viewer.id]; + viewer.value = viewer.parseValue(viewer.json); + }); + } } diff --git a/lib/core/form/components/widgets/core/index.ts b/lib/core/form/components/widgets/core/index.ts index 9dbd603c39..28a50483a6 100644 --- a/lib/core/form/components/widgets/core/index.ts +++ b/lib/core/form/components/widgets/core/index.ts @@ -38,3 +38,4 @@ export * from './external-content-link'; export * from './group.model'; export * from './form-variable.model'; export * from './process-variable.model'; +export * from './upload-widget-content-link.model'; diff --git a/lib/core/form/components/widgets/core/upload-widget-content-link.model.ts b/lib/core/form/components/widgets/core/upload-widget-content-link.model.ts new file mode 100644 index 0000000000..1c6f8e7f95 --- /dev/null +++ b/lib/core/form/components/widgets/core/upload-widget-content-link.model.ts @@ -0,0 +1,27 @@ +/*! + * @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 { ContentLinkModel } from './content-link.model'; + +export class UploadWidgetContentLinkModel extends ContentLinkModel { + uploadWidgetId: string; + + constructor(obj?: any, uploadWidgetId?: string) { + super(obj); + this.uploadWidgetId = uploadWidgetId; + } +} diff --git a/lib/core/form/components/widgets/file-viewer/file-viewer.widget.html b/lib/core/form/components/widgets/file-viewer/file-viewer.widget.html new file mode 100644 index 0000000000..95e3b0497f --- /dev/null +++ b/lib/core/form/components/widgets/file-viewer/file-viewer.widget.html @@ -0,0 +1,7 @@ +
+ + + +
diff --git a/lib/core/form/components/widgets/file-viewer/file-viewer.widget.scss b/lib/core/form/components/widgets/file-viewer/file-viewer.widget.scss new file mode 100644 index 0000000000..b1d93c8cd0 --- /dev/null +++ b/lib/core/form/components/widgets/file-viewer/file-viewer.widget.scss @@ -0,0 +1,21 @@ +@import '../form'; + +file-viewer-widget { + height: 100%; + width: 100%; + + .adf-file-viewer-widget { + height: 100%; + width: 100%; + + adf-viewer.adf-viewer { + position: relative; + + .adf-viewer-container { + .adf-viewer-content > div { + height: 90vh; + } + } + } + } +} diff --git a/lib/core/form/components/widgets/file-viewer/file-viewer.widget.ts b/lib/core/form/components/widgets/file-viewer/file-viewer.widget.ts new file mode 100644 index 0000000000..d148c2b316 --- /dev/null +++ b/lib/core/form/components/widgets/file-viewer/file-viewer.widget.ts @@ -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. + */ + +import { Component, ViewEncapsulation } from '@angular/core'; +import { FormService } from '../../../services/form.service'; +import { WidgetComponent } from './../widget.component'; + + /* tslint:disable:component-selector */ + +@Component({ + selector: 'file-viewer-widget', + templateUrl: './file-viewer.widget.html', + styleUrls: ['./file-viewer.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 FileViewerWidgetComponent extends WidgetComponent { + + constructor(formService: FormService) { + super(formService); + } +} diff --git a/lib/core/form/components/widgets/index.ts b/lib/core/form/components/widgets/index.ts index 5f679b8da8..451a20d161 100644 --- a/lib/core/form/components/widgets/index.ts +++ b/lib/core/form/components/widgets/index.ts @@ -47,6 +47,7 @@ import { UploadWidgetComponent } from './upload/upload.widget'; import { DateTimeWidgetComponent } from './date-time/date-time.widget'; import { JsonWidgetComponent } from './json/json.widget'; import { UploadFolderWidgetComponent } from './upload-folder/upload-folder.widget'; +import { FileViewerWidgetComponent } from './file-viewer/file-viewer.widget'; // core export * from './widget.component'; @@ -78,6 +79,7 @@ export * from './document/document.widget'; export * from './date-time/date-time.widget'; export * from './json/json.widget'; export * from './upload-folder/upload-folder.widget'; +export * from './file-viewer/file-viewer.widget'; // editors (dynamic table) export * from './dynamic-table/dynamic-table.widget.model'; @@ -120,7 +122,8 @@ export const WIDGET_DIRECTIVES: any[] = [ DateTimeEditorComponent, JsonWidgetComponent, AmountEditorComponent, - UploadFolderWidgetComponent + UploadFolderWidgetComponent, + FileViewerWidgetComponent ]; export const MASK_DIRECTIVE: any[] = [ diff --git a/lib/core/form/form-base.module.ts b/lib/core/form/form-base.module.ts index 1bea26b94a..83f7225ca0 100644 --- a/lib/core/form/form-base.module.ts +++ b/lib/core/form/form-base.module.ts @@ -39,6 +39,7 @@ import { FormRendererComponent } from './components/form-renderer.component'; import { EditJsonDialogModule } from '../dialogs/edit-json/edit-json.dialog.module'; import { A11yModule } from '@angular/cdk/a11y'; import { FlexLayoutModule } from '@angular/flex-layout'; +import { ViewerModule } from '../viewer/viewer.module'; @NgModule({ imports: [ @@ -55,7 +56,8 @@ import { FlexLayoutModule } from '@angular/flex-layout'; PipeModule, MatDatetimepickerModule, MatNativeDatetimeModule, - EditJsonDialogModule + EditJsonDialogModule, + ViewerModule ], declarations: [ ContentWidgetComponent, diff --git a/lib/core/form/services/form-rendering.service.ts b/lib/core/form/services/form-rendering.service.ts index b1bed467d9..f12d516ce0 100644 --- a/lib/core/form/services/form-rendering.service.ts +++ b/lib/core/form/services/form-rendering.service.ts @@ -47,6 +47,7 @@ export class FormRenderingService extends DynamicComponentMapper { 'group': DynamicComponentResolver.fromType(widgets.ContainerWidgetComponent), 'document': DynamicComponentResolver.fromType(widgets.DocumentWidgetComponent), 'upload': DynamicComponentResolver.fromType(widgets.UploadWidgetComponent), - 'datetime': DynamicComponentResolver.fromType(widgets.DateTimeWidgetComponent) + 'datetime': DynamicComponentResolver.fromType(widgets.DateTimeWidgetComponent), + 'file-viewer': DynamicComponentResolver.fromType(widgets.FileViewerWidgetComponent) }; } diff --git a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts index 516fd602bd..0e7d86cfae 100644 --- a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.spec.ts @@ -31,7 +31,9 @@ import { TRANSLATION_PROVIDER, WidgetVisibilityService, VersionCompatibilityService, - FormService + FormService, + UploadWidgetContentLinkModel, + ContentLinkModel } from '@alfresco/adf-core'; import { ProcessServiceCloudTestingModule } from '../../testing/process-service-cloud.testing.module'; import { FormCloudService } from '../services/form-cloud.service'; @@ -49,6 +51,7 @@ import { FormCloudModule } from '../form-cloud.module'; import { TranslateService, TranslateModule } from '@ngx-translate/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { CloudFormRenderingService } from './cloud-form-rendering.service'; +import { Node } from '@alfresco/js-api'; describe('FormCloudComponent', () => { let formCloudService: FormCloudService; @@ -1123,6 +1126,18 @@ describe('retrieve metadata on submit', () => { let fixture: ComponentFixture; let formService: FormService; + const fakeNodeWithProperties: Node = { + id: 'fake-properties', + name: 'fake-properties-name', + content: { + mimeType: 'application/pdf' + }, + properties: { + 'pfx:property_one': 'testValue', + 'pfx:property_two': true + } + }; + beforeEach(async(() => { const appConfigService = TestBed.inject(AppConfigService); spyOn(appConfigService, 'get').and.returnValue([]); @@ -1140,7 +1155,7 @@ describe('retrieve metadata on submit', () => { formComponent.form.values['pfx_property_five'] = 'green'; const addValuesNotPresent = spyOn(formComponent.form, 'addValuesNotPresent').and.callThrough(); - const refreshFormSpy = spyOn(formComponent, 'refreshFormData').and.stub(); + const formDataRefreshed = spyOn(formComponent.formDataRefreshed, 'emit').and.callThrough(); const values = { pfx_property_one: 'testValue', @@ -1154,11 +1169,39 @@ describe('retrieve metadata on submit', () => { formService.updateFormValuesRequested.next(values); expect(addValuesNotPresent).toHaveBeenCalledWith(values); - expect(refreshFormSpy).toHaveBeenCalled(); - expect(formComponent.data).toContain({ name: 'pfx_property_one', value: 'testValue' }); - expect(formComponent.data).toContain({ name: 'pfx_property_two', value: true }); - expect(formComponent.data).toContain({ name: 'pfx_property_three', value: 'opt_1' }); - expect(formComponent.data).toContain({ name: 'pfx_property_four', value: 'option_2' }); - expect(formComponent.data).toContain({ name: 'pfx_property_five', value: 'green' }); + expect(formComponent.form.values['pfx_property_one']).toBe('testValue'); + expect(formComponent.form.values['pfx_property_two']).toBe(true); + expect(formComponent.form.values['pfx_property_three']).toEqual({ id: 'opt_1', name: 'Option 1'}); + expect(formComponent.form.values['pfx_property_four']).toEqual({ id: 'option_2', name: 'Option: 2'}); + expect(formComponent.form.values['pfx_property_five']).toEqual('green'); + expect(formDataRefreshed).toHaveBeenCalled(); + }); + + it('should call setNodeIdValueForViewersLinkedToUploadWidget when content is UploadWidgetContentLinkModel', async () => { + const uploadWidgetContentLinkModel = new UploadWidgetContentLinkModel(fakeNodeWithProperties, 'attach-file-alfresco'); + + const setNodeIdValueForViewersLinkedToUploadWidget = spyOn(formComponent.form, 'setNodeIdValueForViewersLinkedToUploadWidget').and.callThrough(); + const formDataRefreshed = spyOn(formComponent.formDataRefreshed, 'emit').and.callThrough(); + const formContentClicked = spyOn(formComponent.formContentClicked, 'emit').and.callThrough(); + + formService.formContentClicked.next(uploadWidgetContentLinkModel); + + expect(setNodeIdValueForViewersLinkedToUploadWidget).toHaveBeenCalledWith(uploadWidgetContentLinkModel); + expect(formDataRefreshed).toHaveBeenCalled(); + expect(formContentClicked).not.toHaveBeenCalled(); + }); + + it('should not call setNodeIdValueForViewersLinkedToUploadWidget when content is not UploadWidgetContentLinkModel', async () => { + const contentLinkModel = new ContentLinkModel(fakeNodeWithProperties); + + const setNodeIdValueForViewersLinkedToUploadWidget = spyOn(formComponent.form, 'setNodeIdValueForViewersLinkedToUploadWidget').and.callThrough(); + const formDataRefreshed = spyOn(formComponent.formDataRefreshed, 'emit').and.callThrough(); + const formContentClicked = spyOn(formComponent.formContentClicked, 'emit').and.callThrough(); + + formService.formContentClicked.next(contentLinkModel); + + expect(setNodeIdValueForViewersLinkedToUploadWidget).not.toHaveBeenCalled(); + expect(formDataRefreshed).not.toHaveBeenCalled(); + expect(formContentClicked).toHaveBeenCalledWith(contentLinkModel); }); }); diff --git a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts index 4011b11fce..b35eb7c977 100644 --- a/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/form/components/form-cloud.component.ts @@ -32,7 +32,8 @@ import { FormFieldValidator, FormValues, FormModel, - ContentLinkModel + ContentLinkModel, + UploadWidgetContentLinkModel } from '@alfresco/adf-core'; import { FormCloudService } from '../services/form-cloud.service'; import { TaskVariableCloud } from '../models/task-variable-cloud.model'; @@ -110,14 +111,19 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, this.formService.formContentClicked .pipe(takeUntil(this.onDestroy$)) .subscribe((content) => { - this.formContentClicked.emit(content); + if (content instanceof UploadWidgetContentLinkModel) { + this.form.setNodeIdValueForViewersLinkedToUploadWidget(content); + this.onFormDataRefreshed(this.form); + } else { + this.formContentClicked.emit(content); + } }); this.formService.updateFormValuesRequested .pipe(takeUntil(this.onDestroy$)) .subscribe((valuesToSetIfNotPresent) => { - this.data = this.form.addValuesNotPresent(valuesToSetIfNotPresent); - this.refreshFormData(); + this.form.addValuesNotPresent(valuesToSetIfNotPresent); + this.onFormDataRefreshed(this.form); }); } @@ -219,7 +225,7 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges, .getForm(appName, formId, appVersion) .pipe( map((form: any) => { - const flattenForm = {...form.formRepresentation, ...form.formRepresentation.formDefinition}; + const flattenForm = { ...form.formRepresentation, ...form.formRepresentation.formDefinition }; delete flattenForm.formDefinition; return flattenForm; }), diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.html b/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.html index 62096a0e4d..d102af3569 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.html @@ -16,11 +16,16 @@
- - - {{file.name}} + + + check_circle + + + {{file.name}} @@ -36,7 +41,7 @@ {{ 'FORM.FIELD.DOWNLOAD_FILE' | translate }} diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.scss b/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.scss index 22b0501089..bf0bab6f7e 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.scss +++ b/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.scss @@ -1,5 +1,7 @@ -.adf { +@mixin adf-cloud-attach-file-cloud-widget($theme) { + $primary: map-get($theme, primary); +.adf { &-attach-widget-container { margin-bottom: 15px; display: flex; @@ -65,9 +67,24 @@ } &-attach-files-row { + + + div.mat-list-item-content { + cursor: pointer; + } + .mat-line { margin-bottom: 0; } } + &-attach-selected-file-row { + div.mat-list-item-content { + .adf-datatable-selected { + color: mat-color($primary); + padding-right: 6px; + } + } + } +} } diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.spec.ts index 27e4dd6917..8b3c3a1de2 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.spec.ts @@ -28,7 +28,9 @@ import { FormFieldMetadata, FormService, DownloadService, - AppConfigService + AppConfigService, + AlfrescoApiService, + UploadWidgetContentLinkModel } from '@alfresco/adf-core'; import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @@ -48,6 +50,11 @@ describe('AttachFileCloudWidgetComponent', () => { let processCloudContentService: ProcessCloudContentService; let formService: FormService; let downloadService: DownloadService; + let alfrescoApiService: AlfrescoApiService; + let apiServiceSpy: jasmine.Spy; + let contentModelFormFileHandlerSpy: jasmine.Spy; + let updateFormSpy: jasmine.Spy; + let contentClickedSpy: jasmine.Spy; const fakePngAnswer = { id: 1155, @@ -66,7 +73,11 @@ describe('AttachFileCloudWidgetComponent', () => { mimeType: 'image/png', simpleType: 'image', previewStatus: 'queued', - thumbnailStatus: 'queued' + thumbnailStatus: 'queued', + properties: { + 'pfx:property_one': 'testValue', + 'pfx:property_two': true + } }; const onlyLocalParams = { @@ -76,6 +87,13 @@ describe('AttachFileCloudWidgetComponent', () => { }; const contentSourceParam = { + fileSource: { + name: 'mock-alf-content', + serviceId: 'alfresco-content' + } + }; + + const menuTestSourceParam = { fileSource: { name: 'mock-alf-content', serviceId: 'alfresco-content' @@ -123,6 +141,14 @@ describe('AttachFileCloudWidgetComponent', () => { const fakeMinimalNode: Node = { id: 'fake', name: 'fake-name', + content: { + mimeType: 'application/pdf' + } + }; + + const fakeNodeWithProperties: Node = { + id: 'fake-properties', + name: 'fake-properties-name', content: { mimeType: 'application/pdf' }, @@ -132,6 +158,8 @@ describe('AttachFileCloudWidgetComponent', () => { } }; + const expectedValues = { pfx_property_one: 'testValue', pfx_property_two: true }; + const mockNodeId = new Promise(function (resolve) { resolve('mock-node-id'); }); @@ -179,6 +207,7 @@ describe('AttachFileCloudWidgetComponent', () => { AppConfigService ); formService = TestBed.inject(FormService); + alfrescoApiService = TestBed.inject(AlfrescoApiService); })); afterEach(() => { @@ -523,16 +552,17 @@ describe('AttachFileCloudWidgetComponent', () => { describe('when a file is uploaded', () => { beforeEach(async () => { + apiServiceSpy = spyOn(alfrescoApiService.getInstance().node, 'getNode').and.returnValue(new Promise(resolve => resolve({entry: fakeNodeWithProperties}))); spyOn( contentCloudNodeSelectorService, 'openUploadFileDialog' - ).and.returnValue(of([fakeMinimalNode])); + ).and.returnValue(of([fakeNodeWithProperties])); widget.field = new FormFieldModel(new FormModel(), { type: FormFieldTypes.UPLOAD, value: [] }); widget.field.id = 'attach-file-alfresco'; - widget.field.params = contentSourceParam; + widget.field.params = menuTestSourceParam; fixture.detectChanges(); await fixture.whenStable(); const attachButton: HTMLButtonElement = element.querySelector('#attach-file-alfresco'); @@ -547,19 +577,19 @@ describe('AttachFileCloudWidgetComponent', () => { it('should remove file when remove is clicked', (done) => { fixture.detectChanges(); const menuButton: HTMLButtonElement = ( - fixture.debugElement.query(By.css('#file-fake-option-menu')) + fixture.debugElement.query(By.css('#file-fake-properties-option-menu')) .nativeElement ); menuButton.click(); fixture.detectChanges(); const removeOption: HTMLButtonElement = ( - fixture.debugElement.query(By.css('#file-fake-remove')) + fixture.debugElement.query(By.css('#file-fake-properties-remove')) .nativeElement ); removeOption.click(); fixture.detectChanges(); fixture.whenRenderingDone().then(() => { - expect(element.querySelector('#file-fake-icon')).toBeNull(); + expect(element.querySelector('#file-fake-properties-icon')).toBeNull(); done(); }); }); @@ -572,7 +602,7 @@ describe('AttachFileCloudWidgetComponent', () => { fixture.detectChanges(); const menuButton: HTMLButtonElement = ( - fixture.debugElement.query(By.css('#file-fake-option-menu')) + fixture.debugElement.query(By.css('#file-fake-properties-option-menu')) .nativeElement ); @@ -580,7 +610,7 @@ describe('AttachFileCloudWidgetComponent', () => { fixture.detectChanges(); const downloadOption: HTMLButtonElement = ( - fixture.debugElement.query(By.css('#file-fake-download-file')) + fixture.debugElement.query(By.css('#file-fake-properties-download-file')) .nativeElement ); @@ -597,7 +627,7 @@ describe('AttachFileCloudWidgetComponent', () => { spyOn(processCloudContentService, 'getRawContentNode').and.returnValue(of(new Blob())); formService.formContentClicked.subscribe( (fileClicked: any) => { - expect(fileClicked.nodeId).toBe('fake'); + expect(fileClicked.nodeId).toBe('fake-properties'); done(); } ); @@ -605,25 +635,24 @@ describe('AttachFileCloudWidgetComponent', () => { fixture.detectChanges(); const menuButton: HTMLButtonElement = ( fixture.debugElement.query( - By.css('#file-fake-option-menu') + By.css('#file-fake-properties-option-menu') ).nativeElement ); menuButton.click(); fixture.detectChanges(); const showOption: HTMLButtonElement = ( fixture.debugElement.query( - By.css('#file-fake-show-file') + By.css('#file-fake-properties-show-file') ).nativeElement ); showOption.click(); }); it('should request form to be updated with metadata when retrieve is clicked', (done) => { - const updateFormSpy = spyOn(formService.updateFormValuesRequested, 'next'); - const expectedValues = { pfx_property_one: 'testValue', pfx_property_two: true }; + updateFormSpy = spyOn(formService.updateFormValuesRequested, 'next'); const menuButton: HTMLButtonElement = ( - fixture.debugElement.query(By.css('#file-fake-option-menu')) + fixture.debugElement.query(By.css('#file-fake-properties-option-menu')) .nativeElement ); @@ -631,11 +660,12 @@ describe('AttachFileCloudWidgetComponent', () => { fixture.detectChanges(); const retrieveMetadataOption: HTMLButtonElement = ( - fixture.debugElement.query(By.css('#file-fake-retrieve-file-metadata')) + fixture.debugElement.query(By.css('#file-fake-properties-retrieve-file-metadata')) .nativeElement ); retrieveMetadataOption.click(); + expect(apiServiceSpy).toHaveBeenCalledWith(fakeNodeWithProperties.id); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -687,4 +717,170 @@ describe('AttachFileCloudWidgetComponent', () => { }); }); }); + + describe('contentModelFormFileHandler', () => { + beforeEach(async () => { + apiServiceSpy = spyOn(alfrescoApiService.getInstance().node, 'getNode').and.returnValue(new Promise(resolve => resolve({ entry: fakeNodeWithProperties }))); + contentModelFormFileHandlerSpy = spyOn(widget, 'contentModelFormFileHandler').and.callThrough(); + updateFormSpy = spyOn(formService.updateFormValuesRequested, 'next'); + contentClickedSpy = spyOn(formService.formContentClicked, 'next'); + + spyOn( + contentCloudNodeSelectorService, + 'openUploadFileDialog' + ).and.returnValue(of([fakeNodeWithProperties])); + widget.field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.UPLOAD, + value: [] + }); + + widget.field.id = 'attach-file-alfresco'; + widget.field.params = menuTestSourceParam; + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('should not be called onInit when widget has no value', (done) => { + widget.ngOnInit(); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(contentModelFormFileHandlerSpy).not.toHaveBeenCalled(); + done(); + }); + }); + + it('should have been called onInit when widget only one file', (done) => { + widget.field.value = [fakeNodeWithProperties]; + widget.ngOnInit(); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(contentModelFormFileHandlerSpy).toHaveBeenCalledWith(fakeNodeWithProperties); + expect(updateFormSpy).toHaveBeenCalledWith(expectedValues); + expect(contentClickedSpy).toHaveBeenCalledWith(new UploadWidgetContentLinkModel(fakeNodeWithProperties, widget.field.id)); + done(); + }); + }); + + it('should not be called onInit when widget has more than one file', (done) => { + widget.field.value = [fakeNodeWithProperties, fakeMinimalNode]; + widget.ngOnInit(); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(contentModelFormFileHandlerSpy).not.toHaveBeenCalled(); + done(); + }); + }); + + it('should not be called on remove node if node removed is not the selected one', (done) => { + widget.field.value = [fakeNodeWithProperties, fakeMinimalNode]; + widget.selectedNode = fakeNodeWithProperties; + widget.ngOnInit(); + fixture.detectChanges(); + + widget.onRemoveAttachFile(fakeMinimalNode); + + fixture.whenStable().then(() => { + expect(contentModelFormFileHandlerSpy).not.toHaveBeenCalled(); + done(); + }); + }); + + it('should have been called on remove node if node removed is the selected one', (done) => { + widget.field.value = [fakeNodeWithProperties, fakeMinimalNode]; + widget.selectedNode = fakeNodeWithProperties; + widget.ngOnInit(); + fixture.detectChanges(); + + widget.onRemoveAttachFile(fakeNodeWithProperties); + + fixture.whenStable().then(() => { + expect(contentModelFormFileHandlerSpy).toHaveBeenCalled(); + expect(updateFormSpy).not.toHaveBeenCalled(); + expect(contentClickedSpy).toHaveBeenCalledWith(new UploadWidgetContentLinkModel(undefined, widget.field.id)); + done(); + }); + }); + + it('should have been called on attach file when value was empty', async () => { + const attachButton: HTMLButtonElement = element.querySelector('#attach-file-alfresco'); + expect(attachButton).not.toBeNull(); + attachButton.click(); + await fixture.whenStable(); + fixture.detectChanges(); + + expect(contentModelFormFileHandlerSpy).toHaveBeenCalledWith(fakeNodeWithProperties); + expect(updateFormSpy).toHaveBeenCalledWith(expectedValues); + expect(contentClickedSpy).toHaveBeenCalledWith(new UploadWidgetContentLinkModel(fakeNodeWithProperties, widget.field.id)); + }); + + it('should not be called on attach file when has a file previously', async () => { + widget.field.value = [fakeNodeWithProperties, fakeMinimalNode]; + widget.field.params['multiple'] = true; + widget.ngOnInit(); + await fixture.whenStable(); + fixture.detectChanges(); + + const attachButton: HTMLButtonElement = element.querySelector('#attach-file-alfresco'); + expect(attachButton).not.toBeNull(); + attachButton.click(); + await fixture.whenStable(); + fixture.detectChanges(); + + expect(contentModelFormFileHandlerSpy).not.toHaveBeenCalled(); + }); + + it('should be called when selecting a row if no previous row was selected', async () => { + widget.field.value = [fakeNodeWithProperties, fakeMinimalNode]; + widget.selectedNode = null; + widget.ngOnInit(); + await fixture.whenStable(); + fixture.detectChanges(); + + widget.onRowClicked(fakeNodeWithProperties); + + await fixture.whenStable(); + + expect(widget.selectedNode).toEqual(fakeNodeWithProperties); + expect(contentModelFormFileHandlerSpy).toHaveBeenCalledWith(fakeNodeWithProperties); + expect(updateFormSpy).toHaveBeenCalledWith(expectedValues); + expect(contentClickedSpy).toHaveBeenCalledWith(new UploadWidgetContentLinkModel(fakeNodeWithProperties, widget.field.id)); + }); + + it('should be called when selecting a row and previous row was selected', async () => { + widget.field.value = [fakeNodeWithProperties, fakeMinimalNode]; + widget.selectedNode = fakeMinimalNode; + widget.ngOnInit(); + await fixture.whenStable(); + fixture.detectChanges(); + + widget.onRowClicked(fakeNodeWithProperties); + + await fixture.whenStable(); + + expect(widget.selectedNode).toEqual(fakeNodeWithProperties); + expect(contentModelFormFileHandlerSpy).toHaveBeenCalledWith(fakeNodeWithProperties); + expect(updateFormSpy).toHaveBeenCalledWith(expectedValues); + expect(contentClickedSpy).toHaveBeenCalledWith(new UploadWidgetContentLinkModel(fakeNodeWithProperties, widget.field.id)); + }); + + it('should be called when deselecting a row', async () => { + widget.field.value = [fakeNodeWithProperties, fakeMinimalNode]; + widget.selectedNode = fakeNodeWithProperties; + widget.ngOnInit(); + await fixture.whenStable(); + fixture.detectChanges(); + + widget.onRowClicked(fakeNodeWithProperties); + + await fixture.whenStable(); + + expect(widget.selectedNode).toBeNull(); + expect(contentModelFormFileHandlerSpy).toHaveBeenCalled(); + expect(updateFormSpy).not.toHaveBeenCalled(); + expect(contentClickedSpy).toHaveBeenCalledWith(new UploadWidgetContentLinkModel(null, widget.field.id)); + }); + }); }); diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.ts b/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.ts index 3c73ea9d02..7c7732d267 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/attach-file/attach-file-cloud-widget.component.ts @@ -19,13 +19,15 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { - FormService, - LogService, - ThumbnailService, - NotificationService, - FormValues, - ContentLinkModel, - AppConfigService + FormService, + LogService, + ThumbnailService, + NotificationService, + FormValues, + ContentLinkModel, + AppConfigService, + AlfrescoApiService, + UploadWidgetContentLinkModel } from '@alfresco/adf-core'; import { Node, RelatedContentRepresentation } from '@alfresco/js-api'; import { ContentCloudNodeSelectorService } from '../../../services/content-cloud-node-selector.service'; @@ -55,9 +57,11 @@ export class AttachFileCloudWidgetComponent extends UploadCloudWidgetComponent i static ALIAS_USER_FOLDER = '-my-'; static APP_NAME = '-appname-'; static VALID_ALIAS = ['-root-', AttachFileCloudWidgetComponent.ALIAS_USER_FOLDER, '-shared-']; + static RETRIEVE_METADATA_OPTION = 'retrieveMetadata'; typeId = 'AttachFileCloudWidgetComponent'; rootNodeId = AttachFileCloudWidgetComponent.ALIAS_USER_FOLDER; + selectedNode: Node; constructor( formService: FormService, @@ -66,11 +70,20 @@ export class AttachFileCloudWidgetComponent extends UploadCloudWidgetComponent i processCloudContentService: ProcessCloudContentService, notificationService: NotificationService, private contentNodeSelectorService: ContentCloudNodeSelectorService, - private appConfigService: AppConfigService + private appConfigService: AppConfigService, + private apiService: AlfrescoApiService ) { super(formService, thumbnails, processCloudContentService, notificationService, logger); } + ngOnInit() { + super.ngOnInit(); + if (this.hasFile && this.field.value.length === 1) { + const files = this.field.value || this.field.form.values[this.field.id]; + this.contentModelFormFileHandler(files[0]); + } + } + isAlfrescoAndLocal(): boolean { return ( this.field.params && @@ -85,6 +98,10 @@ export class AttachFileCloudWidgetComponent extends UploadCloudWidgetComponent i onRemoveAttachFile(file: File | RelatedContentRepresentation | Node) { this.removeFile(file); + if (file['id'] === this.selectedNode?.id) { + this.selectedNode = null; + this.contentModelFormFileHandler(); + } } fetchAppNameFromAppConfig(): string { @@ -115,6 +132,9 @@ export class AttachFileCloudWidgetComponent extends UploadCloudWidgetComponent i selections.forEach(node => (node['isExternal'] = true)); const selectionWithoutDuplication = this.removeExistingSelection(selections); this.fixIncompatibilityFromPreviousAndNewForm(selectionWithoutDuplication); + if (this.field.value.length === 1) { + this.contentModelFormFileHandler(selections && selections.length > 0 ? selections[0] : null); + } }); } @@ -152,20 +172,38 @@ export class AttachFileCloudWidgetComponent extends UploadCloudWidgetComponent i } displayMenuOption(option: string): boolean { - return this.field.params.menuOptions ? this.field.params.menuOptions[option] : option !== 'retrieveMetadata'; + return this.field?.params?.menuOptions ? this.field.params.menuOptions[option] : option !== AttachFileCloudWidgetComponent.RETRIEVE_METADATA_OPTION; } - onRetrieveFileMetadata(file: Node) { - const values: FormValues = {}; - const metadata = file?.properties; - if (metadata) { - const keys = Object.keys(metadata); - keys.forEach(key => { - const sanitizedKey = key.replace(':', '_'); - values[sanitizedKey] = metadata[key]; - }); - this.formService.updateFormValuesRequested.next(values); + onRowClicked(file?: Node) { + if (this.selectedNode?.id === file?.id) { + this.selectedNode = null; + } else { + this.selectedNode = file; } + this.contentModelFormFileHandler(this.selectedNode); + } + + contentModelFormFileHandler(file?: Node) { + if (file?.id && this.isRetrieveMetadataOptionEnabled()) { + const values: FormValues = {}; + this.apiService.getInstance().node.getNode(file.id).then(acsNode => { + const metadata = acsNode?.entry?.properties; + if (metadata) { + const keys = Object.keys(metadata); + keys.forEach(key => { + const sanitizedKey = key.replace(':', '_'); + values[sanitizedKey] = metadata[key]; + }); + this.formService.updateFormValuesRequested.next(values); + } + }); + } + this.fileClicked(new UploadWidgetContentLinkModel(file, this.field.id)); + } + + isRetrieveMetadataOptionEnabled(): boolean { + return this.field?.params?.menuOptions && this.field.params.menuOptions[AttachFileCloudWidgetComponent.RETRIEVE_METADATA_OPTION]; } isValidAlias(alias: string): boolean { diff --git a/lib/process-services-cloud/src/lib/form/mocks/cloud-form.mock.ts b/lib/process-services-cloud/src/lib/form/mocks/cloud-form.mock.ts index ba69c0bea3..68451ef5d8 100644 --- a/lib/process-services-cloud/src/lib/form/mocks/cloud-form.mock.ts +++ b/lib/process-services-cloud/src/lib/form/mocks/cloud-form.mock.ts @@ -1150,6 +1150,175 @@ export let fakeMetadataForm = { 'restIdProperty': null, 'restLabelProperty': null } + ], + '7': [ + { + 'id': 'cmfb85b2a7295ba41209750bca176ccaf9a', + 'name': 'File viewer', + 'type': 'file-viewer', + 'readOnly': false, + 'required': false, + 'colspan': 1, + 'visibilityCondition': null, + 'params': { + 'existingColspan': 1, + 'maxColspan': 2, + 'uploadWidget': 'content_form_nodes' + } + } + ], + '8': [ + { + 'type': 'text', + 'id': 'pfx_property_six', + 'name': 'pfx_property_six', + 'colspan': 1, + 'params': { + 'existingColspan': 1, + 'maxColspan': 2 + }, + 'visibilityCondition': null, + 'placeholder': null, + 'value': null, + 'required': false, + 'minLength': 0, + 'maxLength': 0, + 'regexPattern': null + } + ], + '9': [ + { + 'type': 'text', + 'id': 'pfx_property_seven', + 'name': 'pfx_property_seven', + 'colspan': 1, + 'params': { + 'existingColspan': 1, + 'maxColspan': 2 + }, + 'visibilityCondition': null, + 'placeholder': null, + 'value': null, + 'required': false, + 'minLength': 0, + 'maxLength': 0, + 'regexPattern': null + } + ], + '10': [ + { + 'type': 'text', + 'id': 'pfx_property_eight', + 'name': 'pfx_property_eight', + 'colspan': 1, + 'params': { + 'existingColspan': 1, + 'maxColspan': 2 + }, + 'visibilityCondition': null, + 'placeholder': null, + 'value': null, + 'required': false, + 'minLength': 0, + 'maxLength': 0, + 'regexPattern': null + } + ] + }, + 'numberOfColumns': 2 + } + ], + 'outcomes': [], + 'metadata': {}, + 'variables': [] + } +}; + +export let fakeViewerForm = { + 'id': 'form-de8895be-d0d7-4434-beef-559b15305d72', + 'name': 'StartEventForm', + 'description': '', + 'version': 0, + 'formDefinition': { + 'tabs': [], + 'fields': [ + { + 'type': 'container', + 'id': '5a6b24c1-db2b-45e9-9aff-142395433d23', + 'name': 'Label', + 'tab': null, + 'fields': { + '1': [ + { + 'id': 'content_form_nodes', + 'name': 'Nodes', + 'type': 'upload', + 'readOnly': false, + 'required': true, + 'colspan': 1, + 'visibilityCondition': null, + 'params': { + 'existingColspan': 1, + 'maxColspan': 2, + 'fileSource': { + 'serviceId': 'alfresco-content', + 'name': 'Alfresco Content', + 'metadataAllowed': true + }, + 'multiple': true, + 'menuOptions': { + 'show': true, + 'download': true, + 'retrieveMetadata': true, + 'remove': true + }, + 'link': false + } + } + ], + '2': [ + { + 'id': 'upload_widget', + 'name': 'Nodes', + 'type': 'upload', + 'readOnly': false, + 'required': true, + 'colspan': 1, + 'visibilityCondition': null, + 'params': { + 'existingColspan': 1, + 'maxColspan': 2, + 'fileSource': { + 'serviceId': 'alfresco-content', + 'name': 'Alfresco Content', + 'metadataAllowed': true + }, + 'multiple': true, + 'menuOptions': { + 'show': true, + 'download': true, + 'retrieveMetadata': true, + 'remove': true + }, + 'link': false + } + } + ], + '3': [ + { + 'id': 'cmfb85b2a7295ba41209750bca176ccaf9a', + 'name': 'File viewer', + 'type': 'file-viewer', + 'readOnly': false, + 'required': false, + 'colspan': 1, + 'visibilityCondition': null, + 'params': { + 'existingColspan': 1, + 'maxColspan': 2, + 'uploadWidget': 'content_form_nodes' + } + } ] }, 'numberOfColumns': 2 diff --git a/lib/process-services-cloud/src/lib/services/form-fields.interfaces.ts b/lib/process-services-cloud/src/lib/services/form-fields.interfaces.ts index 0e0675903d..807a7e7a04 100644 --- a/lib/process-services-cloud/src/lib/services/form-fields.interfaces.ts +++ b/lib/process-services-cloud/src/lib/services/form-fields.interfaces.ts @@ -61,7 +61,7 @@ export interface Container { } export type FormFieldRepresentation = (DateField | DateTimeField | TextField | AttachFileField | DropDownField | - RadioField | TypeaheadField | PeopleField | AmountField | NumberField | CheckboxField | HyperlinkField | NumberField); + RadioField | TypeaheadField | PeopleField | AmountField | NumberField | CheckboxField | HyperlinkField ); export interface AttachFileField extends FormField { required: boolean; @@ -229,5 +229,6 @@ export enum FormFieldType { uploadFile = 'upload', uploadFolder = 'uploadFolder', displayValue = 'readonly', - displayText = 'readonly-text' + displayText = 'readonly-text', + fileViewer = 'file-viewer' } diff --git a/lib/process-services-cloud/src/lib/styles/_index.scss b/lib/process-services-cloud/src/lib/styles/_index.scss index 0af3ecc088..6f59178c9d 100644 --- a/lib/process-services-cloud/src/lib/styles/_index.scss +++ b/lib/process-services-cloud/src/lib/styles/_index.scss @@ -9,6 +9,7 @@ @import './../task/task-filters/components/edit-task-filter-cloud.component.scss'; @import './../task/task-filters/components/task-filters-cloud.component.scss'; @import './../process/start-process/components/start-process-cloud.component'; +@import './../form/components/widgets/attach-file/attach-file-cloud-widget.component.scss'; @mixin adf-process-services-cloud-theme($theme) { @@ -23,4 +24,5 @@ @include adf-cloud-group-theme($theme); @include adf-cloud-task-form-theme($theme); @include adf-cloud-start-service-theme($theme); + @include adf-cloud-attach-file-cloud-widget($theme); }