From 8603d13f71889db5cadcf112bfbe5fc97818dcff Mon Sep 17 00:00:00 2001 From: Cilibiu Bogdan Date: Wed, 22 Jan 2020 10:35:32 +0200 Subject: [PATCH] [ACA-1921] Create folder structure from template (#1309) * update template actions * update template effects * declare menu option * rename dialog component * rename service * update tests * update docs * e2e fix locator * fix translation reference --- docs/extending/application-actions.md | 6 +- .../dialog/create-from-template-dialog.ts | 2 +- .../store/src/actions/template.actions.ts | 20 +- src/app/app.module.ts | 6 +- .../create-from-template.dialog.html | 19 +- .../create-from-template.dialog.scss | 4 +- .../create-from-template.dialog.spec.ts | 95 +++--- .../create-from-template.dialog.ts | 40 ++- .../create-from-template-dialog.service.ts | 35 -- .../create-file-from-template.service.spec.ts | 204 ------------ .../services/node-template.service.spec.ts | 308 ++++++++++++++++++ ...te.service.ts => node-template.service.ts} | 45 ++- .../store/effects/template.effects.spec.ts | 117 ++++--- src/app/store/effects/template.effects.ts | 81 +++-- src/app/ui/custom-theme.scss | 4 +- src/assets/app.extensions.json | 14 + src/assets/i18n/en.json | 11 +- 17 files changed, 598 insertions(+), 413 deletions(-) rename src/app/dialogs/{node-templates => node-template}/create-from-template.dialog.html (69%) rename src/app/dialogs/{node-templates => node-template}/create-from-template.dialog.scss (93%) rename src/app/dialogs/{node-templates => node-template}/create-from-template.dialog.spec.ts (73%) rename src/app/dialogs/{node-templates => node-template}/create-from-template.dialog.ts (76%) delete mode 100644 src/app/dialogs/node-templates/create-from-template-dialog.service.ts delete mode 100644 src/app/services/create-file-from-template.service.spec.ts create mode 100644 src/app/services/node-template.service.spec.ts rename src/app/services/{create-file-from-template.service.ts => node-template.service.ts} (77%) diff --git a/docs/extending/application-actions.md b/docs/extending/application-actions.md index 0d6108763..4e14b50b3 100644 --- a/docs/extending/application-actions.md +++ b/docs/extending/application-actions.md @@ -124,6 +124,6 @@ Below is the list of public actions types you can use in the plugin definitions | 1.8.0 | VIEW_NODE | NodeId<`string`> , [ViewNodeExtras](../features/file-viewer.md#details)<`any`> | Lightweight preview of a node by id. Can be invoked from extensions. For details also see [File Viewer](../features/file-viewer.md#details) | | 1.8.0 | CLOSE_PREVIEW | n/a | Closes the viewer ( preview of the item ) | | 1.9.0 | RESET_SELECTION | n/a | Resets active document list selection | -| 1.10.0 | FILE_FROM_TEMPLATE | n/a | Invoke dialogs flow for creating a file from selected template| -| 1.10.0 | CREATE_FILE_FROM_TEMPLATE | Node | Copy selected template into current folder | -| 1.10.0 | CONTEXT_MENU | MouseEvent | Invoke context menu for [DocumentListComponent](https://www.alfresco.com/abn/adf/docs/content-services/components/document-list.component) | +| 1.10.0 | FILE_FROM_TEMPLATE | n/a | Invoke dialogs flow for creating a file from a template into current folder | +| 1.10.0 | FOLDER_FROM_TEMPLATE | n/a | Invoke dialogs flow for creating a folder structure from a template into current folder | +| 1.10.0 | CONTEXT_MENU | MouseEvent | Invoke context menu for [DocumentListComponent](https://www.alfresco.com/abn/adf/docs/content-services/components/document-list.component) | diff --git a/e2e/components/dialog/create-from-template-dialog.ts b/e2e/components/dialog/create-from-template-dialog.ts index 66df52621..c98b245b5 100755 --- a/e2e/components/dialog/create-from-template-dialog.ts +++ b/e2e/components/dialog/create-from-template-dialog.ts @@ -29,7 +29,7 @@ import { Component } from '../component'; export class CreateFromTemplateDialog extends Component { private static selectors = { - root: '.aca-file-from-template-dialog', + root: '.aca-create-from-template-dialog', title: '.mat-dialog-title', nameInput: 'input[placeholder="Name" i]', diff --git a/projects/aca-shared/store/src/actions/template.actions.ts b/projects/aca-shared/store/src/actions/template.actions.ts index 6b8feb285..156d94348 100644 --- a/projects/aca-shared/store/src/actions/template.actions.ts +++ b/projects/aca-shared/store/src/actions/template.actions.ts @@ -28,7 +28,9 @@ import { Node } from '@alfresco/js-api'; export enum TemplateActionTypes { FileFromTemplate = 'FILE_FROM_TEMPLATE', - CreateFileFromTemplate = 'CREATE_FILE_FROM_TEMPLATE' + FolderFromTemplate = 'FOLDER_FROM_TEMPLATE', + CreateFromTemplate = 'CREATE_FROM_TEMPLATE', + CreateFromTemplateSuccess = 'CREATE_FROM_TEMPLATE_SUCCESS' } export class FileFromTemplate implements Action { @@ -37,8 +39,20 @@ export class FileFromTemplate implements Action { constructor() {} } -export class CreateFileFromTemplate implements Action { - readonly type = TemplateActionTypes.CreateFileFromTemplate; +export class FolderFromTemplate implements Action { + readonly type = TemplateActionTypes.FolderFromTemplate; + + constructor() {} +} + +export class CreateFromTemplate implements Action { + readonly type = TemplateActionTypes.CreateFromTemplate; constructor(public payload: Node) {} } + +export class CreateFromTemplateSuccess implements Action { + readonly type = TemplateActionTypes.CreateFromTemplateSuccess; + + constructor(public node: Node) {} +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index f9edc2cf5..3bbeeb1ed 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -76,7 +76,7 @@ import { AppNodeVersionModule } from './components/node-version/node-version.mod import { FavoritesComponent } from './components/favorites/favorites.component'; import { RecentFilesComponent } from './components/recent-files/recent-files.component'; import { SharedFilesComponent } from './components/shared-files/shared-files.component'; -import { CreateFileFromTemplateDialogComponent } from './dialogs/node-templates/create-from-template.dialog'; +import { CreateFromTemplateDialogComponent } from './dialogs/node-template/create-from-template.dialog'; import { environment } from '../environments/environment'; import { registerLocaleData } from '@angular/common'; @@ -159,7 +159,7 @@ registerLocaleData(localeSv); FavoritesComponent, RecentFilesComponent, SharedFilesComponent, - CreateFileFromTemplateDialogComponent + CreateFromTemplateDialogComponent ], providers: [ { provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy }, @@ -177,7 +177,7 @@ registerLocaleData(localeSv); NodeVersionsDialogComponent, NodeVersionUploadDialogComponent, LibraryDialogComponent, - CreateFileFromTemplateDialogComponent + CreateFromTemplateDialogComponent ], bootstrap: [AppComponent] }) diff --git a/src/app/dialogs/node-templates/create-from-template.dialog.html b/src/app/dialogs/node-template/create-from-template.dialog.html similarity index 69% rename from src/app/dialogs/node-templates/create-from-template.dialog.html rename to src/app/dialogs/node-template/create-from-template.dialog.html index a542bebd2..7fc0a9b55 100644 --- a/src/app/dialogs/node-templates/create-from-template.dialog.html +++ b/src/app/dialogs/node-template/create-from-template.dialog.html @@ -1,13 +1,10 @@ -

+

- {{ 'FILE_FROM_TEMPLATE.FORM.ERRORS.TITLE_TOO_LONG' | translate }} + {{ 'NODE_FROM_TEMPLATE.FORM.ERRORS.TITLE_TOO_LONG' | translate }} - {{ 'FILE_FROM_TEMPLATE.FORM.ERRORS.DESCRIPTION_TOO_LONG' | translate }} + {{ 'NODE_FROM_TEMPLATE.FORM.ERRORS.DESCRIPTION_TOO_LONG' | translate }}
diff --git a/src/app/dialogs/node-templates/create-from-template.dialog.scss b/src/app/dialogs/node-template/create-from-template.dialog.scss similarity index 93% rename from src/app/dialogs/node-templates/create-from-template.dialog.scss rename to src/app/dialogs/node-template/create-from-template.dialog.scss index 1cc586b47..7cbbb2947 100644 --- a/src/app/dialogs/node-templates/create-from-template.dialog.scss +++ b/src/app/dialogs/node-template/create-from-template.dialog.scss @@ -1,10 +1,10 @@ -@mixin app-create-file-from-template-theme($theme) { +@mixin app-create-from-template-theme($theme) { $primary: map-get($theme, primary); $accent: map-get($theme, accent); $foreground: map-get($theme, foreground); $background: map-get($theme, background); - .aca-file-from-template-dialog { + .aca-create-from-template-dialog { ng-component { overflow: visible; } diff --git a/src/app/dialogs/node-templates/create-from-template.dialog.spec.ts b/src/app/dialogs/node-template/create-from-template.dialog.spec.ts similarity index 73% rename from src/app/dialogs/node-templates/create-from-template.dialog.spec.ts rename to src/app/dialogs/node-template/create-from-template.dialog.spec.ts index 80eb35e2a..cf710cadb 100644 --- a/src/app/dialogs/node-templates/create-from-template.dialog.spec.ts +++ b/src/app/dialogs/node-template/create-from-template.dialog.spec.ts @@ -23,19 +23,18 @@ * along with Alfresco. If not, see . */ -import { CreateFileFromTemplateDialogComponent } from './create-from-template.dialog'; +import { CreateFromTemplateDialogComponent } from './create-from-template.dialog'; import { TestBed, ComponentFixture } from '@angular/core/testing'; import { AppTestingModule } from '../../testing/app-testing.module'; -import { CoreModule } from '@alfresco/adf-core'; +import { CoreModule, TranslationMock } from '@alfresco/adf-core'; import { MatDialogModule, - MatDialogRef, - MAT_DIALOG_DATA + MAT_DIALOG_DATA, + MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; -import { CreateFileFromTemplate } from '@alfresco/aca-shared/store'; +import { CreateFromTemplate } from '@alfresco/aca-shared/store'; import { Node } from '@alfresco/js-api'; -import { CreateFromTemplateDialogService } from './create-from-template-dialog.service'; function text(length: number) { return new Array(length) @@ -48,15 +47,15 @@ function text(length: number) { } describe('CreateFileFromTemplateDialogComponent', () => { - let fixture: ComponentFixture; - let component: CreateFileFromTemplateDialogComponent; - let dialogRef: MatDialogRef; + let fixture: ComponentFixture; + let component: CreateFromTemplateDialogComponent; let store; - let createFromTemplateDialogService: CreateFromTemplateDialogService; const data = { id: 'node-id', name: 'node-name', + isFolder: false, + isFile: true, properties: { 'cm:title': 'node-title', 'cm:description': '' @@ -66,36 +65,39 @@ describe('CreateFileFromTemplateDialogComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [CoreModule.forRoot(), AppTestingModule, MatDialogModule], - declarations: [CreateFileFromTemplateDialogComponent], + declarations: [CreateFromTemplateDialogComponent], providers: [ + { + provide: MatDialogRef, + useValue: { + close: jasmine.createSpy('close') + } + }, + { + provide: TranslationMock, + useValue: { + instant: jasmine.createSpy('instant') + } + }, { provide: Store, useValue: { dispatch: jasmine.createSpy('dispatch') } }, - { provide: MAT_DIALOG_DATA, useValue: data }, - { - provide: MatDialogRef, - useValue: { - close: jasmine.createSpy('close') - } - } + { provide: MAT_DIALOG_DATA, useValue: {} } ] }); - fixture = TestBed.createComponent(CreateFileFromTemplateDialogComponent); - dialogRef = TestBed.get(MatDialogRef); + fixture = TestBed.createComponent(CreateFromTemplateDialogComponent); store = TestBed.get(Store); - createFromTemplateDialogService = TestBed.get( - CreateFromTemplateDialogService - ); component = fixture.componentInstance; - - fixture.detectChanges(); + component.data = data as Node; }); it('should populate form with provided dialog data', () => { + fixture.detectChanges(); + expect(component.form.controls.name.value).toBe(data.name); expect(component.form.controls.title.value).toBe( data.properties['cm:title'] @@ -106,32 +108,47 @@ describe('CreateFileFromTemplateDialogComponent', () => { }); it('should invalidate form if required `name` field is invalid', () => { + fixture.detectChanges(); + component.form.controls.name.setValue(''); fixture.detectChanges(); + expect(component.form.invalid).toBe(true); }); it('should invalidate form if required `name` field has `only spaces`', () => { + fixture.detectChanges(); + component.form.controls.name.setValue(' '); fixture.detectChanges(); + expect(component.form.invalid).toBe(true); }); it('should invalidate form if required `name` field has `ending dot`', () => { + fixture.detectChanges(); + component.form.controls.name.setValue('something.'); fixture.detectChanges(); + expect(component.form.invalid).toBe(true); }); it('should invalidate form if `title` text length is long', () => { + fixture.detectChanges(); + component.form.controls.title.setValue(text(260)); fixture.detectChanges(); + expect(component.form.invalid).toBe(true); }); it('should invalidate form if `description` text length is long', () => { + fixture.detectChanges(); + component.form.controls.description.setValue(text(520)); fixture.detectChanges(); + expect(component.form.invalid).toBe(true); }); @@ -139,11 +156,16 @@ describe('CreateFileFromTemplateDialogComponent', () => { const newNode = { id: 'node-id', name: 'new-node-name', + isFolder: false, + isFile: true, properties: { 'cm:title': 'new-node-title', 'cm:description': 'new-node-description' } } as Node; + + fixture.detectChanges(); + component.form.controls.name.setValue('new-node-name'); component.form.controls.title.setValue('new-node-title'); component.form.controls.description.setValue('new-node-description'); @@ -152,27 +174,8 @@ describe('CreateFileFromTemplateDialogComponent', () => { component.onSubmit(); - expect(store.dispatch).toHaveBeenCalledWith( - new CreateFileFromTemplate(newNode) + expect(store.dispatch['calls'].mostRecent().args[0]).toEqual( + new CreateFromTemplate(newNode) ); }); - - it('should close dialog on create file from template success', done => { - const newNode = { - id: 'node-id', - name: 'new-node-name', - properties: { - 'cm:title': 'new-node-title', - 'cm:description': 'new-node-description' - } - } as Node; - - fixture.detectChanges(); - createFromTemplateDialogService.success$.subscribe(node => { - expect(dialogRef.close).toHaveBeenCalledWith(node); - done(); - }); - - createFromTemplateDialogService.success$.next(newNode); - }); }); diff --git a/src/app/dialogs/node-templates/create-from-template.dialog.ts b/src/app/dialogs/node-template/create-from-template.dialog.ts similarity index 76% rename from src/app/dialogs/node-templates/create-from-template.dialog.ts rename to src/app/dialogs/node-template/create-from-template.dialog.ts index 9a247a738..2b5ad1f90 100644 --- a/src/app/dialogs/node-templates/create-from-template.dialog.ts +++ b/src/app/dialogs/node-template/create-from-template.dialog.ts @@ -33,31 +33,27 @@ import { FormControl, ValidationErrors } from '@angular/forms'; -import { CreateFromTemplateDialogService } from './create-from-template-dialog.service'; import { Store } from '@ngrx/store'; -import { AppStore, CreateFileFromTemplate } from '@alfresco/aca-shared/store'; +import { AppStore, CreateFromTemplate } from '@alfresco/aca-shared/store'; +import { TranslationService } from '@alfresco/adf-core'; @Component({ templateUrl: './create-from-template.dialog.html', encapsulation: ViewEncapsulation.None, styleUrls: ['./create-from-template.dialog.scss'] }) -export class CreateFileFromTemplateDialogComponent implements OnInit { +export class CreateFromTemplateDialogComponent implements OnInit { public form: FormGroup; constructor( - private createFromTemplateDialogService: CreateFromTemplateDialogService, + private translationService: TranslationService, private store: Store, private formBuilder: FormBuilder, - private dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: Node ) {} ngOnInit() { - this.createFromTemplateDialogService.success$.subscribe((data: Node) => { - this.dialogRef.close(data); - }); - this.form = this.formBuilder.group({ name: [ this.data.name, @@ -85,7 +81,21 @@ export class CreateFileFromTemplateDialogComponent implements OnInit { } }; const data: Node = Object.assign({}, this.data, update); - this.store.dispatch(new CreateFileFromTemplate(data)); + this.store.dispatch(new CreateFromTemplate(data)); + } + + title(): string { + if (this.data.isFolder) { + return this.translationService.instant( + 'NODE_FROM_TEMPLATE.FOLDER_DIALOG_TITLE', + { template: this.data.name } + ); + } + + return this.translationService.instant( + 'NODE_FROM_TEMPLATE.FILE_DIALOG_TITLE', + { template: this.data.name } + ); } close() { @@ -101,7 +111,7 @@ export class CreateFileFromTemplateDialogComponent implements OnInit { return isValid ? null : { - message: `FILE_FROM_TEMPLATE.FORM.ERRORS.SPECIAL_CHARACTERS` + message: `NODE_FROM_TEMPLATE.FORM.ERRORS.SPECIAL_CHARACTERS` }; } @@ -115,7 +125,7 @@ export class CreateFileFromTemplateDialogComponent implements OnInit { return isValid ? null : { - message: `FILE_FROM_TEMPLATE.FORM.ERRORS.ENDING_DOT` + message: `NODE_FROM_TEMPLATE.FORM.ERRORS.ENDING_DOT` }; } @@ -126,11 +136,11 @@ export class CreateFileFromTemplateDialogComponent implements OnInit { return isValid ? null : { - message: `FILE_FROM_TEMPLATE.FORM.ERRORS.ONLY_SPACES` + message: `NODE_FROM_TEMPLATE.FORM.ERRORS.ONLY_SPACES` }; } else { return { - message: `FILE_FROM_TEMPLATE.FORM.ERRORS.REQUIRED` + message: `NODE_FROM_TEMPLATE.FORM.ERRORS.REQUIRED` }; } } diff --git a/src/app/dialogs/node-templates/create-from-template-dialog.service.ts b/src/app/dialogs/node-templates/create-from-template-dialog.service.ts deleted file mode 100644 index c6d95a3bf..000000000 --- a/src/app/dialogs/node-templates/create-from-template-dialog.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -/*! - * @license - * Alfresco Example Content Application - * - * Copyright (C) 2005 - 2020 Alfresco Software Limited - * - * This file is part of the Alfresco Example Content Application. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * The Alfresco Example Content Application is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * The Alfresco Example Content Application is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - */ - -import { Injectable } from '@angular/core'; -import { Subject } from 'rxjs'; -import { Node } from '@alfresco/js-api'; - -@Injectable({ - providedIn: 'root' -}) -export class CreateFromTemplateDialogService { - success$: Subject = new Subject(); -} diff --git a/src/app/services/create-file-from-template.service.spec.ts b/src/app/services/create-file-from-template.service.spec.ts deleted file mode 100644 index ca066dfec..000000000 --- a/src/app/services/create-file-from-template.service.spec.ts +++ /dev/null @@ -1,204 +0,0 @@ -/*! - * @license - * Alfresco Example Content Application - * - * Copyright (C) 2005 - 2020 Alfresco Software Limited - * - * This file is part of the Alfresco Example Content Application. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * The Alfresco Example Content Application is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * The Alfresco Example Content Application is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - */ - -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { EffectsModule } from '@ngrx/effects'; -import { AppStore, SnackbarErrorAction } from '@alfresco/aca-shared/store'; -import { TemplateEffects } from '../store/effects/template.effects'; -import { AppTestingModule } from '../testing/app-testing.module'; -import { Store } from '@ngrx/store'; -import { MatDialog } from '@angular/material/dialog'; -import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-core'; -import { CreateFileFromTemplateService } from './create-file-from-template.service'; -import { of } from 'rxjs'; - -describe('CreateFileFromTemplateService', () => { - let dialog: MatDialog; - let store: Store; - let alfrescoApiService: AlfrescoApiService; - let createFileFromTemplateService: CreateFileFromTemplateService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [AppTestingModule, EffectsModule.forRoot([TemplateEffects])], - providers: [ - CreateFileFromTemplateService, - { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock } - ] - }); - - store = TestBed.get(Store); - alfrescoApiService = TestBed.get(AlfrescoApiService); - dialog = TestBed.get(MatDialog); - createFileFromTemplateService = TestBed.get(CreateFileFromTemplateService); - }); - - it('should open dialog with `Node Templates` folder id as data property', () => { - spyOn( - alfrescoApiService.getInstance().nodes, - 'getNodeInfo' - ).and.returnValue(of({ id: 'templates-folder-id' })); - spyOn(dialog, 'open'); - - createFileFromTemplateService.openTemplatesDialog(); - - expect(dialog.open['calls'].argsFor(0)[1].data).toEqual( - jasmine.objectContaining({ currentFolderId: 'templates-folder-id' }) - ); - }); - - it('should remove parents for templates node breadcrumb path', () => { - spyOn( - alfrescoApiService.getInstance().nodes, - 'getNodeInfo' - ).and.returnValue( - of({ - id: 'templates-folder-id', - path: { - elements: [], - name: '/Company Home/Data Dictionary' - } - }) - ); - spyOn(dialog, 'open'); - - createFileFromTemplateService.openTemplatesDialog(); - - const breadcrumb = dialog.open['calls'] - .argsFor(0)[1] - .data.breadcrumbTransform({ - name: 'Node Templates', - path: { - elements: [{ name: 'Company Home' }, { name: 'Data Dictionary' }], - name: '/Company Home/Data Dictionary' - } - }); - - expect(breadcrumb.path.elements).toEqual([]); - }); - - it('should return false if selected node is not a template file', () => { - spyOn( - alfrescoApiService.getInstance().nodes, - 'getNodeInfo' - ).and.returnValue(of({ id: 'templates-folder-id' })); - spyOn(dialog, 'open'); - - createFileFromTemplateService.openTemplatesDialog(); - - const isSelectionValid = dialog.open['calls'] - .argsFor(0)[1] - .data.isSelectionValid({ - isFile: false - }); - - expect(isSelectionValid).toBe(false); - }); - - it('should return true if selected node is a template file', () => { - spyOn( - alfrescoApiService.getInstance().nodes, - 'getNodeInfo' - ).and.returnValue(of({ id: 'templates-folder-id' })); - spyOn(dialog, 'open'); - - createFileFromTemplateService.openTemplatesDialog(); - - const isSelectionValid = dialog.open['calls'] - .argsFor(0)[1] - .data.isSelectionValid({ - isFile: true - }); - - expect(isSelectionValid).toBe(true); - }); - - it('should raise an error when getNodeInfo fails', fakeAsync(() => { - spyOn( - alfrescoApiService.getInstance().nodes, - 'getNodeInfo' - ).and.returnValue( - Promise.reject({ - message: `{ "error": { "statusCode": 404 } } ` - }) - ); - spyOn(store, 'dispatch'); - - createFileFromTemplateService.openTemplatesDialog(); - tick(); - - expect(store.dispatch).toHaveBeenCalledWith( - new SnackbarErrorAction('APP.MESSAGES.ERRORS.GENERIC') - ); - })); - - it('should return true if row is not a `link` nodeType', () => { - spyOn( - alfrescoApiService.getInstance().nodes, - 'getNodeInfo' - ).and.returnValue( - of({ - id: 'templates-folder-id', - path: { - elements: [], - name: '/Company Home/Data Dictionary' - } - }) - ); - spyOn(dialog, 'open'); - - createFileFromTemplateService.openTemplatesDialog(); - - expect( - dialog.open['calls'].argsFor(0)[1].data.rowFilter({ - node: { entry: { nodeType: 'text' } } - }) - ).toBe(true); - }); - - it('should return false if row is a `link` nodeType', () => { - spyOn( - alfrescoApiService.getInstance().nodes, - 'getNodeInfo' - ).and.returnValue( - of({ - id: 'templates-folder-id', - path: { - elements: [], - name: '/Company Home/Data Dictionary' - } - }) - ); - spyOn(dialog, 'open'); - - createFileFromTemplateService.openTemplatesDialog(); - - expect( - dialog.open['calls'].argsFor(0)[1].data.rowFilter({ - node: { entry: { nodeType: 'app:filelink' } } - }) - ).toBe(false); - }); -}); diff --git a/src/app/services/node-template.service.spec.ts b/src/app/services/node-template.service.spec.ts new file mode 100644 index 000000000..3e94713e5 --- /dev/null +++ b/src/app/services/node-template.service.spec.ts @@ -0,0 +1,308 @@ +/*! + * @license + * Alfresco Example Content Application + * + * Copyright (C) 2005 - 2020 Alfresco Software Limited + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { EffectsModule } from '@ngrx/effects'; +import { AppStore, SnackbarErrorAction } from '@alfresco/aca-shared/store'; +import { TemplateEffects } from '../store/effects/template.effects'; +import { AppTestingModule } from '../testing/app-testing.module'; +import { Store } from '@ngrx/store'; +import { MatDialog } from '@angular/material/dialog'; +import { AlfrescoApiService, AlfrescoApiServiceMock } from '@alfresco/adf-core'; +import { NodeTemplateService } from './node-template.service'; +import { of } from 'rxjs'; + +describe('NodeTemplateService', () => { + let dialog: MatDialog; + let store: Store; + let alfrescoApiService: AlfrescoApiService; + let nodeTemplateService: NodeTemplateService; + const fileTemplateConfig = { + relativePath: 'relative-path/parent-file-templates', + selectionType: 'file' + }; + const folderTemplateConfig = { + relativePath: 'relative-path/parent-folder-templates', + selectionType: 'folder' + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [AppTestingModule, EffectsModule.forRoot([TemplateEffects])], + providers: [ + NodeTemplateService, + { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock } + ] + }); + + store = TestBed.get(Store); + alfrescoApiService = TestBed.get(AlfrescoApiService); + dialog = TestBed.get(MatDialog); + nodeTemplateService = TestBed.get(NodeTemplateService); + }); + + it('should open dialog with parent node `id` as data property', () => { + spyOn( + alfrescoApiService.getInstance().nodes, + 'getNodeInfo' + ).and.returnValue(of({ id: 'parent-node-id' })); + spyOn(dialog, 'open'); + + nodeTemplateService.selectTemplateDialog(fileTemplateConfig); + + expect(dialog.open['calls'].argsFor(0)[1].data).toEqual( + jasmine.objectContaining({ currentFolderId: 'parent-node-id' }) + ); + }); + + it('should remove parents for templates node breadcrumb path', () => { + spyOn( + alfrescoApiService.getInstance().nodes, + 'getNodeInfo' + ).and.returnValue( + of({ + id: 'parent-node-id', + path: { + elements: [], + name: '/Company Home/Data Dictionary' + } + }) + ); + spyOn(dialog, 'open'); + + nodeTemplateService.selectTemplateDialog(fileTemplateConfig); + + const breadcrumb = dialog.open['calls'] + .argsFor(0)[1] + .data.breadcrumbTransform({ + name: 'Node Templates', + path: { + elements: [{ name: 'Company Home' }, { name: 'Data Dictionary' }], + name: '/Company Home/Data Dictionary' + } + }); + + expect(breadcrumb.path.elements).toEqual([]); + }); + + it('should raise an error when getNodeInfo fails', fakeAsync(() => { + spyOn( + alfrescoApiService.getInstance().nodes, + 'getNodeInfo' + ).and.returnValue( + Promise.reject({ + message: `{ "error": { "statusCode": 404 } } ` + }) + ); + spyOn(store, 'dispatch'); + + nodeTemplateService.selectTemplateDialog(fileTemplateConfig); + tick(); + + expect(store.dispatch).toHaveBeenCalledWith( + new SnackbarErrorAction('APP.MESSAGES.ERRORS.GENERIC') + ); + })); + + it('should return true if row is not a `link` nodeType', () => { + spyOn( + alfrescoApiService.getInstance().nodes, + 'getNodeInfo' + ).and.returnValue( + of({ + id: 'templates-folder-id', + path: { + elements: [], + name: '/Company Home/Data Dictionary' + } + }) + ); + spyOn(dialog, 'open'); + + nodeTemplateService.selectTemplateDialog(fileTemplateConfig); + + expect( + dialog.open['calls'].argsFor(0)[1].data.rowFilter({ + node: { entry: { nodeType: 'text' } } + }) + ).toBe(true); + }); + + it('should return false if row is a `link` nodeType', () => { + spyOn( + alfrescoApiService.getInstance().nodes, + 'getNodeInfo' + ).and.returnValue( + of({ + id: 'templates-folder-id', + path: { + elements: [], + name: '/Company Home/Data Dictionary' + } + }) + ); + spyOn(dialog, 'open'); + + nodeTemplateService.selectTemplateDialog(fileTemplateConfig); + + expect( + dialog.open['calls'].argsFor(0)[1].data.rowFilter({ + node: { entry: { nodeType: 'app:filelink' } } + }) + ).toBe(false); + }); + + describe('File templates', () => { + it('should return false if selected node is not a file', () => { + spyOn( + alfrescoApiService.getInstance().nodes, + 'getNodeInfo' + ).and.returnValue(of({ id: 'templates-folder-id' })); + spyOn(dialog, 'open'); + + nodeTemplateService.selectTemplateDialog(fileTemplateConfig); + + const isSelectionValid = dialog.open['calls'] + .argsFor(0)[1] + .data.isSelectionValid({ + name: 'some-folder-template', + isFile: false, + isFolder: true + }); + + expect(isSelectionValid).toBe(false); + }); + + it('should return true if selected node is a template file', () => { + spyOn( + alfrescoApiService.getInstance().nodes, + 'getNodeInfo' + ).and.returnValue(of({ id: 'templates-folder-id' })); + spyOn(dialog, 'open'); + + nodeTemplateService.selectTemplateDialog(fileTemplateConfig); + + const isSelectionValid = dialog.open['calls'] + .argsFor(0)[1] + .data.isSelectionValid({ + name: 'some-file-template', + isFile: true, + isFolder: false + }); + + expect(isSelectionValid).toBe(true); + }); + + it('should set dialog title for file templates', () => { + spyOn( + alfrescoApiService.getInstance().nodes, + 'getNodeInfo' + ).and.returnValue(of({ id: 'templates-folder-id' })); + spyOn(dialog, 'open'); + + nodeTemplateService.selectTemplateDialog(fileTemplateConfig); + + const title = dialog.open['calls'].argsFor(0)[1].data.title; + + expect(title).toBe('NODE_SELECTOR.SELECT_FILE_TEMPLATE_TITLE'); + }); + }); + + describe('Folder templates', () => { + it('should return false if selected node is not a folder', () => { + spyOn( + alfrescoApiService.getInstance().nodes, + 'getNodeInfo' + ).and.returnValue(of({ id: 'templates-folder-id' })); + spyOn(dialog, 'open'); + + nodeTemplateService.selectTemplateDialog(folderTemplateConfig); + + const isSelectionValid = dialog.open['calls'] + .argsFor(0)[1] + .data.isSelectionValid({ + name: 'some-file-template', + isFile: true, + isFolder: false + }); + + expect(isSelectionValid).toBe(false); + }); + + it('should return false if current node is the parent folder', () => { + spyOn( + alfrescoApiService.getInstance().nodes, + 'getNodeInfo' + ).and.returnValue(of({ id: 'templates-folder-id' })); + spyOn(dialog, 'open'); + + nodeTemplateService.selectTemplateDialog(folderTemplateConfig); + + const isSelectionValid = dialog.open['calls'] + .argsFor(0)[1] + .data.isSelectionValid({ + name: 'parent-folder-templates', + isFile: false, + isFolder: true + }); + + expect(isSelectionValid).toBe(false); + }); + + it('should return true if selected node is a folder template', () => { + spyOn( + alfrescoApiService.getInstance().nodes, + 'getNodeInfo' + ).and.returnValue(of({ id: 'templates-folder-id' })); + spyOn(dialog, 'open'); + + nodeTemplateService.selectTemplateDialog(folderTemplateConfig); + + const isSelectionValid = dialog.open['calls'] + .argsFor(0)[1] + .data.isSelectionValid({ + name: 'some-folder-template', + isFile: false, + isFolder: true + }); + + expect(isSelectionValid).toBe(true); + }); + + it('should set dialog title for folder templates', () => { + spyOn( + alfrescoApiService.getInstance().nodes, + 'getNodeInfo' + ).and.returnValue(of({ id: 'templates-folder-id' })); + spyOn(dialog, 'open'); + + nodeTemplateService.selectTemplateDialog(folderTemplateConfig); + + const title = dialog.open['calls'].argsFor(0)[1].data.title; + + expect(title).toBe('NODE_SELECTOR.SELECT_FOLDER_TEMPLATE_TITLE'); + }); + }); +}); diff --git a/src/app/services/create-file-from-template.service.ts b/src/app/services/node-template.service.ts similarity index 77% rename from src/app/services/create-file-from-template.service.ts rename to src/app/services/node-template.service.ts index 761c550ce..e13a66838 100644 --- a/src/app/services/create-file-from-template.service.ts +++ b/src/app/services/node-template.service.ts @@ -25,7 +25,7 @@ import { Injectable } from '@angular/core'; import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material'; -import { CreateFileFromTemplateDialogComponent } from '../dialogs/node-templates/create-from-template.dialog'; +import { CreateFromTemplateDialogComponent } from '../dialogs/node-template/create-from-template.dialog'; import { Subject, from, of } from 'rxjs'; import { Node, MinimalNode, MinimalNodeEntryEntity } from '@alfresco/js-api'; import { AlfrescoApiService, TranslationService } from '@alfresco/adf-core'; @@ -38,10 +38,17 @@ import { ShareDataRow } from '@alfresco/adf-content-services'; +export interface TemplateDialogConfig { + relativePath: string; + selectionType: string; +} + @Injectable({ providedIn: 'root' }) -export class CreateFileFromTemplateService { +export class NodeTemplateService { + private currentTemplateConfig: TemplateDialogConfig = null; + constructor( private store: Store, private alfrescoApiService: AlfrescoApiService, @@ -49,14 +56,16 @@ export class CreateFileFromTemplateService { public dialog: MatDialog ) {} - openTemplatesDialog(): Subject { + selectTemplateDialog(config: TemplateDialogConfig): Subject { + this.currentTemplateConfig = config; + const select = new Subject(); select.subscribe({ complete: this.close.bind(this) }); const data: ContentNodeSelectorComponentData = { - title: this.title, + title: this.title(config.selectionType), actionName: 'NEXT', dropdownHideMyFiles: true, currentFolderId: null, @@ -69,7 +78,7 @@ export class CreateFileFromTemplateService { from( this.alfrescoApiService.getInstance().nodes.getNodeInfo('-root-', { - relativePath: 'Data Dictionary/Node Templates' + relativePath: config.relativePath }) ) .pipe( @@ -100,10 +109,10 @@ export class CreateFileFromTemplateService { createTemplateDialog( node: Node - ): MatDialogRef { - return this.dialog.open(CreateFileFromTemplateDialogComponent, { + ): MatDialogRef { + return this.dialog.open(CreateFromTemplateDialogComponent, { data: node, - panelClass: 'aca-file-from-template-dialog', + panelClass: 'aca-create-from-template-dialog', width: '630px' }); } @@ -123,6 +132,14 @@ export class CreateFileFromTemplateService { } private isSelectionValid(node: Node): boolean { + if (node.name === this.currentTemplateConfig.relativePath.split('/')[1]) { + return false; + } + + if (this.currentTemplateConfig.selectionType === 'folder') { + return node.isFolder; + } + return node.isFile; } @@ -130,8 +147,16 @@ export class CreateFileFromTemplateService { this.dialog.closeAll(); } - private get title() { - return this.translation.instant('NODE_SELECTOR.SELECT_TEMPLATE_TITLE'); + private title(selectionType: string) { + if (selectionType === 'file') { + return this.translation.instant( + 'NODE_SELECTOR.SELECT_FILE_TEMPLATE_TITLE' + ); + } + + return this.translation.instant( + 'NODE_SELECTOR.SELECT_FOLDER_TEMPLATE_TITLE' + ); } private rowFilter(row: ShareDataRow): boolean { diff --git a/src/app/store/effects/template.effects.spec.ts b/src/app/store/effects/template.effects.spec.ts index 6f4cb9808..e0d522d00 100644 --- a/src/app/store/effects/template.effects.spec.ts +++ b/src/app/store/effects/template.effects.spec.ts @@ -29,25 +29,27 @@ import { TemplateEffects } from './template.effects'; import { EffectsModule } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { - CreateFileFromTemplate, + CreateFromTemplate, + CreateFromTemplateSuccess, FileFromTemplate, + FolderFromTemplate, SnackbarErrorAction } from '@alfresco/aca-shared/store'; -import { CreateFileFromTemplateService } from '../../services/create-file-from-template.service'; +import { NodeTemplateService } from '../../services/node-template.service'; import { of } from 'rxjs'; import { AlfrescoApiServiceMock, AlfrescoApiService } from '@alfresco/adf-core'; import { ContentManagementService } from '../../services/content-management.service'; import { Node, NodeEntry } from '@alfresco/js-api'; -import { CreateFromTemplateDialogService } from '../../dialogs/node-templates/create-from-template-dialog.service'; +import { MatDialog } from '@angular/material/dialog'; describe('TemplateEffects', () => { let store: Store; - let createFileFromTemplateService: CreateFileFromTemplateService; + let nodeTemplateService: NodeTemplateService; let alfrescoApiService: AlfrescoApiService; let contentManagementService: ContentManagementService; - let createFromTemplateDialogService: CreateFromTemplateDialogService; let copyNodeSpy; let updateNodeSpy; + let matDialog: MatDialog; const node: Node = { name: 'node-name', id: 'node-id', @@ -63,29 +65,41 @@ describe('TemplateEffects', () => { 'cm:description': 'description' } }; + const fileTemplateConfig = { + relativePath: 'Data Dictionary/Node Templates', + selectionType: 'file' + }; + + const folderTemplateConfig = { + relativePath: 'Data Dictionary/Space Templates', + selectionType: 'folder' + }; beforeEach(() => { TestBed.configureTestingModule({ imports: [AppTestingModule, EffectsModule.forRoot([TemplateEffects])], providers: [ - CreateFileFromTemplateService, - { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock } + NodeTemplateService, + { provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock }, + { + provide: MatDialog, + useValue: { + closeAll: jasmine.createSpy('closeAll') + } + } ] }); store = TestBed.get(Store); - createFileFromTemplateService = TestBed.get(CreateFileFromTemplateService); + nodeTemplateService = TestBed.get(NodeTemplateService); alfrescoApiService = TestBed.get(AlfrescoApiService); - createFromTemplateDialogService = TestBed.get( - CreateFromTemplateDialogService - ); contentManagementService = TestBed.get(ContentManagementService); + matDialog = TestBed.get(MatDialog); spyOn(store, 'dispatch').and.callThrough(); - spyOn(createFromTemplateDialogService.success$, 'next'); spyOn(contentManagementService.reload, 'next'); spyOn(store, 'select').and.returnValue(of({ id: 'parent-id' })); - spyOn(createFileFromTemplateService, 'openTemplatesDialog').and.returnValue( + spyOn(nodeTemplateService, 'selectTemplateDialog').and.returnValue( of([{ id: 'template-id' }]) ); @@ -98,41 +112,43 @@ describe('TemplateEffects', () => { updateNodeSpy.calls.reset(); }); - it('should reload content on create file from template', fakeAsync(() => { - spyOn( - createFileFromTemplateService, - 'createTemplateDialog' - ).and.returnValue({ afterClosed: () => of(node) }); + it('should open dialog to select template files', fakeAsync(() => { + spyOn(nodeTemplateService, 'createTemplateDialog').and.returnValue({ + afterClosed: () => of(node) + }); store.dispatch(new FileFromTemplate()); - tick(300); + tick(); - expect(contentManagementService.reload.next).toHaveBeenCalled(); + expect(nodeTemplateService.selectTemplateDialog).toHaveBeenCalledWith( + fileTemplateConfig + ); })); - it('should not reload content if no file was created', fakeAsync(() => { - spyOn( - createFileFromTemplateService, - 'createTemplateDialog' - ).and.returnValue({ afterClosed: () => of(null) }); + it('should open dialog to select template folders', fakeAsync(() => { + spyOn(nodeTemplateService, 'createTemplateDialog').and.returnValue({ + afterClosed: () => of(node) + }); - store.dispatch(new FileFromTemplate()); - tick(300); + store.dispatch(new FolderFromTemplate()); + tick(); - expect(contentManagementService.reload.next).not.toHaveBeenCalled(); + expect(nodeTemplateService.selectTemplateDialog).toHaveBeenCalledWith( + folderTemplateConfig + ); })); - it('should call dialog service success event on create file from template', fakeAsync(() => { + it('should create node from template successful', fakeAsync(() => { copyNodeSpy.and.returnValue( of({ entry: { id: 'node-id', properties: {} } }) ); updateNodeSpy.and.returnValue(of({ entry: node })); - store.dispatch(new CreateFileFromTemplate(node)); + store.dispatch(new CreateFromTemplate(node)); tick(); - expect(createFromTemplateDialogService.success$.next).toHaveBeenCalledWith( - node + expect(store.dispatch['calls'].mostRecent().args[0]).toEqual( + new CreateFromTemplateSuccess(node) ); })); @@ -143,12 +159,12 @@ describe('TemplateEffects', () => { }) ); - store.dispatch(new CreateFileFromTemplate(node)); + store.dispatch(new CreateFromTemplate(node)); tick(); - expect( - createFromTemplateDialogService.success$.next - ).not.toHaveBeenCalledWith(); + expect(store.dispatch['calls'].mostRecent().args[0]).not.toEqual( + new CreateFromTemplateSuccess(node) + ); expect(store.dispatch['calls'].argsFor(1)[0]).toEqual( new SnackbarErrorAction('APP.MESSAGES.ERRORS.GENERIC') ); @@ -161,12 +177,12 @@ describe('TemplateEffects', () => { }) ); - store.dispatch(new CreateFileFromTemplate(node)); + store.dispatch(new CreateFromTemplate(node)); tick(); - expect( - createFromTemplateDialogService.success$.next - ).not.toHaveBeenCalledWith(); + expect(store.dispatch['calls'].mostRecent().args[0]).not.toEqual( + new CreateFromTemplateSuccess(node) + ); expect(store.dispatch['calls'].argsFor(1)[0]).toEqual( new SnackbarErrorAction('APP.MESSAGES.ERRORS.CONFLICT') ); @@ -190,11 +206,26 @@ describe('TemplateEffects', () => { }) ); - store.dispatch(new CreateFileFromTemplate(test_node.entry)); + store.dispatch(new CreateFromTemplate(test_node.entry)); tick(); - expect(createFromTemplateDialogService.success$.next).toHaveBeenCalledWith( - test_node.entry + expect(store.dispatch['calls'].mostRecent().args[0]).toEqual( + new CreateFromTemplateSuccess(test_node.entry) + ); + })); + + it('should close dialog on create template success', fakeAsync(() => { + store.dispatch(new CreateFromTemplateSuccess({} as Node)); + tick(); + expect(matDialog.closeAll).toHaveBeenCalled(); + })); + + it('should should reload content on create template success', fakeAsync(() => { + const test_node = { id: 'test-node-id' } as Node; + store.dispatch(new CreateFromTemplateSuccess(test_node)); + tick(); + expect(contentManagementService.reload.next).toHaveBeenCalledWith( + test_node ); })); }); diff --git a/src/app/store/effects/template.effects.ts b/src/app/store/effects/template.effects.ts index 713ad2574..ab6a778de 100644 --- a/src/app/store/effects/template.effects.ts +++ b/src/app/store/effects/template.effects.ts @@ -25,65 +25,64 @@ import { Effect, Actions, ofType } from '@ngrx/effects'; import { Injectable } from '@angular/core'; -import { - map, - switchMap, - debounceTime, - flatMap, - take, - catchError -} from 'rxjs/operators'; +import { map, switchMap, debounceTime, take, catchError } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { FileFromTemplate, - CreateFileFromTemplate, + FolderFromTemplate, + CreateFromTemplate, + CreateFromTemplateSuccess, TemplateActionTypes, getCurrentFolder, AppStore, SnackbarErrorAction } from '@alfresco/aca-shared/store'; -import { CreateFileFromTemplateService } from '../../services/create-file-from-template.service'; +import { + NodeTemplateService, + TemplateDialogConfig +} from '../../services/node-template.service'; import { AlfrescoApiService } from '@alfresco/adf-core'; import { ContentManagementService } from '../../services/content-management.service'; import { from, Observable, of } from 'rxjs'; import { NodeEntry, NodeBodyUpdate, Node } from '@alfresco/js-api'; -import { CreateFromTemplateDialogService } from '../../dialogs/node-templates/create-from-template-dialog.service'; +import { MatDialog } from '@angular/material/dialog'; + @Injectable() export class TemplateEffects { constructor( + private matDialog: MatDialog, private content: ContentManagementService, private store: Store, private apiService: AlfrescoApiService, private actions$: Actions, - private createFromTemplateDialogService: CreateFromTemplateDialogService, - private createFileFromTemplateService: CreateFileFromTemplateService + private nodeTemplateService: NodeTemplateService ) {} @Effect({ dispatch: false }) fileFromTemplate$ = this.actions$.pipe( ofType(TemplateActionTypes.FileFromTemplate), map(() => { - this.createFileFromTemplateService - .openTemplatesDialog() - .pipe( - debounceTime(300), - flatMap(([node]) => - this.createFileFromTemplateService - .createTemplateDialog(node) - .afterClosed() - ) - ) - .subscribe((node: NodeEntry | null) => { - if (node) { - this.content.reload.next(node); - } - }); + this.openDialog({ + relativePath: 'Data Dictionary/Node Templates', + selectionType: 'file' + }); }) ); @Effect({ dispatch: false }) - createFileFromTemplate$ = this.actions$.pipe( - ofType(TemplateActionTypes.CreateFileFromTemplate), + folderFromTemplate$ = this.actions$.pipe( + ofType(TemplateActionTypes.FolderFromTemplate), + map(() => + this.openDialog({ + relativePath: 'Data Dictionary/Space Templates', + selectionType: 'folder' + }) + ) + ); + + @Effect({ dispatch: false }) + createFromTemplate$ = this.actions$.pipe( + ofType(TemplateActionTypes.CreateFromTemplate), map(action => { this.store .select(getCurrentFolder) @@ -95,12 +94,32 @@ export class TemplateEffects { ) .subscribe((node: NodeEntry | null) => { if (node) { - this.createFromTemplateDialogService.success$.next(node.entry); + this.store.dispatch(new CreateFromTemplateSuccess(node.entry)); } }); }) ); + @Effect({ dispatch: false }) + createFromTemplateSuccess$ = this.actions$.pipe( + ofType( + TemplateActionTypes.CreateFromTemplateSuccess + ), + map(payload => { + this.matDialog.closeAll(); + this.content.reload.next(payload.node); + }) + ); + + private openDialog(config: TemplateDialogConfig) { + this.nodeTemplateService + .selectTemplateDialog(config) + .pipe(debounceTime(300)) + .subscribe(([node]) => + this.nodeTemplateService.createTemplateDialog(node) + ); + } + private copyNode(source: Node, parentId: string): Observable { return from( this.apiService.getInstance().nodes.copyNode(source.id, { diff --git a/src/app/ui/custom-theme.scss b/src/app/ui/custom-theme.scss index 3059f4a95..fa20674ca 100644 --- a/src/app/ui/custom-theme.scss +++ b/src/app/ui/custom-theme.scss @@ -10,7 +10,7 @@ @import '../dialogs/node-versions/node-versions.dialog.theme'; @import '../components/create-menu/create-menu.component.scss'; @import '../components/layout/layout.theme.scss'; -@import '../dialogs/node-templates/create-from-template.dialog.scss'; +@import '../dialogs/node-template/create-from-template.dialog.scss'; @import './overrides/adf-style-fixes.theme'; @@ -68,7 +68,7 @@ $warn: map-get($custom-theme, warn); @include sidenav-component-theme($theme); @include aca-current-user-theme($theme); @include aca-context-menu-theme($theme); - @include app-create-file-from-template-theme($theme); + @include app-create-from-template-theme($theme); @include app-create-menu-theme($theme); @include adf-style-fixes($theme); diff --git a/src/assets/app.extensions.json b/src/assets/app.extensions.json index 7f5aa5857..8c4301175 100644 --- a/src/assets/app.extensions.json +++ b/src/assets/app.extensions.json @@ -121,6 +121,20 @@ "rules": { "enabled": "app.navigation.folder.canUpload" } + }, + { + "id": "app.create.folderFromTemplate", + "order": 800, + "icon": "create_new_folder", + "title": "APP.NEW_MENU.MENU_ITEMS.FOLDER_TEMPLATE", + "description": "APP.NEW_MENU.MENU_ITEMS.FOLDER_TEMPLATE", + "description-disabled": "APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER_NOT_ALLOWED", + "actions": { + "click": "FOLDER_FROM_TEMPLATE" + }, + "rules": { + "enabled": "app.navigation.folder.canUpload" + } } ], "navbar": [ diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index e31b9392f..8825d3e58 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -58,7 +58,8 @@ "UPLOAD_FILE": "Upload File", "UPLOAD_FOLDER": "Upload Folder", "CREATE_LIBRARY": "Create Library", - "FILE_TEMPLATE": "Create file from template" + "FILE_TEMPLATE": "Create file from template", + "FOLDER_TEMPLATE": "Create folder from template" }, "TOOLTIPS": { "CREATE_FOLDER": "Create new folder", @@ -359,12 +360,14 @@ "MOVE_ITEMS": "Move {{ number }} items to...", "SEARCH": "Search", "NEXT": "Next", - "SELECT_TEMPLATE_TITLE": "Select a document template" + "SELECT_FILE_TEMPLATE_TITLE": "Select a document template", + "SELECT_FOLDER_TEMPLATE_TITLE": "Select a folder template" }, - "FILE_FROM_TEMPLATE": { + "NODE_FROM_TEMPLATE": { "CANCEL": "CANCEL", "CREATE": "Create", - "TITLE": "Create new document from '{{ template }}'", + "FOLDER_DIALOG_TITLE": "Create new folder from '{{ template }}'", + "FILE_DIALOG_TITLE": "Create new document from '{{ template }}'", "FORM": { "PLACEHOLDER": { "NAME": "Name",