From ee9393caf0dc8f397d2a264f615a5bdc5133209f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Popovics=20Andr=C3=A1s?= Date: Tue, 17 Apr 2018 20:27:42 +0100 Subject: [PATCH] [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 --- .../folder-create.directive.md | 6 +- .../content-services/folder-edit.directive.md | 6 +- docs/core/sidenav-layout.component.md | 20 +- .../dialogs/folder.dialog.html | 7 +- .../dialogs/folder.dialog.spec.ts | 562 +++++++++++------- lib/content-services/dialogs/folder.dialog.ts | 18 +- .../folder-create.directive.spec.ts | 47 +- .../folder-create.directive.ts | 15 +- .../folder-edit.directive.spec.ts | 42 +- .../folder-directive/folder-edit.directive.ts | 15 +- .../sidenav-layout.component.spec.ts | 48 ++ .../sidenav-layout.component.ts | 25 +- 12 files changed, 569 insertions(+), 242 deletions(-) diff --git a/docs/content-services/folder-create.directive.md b/docs/content-services/folder-create.directive.md index 3f90b211ba..0cc9c6eeb3 100644 --- a/docs/content-services/folder-create.directive.md +++ b/docs/content-services/folder-create.directive.md @@ -13,7 +13,9 @@ Creates folders. ```html @@ -30,12 +32,14 @@ Creates folders. | Name | Type | Default value | Description | | -- | -- | -- | -- | | 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 | Name | Type | Description | | -- | -- | -- | | error | `EventEmitter` | Emitted when an error occurs (for example a folder with same name already exists) | +| success | `EventEmitter` | Emitted when the creation successfully happened | ## Details diff --git a/docs/content-services/folder-edit.directive.md b/docs/content-services/folder-edit.directive.md index 05c67adba9..172ba35b9f 100644 --- a/docs/content-services/folder-edit.directive.md +++ b/docs/content-services/folder-edit.directive.md @@ -13,7 +13,9 @@ Allows folders to be edited. ```html @@ -30,12 +32,14 @@ Allows folders to be edited. | Name | Type | Default value | Description | | -- | -- | -- | -- | | adf-edit-folder | `MinimalNodeEntryEntity` | | Folder node to edit. | +| title | `string` | null | The title of the opened dialog. | ### Events | Name | Type | Description | | -- | -- | -- | | error | `EventEmitter` | Emitted when an error occurs (for example a folder with same name already exists) | +| success | `EventEmitter` | Emitted when the edition successfully happened | ## Details diff --git a/docs/core/sidenav-layout.component.md b/docs/core/sidenav-layout.component.md index 50a30e7e04..34e040f03e 100644 --- a/docs/core/sidenav-layout.component.md +++ b/docs/core/sidenav-layout.component.md @@ -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 number of pixels at which the switch will occur). -The small screen layout uses the Angular Material -[Sidenav component](https://material.angularjs.org/latest/api/directive/mdSidenav) which is +The small screen layout uses the Angular Material [Sidenav component](https://material.angularjs.org/latest/api/directive/mdSidenav) which is described in detail on their website. 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): ![Sidenav on mobile](../docassets/images/sidenav-layout-mobile.png) +### Public attributes + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| menuOpenState$ | Observable<boolean> | true | Another way to listen to menu open/closed state | + ### Template context 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. - `isMenuMinimized(): boolean` - Is the menu in minimized/compacted state? Only works for large screen layouts. \ No newline at end of file + 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 \ No newline at end of file diff --git a/lib/content-services/dialogs/folder.dialog.html b/lib/content-services/dialogs/folder.dialog.html index e4cdf6b4b6..465bdd2041 100644 --- a/lib/content-services/dialogs/folder.dialog.html +++ b/lib/content-services/dialogs/folder.dialog.html @@ -1,10 +1,5 @@

- {{ - (editing - ? 'CORE.FOLDER_DIALOG.EDIT_FOLDER_TITLE' - : 'CORE.FOLDER_DIALOG.CREATE_FOLDER_TITLE' - ) | translate - }} + {{ (editing ? editTitle : createTitle) | translate }}

diff --git a/lib/content-services/dialogs/folder.dialog.spec.ts b/lib/content-services/dialogs/folder.dialog.spec.ts index 1bcc3608b7..0f01522022 100644 --- a/lib/content-services/dialogs/folder.dialog.spec.ts +++ b/lib/content-services/dialogs/folder.dialog.spec.ts @@ -18,12 +18,13 @@ import { async, TestBed } from '@angular/core/testing'; import { ComponentFixture } from '@angular/core/testing'; 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 { Observable } from 'rxjs/Observable'; import { NodesApiService, TranslationService } from '@alfresco/adf-core'; import { FolderDialogComponent } from './folder.dialog'; +import { By } from '@angular/platform-browser'; describe('FolderDialogComponent', () => { @@ -33,47 +34,55 @@ describe('FolderDialogComponent', () => { let nodesApi: NodesApiService; let dialogRef; - 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')); + afterEach(() => { + fixture.destroy(); + TestBed.resetTestingModule(); }); - 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(() => { + 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 = { folder: { id: 'node-id', @@ -83,197 +92,312 @@ describe('FolderDialogComponent', () => { } } }; + 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', () => { - expect(component.name).toBe('folder-name'); - expect(component.description).toBe('folder-description'); - }); + it('should have the proper overridden title in case of creating', () => { - 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 = { parentNodeId: 'parentNodeId', folder: null }; + 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(); + }); + }); + + }); + }); }); diff --git a/lib/content-services/dialogs/folder.dialog.ts b/lib/content-services/dialogs/folder.dialog.ts index 394a2f4c3c..abdb640e9e 100644 --- a/lib/content-services/dialogs/folder.dialog.ts +++ b/lib/content-services/dialogs/folder.dialog.ts @@ -42,6 +42,12 @@ export class FolderDialogComponent implements OnInit { @Output() error: EventEmitter = new EventEmitter(); + @Output() + success: EventEmitter = new EventEmitter(); + + editTitle = 'CORE.FOLDER_DIALOG.EDIT_FOLDER_TITLE'; + createTitle = 'CORE.FOLDER_DIALOG.CREATE_FOLDER_TITLE'; + constructor( private formBuilder: FormBuilder, private dialog: MatDialogRef, @@ -50,7 +56,12 @@ export class FolderDialogComponent implements OnInit { @Optional() @Inject(MAT_DIALOG_DATA) public data: any - ) {} + ) { + if (data) { + this.editTitle = data.editTitle || this.editTitle; + this.createTitle = data.createTitle || this.createTitle; + } + } get editing(): boolean { return !!this.data.folder; @@ -121,7 +132,10 @@ export class FolderDialogComponent implements OnInit { (editing ? this.edit() : this.create()) .subscribe( - (folder: MinimalNodeEntryEntity) => dialog.close(folder), + (folder: MinimalNodeEntryEntity) => { + this.success.emit(folder); + dialog.close(folder); + }, (error) => this.handleError(error) ); } diff --git a/lib/content-services/folder-directive/folder-create.directive.spec.ts b/lib/content-services/folder-directive/folder-create.directive.spec.ts index 7ef11a483a..8580d2be56 100644 --- a/lib/content-services/folder-directive/folder-create.directive.spec.ts +++ b/lib/content-services/folder-directive/folder-create.directive.spec.ts @@ -17,7 +17,7 @@ import { HttpClientModule } from '@angular/common/http'; 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 { MatDialog, MatDialogModule } from '@angular/material'; 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 { FolderCreateDirective } from './folder-create.directive'; +import { Subject } from 'rxjs/Subject'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; @Component({ - template: '
' + template: '
' }) class TestComponent { parentNode = ''; + public successParameter: MinimalNodeEntryEntity = null; + + success(node: MinimalNodeEntryEntity) { + this.successParameter = node; + } } describe('FolderCreateDirective', () => { @@ -43,6 +50,11 @@ describe('FolderCreateDirective', () => { let contentService: ContentService; let dialogRefMock; + const event = { + type: 'click', + preventDefault: () => null + }; + beforeEach(() => { TestBed.configureTestingModule({ imports: [ @@ -80,7 +92,11 @@ describe('FolderCreateDirective', () => { node = { entry: { id: 'nodeId' } }; dialogRefMock = { - afterClosed: val => Observable.of(val) + afterClosed: val => Observable.of(val), + componentInstance: { + error: new Subject(), + success: new Subject() + } }; spyOn(dialog, 'open').and.returnValue(dialogRefMock); @@ -113,4 +129,29 @@ describe('FolderCreateDirective', () => { expect(contentService.folderCreate.next).not.toHaveBeenCalled(); }); }); + + it('should emit success event with node if the folder creation was successful', async(() => { + const testNode = {}; + 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) + }); + })); }); diff --git a/lib/content-services/folder-directive/folder-create.directive.ts b/lib/content-services/folder-directive/folder-create.directive.ts index a6969c4950..946a7357ea 100644 --- a/lib/content-services/folder-directive/folder-create.directive.ts +++ b/lib/content-services/folder-directive/folder-create.directive.ts @@ -35,10 +35,16 @@ export class FolderCreateDirective { @Input('adf-create-folder') 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 */ @Output() error: EventEmitter = new EventEmitter(); + @Output() + success: EventEmitter = new EventEmitter(); + @HostListener('click', [ '$event' ]) onClick(event) { event.preventDefault(); @@ -55,7 +61,10 @@ export class FolderCreateDirective { const { parentNodeId } = this; return { - data: { parentNodeId }, + data: { + parentNodeId, + createTitle: this.title + }, width: `${width}px` }; } @@ -68,6 +77,10 @@ export class FolderCreateDirective { this.error.emit(error); }); + dialogInstance.componentInstance.success.subscribe((node: MinimalNodeEntryEntity) => { + this.success.emit(node); + }); + dialogInstance.afterClosed().subscribe((node: MinimalNodeEntryEntity) => { if (node) { content.folderCreate.next(node); diff --git a/lib/content-services/folder-directive/folder-edit.directive.spec.ts b/lib/content-services/folder-directive/folder-edit.directive.spec.ts index 4e29979782..cfdd9f746c 100644 --- a/lib/content-services/folder-directive/folder-edit.directive.spec.ts +++ b/lib/content-services/folder-directive/folder-edit.directive.spec.ts @@ -17,7 +17,7 @@ import { HttpClientModule } from '@angular/common/http'; 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 { By } from '@angular/platform-browser'; @@ -27,12 +27,19 @@ import { Observable } from 'rxjs/Observable'; import { ContentService, TranslateLoaderService, DirectiveModule } from '@alfresco/adf-core'; import { FolderEditDirective } from './folder-edit.directive'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { Subject } from 'rxjs/Subject'; @Component({ - template: '
' + template: '
' }) class TestComponent { folder = {}; + public successParameter: MinimalNodeEntryEntity = null; + + success(node: MinimalNodeEntryEntity) { + this.successParameter = node; + } } describe('FolderEditDirective', () => { @@ -85,7 +92,11 @@ describe('FolderEditDirective', () => { node = { entry: { id: 'folderId' } }; dialogRefMock = { - afterClosed: val => Observable.of(val) + afterClosed: val => Observable.of(val), + componentInstance: { + error: new Subject(), + success: new Subject() + } }; spyOn(dialog, 'open').and.returnValue(dialogRefMock); @@ -114,4 +125,29 @@ describe('FolderEditDirective', () => { expect(contentService.folderEdit.next).not.toHaveBeenCalled(); }); }); + + it('should emit success event with node if the folder creation was successful', async(() => { + const testNode = {}; + 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) + }); + })); }); diff --git a/lib/content-services/folder-directive/folder-edit.directive.ts b/lib/content-services/folder-directive/folder-edit.directive.ts index b183820d0b..b54bf23363 100644 --- a/lib/content-services/folder-directive/folder-edit.directive.ts +++ b/lib/content-services/folder-directive/folder-edit.directive.ts @@ -39,6 +39,12 @@ export class FolderEditDirective { @Output() error: EventEmitter = new EventEmitter(); + @Input() + title: string = null; + + @Output() + success: EventEmitter = new EventEmitter(); + @HostListener('click', [ '$event' ]) onClick(event) { event.preventDefault(); @@ -58,7 +64,10 @@ export class FolderEditDirective { const { folder } = this; return { - data: { folder }, + data: { + folder, + editTitle: this.title + }, width: `${width}px` }; } @@ -71,6 +80,10 @@ export class FolderEditDirective { this.error.emit(error); }); + dialogInstance.componentInstance.success.subscribe((node: MinimalNodeEntryEntity) => { + this.success.emit(node); + }); + dialogInstance.afterClosed().subscribe((node: MinimalNodeEntryEntity) => { if (node) { content.folderEdit.next(node); diff --git a/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.spec.ts b/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.spec.ts index b0c924bc5d..9e6781f290 100644 --- a/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.spec.ts +++ b/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.spec.ts @@ -277,4 +277,52 @@ describe('SidenavLayoutComponent', () => { 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(); + }); + }); + }); }); diff --git a/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.ts b/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.ts index 734ef08dae..e831b937f3 100644 --- a/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.ts +++ b/lib/core/sidenav-layout/components/sidenav-layout/sidenav-layout.component.ts @@ -20,6 +20,8 @@ import { MediaMatcher } from '@angular/cdk/layout'; import { SidenavLayoutContentDirective } from '../../directives/sidenav-layout-content.directive'; import { SidenavLayoutHeaderDirective } from '../../directives/sidenav-layout-header.directive'; import { SidenavLayoutNavigationDirective } from '../../directives/sidenav-layout-navigation.directive'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { Observable } from 'rxjs/Observable'; @Component({ selector: 'adf-sidenav-layout', @@ -40,11 +42,15 @@ export class SidenavLayoutComponent implements OnInit, AfterViewInit, OnDestroy @ContentChild(SidenavLayoutNavigationDirective) navigationDirective: SidenavLayoutNavigationDirective; @ContentChild(SidenavLayoutContentDirective) contentDirective: SidenavLayoutContentDirective; + private menuOpenStateSubject: BehaviorSubject; + public menuOpenState$: Observable; + @ViewChild('container') container: any; @ViewChild('emptyTemplate') emptyTemplate: any; mediaQueryList: MediaQueryList; - isMenuMinimized; + _isMenuMinimized; + templateContext = { toggleMenu: () => {}, isMenuMinimized: () => this.isMenuMinimized @@ -55,8 +61,14 @@ export class SidenavLayoutComponent implements OnInit, AfterViewInit, OnDestroy } ngOnInit() { + const initialMenuState = !this.expandedSidenav; + + this.menuOpenStateSubject = new BehaviorSubject(initialMenuState); + this.menuOpenState$ = this.menuOpenStateSubject.asObservable(); + 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.addListener(this.onMediaQueryChange); } @@ -79,6 +91,15 @@ export class SidenavLayoutComponent implements OnInit, AfterViewInit, OnDestroy this.container.toggleMenu(); } + get isMenuMinimized() { + return this._isMenuMinimized; + } + + set isMenuMinimized(menuState: boolean) { + this._isMenuMinimized = menuState; + this.menuOpenStateSubject.next(!menuState); + } + get isHeaderInside() { return this.mediaQueryList.matches; }