diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6bc9b1f84..09e65bc1c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -61,6 +61,7 @@ import { NodePermanentDeleteDirective } from './common/directives/node-permanent import { NodeUnshareDirective } from './common/directives/node-unshare.directive'; import { NodeVersionsDirective } from './common/directives/node-versions.directive'; 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 { NodeActionsService } from './common/services/node-actions.service'; import { NodePermissionService } from './common/services/node-permission.service'; @@ -132,6 +133,7 @@ import { PermissionsManagerComponent } from './components/permission-manager/per NodeVersionsDirective, NodePermissionsDirective, NodeVersionsDialogComponent, + LibraryDialogComponent, NodePermissionsDialogComponent, PermissionsManagerComponent, SearchResultsComponent, @@ -162,6 +164,7 @@ import { PermissionsManagerComponent } from './components/permission-manager/per ExtensionService ], entryComponents: [ + LibraryDialogComponent, NodeVersionsDialogComponent, NodePermissionsDialogComponent ], diff --git a/src/app/common/services/content-management.service.ts b/src/app/common/services/content-management.service.ts index fd5314106..ecba58427 100644 --- a/src/app/common/services/content-management.service.ts +++ b/src/app/common/services/content-management.service.ts @@ -27,6 +27,7 @@ import { Subject } from 'rxjs/Rx'; import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material'; import { FolderDialogComponent } from '@alfresco/adf-content-services'; +import { LibraryDialogComponent } from '../../dialogs/library/library.dialog'; import { SnackbarErrorAction } from '../../store/actions'; import { Store } from '@ngrx/store'; import { AppStore } from '../../store/states'; @@ -45,7 +46,8 @@ export class ContentManagementService { nodesRestored = new Subject(); folderEdited = new Subject(); folderCreated = new Subject(); - siteDeleted = new Subject(); + libraryDeleted = new Subject(); + libraryCreated = new Subject(); linksUnshared = new Subject(); 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 { return this.permission.check(node, ['delete']); } diff --git a/src/app/components/libraries/libraries.component.html b/src/app/components/libraries/libraries.component.html index 85900e602..932eef78f 100644 --- a/src/app/components/libraries/libraries.component.html +++ b/src/app/components/libraries/libraries.component.html @@ -12,6 +12,14 @@ list + + + + + diff --git a/src/app/dialogs/library/library.dialog.scss b/src/app/dialogs/library/library.dialog.scss new file mode 100644 index 000000000..a27ebb62e --- /dev/null +++ b/src/app/dialogs/library/library.dialog.scss @@ -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; +} diff --git a/src/app/dialogs/library/library.dialog.ts b/src/app/dialogs/library/library.dialog.ts new file mode 100644 index 000000000..e33ec4b09 --- /dev/null +++ b/src/app/dialogs/library/library.dialog.ts @@ -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 = new EventEmitter(); + + @Output() + success: EventEmitter = new EventEmitter(); + + 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, + 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 { + const { contentApi, title, id, description, visibility } = this; + const siteBody = { + id, + title, + description, + visibility + }; + + return contentApi.createSite(siteBody); + } + + private sanitize(input: string) { + return input + .replace(/[\s]/g, '-') + .replace(/[^A-Za-z0-9-]/g, ''); + } +} diff --git a/src/app/services/content-api.service.ts b/src/app/services/content-api.service.ts index a5ecff227..992997678 100644 --- a/src/app/services/content-api.service.ts +++ b/src/app/services/content-api.service.ts @@ -37,7 +37,9 @@ import { FavoritePaging, SharedLinkPaging, SearchRequest, - ResultSetPaging + ResultSetPaging, + SiteBody, + SiteEntry } from 'alfresco-js-api'; @Injectable() @@ -226,4 +228,16 @@ export class ContentApiService { this.api.sitesApi.deleteSite(siteId, opts) ); } + + createSite(siteBody: SiteBody, opts?: {skipConfiguration?: boolean, skipAddToFavorites?: boolean}): Observable { + return Observable.fromPromise( + this.api.sitesApi.createSite(siteBody, opts) + ); + } + + getSite(siteId?: string, opts?: { relations?: Array, fields?: Array }): Observable { + return Observable.fromPromise( + this.api.sitesApi.getSite(siteId, opts) + ); + } } diff --git a/src/app/store/actions/library.actions.ts b/src/app/store/actions/library.actions.ts index d2fad4aef..bc6540028 100644 --- a/src/app/store/actions/library.actions.ts +++ b/src/app/store/actions/library.actions.ts @@ -26,8 +26,14 @@ import { Action } from '@ngrx/store'; export const DELETE_LIBRARY = 'DELETE_LIBRARY'; +export const CREATE_LIBRARY = 'CREATE_LIBRARY'; export class DeleteLibraryAction implements Action { readonly type = DELETE_LIBRARY; constructor(public payload: string) {} } + +export class CreateLibraryAction implements Action { + readonly type = CREATE_LIBRARY; + constructor() {} +} diff --git a/src/app/store/effects/library.effects.ts b/src/app/store/effects/library.effects.ts index a8df0f9bb..115846e16 100644 --- a/src/app/store/effects/library.effects.ts +++ b/src/app/store/effects/library.effects.ts @@ -26,7 +26,10 @@ import { Effect, Actions, ofType } from '@ngrx/effects'; import { Injectable } from '@angular/core'; import { map } from 'rxjs/operators'; -import { DeleteLibraryAction, DELETE_LIBRARY } from '../actions'; +import { + DeleteLibraryAction, DELETE_LIBRARY, + CreateLibraryAction, CREATE_LIBRARY +} from '../actions'; import { SnackbarInfoAction, SnackbarErrorAction @@ -49,23 +52,41 @@ export class SiteEffects { deleteLibrary$ = this.actions$.pipe( ofType(DELETE_LIBRARY), map(action => { - this.contentApi.deleteSite(action.payload).subscribe( - () => { - 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' - ) - ); - } - ); + if (action.payload) { + this.deleteLibrary(action.payload); + } }) ); + + @Effect({ dispatch: false }) + createLibrary$ = this.actions$.pipe( + ofType(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(); + } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index b3c2fa9b6..62d61ab4d 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -269,6 +269,32 @@ "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": { "SORT": { "RELEVANCE": "Relevance",