[AAE-3110] Move upload button inside the node selector dialog (#5901)

* Open select dialog on all types, fetch destination folder from a relative path

* Dialog UI refactor, multiple upload from local

* Fix document list should automatically reload after upload

* Remove not used ViewChild

* Fix imports, read destination folder from form field

* support different alias

* Remove not needed property, reuse selection mode

* Remove unused methods

* Fix unit tests

* * Added unit tests
* Fixed failing unit tests

* * Added unit for upload button
* Skipped failing e2e

* * Removed process-storage related code

* * Removed unncessory model and code
*

* * Removed contentHost from formCloud model

* * Skiped content-services e2e

* Skip failing process e2e related to attachment

Co-authored-by: sivakumar414ram <siva.kumar@muraai.com>
Co-authored-by: maurizio vitale <maurizio.vitale@alfresco.com>
This commit is contained in:
arditdomi
2020-07-31 07:38:58 +02:00
committed by GitHub
parent 1e692252a5
commit d553c71b1e
31 changed files with 338 additions and 432 deletions

View File

@@ -25,8 +25,7 @@ import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Params, Router } from '@angular/router'; import { ActivatedRoute, Params, Router } from '@angular/router';
import { MinimalNodeEntity, NodePaging, Pagination, MinimalNodeEntryEntity, SiteEntry, SearchEntry } from '@alfresco/js-api'; import { MinimalNodeEntity, NodePaging, Pagination, MinimalNodeEntryEntity, SiteEntry, SearchEntry } from '@alfresco/js-api';
import { import {
AlfrescoApiService, AuthenticationService, AppConfigService, AppConfigValues, ContentService, TranslationService, AlfrescoApiService, AuthenticationService, AppConfigService, AppConfigValues, ContentService, TranslationService, FolderCreatedEvent, LogService, NotificationService,
FileUploadEvent, FolderCreatedEvent, LogService, NotificationService,
UploadService, DataRow, UserPreferencesService, UploadService, DataRow, UserPreferencesService,
PaginationComponent, FormValues, DisplayMode, ShowHeaderMode, InfinitePaginationComponent, HighlightDirective, PaginationComponent, FormValues, DisplayMode, ShowHeaderMode, InfinitePaginationComponent, HighlightDirective,
SharedLinksApiService SharedLinksApiService
@@ -47,7 +46,7 @@ import { VersionManagerDialogAdapterComponent } from './version-manager-dialog-a
import { MetadataDialogAdapterComponent } from './metadata-dialog-adapter.component'; import { MetadataDialogAdapterComponent } from './metadata-dialog-adapter.component';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { PreviewService } from '../../services/preview.service'; import { PreviewService } from '../../services/preview.service';
import { debounceTime, takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
const DEFAULT_FOLDER_TO_SHOW = '-my-'; const DEFAULT_FOLDER_TO_SHOW = '-my-';
@@ -272,17 +271,6 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy {
}); });
} }
this.uploadService.fileUploadComplete
.pipe(
debounceTime(300),
takeUntil(this.onDestroy$)
)
.subscribe(value => this.onFileUploadEvent(value));
this.uploadService.fileUploadDeleted
.pipe(takeUntil(this.onDestroy$))
.subscribe(value => this.onFileUploadEvent(value));
this.contentService.folderCreated this.contentService.folderCreated
.pipe(takeUntil(this.onDestroy$)) .pipe(takeUntil(this.onDestroy$))
.subscribe(value => this.onFolderCreated(value)); .subscribe(value => this.onFolderCreated(value));
@@ -350,12 +338,6 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy {
this.errorMessage = null; this.errorMessage = null;
} }
onFileUploadEvent(event: FileUploadEvent) {
if (event && event.file.options.parentId === this.documentList.currentFolderId) {
this.documentList.reload();
}
}
onFolderCreated(event: FolderCreatedEvent) { onFolderCreated(event: FolderCreatedEvent) {
this.logService.log('FOLDER CREATED'); this.logService.log('FOLDER CREATED');
this.logService.log(event); this.logService.log(event);

View File

@@ -52,6 +52,7 @@ Opens a [Content Node Selector](content-node-selector.component.md) in its own
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| select | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`Node`](https://github.com/Alfresco/alfresco-js-api/blob/development/src/api/content-rest-api/docs/Node.md)`[]>` | Emitted when the user has chosen an item. | | select | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`Node`](https://github.com/Alfresco/alfresco-js-api/blob/development/src/api/content-rest-api/docs/Node.md)`[]>` | Emitted when the user has chosen an item. |
| siteChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the select site changes. | | siteChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted when the select site changes. |
| navigationChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<NodeEntryEvent>` | Emitted when the navigation changes. |
## Details ## Details

View File

@@ -36,11 +36,10 @@ class MyComponent {
- _outcome:_ `string` - [Form](../../../lib/process-services/src/lib/task-list/models/form.model.ts) outcome - _outcome:_ `string` - [Form](../../../lib/process-services/src/lib/task-list/models/form.model.ts) outcome
- _version:_ `number` - of the form - _version:_ `number` - of the form
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TaskDetailsCloudModel`](../../../lib/process-services-cloud/src/lib/task/start-task/models/task-details-cloud.model.ts)`>` - Updated task details - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TaskDetailsCloudModel`](../../../lib/process-services-cloud/src/lib/task/start-task/models/task-details-cloud.model.ts)`>` - Updated task details
- **createTemporaryRawRelatedContent**(file: `any`, nodeId: `string`, contentHost: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<any>`<br/> - **createTemporaryRawRelatedContent**(file: `any`, nodeId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<any>`<br/>
- _file:_ `any` - - _file:_ `any` -
- _nodeId:_ `string` - - _nodeId:_ `string` -
- _contentHost:_ `string` -
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<any>` - - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<any>` -
- **getBasePath**(appName: `string`): `string`<br/> - **getBasePath**(appName: `string`): `string`<br/>
@@ -58,13 +57,6 @@ class MyComponent {
- _formKey:_ `string` - key of the target task - _formKey:_ `string` - key of the target task
- _version:_ `number` - (Optional) Version of the form - _version:_ `number` - (Optional) Version of the form
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`FormContent`](../../../lib/process-services-cloud/src/lib/services/form-fields.interfaces.ts)`>` - Form definition - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`FormContent`](../../../lib/process-services-cloud/src/lib/services/form-fields.interfaces.ts)`>` - Form definition
- **getProcessStorageFolderTask**(appName: `string`, taskId: `string`, processInstanceId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ProcessStorageCloudModel`](../../../lib/process-services-cloud/src/lib/form/models/task-variable-cloud.model.ts)`>`<br/>
- _appName:_ `string` -
- _taskId:_ `string` -
- _processInstanceId:_ `string` -
- **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`ProcessStorageCloudModel`](../../../lib/process-services-cloud/src/lib/form/models/task-variable-cloud.model.ts)`>` -
- **getTask**(appName: `string`, taskId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TaskDetailsCloudModel`](../../../lib/process-services-cloud/src/lib/task/start-task/models/task-details-cloud.model.ts)`>`<br/> - **getTask**(appName: `string`, taskId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TaskDetailsCloudModel`](../../../lib/process-services-cloud/src/lib/task/start-task/models/task-details-cloud.model.ts)`>`<br/>
Gets details of a task Gets details of a task
- _appName:_ `string` - Name of the app - _appName:_ `string` - Name of the app

View File

@@ -4,6 +4,19 @@
"C362240": "Include once ADF starts using ACS 7 , https://issues.alfresco.com/jira/browse/ADF-5182", "C362240": "Include once ADF starts using ACS 7 , https://issues.alfresco.com/jira/browse/ADF-5182",
"C362241": "Include once ADF starts using ACS 7, https://issues.alfresco.com/jira/browse/ADF-5182", "C362241": "Include once ADF starts using ACS 7, https://issues.alfresco.com/jira/browse/ADF-5182",
"C362242": "Include once ADF starts using ACS 7, https://issues.alfresco.com/jira/browse/ADF-5182", "C362242": "Include once ADF starts using ACS 7, https://issues.alfresco.com/jira/browse/ADF-5182",
"C362265": "Include once ADF starts using ACS 7, https://issues.alfresco.com/jira/browse/ADF-5182" "C362265": "Include once ADF starts using ACS 7, https://issues.alfresco.com/jira/browse/ADF-5182",
"C311290": "Include once process storage services removed, https://issues.alfresco.com/jira/browse/AAE-3177",
"C310358": "Include once process storage services removed, https://issues.alfresco.com/jira/browse/AAE-3177",
"C311285": "Include once process storage services removed, https://issues.alfresco.com/jira/browse/AAE-3177",
"C311287": "Include once process storage services removed, https://issues.alfresco.com/jira/browse/AAE-3177",
"C311288": "Include once process storage services removed, https://issues.alfresco.com/jira/browse/AAE-3177",
"C311289": "Include once process storage services removed, https://issues.alfresco.com/jira/browse/AAE-3177",
"C311292": "Include once process storage services removed, https://issues.alfresco.com/jira/browse/AAE-3177",
"C311293": "Include once process storage services removed, https://issues.alfresco.com/jira/browse/AAE-3177",
"C311295": "Include once process storage services removed, https://issues.alfresco.com/jira/browse/AAE-3177",
"C315292": "Include once process storage services removed, https://issues.alfresco.com/jira/browse/AAE-3177",
"C260140": "Include once process storage services removed, https://issues.alfresco.com/jira/browse/AAE-3177",
"C261160": "Include once process storage services removed, https://issues.alfresco.com/jira/browse/AAE-3177",
"C246534": "Include once process storage services removed, https://issues.alfresco.com/jira/browse/AAE-3177"
} }

View File

@@ -71,7 +71,7 @@
[allowDropFiles]="false" [allowDropFiles]="false"
[sorting]="'server'" [sorting]="'server'"
[where]="where" [where]="where"
(folderChange)="onFolderChange()" (folderChange)="onFolderChange($event)"
(ready)="onFolderLoaded()" (ready)="onFolderLoaded()"
(nodeSelected)="onCurrentSelection($event)" (nodeSelected)="onCurrentSelection($event)"
data-automation-id="content-node-selector-document-list"> data-automation-id="content-node-selector-document-list">

View File

@@ -29,7 +29,7 @@ import { DocumentListService } from '../document-list/services/document-list.ser
import { DocumentListComponent } from '../document-list/components/document-list.component'; import { DocumentListComponent } from '../document-list/components/document-list.component';
import { DropdownSitesComponent } from '../site-dropdown/sites-dropdown.component'; import { DropdownSitesComponent } from '../site-dropdown/sites-dropdown.component';
import { CustomResourcesService } from '../document-list/services/custom-resources.service'; import { CustomResourcesService } from '../document-list/services/custom-resources.service';
import { ShareDataRow } from '../document-list'; import { NodeEntryEvent, ShareDataRow } from '../document-list';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
const ONE_FOLDER_RESULT = { const ONE_FOLDER_RESULT = {
@@ -59,6 +59,8 @@ describe('ContentNodeSelectorComponent', () => {
let sitesService: SitesService; let sitesService: SitesService;
let searchSpy: jasmine.Spy; let searchSpy: jasmine.Spy;
let cnSearchSpy: jasmine.Spy; let cnSearchSpy: jasmine.Spy;
const fakeNodeEntry = new Node({ id: 'fakeId' });
const nodeEntryEvent = new NodeEntryEvent(fakeNodeEntry);
let _observer: Observer<NodePaging>; let _observer: Observer<NodePaging>;
@@ -271,7 +273,7 @@ describe('ContentNodeSelectorComponent', () => {
tick(debounceSearch); tick(debounceSearch);
component.onFolderChange(); component.onFolderChange(nodeEntryEvent);
fixture.detectChanges(); fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent)); const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull(); expect(breadcrumb).not.toBeNull();
@@ -309,7 +311,7 @@ describe('ContentNodeSelectorComponent', () => {
respondWithSearchResults(ONE_FOLDER_RESULT); respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.detectChanges(); fixture.detectChanges();
component.onFolderChange(); component.onFolderChange(nodeEntryEvent);
fixture.detectChanges(); fixture.detectChanges();
const chosenNode = <Node> { path: { elements: [] } }; const chosenNode = <Node> { path: { elements: [] } };
@@ -700,7 +702,7 @@ describe('ContentNodeSelectorComponent', () => {
tick(); tick();
fixture.detectChanges(); fixture.detectChanges();
component.onFolderChange(); component.onFolderChange(nodeEntryEvent);
fixture.detectChanges(); fixture.detectChanges();
expect(component.clearSearch).toHaveBeenCalled(); expect(component.clearSearch).toHaveBeenCalled();

View File

@@ -33,7 +33,7 @@ import { ImageResolver } from '../document-list/data/image-resolver.model';
import { ContentNodeSelectorService } from './content-node-selector.service'; import { ContentNodeSelectorService } from './content-node-selector.service';
import { debounceTime, takeUntil } from 'rxjs/operators'; import { debounceTime, takeUntil } from 'rxjs/operators';
import { CustomResourcesService } from '../document-list/services/custom-resources.service'; import { CustomResourcesService } from '../document-list/services/custom-resources.service';
import { ShareDataRow } from '../document-list'; import { NodeEntryEvent, ShareDataRow } from '../document-list';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
export type ValidationFunction = (entry: Node) => boolean; export type ValidationFunction = (entry: Node) => boolean;
@@ -186,6 +186,10 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy {
@Output() @Output()
select: EventEmitter<Node[]> = new EventEmitter<Node[]>(); select: EventEmitter<Node[]> = new EventEmitter<Node[]>();
/** Emitted when the navigation changes. */
@Output()
navigationChange: EventEmitter<NodeEntryEvent> = new EventEmitter<NodeEntryEvent>();
/** Emitted when the select site changes. */ /** Emitted when the select site changes. */
@Output() @Output()
siteChange: EventEmitter<string> = new EventEmitter<string>(); siteChange: EventEmitter<string> = new EventEmitter<string>();
@@ -426,11 +430,12 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy {
/** /**
* Sets showingSearchResults state to be able to differentiate between search results or folder results * Sets showingSearchResults state to be able to differentiate between search results or folder results
*/ */
onFolderChange(): void { onFolderChange($event: NodeEntryEvent): void {
this.showingSearchResults = false; this.showingSearchResults = false;
this.infiniteScroll = false; this.infiniteScroll = false;
this.breadcrumbFolderTitle = null; this.breadcrumbFolderTitle = null;
this.clearSearch(); this.clearSearch();
this.navigationChange.emit($event);
} }
/** /**

View File

@@ -36,4 +36,6 @@ export interface ContentNodeSelectorComponentData {
showSearch?: boolean; showSearch?: boolean;
showFilesInResult?: boolean; showFilesInResult?: boolean;
showDropdownSiteList?: boolean; showDropdownSiteList?: boolean;
showLocalUploadButton?: boolean;
multipleUpload?: boolean;
} }

View File

@@ -21,21 +21,33 @@
[showDropdownSiteList]="data?.showDropdownSiteList" [showDropdownSiteList]="data?.showDropdownSiteList"
[showFilesInResult]="data?.showFilesInResult" [showFilesInResult]="data?.showFilesInResult"
(select)="onSelect($event)" (select)="onSelect($event)"
(siteChange)="onSiteChange($event)"> (siteChange)="onSiteChange($event)"
(navigationChange)="onNavigationChange($event)">
</adf-content-node-selector-panel> </adf-content-node-selector-panel>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions align="end"> <mat-dialog-actions>
<button <div>
mat-button <adf-upload-button
(click)="close()" *ngIf="data?.showLocalUploadButton"
data-automation-id="content-node-selector-actions-cancel">{{ 'NODE_SELECTOR.CANCEL' | translate }} [staticTitle]="'FORM.FIELD.UPLOAD' | translate "
</button> [multipleFiles]="isMultipleSelection()"
[rootFolderId]="currentDirectoryId"
(error)="onError($event)">
</adf-upload-button>
</div>
<div>
<button
mat-button
(click)="close()"
data-automation-id="content-node-selector-actions-cancel">{{ 'NODE_SELECTOR.CANCEL' | translate }}
</button>
<button mat-button <button mat-button
[disabled]="!hasNodeSelected()" [disabled]="!chosenNode"
class="adf-choose-action" class="adf-choose-action"
(click)="onClick()" (click)="onClick()"
data-automation-id="content-node-selector-actions-choose">{{ buttonActionName | translate }} data-automation-id="content-node-selector-actions-choose">{{ buttonActionName | translate }}
</button> </button>
</div>
</mat-dialog-actions> </mat-dialog-actions>

View File

@@ -36,7 +36,8 @@
padding: 8px; padding: 8px;
background-color: mat-color($background, background); background-color: mat-color($background, background);
display: flex; display: flex;
justify-content: flex-end; flex-direction: row;
justify-content: space-between;
color: mat-color($foreground, text, 0.72); color: mat-color($foreground, text, 0.72);
button { button {

View File

@@ -40,7 +40,8 @@ describe('ContentNodeSelectorDialogComponent', () => {
select: new EventEmitter<Node>(), select: new EventEmitter<Node>(),
rowFilter: (shareDataRow: ShareDataRow) => shareDataRow.node.entry.name === 'impossible-name', rowFilter: (shareDataRow: ShareDataRow) => shareDataRow.node.entry.name === 'impossible-name',
imageResolver: () => 'piccolo', imageResolver: () => 'piccolo',
currentFolderId: 'cat-girl-nuku-nuku' currentFolderId: 'cat-girl-nuku-nuku',
showLocalUploadButton: true
}; };
setupTestBed({ setupTestBed({
@@ -175,4 +176,22 @@ describe('ContentNodeSelectorDialogComponent', () => {
expect(titleElement.nativeElement.innerText).toBe('NODE_SELECTOR.CHOOSE_ITEM'); expect(titleElement.nativeElement.innerText).toBe('NODE_SELECTOR.CHOOSE_ITEM');
}); });
}); });
describe('Upload button', () => {
it('should be able to show upload button if showLocalUploadButton set to true', () => {
const adfUploadButton = fixture.debugElement.query(By.css('adf-upload-button'));
expect(adfUploadButton).not.toBeNull();
expect(adfUploadButton.nativeElement.innerText).toEqual('file_uploadFORM.FIELD.UPLOAD');
});
it('should not be able to show upload button if showLocalUploadButton set to false', () => {
component.data.showLocalUploadButton = false;
fixture.detectChanges();
const adfUploadButton = fixture.debugElement.query(By.css('adf-upload-button span'));
expect(adfUploadButton).toBeNull();
});
});
}); });

View File

@@ -17,9 +17,10 @@
import { Component, Inject, ViewEncapsulation } from '@angular/core'; import { Component, Inject, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { TranslationService } from '@alfresco/adf-core'; import { TranslationService, NotificationService } from '@alfresco/adf-core';
import { Node } from '@alfresco/js-api'; import { Node } from '@alfresco/js-api';
import { ContentNodeSelectorComponentData } from './content-node-selector.component-data.interface'; import { ContentNodeSelectorComponentData } from './content-node-selector.component-data.interface';
import { NodeEntryEvent } from '../document-list/components/node.event';
@Component({ @Component({
selector: 'adf-content-node-selector', selector: 'adf-content-node-selector',
@@ -28,17 +29,19 @@ import { ContentNodeSelectorComponentData } from './content-node-selector.compon
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class ContentNodeSelectorComponent { export class ContentNodeSelectorComponent {
title: string; title: string;
action: string; action: string;
buttonActionName: string; buttonActionName: string;
chosenNode: Node[]; chosenNode: Node[];
currentDirectoryId: string;
constructor(private translation: TranslationService, constructor(private translation: TranslationService,
private notificationService: NotificationService,
@Inject(MAT_DIALOG_DATA) public data: ContentNodeSelectorComponentData) { @Inject(MAT_DIALOG_DATA) public data: ContentNodeSelectorComponentData) {
this.action = data.actionName ? data.actionName.toUpperCase() : 'CHOOSE'; this.action = data.actionName ? data.actionName.toUpperCase() : 'CHOOSE';
this.buttonActionName = `NODE_SELECTOR.${this.action}`; this.buttonActionName = `NODE_SELECTOR.${this.action}`;
this.title = data.title; this.title = data.title;
this.currentDirectoryId = data.currentFolderId;
} }
close() { close() {
@@ -49,14 +52,14 @@ export class ContentNodeSelectorComponent {
this.chosenNode = nodeList; this.chosenNode = nodeList;
} }
hasNodeSelected(): boolean {
return this.chosenNode?.length > 0;
}
onSiteChange(siteTitle: string) { onSiteChange(siteTitle: string) {
this.updateTitle(siteTitle); this.updateTitle(siteTitle);
} }
onNavigationChange(pathElement: NodeEntryEvent) {
this.currentDirectoryId = pathElement.value.id;
}
onClick(): void { onClick(): void {
this.data.select.next(this.chosenNode); this.data.select.next(this.chosenNode);
this.data.select.complete(); this.data.select.complete();
@@ -71,4 +74,12 @@ export class ContentNodeSelectorComponent {
getTitleTranslation(action: string, name: string): string { getTitleTranslation(action: string, name: string): string {
return this.translation.instant(`NODE_SELECTOR.${action}_ITEM`, { name: this.translation.instant(name) }); return this.translation.instant(`NODE_SELECTOR.${action}_ITEM`, { name: this.translation.instant(name) });
} }
isMultipleSelection(): boolean {
return this.data.selectionMode === 'multiple';
}
onError(error) {
this.notificationService.showError(error);
}
} }

View File

@@ -27,6 +27,7 @@ import { BreadcrumbModule } from '../breadcrumb/breadcrumb.module';
import { CoreModule } from '@alfresco/adf-core'; import { CoreModule } from '@alfresco/adf-core';
import { DocumentListModule } from '../document-list/document-list.module'; import { DocumentListModule } from '../document-list/document-list.module';
import { NameLocationCellComponent } from './name-location-cell/name-location-cell.component'; import { NameLocationCellComponent } from './name-location-cell/name-location-cell.component';
import { UploadModule } from '../upload/upload.module';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -37,7 +38,8 @@ import { NameLocationCellComponent } from './name-location-cell/name-location-ce
MaterialModule, MaterialModule,
SitesDropdownModule, SitesDropdownModule,
BreadcrumbModule, BreadcrumbModule,
DocumentListModule DocumentListModule,
UploadModule
], ],
exports: [ exports: [
ContentNodeSelectorPanelComponent, ContentNodeSelectorPanelComponent,

View File

@@ -45,7 +45,8 @@ import {
RequestPaginationModel, RequestPaginationModel,
AlfrescoApiService, AlfrescoApiService,
UserPreferenceValues, UserPreferenceValues,
LockService LockService,
UploadService
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { Node, NodeEntry, NodePaging, Pagination } from '@alfresco/js-api'; import { Node, NodeEntry, NodePaging, Pagination } from '@alfresco/js-api';
@@ -60,7 +61,7 @@ import { NavigableComponentInterface } from '../../breadcrumb/navigable-componen
import { RowFilter } from '../data/row-filter.model'; import { RowFilter } from '../data/row-filter.model';
import { DocumentListService } from '../services/document-list.service'; import { DocumentListService } from '../services/document-list.service';
import { DocumentLoaderNode } from '../models/document-folder.model'; import { DocumentLoaderNode } from '../models/document-folder.model';
import { takeUntil } from 'rxjs/operators'; import { debounceTime, takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'adf-document-list', selector: 'adf-document-list',
@@ -337,6 +338,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
private appConfig: AppConfigService, private appConfig: AppConfigService,
private userPreferencesService: UserPreferencesService, private userPreferencesService: UserPreferencesService,
private contentService: ContentService, private contentService: ContentService,
private uploadService: UploadService,
private thumbnailService: ThumbnailService, private thumbnailService: ThumbnailService,
private alfrescoApiService: AlfrescoApiService, private alfrescoApiService: AlfrescoApiService,
private lockService: LockService) { private lockService: LockService) {
@@ -346,6 +348,18 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
.subscribe(pagSize => { .subscribe(pagSize => {
this.maxItems = this._pagination.maxItems = pagSize; this.maxItems = this._pagination.maxItems = pagSize;
}); });
this.uploadService.fileUploadComplete
.pipe(
debounceTime(300),
takeUntil(this.onDestroy$))
.subscribe(() => this.reload());
this.uploadService.fileUploadDeleted
.pipe(
debounceTime(300),
takeUntil(this.onDestroy$))
.subscribe(() => this.reload());
} }
getContextActions(node: NodeEntry) { getContextActions(node: NodeEntry) {

View File

@@ -80,6 +80,7 @@
"NODE_SELECTOR": { "NODE_SELECTOR": {
"CANCEL": "Cancel", "CANCEL": "Cancel",
"CHOOSE": "Select", "CHOOSE": "Select",
"ATTACH": "Attach",
"CHOOSE_ITEM": "Select content to attach from '{{ name }}'", "CHOOSE_ITEM": "Select content to attach from '{{ name }}'",
"COPY": "Copy", "COPY": "Copy",
"COPY_ITEM": "Copy '{{ name }}' to...", "COPY_ITEM": "Copy '{{ name }}' to...",

View File

@@ -5,7 +5,7 @@
<!--Single Files Upload--> <!--Single Files Upload-->
<button *ngIf="!multipleFiles" <button *ngIf="!multipleFiles"
[disabled]="isButtonDisabled()" [disabled]="isButtonDisabled()"
mat-raised-button color="primary" mat-button
(click)="uploadSingleFile.click()"> (click)="uploadSingleFile.click()">
<mat-icon>file_upload</mat-icon> <mat-icon>file_upload</mat-icon>
<span id="upload-single-file-label" <span id="upload-single-file-label"
@@ -27,7 +27,7 @@
<!--Multiple Files Upload--> <!--Multiple Files Upload-->
<button *ngIf="multipleFiles" <button *ngIf="multipleFiles"
[disabled]="isButtonDisabled()" [disabled]="isButtonDisabled()"
mat-raised-button color="primary" mat-button
(click)="uploadMultipleFiles.click()"> (click)="uploadMultipleFiles.click()">
<mat-icon>file_upload</mat-icon> <mat-icon>file_upload</mat-icon>
@@ -53,7 +53,7 @@
<div *ngIf="uploadFolders"> <div *ngIf="uploadFolders">
<button <button
[disabled]="isButtonDisabled()" [disabled]="isButtonDisabled()"
mat-raised-button color="primary" mat-button
(click)="uploadFolders.click()"> (click)="uploadFolders.click()">
<mat-icon>file_upload</mat-icon> <mat-icon>file_upload</mat-icon>
<span id="uploadFolder-label" <span id="uploadFolder-label"

View File

@@ -24,4 +24,5 @@ export interface FormFieldFileSource {
name: string; name: string;
selectedFolder: FormFieldSelectedFolder; selectedFolder: FormFieldSelectedFolder;
serviceId: string; serviceId: string;
destinationFolderPath: string;
} }

View File

@@ -69,7 +69,6 @@ export class FormModel {
json: any; json: any;
nodeId: string; nodeId: string;
contentHost: string;
values: FormValues = {}; values: FormValues = {};
tabs: TabModel[] = []; tabs: TabModel[] = [];
fields: FormWidgetModel[] = []; fields: FormWidgetModel[] = [];

View File

@@ -36,7 +36,8 @@
"AT_LEAST_LONG": "Enter at least {{ minLength }} characters", "AT_LEAST_LONG": "Enter at least {{ minLength }} characters",
"NO_LONGER_THAN": "Enter no more than {{ maxLength }} characters" "NO_LONGER_THAN": "Enter no more than {{ maxLength }} characters"
}, },
"FILE_ALREADY_UPLOADED": "A file with the same name is already uploaded." "FILE_ALREADY_UPLOADED": "A file with the same name is already uploaded.",
"ATTACH":"Attach"
}, },
"FORM_RENDERER": { "FORM_RENDERER": {
"NAMELESS_TASK": "Nameless task" "NAMELESS_TASK": "Nameless task"

View File

@@ -17,7 +17,7 @@
import { Component, DebugElement, SimpleChange, NgModule, Injector, ComponentFactoryResolver, ViewChild } from '@angular/core'; import { Component, DebugElement, SimpleChange, NgModule, Injector, ComponentFactoryResolver, ViewChild } from '@angular/core';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable, of, throwError } from 'rxjs'; import { Observable, of, throwError } from 'rxjs';
import { import {
AppConfigService, AppConfigService,
@@ -370,45 +370,6 @@ describe('FormCloudComponent', () => {
expect(formComponent.getFormByTaskId).toHaveBeenCalledWith(appName, taskId, 1); expect(formComponent.getFormByTaskId).toHaveBeenCalledWith(appName, taskId, 1);
}); });
it('should call the process storage to retrieve the folder with only the taskId', fakeAsync(() => {
spyOn(formCloudService, 'getTaskForm').and.returnValue(of(cloudFormMock));
spyOn(formCloudService, 'getTaskVariables').and.returnValue(of([]));
spyOn(formCloudService, 'getProcessStorageFolderTask')
.and.returnValue(of({ nodeId: '123', path: '/a/path/type', type: 'fakeType' }));
const taskId = '<task id>';
const appName = 'test-app';
formComponent.appName = appName;
formComponent.taskId = taskId;
const change = new SimpleChange(null, appName, true);
formComponent.ngOnChanges({ 'appName': change });
tick();
expect(formCloudService.getProcessStorageFolderTask).toHaveBeenCalledWith(appName, taskId, undefined);
expect(formComponent.form.nodeId).toBe('123');
expect(formComponent.form.contentHost).toBe('/a/path/type');
}));
it('should call the process storage to retrieve the folder with taskId and processInstanceId', fakeAsync(() => {
spyOn(formCloudService, 'getTaskForm').and.returnValue(of(cloudFormMock));
spyOn(formCloudService, 'getTaskVariables').and.returnValue(of([]));
spyOn(formCloudService, 'getProcessStorageFolderTask')
.and.returnValue(of({ nodeId: '123', path: '/a/path/type', type: 'fakeType' }));
const taskId = '<task id>';
const processInstanceId = 'i-am-the-process-instance-id';
const appName = 'test-app';
formComponent.appName = appName;
formComponent.taskId = taskId;
formComponent.processInstanceId = processInstanceId;
const change = new SimpleChange(null, 'new-app-name', true);
formComponent.ngOnChanges({ 'appName': change });
tick();
expect(formCloudService.getProcessStorageFolderTask).toHaveBeenCalledWith(appName, taskId, processInstanceId);
expect(formComponent.form.nodeId).toBe('123');
expect(formComponent.form.contentHost).toBe('/a/path/type');
}));
it('should reload form definition by form id on binding changes', () => { it('should reload form definition by form id on binding changes', () => {
spyOn(formComponent, 'getFormById').and.stub(); spyOn(formComponent, 'getFormById').and.stub();
const formId = '123'; const formId = '123';

View File

@@ -28,12 +28,10 @@ import {
FormOutcomeModel, FormOutcomeModel,
WidgetVisibilityService, WidgetVisibilityService,
FormService, FormService,
NotificationService,
FORM_FIELD_VALIDATORS, FORM_FIELD_VALIDATORS,
FormFieldValidator, FormFieldValidator,
FormValues, FormValues,
FormModel, FormModel,
AppConfigService,
ContentLinkModel ContentLinkModel
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { FormCloudService } from '../services/form-cloud.service'; import { FormCloudService } from '../services/form-cloud.service';
@@ -106,9 +104,7 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
constructor(protected formCloudService: FormCloudService, constructor(protected formCloudService: FormCloudService,
protected formService: FormService, protected formService: FormService,
private notificationService: NotificationService, protected visibilityService: WidgetVisibilityService) {
protected visibilityService: WidgetVisibilityService,
private appConfigService: AppConfigService) {
super(); super();
this.formService.formContentClicked this.formService.formContentClicked
@@ -123,7 +119,7 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
if (appName && appName.currentValue) { if (appName && appName.currentValue) {
if (this.taskId) { if (this.taskId) {
this.getFormDefinitionWithFolderTask(this.appName, this.taskId, this.processInstanceId); this.getFormByTaskId(appName.currentValue, this.taskId, this.appVersion);
} else if (this.formId) { } else if (this.formId) {
this.getFormById(appName.currentValue, this.formId, this.appVersion); this.getFormById(appName.currentValue, this.formId, this.appVersion);
} }
@@ -200,7 +196,6 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
parsedForm.validateForm(); parsedForm.validateForm();
this.form = parsedForm; this.form = parsedForm;
this.form.nodeId = '-my-'; this.form.nodeId = '-my-';
this.form.contentHost = this.appConfigService.get('ecmHost');
this.onFormLoaded(this.form); this.onFormLoaded(this.form);
resolve(this.form); resolve(this.form);
}, },
@@ -230,7 +225,6 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
parsedForm.validateForm(); parsedForm.validateForm();
this.form = parsedForm; this.form = parsedForm;
this.form.nodeId = '-my-'; this.form.nodeId = '-my-';
this.form.contentHost = this.appConfigService.get('ecmHost');
this.onFormLoaded(this.form); this.onFormLoaded(this.form);
}, },
(error) => { (error) => {
@@ -239,31 +233,6 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
); );
} }
getFormDefinitionWithFolderTask(appName: string, taskId: string, processInstanceId: string) {
this.getFormDefinitionWithFolder(appName, taskId, processInstanceId);
}
async getFormDefinitionWithFolder(appName: string, taskId: string, processInstanceId: string) {
try {
await this.getFormByTaskId(appName, taskId, this.appVersion);
const hasUploadWidget = (<any> this.form).hasUpload;
if (hasUploadWidget) {
try {
const processStorageCloudModel = await this.formCloudService.getProcessStorageFolderTask(appName, taskId, processInstanceId).toPromise();
this.form.nodeId = processStorageCloudModel.nodeId;
this.form.contentHost = processStorageCloudModel.path;
} catch (error) {
this.notificationService.openSnackMessage('The content repo is not configured');
}
}
} catch (error) {
this.notificationService.openSnackMessage('Form service an error occour');
}
}
saveTaskForm() { saveTaskForm() {
if (this.form && this.appName && this.taskId) { if (this.form && this.appName && this.taskId) {
this.formCloudService this.formCloudService

View File

@@ -5,35 +5,11 @@
<span *ngIf="isRequired()">*</span> <span *ngIf="isRequired()">*</span>
</label> </label>
<div class="adf-attach-widget-container"> <div class="adf-attach-widget-container">
<div id="adf-attach-widget-simple-upload" *ngIf="isSimpleUploadButton() && isUploadButtonVisible()"> <div class="adf-attach-widget__menu-upload" *ngIf="isUploadButtonVisible()">
<a mat-raised-button color="primary"> <button (click)="openSelectDialog()" mat-raised-button color="primary" [id]="field.id">
{{ 'FORM.FIELD.UPLOAD' | translate }} {{ 'FORM.FIELD.ATTACH' | translate }}
<mat-icon>file_upload</mat-icon> <mat-icon>{{getWidgetIcon()}}</mat-icon>
<input #uploadFiles
[multiple]="multipleOption"
type="file"
[id]="field.id"
(change)="onAttachFileChanged($event)"/>
</a>
</div>
<div class="adf-attach-widget__menu-upload" *ngIf="isUploadButtonVisible() && isMultipleSourceUpload()">
<button mat-raised-button color="primary" [matMenuTriggerFor]="menu" [id]="field.id">
{{ 'FORM.FIELD.UPLOAD' | translate }}
<mat-icon>attach_file</mat-icon>
</button> </button>
<mat-menu #menu="matMenu" class="adf-attach-widget__menu-content">
<div *ngIf="isContentSourceSelected()">
<button mat-menu-item
id="attach-{{field.params?.fileSource?.name}}"
(click)="uploadFileFromCS()">
{{field.params?.fileSource?.name}}
<mat-icon>
<img alt="alfresco" class="adf-attach-widget__image-logo"
src="./assets/images/alfresco-flower.svg">
</mat-icon>
</button>
</div>
</mat-menu>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -27,7 +27,8 @@ import {
FormFieldTypes, FormFieldTypes,
FormFieldMetadata, FormFieldMetadata,
FormService, FormService,
DownloadService DownloadService,
NotificationService
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module'; import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@@ -44,6 +45,7 @@ describe('AttachFileCloudWidgetComponent', () => {
let element: HTMLInputElement; let element: HTMLInputElement;
let contentCloudNodeSelectorService: ContentCloudNodeSelectorService; let contentCloudNodeSelectorService: ContentCloudNodeSelectorService;
let processCloudContentService: ProcessCloudContentService; let processCloudContentService: ProcessCloudContentService;
let notificationService: NotificationService;
let formService: FormService; let formService: FormService;
let downloadService: DownloadService; let downloadService: DownloadService;
@@ -80,6 +82,22 @@ describe('AttachFileCloudWidgetComponent', () => {
} }
}; };
const allSourceParams = {
fileSource: {
name: 'all file sources',
serviceId: 'all-file-sources',
destinationFolderPath: '-root-/myfiles'
}
};
const allSourceParamsWithWrongPath = {
fileSource: {
name: 'all file sources',
serviceId: 'all-file-sources',
destinationFolderPath: 'mock-folder'
}
};
const fakeMinimalNode: Node = <Node> { const fakeMinimalNode: Node = <Node> {
id: 'fake', id: 'fake',
name: 'fake-name', name: 'fake-name',
@@ -88,25 +106,9 @@ describe('AttachFileCloudWidgetComponent', () => {
} }
}; };
const fakeLocalPngAnswer = { const mockNodeId = new Promise(function (resolve) {
id: 1155, resolve('mock-node-id');
nodeId: 1155, });
name: 'a_png_file.png',
created: '2017-07-25T17:17:37.099Z',
createdBy: {
id: 1001,
firstName: 'Admin',
lastName: 'admin',
email: 'admin'
},
relatedContent: false,
contentAvailable: true,
link: false,
mimeType: 'image/png',
simpleType: 'image',
previewStatus: 'queued',
thumbnailStatus: 'queued'
};
setupTestBed({ setupTestBed({
imports: [ imports: [
@@ -128,23 +130,20 @@ describe('AttachFileCloudWidgetComponent', () => {
ContentCloudNodeSelectorService ContentCloudNodeSelectorService
); );
formService = TestBed.inject(FormService); formService = TestBed.inject(FormService);
notificationService = TestBed.inject(NotificationService);
})); }));
afterEach(() => { afterEach(() => {
fixture.destroy(); fixture.destroy();
}); });
it('should be able to create the widget', () => {
expect(widget).not.toBeNull();
});
it('should show up as simple upload when is configured for only local files', async(() => { it('should show up as simple upload when is configured for only local files', async(() => {
widget.field = new FormFieldModel(new FormModel(), { widget.field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.UPLOAD, type: FormFieldTypes.UPLOAD,
value: [] value: []
}); });
widget.field.id = 'simple-upload-button'; widget.field.id = 'simple-upload-button';
widget.field.params = <FormFieldMetadata> onlyLocalParams; widget.field.params = <FormFieldMetadata> allSourceParams;
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
expect( expect(
@@ -168,7 +167,7 @@ describe('AttachFileCloudWidgetComponent', () => {
}); });
})); }));
it('should be able to attach files coming from content selector', async(() => { it('should be able to attach files coming from content selector', async() => {
spyOn( spyOn(
contentCloudNodeSelectorService, contentCloudNodeSelectorService,
'openUploadFileDialog' 'openUploadFileDialog'
@@ -180,47 +179,108 @@ describe('AttachFileCloudWidgetComponent', () => {
widget.field.id = 'attach-file-alfresco'; widget.field.id = 'attach-file-alfresco';
widget.field.params = <FormFieldMetadata> contentSourceParam; widget.field.params = <FormFieldMetadata> contentSourceParam;
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { await fixture.whenStable();
const attachButton: HTMLButtonElement = element.querySelector( const attachButton: HTMLButtonElement = element.querySelector('#attach-file-alfresco');
'#attach-file-alfresco'
);
expect(attachButton).not.toBeNull();
attachButton.click();
fixture.whenStable().then(() => {
fixture.detectChanges();
fixture.debugElement
.query(By.css('#attach-mock-alf-content'))
.nativeElement.click();
fixture.detectChanges();
expect(element.querySelector('#file-fake-icon')).not.toBeNull();
});
});
}));
it('should be able to upload files from local source', async(() => { expect(attachButton).not.toBeNull();
attachButton.click();
await fixture.whenStable();
fixture.detectChanges();
const attachedFileName = fixture.debugElement.query(By.css('.adf-file'));
const fileIcon = element.querySelector('#file-fake-icon');
expect(attachedFileName.nativeElement.innerText).toEqual('fake-name');
expect(fileIcon).not.toBeNull();
});
it('should be able to attach files coming from all files source', async() => {
spyOn(contentCloudNodeSelectorService, 'fetchNodeIdFromRelativePath').and.returnValue(mockNodeId);
spyOn(
contentCloudNodeSelectorService,
'openUploadFileDialog'
).and.returnValue(of([fakeMinimalNode]));
widget.field = new FormFieldModel(new FormModel(), { widget.field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.UPLOAD, type: FormFieldTypes.UPLOAD,
value: [] value: []
}); });
widget.field.id = 'attach-file-local'; widget.field.id = 'attach-file-alfresco';
widget.field.params = <FormFieldMetadata> onlyLocalParams; widget.field.params = <FormFieldMetadata> allSourceParams;
spyOn(
processCloudContentService,
'createTemporaryRawRelatedContent'
).and.returnValue(of(fakeLocalPngAnswer));
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { await fixture.whenStable();
fixture.detectChanges(); const attachButton: HTMLButtonElement = element.querySelector('#attach-file-alfresco');
const inputDebugElement = fixture.debugElement.query(
By.css('#attach-file-local') expect(attachButton).not.toBeNull();
);
inputDebugElement.triggerEventHandler('change', { attachButton.click();
target: { files: [fakeLocalPngAnswer] } await fixture.whenStable();
}); fixture.detectChanges();
fixture.detectChanges(); const attachedFileName = fixture.debugElement.query(By.css('.adf-file'));
expect(element.querySelector('#file-1155-icon')).not.toBeNull(); const fileIcon = element.querySelector('#file-fake-icon');
expect(attachedFileName.nativeElement.innerText).toEqual('fake-name');
expect(fileIcon).not.toBeNull();
});
it('should be able to fetch nodeId if destinationFolderPtah defined ', async() => {
const fetchNodeIdFromRelativePathSpy = spyOn(contentCloudNodeSelectorService, 'fetchNodeIdFromRelativePath').and.returnValue(mockNodeId);
spyOn(
contentCloudNodeSelectorService,
'openUploadFileDialog'
).and.returnValue(of([fakeMinimalNode]));
widget.field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.UPLOAD,
value: []
}); });
})); widget.field.id = 'attach-file-alfresco';
widget.field.params = <FormFieldMetadata> allSourceParams;
fixture.detectChanges();
await fixture.whenStable();
const attachButton: HTMLButtonElement = element.querySelector('#attach-file-alfresco');
expect(attachButton).not.toBeNull();
attachButton.click();
await fixture.whenStable();
fixture.detectChanges();
const alias = '-root-';
const opt = { relativePath: '/myfiles' };
expect(fetchNodeIdFromRelativePathSpy).toHaveBeenCalledWith(alias, opt);
expect(widget.field.params.fileSource.destinationFolderPath).toBe('-root-/myfiles');
expect(widget.rootNodeId).toEqual('mock-node-id');
});
it('should be able to show error notification if destinationFolderPtah wrong/undefined', async() => {
const fetchNodeIdFromRelativePathSpy = spyOn(contentCloudNodeSelectorService, 'fetchNodeIdFromRelativePath').and.returnValue(mockNodeId);
spyOn(
contentCloudNodeSelectorService,
'openUploadFileDialog'
).and.returnValue(of([fakeMinimalNode]));
widget.field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.UPLOAD,
value: []
});
const showErrorSpy = spyOn(notificationService, 'showError').and.callThrough();
widget.field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.UPLOAD,
value: []
});
widget.field.id = 'attach-file-alfresco';
widget.field.params = <FormFieldMetadata> allSourceParamsWithWrongPath;
fixture.detectChanges();
await fixture.whenStable();
const attachButton: HTMLButtonElement = element.querySelector('#attach-file-alfresco');
expect(attachButton).not.toBeNull();
attachButton.click();
await fixture.whenStable();
fixture.detectChanges();
expect(fetchNodeIdFromRelativePathSpy).not.toHaveBeenCalled();
expect(showErrorSpy).toHaveBeenCalled();
});
it('should display file list when field has value', async(() => { it('should display file list when field has value', async(() => {
widget.field = new FormFieldModel(new FormModel(), { widget.field = new FormFieldModel(new FormModel(), {
@@ -249,38 +309,6 @@ describe('AttachFileCloudWidgetComponent', () => {
}); });
}); });
it('should be able to enable multiple file upload', async(() => {
const files = [fakeLocalPngAnswer, { ...fakeLocalPngAnswer, id: 1166, nodeId: 1166, name: 'second_png_file.png' }];
widget.field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.UPLOAD,
id: 'attach-file',
name: 'Upload',
value: [],
params: { onlyLocalParams, multiple: true }
});
spyOn(processCloudContentService, 'createTemporaryRawRelatedContent')
.and.returnValues(of(files[0]), of(files[1]));
fixture.detectChanges();
fixture.whenStable().then(() => {
const inputDebugElement = fixture.debugElement.query(By.css('#attach-file'));
expect(inputDebugElement.nativeElement.multiple).toBe(true);
inputDebugElement.triggerEventHandler('change', {
target: { files: [ files[0] ] }
});
fixture.detectChanges();
expect(element.querySelector('#file-1155-icon')).toBeDefined();
let name: HTMLElement = element.querySelector('span[id="file-1155"]');
expect(name.innerText).toEqual('a_png_file.png');
inputDebugElement.triggerEventHandler('change', {
target: { files: [ files[1] ] }
});
fixture.detectChanges();
expect(element.querySelector('#file-1166-icon')).toBeDefined();
name = element.querySelector('span[id="file-1166"]');
expect(name.innerText).toEqual('second_png_file.png');
});
}));
describe('when is readonly', () => { describe('when is readonly', () => {
it('should show empty list message when there are no file', async(() => { it('should show empty list message when there are no file', async(() => {
@@ -332,46 +360,44 @@ describe('AttachFileCloudWidgetComponent', () => {
}); });
describe('when a file is uploaded', () => { describe('when a file is uploaded', () => {
beforeEach(async(() => { beforeEach(async() => {
spyOn(
contentCloudNodeSelectorService,
'openUploadFileDialog'
).and.returnValue(of([fakeMinimalNode]));
widget.field = new FormFieldModel(new FormModel(), { widget.field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.UPLOAD, type: FormFieldTypes.UPLOAD,
value: [] value: []
}); });
widget.field.id = 'attach-file-attach'; widget.field.id = 'attach-file-alfresco';
widget.field.params = <FormFieldMetadata> onlyLocalParams; widget.field.params = <FormFieldMetadata> contentSourceParam;
spyOn(
processCloudContentService,
'createTemporaryRawRelatedContent'
).and.returnValue(of(fakeLocalPngAnswer));
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { await fixture.whenStable();
const inputDebugElement = fixture.debugElement.query( const attachButton: HTMLButtonElement = element.querySelector('#attach-file-alfresco');
By.css('#attach-file-attach')
); expect(attachButton).not.toBeNull();
inputDebugElement.triggerEventHandler('change', {
target: { files: [fakeLocalPngAnswer] } attachButton.click();
}); fixture.detectChanges();
fixture.detectChanges(); await fixture.whenStable();
expect(element.querySelector('#file-1155-icon')).not.toBeNull(); });
});
}));
it('should remove file when remove is clicked', (done) => { it('should remove file when remove is clicked', (done) => {
fixture.detectChanges(); fixture.detectChanges();
const menuButton: HTMLButtonElement = <HTMLButtonElement> ( const menuButton: HTMLButtonElement = <HTMLButtonElement> (
fixture.debugElement.query(By.css('#file-1155-option-menu')) fixture.debugElement.query(By.css('#file-fake-option-menu'))
.nativeElement .nativeElement
); );
menuButton.click(); menuButton.click();
fixture.detectChanges(); fixture.detectChanges();
const removeOption: HTMLButtonElement = <HTMLButtonElement> ( const removeOption: HTMLButtonElement = <HTMLButtonElement> (
fixture.debugElement.query(By.css('#file-1155-remove')) fixture.debugElement.query(By.css('#file-fake-remove'))
.nativeElement .nativeElement
); );
removeOption.click(); removeOption.click();
fixture.detectChanges(); fixture.detectChanges();
fixture.whenRenderingDone().then(() => { fixture.whenRenderingDone().then(() => {
expect(element.querySelector('#file-1155-icon')).toBeNull(); expect(element.querySelector('#file-fake-icon')).toBeNull();
done(); done();
}); });
}); });
@@ -384,7 +410,7 @@ describe('AttachFileCloudWidgetComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
const menuButton: HTMLButtonElement = <HTMLButtonElement> ( const menuButton: HTMLButtonElement = <HTMLButtonElement> (
fixture.debugElement.query(By.css('#file-1155-option-menu')) fixture.debugElement.query(By.css('#file-fake-option-menu'))
.nativeElement .nativeElement
); );
@@ -392,7 +418,7 @@ describe('AttachFileCloudWidgetComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
const downloadOption: HTMLButtonElement = <HTMLButtonElement> ( const downloadOption: HTMLButtonElement = <HTMLButtonElement> (
fixture.debugElement.query(By.css('#file-1155-download-file')) fixture.debugElement.query(By.css('#file-fake-download-file'))
.nativeElement .nativeElement
); );
@@ -409,7 +435,7 @@ describe('AttachFileCloudWidgetComponent', () => {
spyOn(processCloudContentService, 'getRawContentNode').and.returnValue(of(new Blob())); spyOn(processCloudContentService, 'getRawContentNode').and.returnValue(of(new Blob()));
formService.formContentClicked.subscribe( formService.formContentClicked.subscribe(
(fileClicked: any) => { (fileClicked: any) => {
expect(fileClicked.nodeId).toBe(1155); expect(fileClicked.nodeId).toBe('fake');
done(); done();
} }
); );
@@ -417,14 +443,14 @@ describe('AttachFileCloudWidgetComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
const menuButton: HTMLButtonElement = <HTMLButtonElement> ( const menuButton: HTMLButtonElement = <HTMLButtonElement> (
fixture.debugElement.query( fixture.debugElement.query(
By.css('#file-1155-option-menu') By.css('#file-fake-option-menu')
).nativeElement ).nativeElement
); );
menuButton.click(); menuButton.click();
fixture.detectChanges(); fixture.detectChanges();
const showOption: HTMLButtonElement = <HTMLButtonElement> ( const showOption: HTMLButtonElement = <HTMLButtonElement> (
fixture.debugElement.query( fixture.debugElement.query(
By.css('#file-1155-show-file') By.css('#file-fake-show-file')
).nativeElement ).nativeElement
); );
showOption.click(); showOption.click();

View File

@@ -23,7 +23,8 @@ import {
LogService, LogService,
ThumbnailService, ThumbnailService,
NotificationService, NotificationService,
ContentLinkModel ContentLinkModel,
TranslationService
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { Node, RelatedContentRepresentation } from '@alfresco/js-api'; import { Node, RelatedContentRepresentation } from '@alfresco/js-api';
import { ContentCloudNodeSelectorService } from '../../../services/content-cloud-node-selector.service'; import { ContentCloudNodeSelectorService } from '../../../services/content-cloud-node-selector.service';
@@ -49,9 +50,9 @@ import { UploadCloudWidgetComponent } from './upload-cloud.widget';
}) })
export class AttachFileCloudWidgetComponent extends UploadCloudWidgetComponent export class AttachFileCloudWidgetComponent extends UploadCloudWidgetComponent
implements OnInit { implements OnInit {
static ACS_SERVICE = 'alfresco-content';
typeId = 'AttachFileCloudWidgetComponent'; typeId = 'AttachFileCloudWidgetComponent';
rootNodeId = '-my-';
constructor( constructor(
formService: FormService, formService: FormService,
@@ -59,35 +60,18 @@ export class AttachFileCloudWidgetComponent extends UploadCloudWidgetComponent
thumbnails: ThumbnailService, thumbnails: ThumbnailService,
processCloudContentService: ProcessCloudContentService, processCloudContentService: ProcessCloudContentService,
notificationService: NotificationService, notificationService: NotificationService,
private contentNodeSelectorService: ContentCloudNodeSelectorService private contentNodeSelectorService: ContentCloudNodeSelectorService,
private translationService: TranslationService
) { ) {
super(formService, thumbnails, processCloudContentService, notificationService, logger); super(formService, thumbnails, processCloudContentService, notificationService, logger);
} }
isFileSourceConfigured(): boolean { isAlfrescoAndLocal(): boolean {
return !!this.field.params && !!this.field.params.fileSource;
}
isMultipleSourceUpload(): boolean {
return (
!this.field.readOnly &&
this.isFileSourceConfigured() &&
!this.isOnlyLocalSourceSelected()
);
}
isOnlyLocalSourceSelected(): boolean {
return ( return (
this.field.params && this.field.params &&
this.field.params.fileSource && this.field.params.fileSource &&
this.field.params.fileSource.serviceId === 'local-file' this.field.params.fileSource.serviceId === 'all-file-sources'
); || this.field.params.fileSource.serviceId === 'local-file'
}
isSimpleUploadButton(): boolean {
return (
(this.isUploadButtonVisible() && !this.isFileSourceConfigured()) ||
this.isOnlyLocalSourceSelected()
); );
} }
@@ -95,23 +79,31 @@ export class AttachFileCloudWidgetComponent extends UploadCloudWidgetComponent
return (!this.hasFile || this.multipleOption) && !this.field.readOnly; return (!this.hasFile || this.multipleOption) && !this.field.readOnly;
} }
onAttachFileChanged(event: any) {
this.onFileChanged(event);
}
onRemoveAttachFile(file: File | RelatedContentRepresentation | Node) { onRemoveAttachFile(file: File | RelatedContentRepresentation | Node) {
this.removeFile(file); this.removeFile(file);
} }
uploadFileFromCS() { async openSelectDialog() {
this.openSelectDialog();
}
openSelectDialog() {
const selectedMode = this.field.params.multiple ? 'multiple' : 'single'; const selectedMode = this.field.params.multiple ? 'multiple' : 'single';
if (this.isAlfrescoAndLocal()) {
const destinationFolderPath = this.field.params.fileSource.destinationFolderPath;
const alias = this.getAliasFromDestinationFolderPath(destinationFolderPath);
const opts = {
relativePath: this.getRelativePathFromDestinationFolderPath(destinationFolderPath)
};
if (alias && opts && opts.relativePath) {
await this.contentNodeSelectorService.fetchNodeIdFromRelativePath(alias, opts).then((nodeId: string) => {
this.rootNodeId = nodeId;
});
} else {
const errorMessage = this.translationService.instant('ADF_CLOUD_TASK_FORM.ERROR.INVALID_DESTINATION_FOLDER_PATH');
this.notificationService.showError(errorMessage);
}
}
this.contentNodeSelectorService this.contentNodeSelectorService
.openUploadFileDialog(this.field.form.contentHost, '-my-', selectedMode) .openUploadFileDialog(this.rootNodeId, selectedMode, this.isAlfrescoAndLocal())
.subscribe((selections: Node[]) => { .subscribe((selections: Node[]) => {
selections.forEach(node => (node['isExternal'] = true)); selections.forEach(node => (node['isExternal'] = true));
const selectionWithoutDuplication = this.removeExistingSelection(selections); const selectionWithoutDuplication = this.removeExistingSelection(selections);
@@ -119,29 +111,31 @@ export class AttachFileCloudWidgetComponent extends UploadCloudWidgetComponent
}); });
} }
getAliasFromDestinationFolderPath(destinationFolderPath: string): string {
const startOfRelativePathIndex = destinationFolderPath.indexOf('/');
return destinationFolderPath.substring(0, startOfRelativePathIndex);
}
getRelativePathFromDestinationFolderPath(destinationFolderPath: string): string {
const startOfRelativePathIndex = destinationFolderPath.indexOf('/');
return destinationFolderPath.substring(startOfRelativePathIndex, destinationFolderPath.length);
}
removeExistingSelection(selections: Node[]) { removeExistingSelection(selections: Node[]) {
const existingNode: Node[] = [...this.field.value || []]; const existingNode: Node[] = [...this.field.value || []];
return selections.filter(opt => !existingNode.some( (node) => node.id === opt.id)); return selections.filter(opt => !existingNode.some( (node) => node.id === opt.id));
} }
isContentSourceSelected(): boolean {
return (
this.field.params &&
this.field.params.fileSource &&
this.field.params.fileSource.serviceId ===
AttachFileCloudWidgetComponent.ACS_SERVICE
);
}
downloadContent(file: Node): void { downloadContent(file: Node): void {
this.processCloudContentService.downloadFile( this.processCloudContentService.downloadFile(file.id);
file.id,
this.field.form.contentHost
);
} }
onAttachFileClicked(nodeSelector: any) { onAttachFileClicked(nodeSelector: any) {
nodeSelector.nodeId = nodeSelector.id; nodeSelector.nodeId = nodeSelector.id;
this.fileClicked(new ContentLinkModel(nodeSelector)); this.fileClicked(new ContentLinkModel(nodeSelector));
} }
getWidgetIcon(): string {
return this.isAlfrescoAndLocal() ? 'file_upload' : 'attach_file';
}
} }

View File

@@ -124,9 +124,7 @@ export class UploadCloudWidgetComponent extends WidgetComponent implements OnIni
} }
private uploadRawContent(file: File): Observable<Node> { private uploadRawContent(file: File): Observable<Node> {
return this.processCloudContentService.createTemporaryRawRelatedContent( return this.processCloudContentService.createTemporaryRawRelatedContent(file, this.field.form.nodeId);
file, this.field.form.nodeId, this.field.form.contentHost
);
} }
getMultipleFileParam() { getMultipleFileParam() {

View File

@@ -23,16 +23,3 @@ export class TaskVariableCloud {
this.value = obj.value || null; this.value = obj.value || null;
} }
} }
export class ProcessStorageCloudModel {
nodeId: string;
path: string;
type: string;
constructor(obj) {
this.nodeId = obj.nodeId || null;
this.path = obj.path || null;
this.type = obj.type || null;
}
}

View File

@@ -32,30 +32,35 @@ export class ContentCloudNodeSelectorService {
private dialog: MatDialog) { private dialog: MatDialog) {
} }
openUploadFileDialog(contentHost: string, currentFolderId?: string, selectionMode?: string): Observable<Node[]> { openUploadFileDialog(currentFolderId?: string, selectionMode?: string, showLocalUploadButton?: boolean): Observable<Node[]> {
const changedConfig = this.apiService.lastConfig;
changedConfig.provider = 'ALL';
changedConfig.hostEcm = contentHost.replace('/alfresco', '');
this.apiService.getInstance().setConfig(changedConfig);
const select = new Subject<Node[]>(); const select = new Subject<Node[]>();
select.subscribe({ select.subscribe({
complete: this.close.bind(this) complete: this.close.bind(this)
}); });
const data = <ContentNodeSelectorComponentData> { const data = <ContentNodeSelectorComponentData> {
title: 'Select a file', title: 'Select a file',
actionName: 'Choose', actionName: 'Attach',
currentFolderId, currentFolderId,
restrictRootToCurrentFolderId: true, restrictRootToCurrentFolderId: true,
select, select,
selectionMode, selectionMode,
isSelectionValid: (entry: Node) => entry.isFile, isSelectionValid: (entry: Node) => entry.isFile,
showFilesInResult: true showFilesInResult: true,
}; showDropdownSiteList: false,
showLocalUploadButton
};
this.openContentNodeDialog(data, 'adf-content-node-selector-dialog', '630px'); this.openContentNodeDialog(data, 'adf-content-node-selector-dialog', '630px');
return select; return select;
} }
async fetchNodeIdFromRelativePath(alias: string, opts: { relativePath: string }): Promise<string> {
let nodeId = '';
await this.apiService.getInstance().node.getNode(alias, opts).then(node => {
nodeId = node.entry.id;
});
return nodeId;
}
private openContentNodeDialog(data: ContentNodeSelectorComponentData, currentPanelClass: string, chosenWidth: string) { private openContentNodeDialog(data: ContentNodeSelectorComponentData, currentPanelClass: string, chosenWidth: string) {
this.dialog.open(ContentNodeSelectorComponent, { data, panelClass: currentPanelClass, width: chosenWidth }); this.dialog.open(ContentNodeSelectorComponent, { data, panelClass: currentPanelClass, width: chosenWidth });
} }

View File

@@ -183,45 +183,6 @@ describe('Form Cloud service', () => {
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[1]).toBe('POST'); expect(oauth2Auth.callCustomApi.calls.mostRecent().args[1]).toBe('POST');
done(); done();
}); });
});
it('should fetch process storage folder with process instance id and task id', (done) => {
oauth2Auth.callCustomApi.and.returnValue(Promise.resolve({
nodeId: 'fake-node-id-really-long',
path: 'path/to/node/id',
type: 'nodeType'
}));
service.getProcessStorageFolderTask(appName, taskId, processInstanceId).subscribe((result) => {
expect(result).toBeDefined();
expect(result).not.toBeNull();
expect(result.nodeId).toBe('fake-node-id-really-long');
expect(result.path).toBe('path/to/node/id');
expect(result.type).toBe('nodeType');
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[0].endsWith(`${appName}/process-storage/v1/folders/${processInstanceId}/${taskId}`)).toBeTruthy();
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[1]).toBe('GET');
done();
});
});
it('should fetch process storage folder with task id only', (done) => {
oauth2Auth.callCustomApi.and.returnValue(Promise.resolve({
nodeId: 'fake-node-id-really-long',
path: 'path/to/node/id',
type: 'nodeType'
}));
service.getProcessStorageFolderTask(appName, taskId, null).subscribe((result) => {
expect(result).toBeDefined();
expect(result).not.toBeNull();
expect(result.nodeId).toBe('fake-node-id-really-long');
expect(result.path).toBe('path/to/node/id');
expect(result.type).toBe('nodeType');
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[0].endsWith(`${appName}/process-storage/v1/folders/${taskId}`)).toBeTruthy();
expect(oauth2Auth.callCustomApi.calls.mostRecent().args[1]).toBe('GET');
done();
});
}); });
}); });
}); });

View File

@@ -27,7 +27,7 @@ import { Observable, from } from 'rxjs';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { TaskDetailsCloudModel } from '../../task/start-task/models/task-details-cloud.model'; import { TaskDetailsCloudModel } from '../../task/start-task/models/task-details-cloud.model';
import { CompleteFormRepresentation } from '@alfresco/js-api'; import { CompleteFormRepresentation } from '@alfresco/js-api';
import { TaskVariableCloud, ProcessStorageCloudModel } from '../models/task-variable-cloud.model'; import { TaskVariableCloud } from '../models/task-variable-cloud.model';
import { BaseCloudService } from '../../services/base-cloud.service'; import { BaseCloudService } from '../../services/base-cloud.service';
import { FormContent } from '../../services/form-fields.interfaces'; import { FormContent } from '../../services/form-fields.interfaces';
@@ -151,16 +151,6 @@ export class FormCloudService extends BaseCloudService {
); );
} }
getProcessStorageFolderTask(appName: string, taskId: string, processInstanceId: string): Observable<ProcessStorageCloudModel> {
const apiUrl = this.buildFolderTask(appName, taskId, processInstanceId);
return this.get(apiUrl).pipe(
map((res: any) => {
return new ProcessStorageCloudModel(res);
})
);
}
/** /**
* Gets the variables of a task. * Gets the variables of a task.
* @param appName Name of the app * @param appName Name of the app
@@ -237,10 +227,4 @@ export class FormCloudService extends BaseCloudService {
} }
return null; return null;
} }
private buildFolderTask(appName: string, taskId: string, processInstanceId: string): string {
return processInstanceId
? `${this.getBasePath(appName)}/process-storage/v1/folders/${processInstanceId}/${taskId}`
: `${this.getBasePath(appName)}/process-storage/v1/folders/${taskId}`;
}
} }

View File

@@ -39,10 +39,8 @@ export class ProcessCloudContentService {
createTemporaryRawRelatedContent( createTemporaryRawRelatedContent(
file: File, file: File,
nodeId: string, nodeId: string
contentHost: string
): Observable<Node> { ): Observable<Node> {
this.updateConfig(contentHost);
return from( return from(
this.apiService this.apiService
@@ -59,8 +57,7 @@ export class ProcessCloudContentService {
); );
} }
getRawContentNode(nodeId: string, contentHost: string): Observable<Blob> { getRawContentNode(nodeId: string): Observable<Blob> {
this.updateConfig(contentHost);
return this.contentService.getNodeContent(nodeId); return this.contentService.getNodeContent(nodeId);
} }
@@ -68,8 +65,7 @@ export class ProcessCloudContentService {
this.contentService.downloadBlob(blob, fileName); this.contentService.downloadBlob(blob, fileName);
} }
async downloadFile(nodeId: string, contentHost: string) { async downloadFile(nodeId: string) {
this.updateConfig(contentHost);
const ticket = await this.getAuthTicket(); const ticket = await this.getAuthTicket();
const url = this.contentService.getContentUrl(nodeId, true, ticket); const url = this.contentService.getContentUrl(nodeId, true, ticket);
@@ -88,18 +84,6 @@ export class ProcessCloudContentService {
return ''; return '';
} }
private updateConfig(contentHost: string) {
const changedConfig = this.apiService.lastConfig;
changedConfig.provider = 'ALL';
if (contentHost) {
changedConfig.hostEcm = contentHost.replace('/alfresco', '');
}
this.apiService.getInstance().setConfig(changedConfig);
}
private handleError(error: any) { private handleError(error: any) {
this.logService.error(error); this.logService.error(error);
return throwError(error || 'Server error'); return throwError(error || 'Server error');

View File

@@ -266,6 +266,9 @@
"CLAIM": "CLAIM", "CLAIM": "CLAIM",
"UNCLAIM": "RELEASE" "UNCLAIM": "RELEASE"
} }
},
"ERROR": {
"INVALID_DESTINATION_FOLDER_PATH": "Invalid destination folder path"
} }
} }