[APM-7] Feature enhancement for the create and edit folder directive (#3179)

* Add observable menu open state to the sidenav-layout component

* add documentation, fix inversed value

* Add success events to folder create/edit directives

* Overridable dialog titles for the directives

* Update the documentation
This commit is contained in:
Popovics András 2018-04-17 20:27:42 +01:00 committed by Eugenio Romano
parent 21ad4c2894
commit ee9393caf0
12 changed files with 569 additions and 242 deletions

View File

@ -13,7 +13,9 @@ Creates folders.
```html ```html
<adf-toolbar> <adf-toolbar>
<button mat-icon-button <button mat-icon-button
[adf-create-folder]="documentList.currentFolderId"> [adf-create-folder]="documentList.currentFolderId"
title="Title of the dialog"
(success)="doSomething($event)">
<mat-icon>create_new_folder</mat-icon> <mat-icon>create_new_folder</mat-icon>
</button> </button>
</adf-toolbar> </adf-toolbar>
@ -30,12 +32,14 @@ Creates folders.
| Name | Type | Default value | Description | | Name | Type | Default value | Description |
| -- | -- | -- | -- | | -- | -- | -- | -- |
| adf-create-folder | `string` | DEFAULT_FOLDER_PARENT_ID | Parent folder where the new folder will be located after creation. | | adf-create-folder | `string` | DEFAULT_FOLDER_PARENT_ID | Parent folder where the new folder will be located after creation. |
| title | `string` | null | The title of the opened dialog. |
### Events ### Events
| Name | Type | Description | | Name | Type | Description |
| -- | -- | -- | | -- | -- | -- |
| error | `EventEmitter<any>` | Emitted when an error occurs (for example a folder with same name already exists) | | error | `EventEmitter<any>` | Emitted when an error occurs (for example a folder with same name already exists) |
| success | `EventEmitter<MinimalNodeEntryEntity>` | Emitted when the creation successfully happened |
## Details ## Details

View File

@ -13,7 +13,9 @@ Allows folders to be edited.
```html ```html
<adf-toolbar title="toolbar example"> <adf-toolbar title="toolbar example">
<button mat-icon-button <button mat-icon-button
[adf-edit-folder]="documentList.selection[0]?.entry"> [adf-edit-folder]="documentList.selection[0]?.entry"
title="Title of the dialog"
(success)="doSomething($event)">
<mat-icon>create</mat-icon> <mat-icon>create</mat-icon>
</button> </button>
</adf-toolbar> </adf-toolbar>
@ -30,12 +32,14 @@ Allows folders to be edited.
| Name | Type | Default value | Description | | Name | Type | Default value | Description |
| -- | -- | -- | -- | | -- | -- | -- | -- |
| adf-edit-folder | `MinimalNodeEntryEntity` | | Folder node to edit. | | adf-edit-folder | `MinimalNodeEntryEntity` | | Folder node to edit. |
| title | `string` | null | The title of the opened dialog. |
### Events ### Events
| Name | Type | Description | | Name | Type | Description |
| -- | -- | -- | | -- | -- | -- |
| error | `EventEmitter<any>` | Emitted when an error occurs (for example a folder with same name already exists) | | error | `EventEmitter<any>` | Emitted when an error occurs (for example a folder with same name already exists) |
| success | `EventEmitter<MinimalNodeEntryEntity>` | Emitted when the edition successfully happened |
## Details ## Details

View File

@ -65,8 +65,7 @@ The layout will select between a small screen (ie, mobile) configuration and a l
configuration according to the screen size in pixels (the `stepOver` property sets the configuration according to the screen size in pixels (the `stepOver` property sets the
number of pixels at which the switch will occur). number of pixels at which the switch will occur).
The small screen layout uses the Angular Material The small screen layout uses the Angular Material [Sidenav component](https://material.angularjs.org/latest/api/directive/mdSidenav) which is
[Sidenav component](https://material.angularjs.org/latest/api/directive/mdSidenav) which is
described in detail on their website. described in detail on their website.
The ADF-style (ie, large screen) Sidenav has two states: **expanded** and **compact**. The ADF-style (ie, large screen) Sidenav has two states: **expanded** and **compact**.
@ -83,6 +82,12 @@ Desktop layout (screen width greater than the `stepOver` value):
Mobile layout (screen width less than the `stepOver` value): Mobile layout (screen width less than the `stepOver` value):
![Sidenav on mobile](../docassets/images/sidenav-layout-mobile.png) ![Sidenav on mobile](../docassets/images/sidenav-layout-mobile.png)
### Public attributes
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| menuOpenState$ | Observable&lt;boolean&gt; | true | Another way to listen to menu open/closed state |
### Template context ### Template context
Each template is given a context containing the following methods: Each template is given a context containing the following methods:
@ -91,4 +96,13 @@ Each template is given a context containing the following methods:
Triggers menu toggling. Triggers menu toggling.
- `isMenuMinimized(): boolean` - `isMenuMinimized(): boolean`
Is the menu in minimized/compacted state? Only works for large screen layouts. The expanded/compact (minimized) state of the navigation. This one only makes sense in case of desktop size, when the screen size is above the value of stepOver.
### menuOpenState$
Beside the template context's **isMenuMinimized** variable, another way to listen to the component's menu's open/closed state is the menuOpenState$ observable, which is driven by a BehaviorSubject at the background. The value emitted on this observable is the opposite of the isMenuMinimized template variable.
Every time the menu state is changed, the following values are emitted:
- true, if the menu got into opened state
- false, if the menu git into closed state

View File

@ -1,10 +1,5 @@
<h2 mat-dialog-title> <h2 mat-dialog-title>
{{ {{ (editing ? editTitle : createTitle) | translate }}
(editing
? 'CORE.FOLDER_DIALOG.EDIT_FOLDER_TITLE'
: 'CORE.FOLDER_DIALOG.CREATE_FOLDER_TITLE'
) | translate
}}
</h2> </h2>
<mat-dialog-content> <mat-dialog-content>

View File

@ -18,12 +18,13 @@
import { async, TestBed } from '@angular/core/testing'; import { async, TestBed } from '@angular/core/testing';
import { ComponentFixture } from '@angular/core/testing'; import { ComponentFixture } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatDialogRef } from '@angular/material'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { NodesApiService, TranslationService } from '@alfresco/adf-core'; import { NodesApiService, TranslationService } from '@alfresco/adf-core';
import { FolderDialogComponent } from './folder.dialog'; import { FolderDialogComponent } from './folder.dialog';
import { By } from '@angular/platform-browser';
describe('FolderDialogComponent', () => { describe('FolderDialogComponent', () => {
@ -33,47 +34,55 @@ describe('FolderDialogComponent', () => {
let nodesApi: NodesApiService; let nodesApi: NodesApiService;
let dialogRef; let dialogRef;
beforeEach(async(() => { afterEach(() => {
dialogRef = { fixture.destroy();
close: jasmine.createSpy('close') TestBed.resetTestingModule();
};
TestBed.configureTestingModule({
imports: [
FormsModule,
ReactiveFormsModule,
BrowserDynamicTestingModule
],
declarations: [
FolderDialogComponent
],
providers: [
{ provide: MatDialogRef, useValue: dialogRef }
]
});
// entryComponents are not supported yet on TestBed, that is why this ugly workaround:
// https://github.com/angular/angular/issues/10760
TestBed.overrideModule(BrowserDynamicTestingModule, {
set: { entryComponents: [FolderDialogComponent] }
});
TestBed.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FolderDialogComponent);
component = fixture.componentInstance;
nodesApi = TestBed.get(NodesApiService);
translationService = TestBed.get(TranslationService);
spyOn(translationService, 'get').and.returnValue(Observable.of('message'));
}); });
describe('Edit', () => { describe('Material dialog behaviour', () => {
beforeEach(async(() => {
dialogRef = { close: jasmine.createSpy('close') };
TestBed.configureTestingModule({
imports: [
FormsModule,
ReactiveFormsModule,
BrowserDynamicTestingModule
],
declarations: [
FolderDialogComponent
],
providers: [
{ provide: MatDialogRef, useValue: dialogRef },
{ provide: MAT_DIALOG_DATA, useValue: {
editTitle: 'edit',
createTitle: 'create'
} }
]
});
// entryComponents are not supported yet on TestBed, that is why this ugly workaround:
// https://github.com/angular/angular/issues/10760
TestBed.overrideModule(BrowserDynamicTestingModule, {
set: { entryComponents: [FolderDialogComponent] }
});
TestBed.compileComponents();
}));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FolderDialogComponent);
component = fixture.componentInstance;
nodesApi = TestBed.get(NodesApiService);
translationService = TestBed.get(TranslationService);
spyOn(translationService, 'get').and.returnValue(Observable.of('message'));
});
it('should have the proper overridden title in case of editing', () => {
component.data = { component.data = {
folder: { folder: {
id: 'node-id', id: 'node-id',
@ -83,197 +92,312 @@ describe('FolderDialogComponent', () => {
} }
} }
}; };
fixture.detectChanges(); fixture.detectChanges();
const title = fixture.debugElement.query(By.css('[mat-dialog-title]'));
expect(title === null).toBe(false);
expect(title.nativeElement.innerText.trim()).toBe('edit');
}); });
it('should init form with folder name and description', () => { it('should have the proper overridden title in case of creating', () => {
expect(component.name).toBe('folder-name');
expect(component.description).toBe('folder-description');
});
it('should update form input', () => {
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
expect(component.name).toBe('folder-name-update');
expect(component.description).toBe('folder-description-update');
});
it('should submit updated values if form is valid', () => {
spyOn(nodesApi, 'updateNode').and.returnValue(Observable.of({}));
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
component.submit();
expect(nodesApi.updateNode).toHaveBeenCalledWith(
'node-id',
{
name: 'folder-name-update',
properties: {
'cm:title': 'folder-name-update',
'cm:description': 'folder-description-update'
}
}
);
});
it('should call dialog to close with form data when submit is succesfluly', () => {
const folder = {
data: 'folder-data'
};
spyOn(nodesApi, 'updateNode').and.returnValue(Observable.of(folder));
component.submit();
expect(dialogRef.close).toHaveBeenCalledWith(folder);
});
it('should not submit if form is invalid', () => {
spyOn(nodesApi, 'updateNode');
component.form.controls['name'].setValue('');
component.form.controls['description'].setValue('');
component.submit();
expect(component.form.valid).toBe(false);
expect(nodesApi.updateNode).not.toHaveBeenCalled();
});
it('should not call dialog to close if submit fails', () => {
spyOn(nodesApi, 'updateNode').and.returnValue(Observable.throw('error'));
spyOn(component, 'handleError').and.callFake(val => val);
component.submit();
expect(component.handleError).toHaveBeenCalled();
expect(dialogRef.close).not.toHaveBeenCalled();
});
});
describe('Create', () => {
beforeEach(() => {
component.data = { component.data = {
parentNodeId: 'parentNodeId', parentNodeId: 'parentNodeId',
folder: null folder: null
}; };
fixture.detectChanges(); fixture.detectChanges();
const title = fixture.debugElement.query(By.css('[mat-dialog-title]'));
expect(title === null).toBe(false);
expect(title.nativeElement.innerText.trim()).toBe('create');
}); });
it('should init form with empty inputs', () => {
expect(component.name).toBe('');
expect(component.description).toBe('');
});
it('should update form input', () => {
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
expect(component.name).toBe('folder-name-update');
expect(component.description).toBe('folder-description-update');
});
it('should submit updated values if form is valid', () => {
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.of({}));
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
component.submit();
expect(nodesApi.createFolder).toHaveBeenCalledWith(
'parentNodeId',
{
name: 'folder-name-update',
properties: {
'cm:title': 'folder-name-update',
'cm:description': 'folder-description-update'
}
}
);
});
it('should call dialog to close with form data when submit is succesfluly', () => {
const folder = {
data: 'folder-data'
};
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.of(folder));
component.submit();
expect(dialogRef.close).toHaveBeenCalledWith(folder);
});
it('should not submit if form is invalid', () => {
spyOn(nodesApi, 'createFolder');
component.form.controls['name'].setValue('');
component.form.controls['description'].setValue('');
component.submit();
expect(component.form.valid).toBe(false);
expect(nodesApi.createFolder).not.toHaveBeenCalled();
});
it('should not call dialog to close if submit fails', () => {
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.throw('error'));
spyOn(component, 'handleError').and.callFake(val => val);
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
component.submit();
expect(component.handleError).toHaveBeenCalled();
expect(dialogRef.close).not.toHaveBeenCalled();
});
describe('Error events ', () => {
it('should raise error for 409', (done) => {
const error = {
message: '{ "error": { "statusCode" : 409 } }'
};
component.error.subscribe((message) => {
expect(message).toBe('CORE.MESSAGES.ERRORS.EXISTENT_FOLDER');
done();
});
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.throw(error));
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
component.submit();
});
it('should raise generic error', (done) => {
const error = {
message: '{ "error": { "statusCode" : 123 } }'
};
component.error.subscribe((message) => {
expect(message).toBe('CORE.MESSAGES.ERRORS.GENERIC');
done();
});
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.throw(error));
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
component.submit();
});
});
}); });
describe('Basic component behaviour', () => {
beforeEach(async(() => {
dialogRef = { close: jasmine.createSpy('close') };
TestBed.configureTestingModule({
imports: [
FormsModule,
ReactiveFormsModule,
BrowserDynamicTestingModule
],
declarations: [
FolderDialogComponent
],
providers: [
{ provide: MatDialogRef, useValue: dialogRef }
]
});
// entryComponents are not supported yet on TestBed, that is why this ugly workaround:
// https://github.com/angular/angular/issues/10760
TestBed.overrideModule(BrowserDynamicTestingModule, {
set: { entryComponents: [FolderDialogComponent] }
});
TestBed.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FolderDialogComponent);
component = fixture.componentInstance;
nodesApi = TestBed.get(NodesApiService);
translationService = TestBed.get(TranslationService);
spyOn(translationService, 'get').and.returnValue(Observable.of('message'));
});
describe('Edit', () => {
beforeEach(() => {
component.data = {
folder: {
id: 'node-id',
name: 'folder-name',
properties: {
['cm:description']: 'folder-description'
}
}
};
fixture.detectChanges();
});
it('should have the proper title', () => {
const title = fixture.debugElement.query(By.css('[mat-dialog-title]'));
expect(title === null).toBe(false);
expect(title.nativeElement.innerText.trim()).toBe('CORE.FOLDER_DIALOG.EDIT_FOLDER_TITLE');
});
it('should init form with folder name and description', () => {
expect(component.name).toBe('folder-name');
expect(component.description).toBe('folder-description');
});
it('should update form input', () => {
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
expect(component.name).toBe('folder-name-update');
expect(component.description).toBe('folder-description-update');
});
it('should submit updated values if form is valid', () => {
spyOn(nodesApi, 'updateNode').and.returnValue(Observable.of({}));
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
component.submit();
expect(nodesApi.updateNode).toHaveBeenCalledWith(
'node-id',
{
name: 'folder-name-update',
properties: {
'cm:title': 'folder-name-update',
'cm:description': 'folder-description-update'
}
}
);
});
it('should call dialog to close with form data when submit is succesfluly', () => {
const folder = {
data: 'folder-data'
};
spyOn(nodesApi, 'updateNode').and.returnValue(Observable.of(folder));
component.submit();
expect(dialogRef.close).toHaveBeenCalledWith(folder);
});
it('should emit success output event with folder when submit is succesfull', async(() => {
const folder = { data: 'folder-data' };
let expectedNode = null;
spyOn(nodesApi, 'updateNode').and.returnValue(Observable.of(folder));
component.success.subscribe((node) => { expectedNode = node; });
component.submit();
fixture.whenStable().then(() => {
expect(expectedNode).toBe(folder);
});
}));
it('should not submit if form is invalid', () => {
spyOn(nodesApi, 'updateNode');
component.form.controls['name'].setValue('');
component.form.controls['description'].setValue('');
component.submit();
expect(component.form.valid).toBe(false);
expect(nodesApi.updateNode).not.toHaveBeenCalled();
});
it('should not call dialog to close if submit fails', () => {
spyOn(nodesApi, 'updateNode').and.returnValue(Observable.throw('error'));
spyOn(component, 'handleError').and.callFake(val => val);
component.submit();
expect(component.handleError).toHaveBeenCalled();
expect(dialogRef.close).not.toHaveBeenCalled();
});
});
describe('Create', () => {
beforeEach(() => {
component.data = {
parentNodeId: 'parentNodeId',
folder: null
};
fixture.detectChanges();
});
it('should have the proper title', () => {
const title = fixture.debugElement.query(By.css('[mat-dialog-title]'));
expect(title === null).toBe(false);
expect(title.nativeElement.innerText.trim()).toBe('CORE.FOLDER_DIALOG.CREATE_FOLDER_TITLE');
});
it('should init form with empty inputs', () => {
expect(component.name).toBe('');
expect(component.description).toBe('');
});
it('should update form input', () => {
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
expect(component.name).toBe('folder-name-update');
expect(component.description).toBe('folder-description-update');
});
it('should submit updated values if form is valid', () => {
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.of({}));
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
component.submit();
expect(nodesApi.createFolder).toHaveBeenCalledWith(
'parentNodeId',
{
name: 'folder-name-update',
properties: {
'cm:title': 'folder-name-update',
'cm:description': 'folder-description-update'
}
}
);
});
it('should call dialog to close with form data when submit is succesfluly', () => {
const folder = {
data: 'folder-data'
};
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.of(folder));
component.submit();
expect(dialogRef.close).toHaveBeenCalledWith(folder);
});
it('should emit success output event with folder when submit is succesfull', async(() => {
const folder = { data: 'folder-data' };
let expectedNode = null;
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.of(folder));
component.success.subscribe((node) => { expectedNode = node; });
component.submit();
fixture.whenStable().then(() => {
expect(expectedNode).toBe(folder);
});
}));
it('should not submit if form is invalid', () => {
spyOn(nodesApi, 'createFolder');
component.form.controls['name'].setValue('');
component.form.controls['description'].setValue('');
component.submit();
expect(component.form.valid).toBe(false);
expect(nodesApi.createFolder).not.toHaveBeenCalled();
});
it('should not call dialog to close if submit fails', () => {
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.throw('error'));
spyOn(component, 'handleError').and.callFake(val => val);
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
component.submit();
expect(component.handleError).toHaveBeenCalled();
expect(dialogRef.close).not.toHaveBeenCalled();
});
describe('Error events ', () => {
it('should raise error for 409', (done) => {
const error = {
message: '{ "error": { "statusCode" : 409 } }'
};
component.error.subscribe((message) => {
expect(message).toBe('CORE.MESSAGES.ERRORS.EXISTENT_FOLDER');
done();
});
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.throw(error));
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
component.submit();
});
it('should raise generic error', (done) => {
const error = {
message: '{ "error": { "statusCode" : 123 } }'
};
component.error.subscribe((message) => {
expect(message).toBe('CORE.MESSAGES.ERRORS.GENERIC');
done();
});
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.throw(error));
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
component.submit();
});
});
});
});
}); });

View File

@ -42,6 +42,12 @@ export class FolderDialogComponent implements OnInit {
@Output() @Output()
error: EventEmitter<any> = new EventEmitter<any>(); error: EventEmitter<any> = new EventEmitter<any>();
@Output()
success: EventEmitter<any> = new EventEmitter<MinimalNodeEntryEntity>();
editTitle = 'CORE.FOLDER_DIALOG.EDIT_FOLDER_TITLE';
createTitle = 'CORE.FOLDER_DIALOG.CREATE_FOLDER_TITLE';
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private dialog: MatDialogRef<FolderDialogComponent>, private dialog: MatDialogRef<FolderDialogComponent>,
@ -50,7 +56,12 @@ export class FolderDialogComponent implements OnInit {
@Optional() @Optional()
@Inject(MAT_DIALOG_DATA) @Inject(MAT_DIALOG_DATA)
public data: any public data: any
) {} ) {
if (data) {
this.editTitle = data.editTitle || this.editTitle;
this.createTitle = data.createTitle || this.createTitle;
}
}
get editing(): boolean { get editing(): boolean {
return !!this.data.folder; return !!this.data.folder;
@ -121,7 +132,10 @@ export class FolderDialogComponent implements OnInit {
(editing ? this.edit() : this.create()) (editing ? this.edit() : this.create())
.subscribe( .subscribe(
(folder: MinimalNodeEntryEntity) => dialog.close(folder), (folder: MinimalNodeEntryEntity) => {
this.success.emit(folder);
dialog.close(folder);
},
(error) => this.handleError(error) (error) => this.handleError(error)
); );
} }

View File

@ -17,7 +17,7 @@
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatDialog, MatDialogModule } from '@angular/material'; import { MatDialog, MatDialogModule } from '@angular/material';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
@ -27,12 +27,19 @@ import { FolderDialogComponent } from '../dialogs/folder.dialog';
import { DirectiveModule, ContentService, TranslateLoaderService } from '@alfresco/adf-core'; import { DirectiveModule, ContentService, TranslateLoaderService } from '@alfresco/adf-core';
import { FolderCreateDirective } from './folder-create.directive'; import { FolderCreateDirective } from './folder-create.directive';
import { Subject } from 'rxjs/Subject';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
@Component({ @Component({
template: '<div [adf-create-folder]="parentNode"></div>' template: '<div [adf-create-folder]="parentNode" (success)="success($event)" title="create-title"></div>'
}) })
class TestComponent { class TestComponent {
parentNode = ''; parentNode = '';
public successParameter: MinimalNodeEntryEntity = null;
success(node: MinimalNodeEntryEntity) {
this.successParameter = node;
}
} }
describe('FolderCreateDirective', () => { describe('FolderCreateDirective', () => {
@ -43,6 +50,11 @@ describe('FolderCreateDirective', () => {
let contentService: ContentService; let contentService: ContentService;
let dialogRefMock; let dialogRefMock;
const event = {
type: 'click',
preventDefault: () => null
};
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
@ -80,7 +92,11 @@ describe('FolderCreateDirective', () => {
node = { entry: { id: 'nodeId' } }; node = { entry: { id: 'nodeId' } };
dialogRefMock = { dialogRefMock = {
afterClosed: val => Observable.of(val) afterClosed: val => Observable.of(val),
componentInstance: {
error: new Subject<any>(),
success: new Subject<MinimalNodeEntryEntity>()
}
}; };
spyOn(dialog, 'open').and.returnValue(dialogRefMock); spyOn(dialog, 'open').and.returnValue(dialogRefMock);
@ -113,4 +129,29 @@ describe('FolderCreateDirective', () => {
expect(contentService.folderCreate.next).not.toHaveBeenCalled(); expect(contentService.folderCreate.next).not.toHaveBeenCalled();
}); });
}); });
it('should emit success event with node if the folder creation was successful', async(() => {
const testNode = <MinimalNodeEntryEntity> {};
fixture.detectChanges();
element.triggerEventHandler('click', event);
dialogRefMock.componentInstance.success.next(testNode);
fixture.whenStable().then(() => {
expect(fixture.componentInstance.successParameter).toBe(testNode);
});
}));
it('should open the dialog with the proper title', async(() => {
fixture.detectChanges();
element.triggerEventHandler('click', event);
expect(dialog.open).toHaveBeenCalledWith(jasmine.any(Function), {
data: {
parentNodeId: jasmine.any(String),
createTitle: 'create-title'
},
width: jasmine.any(String)
});
}));
}); });

View File

@ -35,10 +35,16 @@ export class FolderCreateDirective {
@Input('adf-create-folder') @Input('adf-create-folder')
parentNodeId: string = DEFAULT_FOLDER_PARENT_ID; parentNodeId: string = DEFAULT_FOLDER_PARENT_ID;
@Input()
title: string = null;
/** Emitted when the create folder give error for example a folder with same name already exist */ /** Emitted when the create folder give error for example a folder with same name already exist */
@Output() @Output()
error: EventEmitter<any> = new EventEmitter<any>(); error: EventEmitter<any> = new EventEmitter<any>();
@Output()
success: EventEmitter<MinimalNodeEntryEntity> = new EventEmitter<MinimalNodeEntryEntity>();
@HostListener('click', [ '$event' ]) @HostListener('click', [ '$event' ])
onClick(event) { onClick(event) {
event.preventDefault(); event.preventDefault();
@ -55,7 +61,10 @@ export class FolderCreateDirective {
const { parentNodeId } = this; const { parentNodeId } = this;
return { return {
data: { parentNodeId }, data: {
parentNodeId,
createTitle: this.title
},
width: `${width}px` width: `${width}px`
}; };
} }
@ -68,6 +77,10 @@ export class FolderCreateDirective {
this.error.emit(error); this.error.emit(error);
}); });
dialogInstance.componentInstance.success.subscribe((node: MinimalNodeEntryEntity) => {
this.success.emit(node);
});
dialogInstance.afterClosed().subscribe((node: MinimalNodeEntryEntity) => { dialogInstance.afterClosed().subscribe((node: MinimalNodeEntryEntity) => {
if (node) { if (node) {
content.folderCreate.next(node); content.folderCreate.next(node);

View File

@ -17,7 +17,7 @@
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { MatDialog, MatDialogModule } from '@angular/material'; import { MatDialog, MatDialogModule } from '@angular/material';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
@ -27,12 +27,19 @@ import { Observable } from 'rxjs/Observable';
import { ContentService, TranslateLoaderService, DirectiveModule } from '@alfresco/adf-core'; import { ContentService, TranslateLoaderService, DirectiveModule } from '@alfresco/adf-core';
import { FolderEditDirective } from './folder-edit.directive'; import { FolderEditDirective } from './folder-edit.directive';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { Subject } from 'rxjs/Subject';
@Component({ @Component({
template: '<div [adf-edit-folder]="folder"></div>' template: '<div [adf-edit-folder]="folder" (success)="success($event)" title="edit-title"></div>'
}) })
class TestComponent { class TestComponent {
folder = {}; folder = {};
public successParameter: MinimalNodeEntryEntity = null;
success(node: MinimalNodeEntryEntity) {
this.successParameter = node;
}
} }
describe('FolderEditDirective', () => { describe('FolderEditDirective', () => {
@ -85,7 +92,11 @@ describe('FolderEditDirective', () => {
node = { entry: { id: 'folderId' } }; node = { entry: { id: 'folderId' } };
dialogRefMock = { dialogRefMock = {
afterClosed: val => Observable.of(val) afterClosed: val => Observable.of(val),
componentInstance: {
error: new Subject<any>(),
success: new Subject<MinimalNodeEntryEntity>()
}
}; };
spyOn(dialog, 'open').and.returnValue(dialogRefMock); spyOn(dialog, 'open').and.returnValue(dialogRefMock);
@ -114,4 +125,29 @@ describe('FolderEditDirective', () => {
expect(contentService.folderEdit.next).not.toHaveBeenCalled(); expect(contentService.folderEdit.next).not.toHaveBeenCalled();
}); });
}); });
it('should emit success event with node if the folder creation was successful', async(() => {
const testNode = <MinimalNodeEntryEntity> {};
fixture.detectChanges();
element.triggerEventHandler('click', event);
dialogRefMock.componentInstance.success.next(testNode);
fixture.whenStable().then(() => {
expect(fixture.componentInstance.successParameter).toBe(testNode);
});
}));
it('should open the dialog with the proper title', async(() => {
fixture.detectChanges();
element.triggerEventHandler('click', event);
expect(dialog.open).toHaveBeenCalledWith(jasmine.any(Function), {
data: {
folder: jasmine.any(Object),
editTitle: 'edit-title'
},
width: jasmine.any(String)
});
}));
}); });

View File

@ -39,6 +39,12 @@ export class FolderEditDirective {
@Output() @Output()
error: EventEmitter<any> = new EventEmitter<any>(); error: EventEmitter<any> = new EventEmitter<any>();
@Input()
title: string = null;
@Output()
success: EventEmitter<MinimalNodeEntryEntity> = new EventEmitter<MinimalNodeEntryEntity>();
@HostListener('click', [ '$event' ]) @HostListener('click', [ '$event' ])
onClick(event) { onClick(event) {
event.preventDefault(); event.preventDefault();
@ -58,7 +64,10 @@ export class FolderEditDirective {
const { folder } = this; const { folder } = this;
return { return {
data: { folder }, data: {
folder,
editTitle: this.title
},
width: `${width}px` width: `${width}px`
}; };
} }
@ -71,6 +80,10 @@ export class FolderEditDirective {
this.error.emit(error); this.error.emit(error);
}); });
dialogInstance.componentInstance.success.subscribe((node: MinimalNodeEntryEntity) => {
this.success.emit(node);
});
dialogInstance.afterClosed().subscribe((node: MinimalNodeEntryEntity) => { dialogInstance.afterClosed().subscribe((node: MinimalNodeEntryEntity) => {
if (node) { if (node) {
content.folderEdit.next(node); content.folderEdit.next(node);

View File

@ -277,4 +277,52 @@ describe('SidenavLayoutComponent', () => {
expect(component.isMenuMinimized).toBe(false); expect(component.isMenuMinimized).toBe(false);
}); });
}); });
describe('menuOpenState', () => {
let component;
beforeEach(async(() => {
TestBed.compileComponents();
}));
beforeEach(() => {
mediaMatcher = TestBed.get(MediaMatcher);
spyOn(mediaMatcher, 'matchMedia').and.returnValue(mediaQueryList);
fixture = TestBed.createComponent(SidenavLayoutComponent);
component = fixture.componentInstance;
});
it('should be true by default', (done) => {
fixture.detectChanges();
component.menuOpenState$.subscribe((value) => {
expect(value).toBe(true);
done();
});
});
it('should be the same as the expandedSidenav\'s value by default', (done) => {
component.expandedSidenav = false;
fixture.detectChanges();
component.menuOpenState$.subscribe((value) => {
expect(value).toBe(false);
done();
});
});
it('should emit value on toggleMenu action', (done) => {
component.expandedSidenav = false;
fixture.detectChanges();
component.toggleMenu();
component.menuOpenState$.subscribe((value) => {
expect(value).toBe(true);
done();
});
});
});
}); });

View File

@ -20,6 +20,8 @@ import { MediaMatcher } from '@angular/cdk/layout';
import { SidenavLayoutContentDirective } from '../../directives/sidenav-layout-content.directive'; import { SidenavLayoutContentDirective } from '../../directives/sidenav-layout-content.directive';
import { SidenavLayoutHeaderDirective } from '../../directives/sidenav-layout-header.directive'; import { SidenavLayoutHeaderDirective } from '../../directives/sidenav-layout-header.directive';
import { SidenavLayoutNavigationDirective } from '../../directives/sidenav-layout-navigation.directive'; import { SidenavLayoutNavigationDirective } from '../../directives/sidenav-layout-navigation.directive';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
@Component({ @Component({
selector: 'adf-sidenav-layout', selector: 'adf-sidenav-layout',
@ -40,11 +42,15 @@ export class SidenavLayoutComponent implements OnInit, AfterViewInit, OnDestroy
@ContentChild(SidenavLayoutNavigationDirective) navigationDirective: SidenavLayoutNavigationDirective; @ContentChild(SidenavLayoutNavigationDirective) navigationDirective: SidenavLayoutNavigationDirective;
@ContentChild(SidenavLayoutContentDirective) contentDirective: SidenavLayoutContentDirective; @ContentChild(SidenavLayoutContentDirective) contentDirective: SidenavLayoutContentDirective;
private menuOpenStateSubject: BehaviorSubject<boolean>;
public menuOpenState$: Observable<boolean>;
@ViewChild('container') container: any; @ViewChild('container') container: any;
@ViewChild('emptyTemplate') emptyTemplate: any; @ViewChild('emptyTemplate') emptyTemplate: any;
mediaQueryList: MediaQueryList; mediaQueryList: MediaQueryList;
isMenuMinimized; _isMenuMinimized;
templateContext = { templateContext = {
toggleMenu: () => {}, toggleMenu: () => {},
isMenuMinimized: () => this.isMenuMinimized isMenuMinimized: () => this.isMenuMinimized
@ -55,8 +61,14 @@ export class SidenavLayoutComponent implements OnInit, AfterViewInit, OnDestroy
} }
ngOnInit() { ngOnInit() {
const initialMenuState = !this.expandedSidenav;
this.menuOpenStateSubject = new BehaviorSubject<boolean>(initialMenuState);
this.menuOpenState$ = this.menuOpenStateSubject.asObservable();
const stepOver = this.stepOver || SidenavLayoutComponent.STEP_OVER; const stepOver = this.stepOver || SidenavLayoutComponent.STEP_OVER;
this.isMenuMinimized = !this.expandedSidenav; this.isMenuMinimized = initialMenuState;
this.mediaQueryList = this.mediaMatcher.matchMedia(`(max-width: ${stepOver}px)`); this.mediaQueryList = this.mediaMatcher.matchMedia(`(max-width: ${stepOver}px)`);
this.mediaQueryList.addListener(this.onMediaQueryChange); this.mediaQueryList.addListener(this.onMediaQueryChange);
} }
@ -79,6 +91,15 @@ export class SidenavLayoutComponent implements OnInit, AfterViewInit, OnDestroy
this.container.toggleMenu(); this.container.toggleMenu();
} }
get isMenuMinimized() {
return this._isMenuMinimized;
}
set isMenuMinimized(menuState: boolean) {
this._isMenuMinimized = menuState;
this.menuOpenStateSubject.next(!menuState);
}
get isHeaderInside() { get isHeaderInside() {
return this.mediaQueryList.matches; return this.mediaQueryList.matches;
} }