[ACA-1545] Library - create (#506)

* create site implementation

* lint

* update validation

* reuse existent service and renamed site to library
This commit is contained in:
Cilibiu Bogdan
2018-07-13 10:12:03 +03:00
committed by Denys Vuika
parent 19021c8b51
commit 0504b28b3c
12 changed files with 413 additions and 22 deletions

View File

@@ -61,6 +61,7 @@ import { NodePermanentDeleteDirective } from './common/directives/node-permanent
import { NodeUnshareDirective } from './common/directives/node-unshare.directive'; import { NodeUnshareDirective } from './common/directives/node-unshare.directive';
import { NodeVersionsDirective } from './common/directives/node-versions.directive'; import { NodeVersionsDirective } from './common/directives/node-versions.directive';
import { NodeVersionsDialogComponent } from './dialogs/node-versions/node-versions.dialog'; import { NodeVersionsDialogComponent } from './dialogs/node-versions/node-versions.dialog';
import { LibraryDialogComponent } from './dialogs/library/library.dialog';
import { ContentManagementService } from './common/services/content-management.service'; import { ContentManagementService } from './common/services/content-management.service';
import { NodeActionsService } from './common/services/node-actions.service'; import { NodeActionsService } from './common/services/node-actions.service';
import { NodePermissionService } from './common/services/node-permission.service'; import { NodePermissionService } from './common/services/node-permission.service';
@@ -132,6 +133,7 @@ import { PermissionsManagerComponent } from './components/permission-manager/per
NodeVersionsDirective, NodeVersionsDirective,
NodePermissionsDirective, NodePermissionsDirective,
NodeVersionsDialogComponent, NodeVersionsDialogComponent,
LibraryDialogComponent,
NodePermissionsDialogComponent, NodePermissionsDialogComponent,
PermissionsManagerComponent, PermissionsManagerComponent,
SearchResultsComponent, SearchResultsComponent,
@@ -162,6 +164,7 @@ import { PermissionsManagerComponent } from './components/permission-manager/per
ExtensionService ExtensionService
], ],
entryComponents: [ entryComponents: [
LibraryDialogComponent,
NodeVersionsDialogComponent, NodeVersionsDialogComponent,
NodePermissionsDialogComponent NodePermissionsDialogComponent
], ],

View File

@@ -27,6 +27,7 @@ import { Subject } from 'rxjs/Rx';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material'; import { MatDialog } from '@angular/material';
import { FolderDialogComponent } from '@alfresco/adf-content-services'; import { FolderDialogComponent } from '@alfresco/adf-content-services';
import { LibraryDialogComponent } from '../../dialogs/library/library.dialog';
import { SnackbarErrorAction } from '../../store/actions'; import { SnackbarErrorAction } from '../../store/actions';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states'; import { AppStore } from '../../store/states';
@@ -45,7 +46,8 @@ export class ContentManagementService {
nodesRestored = new Subject<any>(); nodesRestored = new Subject<any>();
folderEdited = new Subject<any>(); folderEdited = new Subject<any>();
folderCreated = new Subject<any>(); folderCreated = new Subject<any>();
siteDeleted = new Subject<string>(); libraryDeleted = new Subject<string>();
libraryCreated = new Subject<string>();
linksUnshared = new Subject<any>(); linksUnshared = new Subject<any>();
constructor( constructor(
@@ -98,6 +100,22 @@ export class ContentManagementService {
}); });
} }
createLibrary() {
const dialogInstance = this.dialogRef.open(LibraryDialogComponent, {
width: '400px'
});
dialogInstance.componentInstance.error.subscribe(message => {
this.store.dispatch(new SnackbarErrorAction(message));
});
dialogInstance.afterClosed().subscribe(node => {
if (node) {
this.libraryCreated.next(node);
}
});
}
canDeleteNode(node: MinimalNodeEntity | Node): boolean { canDeleteNode(node: MinimalNodeEntity | Node): boolean {
return this.permission.check(node, ['delete']); return this.permission.check(node, ['delete']);
} }

View File

@@ -12,6 +12,14 @@
<mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon> <mat-icon *ngIf="displayMode === 'gallery'" matTooltip="{{ 'APP.DOCUMENT_LIST.TOOLBAR.LISTVIEW' | translate }}">list</mat-icon>
</button> </button>
<button
mat-icon-button
color="primary"
*ifExperimental="'libraries'"
(click)="createLibrary()">
<mat-icon>create_new_folder</mat-icon>
</button>
<ng-container *ngIf="!selection.isEmpty"> <ng-container *ngIf="!selection.isEmpty">
<ng-container *ifExperimental="'libraries'"> <ng-container *ifExperimental="'libraries'">
<button <button

View File

@@ -30,7 +30,7 @@ import { ShareDataRow } from '@alfresco/adf-content-services';
import { PageComponent } from '../page.component'; import { PageComponent } from '../page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states/app.state'; import { AppStore } from '../../store/states/app.state';
import { DeleteLibraryAction } from '../../store/actions'; import { DeleteLibraryAction, CreateLibraryAction } from '../../store/actions';
import { SiteEntry } from 'alfresco-js-api'; import { SiteEntry } from 'alfresco-js-api';
import { ContentManagementService } from '../../common/services/content-management.service'; import { ContentManagementService } from '../../common/services/content-management.service';
import { ContentApiService } from '../../services/content-api.service'; import { ContentApiService } from '../../services/content-api.service';
@@ -54,7 +54,8 @@ export class LibrariesComponent extends PageComponent implements OnInit {
super.ngOnInit(); super.ngOnInit();
this.subscriptions.push( this.subscriptions.push(
this.content.siteDeleted.subscribe(() => this.reload()) this.content.libraryDeleted.subscribe(() => this.reload()),
this.content.libraryCreated.subscribe(() => this.reload())
); );
} }
@@ -105,4 +106,8 @@ export class LibrariesComponent extends PageComponent implements OnInit {
this.store.dispatch(new DeleteLibraryAction(node.entry.id)); this.store.dispatch(new DeleteLibraryAction(node.entry.id));
} }
} }
createLibrary() {
this.store.dispatch(new CreateLibraryAction());
}
} }

View File

@@ -0,0 +1,51 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AbstractControl, FormControl } from '@angular/forms';
import { ContentApiService } from '../../services/content-api.service';
export class SiteIdValidator {
static createValidator(contentApiService: ContentApiService) {
let timer;
return (control: AbstractControl) => {
if (timer) {
clearTimeout(timer);
}
return new Promise(resolve => {
timer = setTimeout(() => {
return contentApiService
.getSite(control.value)
.subscribe(
() => resolve({ message: 'LIBRARY.ERRORS.EXISTENT_SITE' }),
() => resolve(null)
);
}, 300);
});
};
}
}
export function forbidSpecialCharacters({ value }: FormControl) {
const validCharacters: RegExp = /[^A-Za-z0-9-]/;
const isValid: boolean = !validCharacters.test(value);
return (isValid) ? null : {
message: 'LIBRARY.ERRORS.ILLEGAL_CHARACTERS'
};
}

View File

@@ -0,0 +1,79 @@
<h2 mat-dialog-title>
{{ createTitle | translate }}
</h2>
<mat-dialog-content>
<form novalidate [formGroup]="form" (submit)="submit()">
<mat-input-container>
<input
placeholder="{{ 'LIBRARY.DIALOG.FORM.NAME' | translate }}"
required
matInput
[formControl]="form.controls['title']"
/>
<mat-error *ngIf="form.controls['title'].hasError('maxlength')">
{{ 'LIBRARY.ERRORS.DESCRIPTION_TOO_LONG' | translate }}
</mat-error>
</mat-input-container>
<mat-input-container>
<input
required
placeholder="{{ 'LIBRARY.DIALOG.FORM.SITE_ID' | translate }}"
matInput
[formControl]="form.controls['id']"
/>
<mat-error *ngIf="form.controls['id'].errors?.message">
{{ form.controls['id'].errors?.message | translate }}
</mat-error>
<mat-error *ngIf="form.controls['id'].hasError('maxlength')">
{{ 'LIBRARY.ERRORS.ID_TOO_LONG' | translate }}
</mat-error>
</mat-input-container>
<mat-input-container>
<textarea
matInput
placeholder="{{ 'LIBRARY.DIALOG.FORM.DESCRIPTION' | translate }}"
rows="3"
[formControl]="form.controls['description']"></textarea>
<mat-error *ngIf="form.controls['description'].hasError('maxlength')">
{{ 'LIBRARY.ERRORS.DESCRIPTION_TOO_LONG' | translate }}
</mat-error>
</mat-input-container>
<mat-radio-group
[ngModelOptions]="{ standalone: true }"
[(ngModel)]="visibilityOption"
(change)="visibilityChangeHandler($event)">
<mat-radio-button
color="primary"
[disabled]="option.disabled"
*ngFor="let option of visibilityOptions"
[value]="option.value"
[checked]="visibilityOption.value === option.value">
{{ option.label | translate }}
</mat-radio-button>
</mat-radio-group>
</form>
</mat-dialog-content>
<mat-dialog-actions class="actions-buttons">
<button
mat-button
mat-dialog-close>
{{ 'LIBRARY.DIALOG.CANCEL' | translate }}
</button>
<button
color="primary"
mat-button
(click)="submit()"
[disabled]="!form.valid">
{{ 'LIBRARY.DIALOG.CREATE' | translate }}
</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,20 @@
.mat-radio-group {
display: flex;
flex-direction: column;
margin: 0 0 20px 0;
}
.mat-radio-group .mat-radio-button {
margin: 10px 0;
}
.mat-input-container {
width: 100%;
}
.actions-buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
}

View File

@@ -0,0 +1,140 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Observable } from 'rxjs/Observable';
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef } from '@angular/material';
import { SiteBody } from 'alfresco-js-api';
import { ContentApiService } from '../../services/content-api.service';
import { SiteIdValidator, forbidSpecialCharacters } from './form.validators';
@Component({
selector: 'app-library-dialog',
styleUrls: ['./library.dialog.scss'],
templateUrl: './library.dialog.html'
})
export class LibraryDialogComponent implements OnInit {
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
@Output()
success: EventEmitter<any> = new EventEmitter<any>();
createTitle = 'LIBRARY.DIALOG.CREATE_TITLE';
form: FormGroup;
visibilityOption: any;
visibilityOptions = [
{ value: 'PUBLIC', label: 'LIBRARY.VISIBILITY.PUBLIC', disabled: false },
{ value: 'PRIVATE', label: 'LIBRARY.VISIBILITY.PRIVATE', disabled: false },
{ value: 'MODERATED', label: 'LIBRARY.VISIBILITY.MODERATED', disabled: false }
];
constructor(
private formBuilder: FormBuilder,
private dialog: MatDialogRef<LibraryDialogComponent>,
private contentApi: ContentApiService
) {}
ngOnInit() {
const validators = {
id: [ Validators.required, Validators.maxLength(72), forbidSpecialCharacters ],
title: [ Validators.required, Validators.maxLength(256) ],
description: [ Validators.maxLength(512) ]
};
this.form = this.formBuilder.group({
title: ['', validators.title ],
id: [ '', validators.id, SiteIdValidator.createValidator(this.contentApi) ],
description: [ '', validators.description ],
});
this.visibilityOption = this.visibilityOptions[0].value;
this.form.controls['title'].valueChanges
.debounceTime(300)
.subscribe((titleValue: string) => {
if (!titleValue.trim().length) {
return;
}
if (!this.form.controls['id'].dirty) {
this.form.patchValue({ id: this.sanitize(titleValue.trim()) });
this.form.controls['id'].markAsTouched();
}
});
}
get title(): string {
const { title } = this.form.value;
return (title || '').trim();
}
get id(): string {
const { id } = this.form.value;
return (id || '').trim();
}
get description(): string {
const { description } = this.form.value;
return (description || '').trim();
}
get visibility(): string {
return this.visibilityOption || '';
}
submit() {
const { form, dialog } = this;
if (!form.valid) { return; }
this.create().subscribe(
(folder: any) => {
this.success.emit(folder);
dialog.close(folder);
},
(error) => this.error.emit('LIBRARY.ERRORS.GENERIC')
);
}
visibilityChangeHandler(event) {
this.visibilityOption = event.value;
}
private create(): Observable<any> {
const { contentApi, title, id, description, visibility } = this;
const siteBody = <SiteBody>{
id,
title,
description,
visibility
};
return contentApi.createSite(siteBody);
}
private sanitize(input: string) {
return input
.replace(/[\s]/g, '-')
.replace(/[^A-Za-z0-9-]/g, '');
}
}

View File

@@ -37,7 +37,9 @@ import {
FavoritePaging, FavoritePaging,
SharedLinkPaging, SharedLinkPaging,
SearchRequest, SearchRequest,
ResultSetPaging ResultSetPaging,
SiteBody,
SiteEntry
} from 'alfresco-js-api'; } from 'alfresco-js-api';
@Injectable() @Injectable()
@@ -226,4 +228,16 @@ export class ContentApiService {
this.api.sitesApi.deleteSite(siteId, opts) this.api.sitesApi.deleteSite(siteId, opts)
); );
} }
createSite(siteBody: SiteBody, opts?: {skipConfiguration?: boolean, skipAddToFavorites?: boolean}): Observable<SiteEntry> {
return Observable.fromPromise(
this.api.sitesApi.createSite(siteBody, opts)
);
}
getSite(siteId?: string, opts?: { relations?: Array<string>, fields?: Array<string> }): Observable<SiteEntry> {
return Observable.fromPromise(
this.api.sitesApi.getSite(siteId, opts)
);
}
} }

View File

@@ -26,8 +26,14 @@
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
export const DELETE_LIBRARY = 'DELETE_LIBRARY'; export const DELETE_LIBRARY = 'DELETE_LIBRARY';
export const CREATE_LIBRARY = 'CREATE_LIBRARY';
export class DeleteLibraryAction implements Action { export class DeleteLibraryAction implements Action {
readonly type = DELETE_LIBRARY; readonly type = DELETE_LIBRARY;
constructor(public payload: string) {} constructor(public payload: string) {}
} }
export class CreateLibraryAction implements Action {
readonly type = CREATE_LIBRARY;
constructor() {}
}

View File

@@ -26,7 +26,10 @@
import { Effect, Actions, ofType } from '@ngrx/effects'; import { Effect, Actions, ofType } from '@ngrx/effects';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { DeleteLibraryAction, DELETE_LIBRARY } from '../actions'; import {
DeleteLibraryAction, DELETE_LIBRARY,
CreateLibraryAction, CREATE_LIBRARY
} from '../actions';
import { import {
SnackbarInfoAction, SnackbarInfoAction,
SnackbarErrorAction SnackbarErrorAction
@@ -49,23 +52,41 @@ export class SiteEffects {
deleteLibrary$ = this.actions$.pipe( deleteLibrary$ = this.actions$.pipe(
ofType<DeleteLibraryAction>(DELETE_LIBRARY), ofType<DeleteLibraryAction>(DELETE_LIBRARY),
map(action => { map(action => {
this.contentApi.deleteSite(action.payload).subscribe( if (action.payload) {
() => { this.deleteLibrary(action.payload);
this.content.siteDeleted.next(action.payload); }
this.store.dispatch(
new SnackbarInfoAction(
'APP.MESSAGES.INFO.LIBRARY_DELETED'
)
);
},
() => {
this.store.dispatch(
new SnackbarErrorAction(
'APP.MESSAGES.ERRORS.DELETE_LIBRARY_FAILED'
)
);
}
);
}) })
); );
@Effect({ dispatch: false })
createLibrary$ = this.actions$.pipe(
ofType<CreateLibraryAction>(CREATE_LIBRARY),
map(action => {
this.createLibrary();
})
);
private deleteLibrary(id: string) {
this.contentApi.deleteSite(id).subscribe(
() => {
this.content.libraryDeleted.next(id);
this.store.dispatch(
new SnackbarInfoAction(
'APP.MESSAGES.INFO.LIBRARY_DELETED'
)
);
},
() => {
this.store.dispatch(
new SnackbarErrorAction(
'APP.MESSAGES.ERRORS.DELETE_LIBRARY_FAILED'
)
);
}
);
}
private createLibrary() {
this.content.createLibrary();
}
} }

View File

@@ -269,6 +269,32 @@
"NO_PERMISSION": "You don't have permission to manage the versions of this content." "NO_PERMISSION": "You don't have permission to manage the versions of this content."
} }
}, },
"LIBRARY": {
"DIALOG": {
"CREATE_TITLE": "Create Site",
"CREATE": "Create",
"CANCEL": "Cancel",
"FORM": {
"DESCRIPTION": "Description",
"SITE_ID": "Site ID",
"NAME": "Name"
}
},
"VISIBILITY": {
"PRIVATE": "Private",
"PUBLIC": "Public",
"MODERATED": "Moderated"
},
"ERRORS": {
"GENERIC": "There was an error",
"EXISTENT_SITE": "ID already used (it might be in the trashcan).",
"ID_TOO_LONG": "Use 72 characters or less for the URL name",
"DESCRIPTION_TOO_LONG": "Use 512 characters or less for description",
"TITLE_TOO_LONG": "Use 256 characters or less for title",
"ILLEGAL_CHARACTERS": "Use characters a-z, A-Z, 0-9 and - only"
}
},
"SEARCH": { "SEARCH": {
"SORT": { "SORT": {
"RELEVANCE": "Relevance", "RELEVANCE": "Relevance",