diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index ec362240d..111bad187 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 { environment } from '../environments/environment';
import { registerLocaleData } from '@angular/common';
@@ -158,7 +158,8 @@ registerLocaleData(localeSv);
NodeVersionsDialogComponent,
FavoritesComponent,
RecentFilesComponent,
- SharedFilesComponent
+ SharedFilesComponent,
+ CreateFileFromTemplateDialogComponent
],
providers: [
{ provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy },
@@ -175,7 +176,8 @@ registerLocaleData(localeSv);
entryComponents: [
NodeVersionsDialogComponent,
NodeVersionUploadDialogComponent,
- LibraryDialogComponent
+ LibraryDialogComponent,
+ CreateFileFromTemplateDialogComponent
],
bootstrap: [AppComponent]
})
diff --git a/src/app/dialogs/node-templates/create-from-template.dialog.html b/src/app/dialogs/node-templates/create-from-template.dialog.html
new file mode 100644
index 000000000..a542bebd2
--- /dev/null
+++ b/src/app/dialogs/node-templates/create-from-template.dialog.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
diff --git a/src/app/dialogs/node-templates/create-from-template.dialog.scss b/src/app/dialogs/node-templates/create-from-template.dialog.scss
new file mode 100644
index 000000000..b88024c6e
--- /dev/null
+++ b/src/app/dialogs/node-templates/create-from-template.dialog.scss
@@ -0,0 +1,64 @@
+@mixin app-create-file-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 {
+ ng-component {
+ overflow: visible;
+ }
+
+ .mat-dialog-title {
+ margin-left: 24px;
+ margin-right: 24px;
+ font-size: 20px;
+ font-style: normal;
+ font-stretch: normal;
+ line-height: 1.6;
+ letter-spacing: -0.5px;
+ color: mat-color($foreground, text, 0.87);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ .bold {
+ font-weight: 600;
+ }
+ }
+
+ .mat-form-field {
+ margin-bottom: 20px;
+ }
+
+ .mat-dialog-container {
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ .mat-dialog-content {
+ margin: 0 2px;
+ overflow: hidden;
+ }
+
+ .mat-dialog-actions {
+ padding: 8px 22px;
+ display: flex;
+ justify-content: flex-end;
+ color: mat-color($foreground, secondary-text);
+
+ button {
+ text-transform: uppercase;
+ font-weight: normal;
+ }
+
+ .create:disabled {
+ color: mat-color($primary);
+ }
+
+ .create {
+ color: mat-color($accent);
+ }
+ }
+ }
+}
diff --git a/src/app/dialogs/node-templates/create-from-template.dialog.spec.ts b/src/app/dialogs/node-templates/create-from-template.dialog.spec.ts
new file mode 100644
index 000000000..0ff8ee84f
--- /dev/null
+++ b/src/app/dialogs/node-templates/create-from-template.dialog.spec.ts
@@ -0,0 +1,140 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2019 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 { CreateFileFromTemplateDialogComponent } 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 {
+ MatDialogModule,
+ MatDialogRef,
+ MAT_DIALOG_DATA
+} from '@angular/material/dialog';
+
+function text(length: number) {
+ return new Array(length)
+ .fill(
+ Math.random()
+ .toString()
+ .substring(2, 3)
+ )
+ .join('');
+}
+
+describe('CreateFileFromTemplateDialogComponent', () => {
+ let fixture: ComponentFixture;
+ let component: CreateFileFromTemplateDialogComponent;
+ let dialogRef: MatDialogRef;
+
+ const data = {
+ id: 'node-id',
+ name: 'node-name',
+ properties: {
+ 'cm:title': 'node-title',
+ 'cm:description': ''
+ }
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [CoreModule.forRoot(), AppTestingModule, MatDialogModule],
+ declarations: [CreateFileFromTemplateDialogComponent],
+ providers: [
+ { provide: MAT_DIALOG_DATA, useValue: data },
+ {
+ provide: MatDialogRef,
+ useValue: {
+ close: jasmine.createSpy('close')
+ }
+ }
+ ]
+ });
+
+ fixture = TestBed.createComponent(CreateFileFromTemplateDialogComponent);
+ dialogRef = TestBed.get(MatDialogRef);
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+ });
+
+ it('should populate form with provided dialog data', () => {
+ expect(component.form.controls.name.value).toBe(data.name);
+ expect(component.form.controls.title.value).toBe(
+ data.properties['cm:title']
+ );
+ expect(component.form.controls.description.value).toBe(
+ data.properties['cm:description']
+ );
+ });
+
+ it('should invalidate form if required `name` field is invalid', () => {
+ component.form.controls.name.setValue('');
+ fixture.detectChanges();
+ expect(component.form.invalid).toBe(true);
+ });
+
+ it('should invalidate form if required `name` field has `only spaces`', () => {
+ component.form.controls.name.setValue(' ');
+ fixture.detectChanges();
+ expect(component.form.invalid).toBe(true);
+ });
+
+ it('should invalidate form if required `name` field has `ending dot`', () => {
+ component.form.controls.name.setValue('something.');
+ fixture.detectChanges();
+ expect(component.form.invalid).toBe(true);
+ });
+
+ it('should invalidate form if `title` text length is long', () => {
+ 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', () => {
+ component.form.controls.description.setValue(text(520));
+ fixture.detectChanges();
+ expect(component.form.invalid).toBe(true);
+ });
+
+ it('should update data with form values', () => {
+ component.form.controls.name.setValue('new-node-name');
+ component.form.controls.title.setValue('new-node-title');
+ component.form.controls.description.setValue('new-node-description');
+
+ fixture.detectChanges();
+
+ component.onSubmit();
+
+ expect(dialogRef.close['calls'].argsFor(0)[0]).toEqual({
+ id: 'node-id',
+ name: 'new-node-name',
+ properties: {
+ 'cm:title': 'new-node-title',
+ 'cm:description': 'new-node-description'
+ }
+ });
+ });
+});
diff --git a/src/app/dialogs/node-templates/create-from-template.dialog.ts b/src/app/dialogs/node-templates/create-from-template.dialog.ts
new file mode 100644
index 000000000..9e7562d57
--- /dev/null
+++ b/src/app/dialogs/node-templates/create-from-template.dialog.ts
@@ -0,0 +1,128 @@
+/*!
+ * @license
+ * Alfresco Example Content Application
+ *
+ * Copyright (C) 2005 - 2019 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 { Component, ViewEncapsulation, Inject, OnInit } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
+import { Node } from '@alfresco/js-api';
+import {
+ FormBuilder,
+ FormGroup,
+ Validators,
+ FormControl,
+ ValidationErrors
+} from '@angular/forms';
+
+@Component({
+ templateUrl: './create-from-template.dialog.html',
+ encapsulation: ViewEncapsulation.None,
+ styleUrls: ['./create-from-template.dialog.scss']
+})
+export class CreateFileFromTemplateDialogComponent implements OnInit {
+ public form: FormGroup;
+
+ constructor(
+ private formBuilder: FormBuilder,
+ private dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: any
+ ) {}
+
+ ngOnInit() {
+ this.form = this.formBuilder.group({
+ name: [
+ this.data.name,
+ [
+ Validators.required,
+ this.forbidEndingDot,
+ this.forbidOnlySpaces,
+ this.forbidSpecialCharacters
+ ]
+ ],
+ title: [this.data.properties['cm:title'], Validators.maxLength(256)],
+ description: [
+ this.data.properties['cm:description'],
+ Validators.maxLength(512)
+ ]
+ });
+ }
+
+ onSubmit() {
+ const update = {
+ name: this.form.value.name,
+ properties: {
+ 'cm:title': this.form.value.title,
+ 'cm:description': this.form.value.description
+ }
+ };
+ const data: Node = Object.assign({}, this.data, update);
+ this.dialogRef.close(data);
+ }
+
+ close() {
+ this.dialogRef.close();
+ }
+
+ private forbidSpecialCharacters({
+ value
+ }: FormControl): ValidationErrors | null {
+ const specialCharacters: RegExp = /([\*\"\<\>\\\/\?\:\|])/;
+ const isValid: boolean = !specialCharacters.test(value);
+
+ return isValid
+ ? null
+ : {
+ message: `FILE_FROM_TEMPLATE.FORM.ERRORS.SPECIAL_CHARACTERS`
+ };
+ }
+
+ private forbidEndingDot({ value }: FormControl): ValidationErrors | null {
+ const isValid: boolean =
+ (value || '')
+ .trim()
+ .split('')
+ .pop() !== '.';
+
+ return isValid
+ ? null
+ : {
+ message: `FILE_FROM_TEMPLATE.FORM.ERRORS.ENDING_DOT`
+ };
+ }
+
+ private forbidOnlySpaces({ value }: FormControl): ValidationErrors | null {
+ if (value.length) {
+ const isValid: boolean = !!(value || '').trim();
+
+ return isValid
+ ? null
+ : {
+ message: `FILE_FROM_TEMPLATE.FORM.ERRORS.ONLY_SPACES`
+ };
+ } else {
+ return {
+ message: `FILE_FROM_TEMPLATE.FORM.ERRORS.REQUIRED`
+ };
+ }
+ }
+}
diff --git a/src/app/services/create-file-from-template.service.ts b/src/app/services/create-file-from-template.service.ts
index 9848b22c0..f9a8d8790 100644
--- a/src/app/services/create-file-from-template.service.ts
+++ b/src/app/services/create-file-from-template.service.ts
@@ -24,17 +24,18 @@
*/
import { Injectable } from '@angular/core';
-import { MatDialog, MatDialogConfig } from '@angular/material';
-import {
- ContentNodeSelectorComponentData,
- ContentNodeSelectorComponent
-} from '@alfresco/adf-content-services';
+import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material';
+import { CreateFileFromTemplateDialogComponent } from '../dialogs/node-templates/create-from-template.dialog';
import { Subject, from, of } from 'rxjs';
-import { Node } from '@alfresco/js-api';
-import { AlfrescoApiService } from '@alfresco/adf-core';
+import { Node, MinimalNode } from '@alfresco/js-api';
+import { AlfrescoApiService, TranslationService } from '@alfresco/adf-core';
import { switchMap, catchError } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { AppStore, SnackbarErrorAction } from '@alfresco/aca-shared/store';
+import {
+ ContentNodeSelectorComponent,
+ ContentNodeSelectorComponentData
+} from '@alfresco/adf-content-services';
@Injectable({
providedIn: 'root'
@@ -43,6 +44,7 @@ export class CreateFileFromTemplateService {
constructor(
private store: Store,
private alfrescoApiService: AlfrescoApiService,
+ private translation: TranslationService,
public dialog: MatDialog
) {}
@@ -53,7 +55,8 @@ export class CreateFileFromTemplateService {
});
const data: ContentNodeSelectorComponentData = {
- title: null,
+ title: this.title,
+ actionName: 'NEXT',
dropdownHideMyFiles: true,
currentFolderId: null,
dropdownSiteList: null,
@@ -62,10 +65,6 @@ export class CreateFileFromTemplateService {
isSelectionValid: this.isSelectionValid.bind(this)
};
- data.select.subscribe({
- complete: this.close.bind(this)
- });
-
from(
this.alfrescoApiService.getInstance().nodes.getNodeInfo('-root-', {
relativePath: 'Data Dictionary/Node Templates'
@@ -97,7 +96,17 @@ export class CreateFileFromTemplateService {
return select;
}
- private transformNode(node: Node): Node {
+ createTemplateDialog(
+ node: Node
+ ): MatDialogRef {
+ return this.dialog.open(CreateFileFromTemplateDialogComponent, {
+ data: node,
+ panelClass: 'aca-file-from-template-dialog',
+ width: '630px'
+ });
+ }
+
+ private transformNode(node: MinimalNode): MinimalNode {
if (node && node.path && node.path && node.path.elements instanceof Array) {
let {
path: { elements: elementsPath = [] }
@@ -118,4 +127,8 @@ export class CreateFileFromTemplateService {
private close() {
this.dialog.closeAll();
}
+
+ private get title() {
+ return this.translation.instant('NODE_SELECTOR.SELECT_TEMPLATE_TITLE');
+ }
}
diff --git a/src/app/store/effects/template.effects.spec.ts b/src/app/store/effects/template.effects.spec.ts
index 51e976b27..266656a88 100644
--- a/src/app/store/effects/template.effects.spec.ts
+++ b/src/app/store/effects/template.effects.spec.ts
@@ -36,12 +36,28 @@ import { CreateFileFromTemplateService } from '../../services/create-file-from-t
import { of } from 'rxjs';
import { AlfrescoApiServiceMock, AlfrescoApiService } from '@alfresco/adf-core';
import { ContentManagementService } from '../../services/content-management.service';
+import { Node } from '@alfresco/js-api';
describe('TemplateEffects', () => {
let store: Store;
let createFileFromTemplateService: CreateFileFromTemplateService;
let alfrescoApiService: AlfrescoApiService;
let contentManagementService: ContentManagementService;
+ const node: Node = {
+ name: 'node-name',
+ id: 'node-id',
+ nodeType: 'cm:content',
+ isFolder: false,
+ isFile: true,
+ modifiedAt: null,
+ modifiedByUser: null,
+ createdAt: null,
+ createdByUser: null,
+ properties: {
+ 'cm:title': 'title',
+ 'cm:description': 'description'
+ }
+ };
beforeEach(() => {
TestBed.configureTestingModule({
@@ -64,17 +80,27 @@ describe('TemplateEffects', () => {
);
});
- it('should reload content on template copy', fakeAsync(() => {
+ it('should reload content on create file from template', fakeAsync(() => {
spyOn(alfrescoApiService.getInstance().nodes, 'copyNode').and.returnValue(
+ of({ entry: { id: 'node-id' } })
+ );
+
+ spyOn(alfrescoApiService.getInstance().nodes, 'updateNode').and.returnValue(
of({})
);
+
+ spyOn(
+ createFileFromTemplateService,
+ 'createTemplateDialog'
+ ).and.returnValue({ afterClosed: () => of(node) });
+
store.dispatch(new CreateFileFromTemplate());
- tick();
+ tick(300);
expect(contentManagementService.reload.next).toHaveBeenCalled();
}));
- it('should raise error when copy template fails', fakeAsync(() => {
+ it('should raise error when copyNode api fails', fakeAsync(() => {
spyOn(store, 'dispatch').and.callThrough();
spyOn(alfrescoApiService.getInstance().nodes, 'copyNode').and.returnValue(
Promise.reject({
@@ -82,12 +108,54 @@ describe('TemplateEffects', () => {
})
);
+ spyOn(
+ createFileFromTemplateService,
+ 'createTemplateDialog'
+ ).and.returnValue({ afterClosed: () => of(node) });
+
store.dispatch(new CreateFileFromTemplate());
- tick();
+ tick(300);
expect(contentManagementService.reload.next).not.toHaveBeenCalled();
expect(store.dispatch['calls'].argsFor(1)[0]).toEqual(
new SnackbarErrorAction('APP.MESSAGES.ERRORS.GENERIC')
);
}));
+
+ it('should raise error when updateNode api fails', fakeAsync(() => {
+ spyOn(store, 'dispatch').and.callThrough();
+ spyOn(alfrescoApiService.getInstance().nodes, 'copyNode').and.returnValue(
+ of({ entry: { id: 'node-id' } })
+ );
+
+ spyOn(alfrescoApiService.getInstance().nodes, 'updateNode').and.returnValue(
+ Promise.reject({
+ message: `{ "error": { "statusCode": 404 } } `
+ })
+ );
+
+ spyOn(
+ createFileFromTemplateService,
+ 'createTemplateDialog'
+ ).and.returnValue({ afterClosed: () => of(node) });
+
+ store.dispatch(new CreateFileFromTemplate());
+ tick(300);
+
+ expect(contentManagementService.reload.next).not.toHaveBeenCalled();
+ expect(store.dispatch['calls'].argsFor(1)[0]).toEqual(
+ new SnackbarErrorAction('APP.MESSAGES.ERRORS.GENERIC')
+ );
+ }));
+
+ it('should update file from template with form data', () => {
+ spyOn(alfrescoApiService.getInstance().nodes, 'copyNode').and.returnValue(
+ of({ entry: { id: 'node-id' } })
+ );
+
+ spyOn(
+ createFileFromTemplateService,
+ 'createTemplateDialog'
+ ).and.returnValue({ afterClosed: () => of(node) });
+ });
});
diff --git a/src/app/store/effects/template.effects.ts b/src/app/store/effects/template.effects.ts
index 9ae79a2e1..ec7485076 100644
--- a/src/app/store/effects/template.effects.ts
+++ b/src/app/store/effects/template.effects.ts
@@ -25,7 +25,15 @@
import { Effect, Actions, ofType } from '@ngrx/effects';
import { Injectable } from '@angular/core';
-import { map, withLatestFrom, switchMap, catchError } from 'rxjs/operators';
+import {
+ map,
+ withLatestFrom,
+ switchMap,
+ catchError,
+ debounceTime,
+ flatMap,
+ skipWhile
+} from 'rxjs/operators';
import { Store } from '@ngrx/store';
import {
CreateFileFromTemplate,
@@ -37,8 +45,8 @@ import {
import { CreateFileFromTemplateService } from '../../services/create-file-from-template.service';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { ContentManagementService } from '../../services/content-management.service';
-import { from, of } from 'rxjs';
-import { NodeEntry } from '@alfresco/js-api';
+import { from, of, Observable } from 'rxjs';
+import { NodeEntry, NodeBodyUpdate, MinimalNode } from '@alfresco/js-api';
@Injectable()
export class TemplateEffects {
@@ -57,31 +65,70 @@ export class TemplateEffects {
this.createFileFromTemplateService
.openTemplatesDialog()
.pipe(
+ debounceTime(300),
+ flatMap(([node]) =>
+ this.createFileFromTemplateService
+ .createTemplateDialog(node)
+ .afterClosed()
+ ),
+ skipWhile(node => !node),
withLatestFrom(this.store.select(getCurrentFolder)),
- switchMap(([[template], parentNode]) => {
- return from(
- this.apiService
- .getInstance()
- .nodes.copyNode(template.id, { targetParentId: parentNode.id })
- );
+ switchMap(([template, parentNode]) => {
+ return this.copyNode(template, parentNode.id);
}),
catchError(error => {
- const { statusCode } = JSON.parse(error.message).error;
-
- if (statusCode !== 409) {
- this.store.dispatch(
- new SnackbarErrorAction('APP.MESSAGES.ERRORS.GENERIC')
- );
- }
-
- return of(null);
+ return this.handleError(error);
})
)
.subscribe((node: NodeEntry | null) => {
if (node) {
- this.content.reload.next();
+ this.content.reload.next(node);
}
});
})
);
+
+ private copyNode(
+ source: MinimalNode,
+ parentId: string
+ ): Observable {
+ return from(
+ this.apiService.getInstance().nodes.copyNode(source.id, {
+ targetParentId: parentId,
+ name: source.name
+ })
+ ).pipe(
+ switchMap(node =>
+ this.updateNode(node.entry.id, {
+ properties: {
+ 'cm:title': source.properties['cm:title'],
+ 'cm:description': source.properties['cm:description']
+ }
+ })
+ )
+ );
+ }
+
+ private updateNode(
+ id: string,
+ update: NodeBodyUpdate
+ ): Observable {
+ return from(this.apiService.getInstance().nodes.updateNode(id, update));
+ }
+
+ private handleError(error: Error): Observable {
+ const { statusCode } = JSON.parse(error.message).error;
+
+ if (statusCode !== 409) {
+ this.store.dispatch(
+ new SnackbarErrorAction('APP.MESSAGES.ERRORS.GENERIC')
+ );
+ } else {
+ this.store.dispatch(
+ new SnackbarErrorAction('APP.MESSAGES.ERRORS.CONFLICT')
+ );
+ }
+
+ return of(null);
+ }
}
diff --git a/src/app/ui/custom-theme.scss b/src/app/ui/custom-theme.scss
index db39d309c..3059f4a95 100644
--- a/src/app/ui/custom-theme.scss
+++ b/src/app/ui/custom-theme.scss
@@ -10,6 +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 './overrides/adf-style-fixes.theme';
@@ -67,6 +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-menu-theme($theme);
@include adf-style-fixes($theme);
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 7813a920c..931b9ad41 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -356,7 +356,29 @@
"COPY_ITEMS": "Copy {{ number }} items to...",
"MOVE_ITEM": "Move '{{ name }}' to...",
"MOVE_ITEMS": "Move {{ number }} items to...",
- "SEARCH": "Search"
+ "SEARCH": "Search",
+ "NEXT": "Next",
+ "SELECT_TEMPLATE_TITLE": "Select a document template"
+ },
+ "FILE_FROM_TEMPLATE": {
+ "CANCEL": "CANCEL",
+ "CREATE": "Create",
+ "TITLE": "Create new document from '{{ template }}'",
+ "FORM": {
+ "PLACEHOLDER": {
+ "NAME": "Name",
+ "TITLE": "Title",
+ "DESCRIPTION": "Description"
+ },
+ "ERRORS": {
+ "DESCRIPTION_TOO_LONG": "Use 512 characters or less for description",
+ "TITLE_TOO_LONG": "Use 256 characters or less for title",
+ "REQUIRED": "File name is required",
+ "SPECIAL_CHARACTERS": "File name can't contain these characters * \" < > \\ / ? : |",
+ "ENDING_DOT": "File name can't end with a period .",
+ "ONLY_SPACES": "File name can't contain only spaces"
+ }
+ }
},
"PERMISSIONS": {
"DIALOG": {