[ACA-1956] Library - metadata info (#760)

* library metadata panel

* add infoDrawer component to Library

* set infoDrawer state to false onDestroy

* infoDrawer action

* update actions index

* infoDrawer set state reducer

* register Library metadata component to extensions

* filter infoDrawer tabs

* update library api

* library actions and effects

* refresh library view on update event

* infoDrawer tests

* refactor

* check permission on update

* check permission on update

* lint

* reference selection library

* add parameter type

* full width
This commit is contained in:
Cilibiu Bogdan
2018-10-29 14:09:05 +02:00
committed by GitHub
parent 8ada58f3a5
commit 1807456a3e
20 changed files with 826 additions and 30 deletions

View File

@@ -22,11 +22,137 @@
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { InfoDrawerComponent } from './info-drawer.component';
import { TestBed, ComponentFixture, async } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { SetInfoDrawerStateAction } from '../../store/actions';
import { AppTestingModule } from '../../testing/app-testing.module';
import { AppExtensionService } from '../../extensions/extension.service';
import { ContentApiService } from '../../services/content-api.service';
import { of } from 'rxjs';
describe('InfoDrawerComponent', () => {
it('should be defined', () => {
expect(InfoDrawerComponent).toBeDefined();
let fixture: ComponentFixture<InfoDrawerComponent>;
let component: InfoDrawerComponent;
let contentApiService;
let tab;
let appExtensionService;
const storeMock = {
dispatch: jasmine.createSpy('dispatch')
};
const extensionServiceMock = {
getSidebarTabs: () => {}
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [InfoDrawerComponent],
providers: [
ContentApiService,
{ provide: AppExtensionService, useValue: extensionServiceMock },
{ provide: Store, useValue: storeMock }
],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(InfoDrawerComponent);
component = fixture.componentInstance;
appExtensionService = TestBed.get(AppExtensionService);
contentApiService = TestBed.get(ContentApiService);
tab = <any>{ title: 'tab1' };
spyOn(appExtensionService, 'getSidebarTabs').and.returnValue([tab]);
});
it('should get tabs configuration on initialization', () => {
fixture.detectChanges();
expect(component.tabs).toEqual([tab]);
});
it('should set state to false OnDestroy event', () => {
fixture.detectChanges();
component.ngOnDestroy();
expect(storeMock.dispatch).toHaveBeenCalledWith(
new SetInfoDrawerStateAction(false)
);
});
it('should set displayNode when node is from personal list', () => {
spyOn(contentApiService, 'getNodeInfo');
const nodeMock = { entry: { id: 'nodeId' } };
component.node = nodeMock;
fixture.detectChanges();
component.ngOnChanges();
expect(component.displayNode).toBe(nodeMock.entry);
expect(contentApiService.getNodeInfo).not.toHaveBeenCalled();
});
it('should set displayNode when node is library', async(() => {
spyOn(contentApiService, 'getNodeInfo');
const nodeMock = <any>{
entry: { id: 'nodeId' },
isLibrary: true
};
component.node = nodeMock;
fixture.detectChanges();
component.ngOnChanges();
expect(component.displayNode).toBe(nodeMock);
expect(contentApiService.getNodeInfo).not.toHaveBeenCalled();
}));
it('should call getNodeInfo() when node is a shared file', async(() => {
const response = { entry: { id: 'nodeId' } };
spyOn(contentApiService, 'getNodeInfo').and.returnValue(of(response));
const nodeMock = { entry: { nodeId: 'nodeId' }, isLibrary: false };
component.node = nodeMock;
fixture.detectChanges();
component.ngOnChanges();
expect(component.displayNode).toBe(response);
expect(contentApiService.getNodeInfo).toHaveBeenCalled();
}));
it('should call getNodeInfo() when node is a favorite file', async(() => {
const response = { entry: { id: 'nodeId' } };
spyOn(contentApiService, 'getNodeInfo').and.returnValue(of(response));
const nodeMock = <any>{
entry: { id: 'nodeId', guid: 'guidId' },
isLibrary: false
};
component.node = nodeMock;
fixture.detectChanges();
component.ngOnChanges();
expect(component.displayNode).toBe(response);
expect(contentApiService.getNodeInfo).toHaveBeenCalled();
}));
it('should call getNodeInfo() when node is a recent file', async(() => {
const response = { entry: { id: 'nodeId' } };
spyOn(contentApiService, 'getNodeInfo').and.returnValue(of(response));
const nodeMock = <any>{
entry: {
id: 'nodeId',
content: { mimeType: 'image/jpeg' }
},
isLibrary: false
};
component.node = nodeMock;
fixture.detectChanges();
component.ngOnChanges();
expect(component.displayNode).toBe(response);
expect(contentApiService.getNodeInfo).toHaveBeenCalled();
}));
});

View File

@@ -23,27 +23,35 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component, Input, OnChanges, OnInit } from '@angular/core';
import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api';
import { Component, Input, OnChanges, OnInit, OnDestroy } from '@angular/core';
import {
MinimalNodeEntity,
MinimalNodeEntryEntity,
SiteEntry
} from 'alfresco-js-api';
import { ContentApiService } from '../../services/content-api.service';
import { AppExtensionService } from '../../extensions/extension.service';
import { SidebarTabRef } from '@alfresco/adf-extensions';
import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states/app.state';
import { SetInfoDrawerStateAction } from '../../store/actions';
@Component({
selector: 'aca-info-drawer',
templateUrl: './info-drawer.component.html'
})
export class InfoDrawerComponent implements OnChanges, OnInit {
export class InfoDrawerComponent implements OnChanges, OnInit, OnDestroy {
@Input()
nodeId: string;
@Input()
node: MinimalNodeEntity;
isLoading = false;
displayNode: MinimalNodeEntryEntity;
displayNode: MinimalNodeEntryEntity | SiteEntry;
tabs: Array<SidebarTabRef> = [];
constructor(
private store: Store<AppStore>,
private contentApi: ContentApiService,
private extensions: AppExtensionService
) {}
@@ -52,22 +60,31 @@ export class InfoDrawerComponent implements OnChanges, OnInit {
this.tabs = this.extensions.getSidebarTabs();
}
ngOnDestroy() {
this.store.dispatch(new SetInfoDrawerStateAction(false));
}
ngOnChanges() {
if (this.node) {
const entry = this.node.entry;
if (entry.nodeId) {
this.loadNodeInfo(entry.nodeId);
} else if ((<any>entry).guid) {
// workaround for Favorite files
this.loadNodeInfo(entry.id);
} else {
// workaround Recent
if (this.isTypeImage(entry) && !this.hasAspectNames(entry)) {
this.loadNodeInfo(this.node.entry.id);
} else {
this.setDisplayNode(this.node.entry);
}
if (this.isLibraryListNode(this.node)) {
return this.setDisplayNode(this.node);
}
if (this.isSharedFilesNode(this.node)) {
return this.loadNodeInfo(entry.nodeId);
}
if (this.isFavoriteListNode(this.node)) {
return this.loadNodeInfo(entry.id);
}
if (this.isRecentListFileNode(this.node)) {
return this.loadNodeInfo(entry.id);
}
this.setDisplayNode(entry);
}
}
@@ -96,7 +113,23 @@ export class InfoDrawerComponent implements OnChanges, OnInit {
}
}
private setDisplayNode(node: MinimalNodeEntryEntity) {
private setDisplayNode(node: MinimalNodeEntryEntity | SiteEntry) {
this.displayNode = node;
}
private isLibraryListNode(node: SiteEntry): boolean {
return (<any>node).isLibrary;
}
private isFavoriteListNode(node: MinimalNodeEntity): boolean {
return !this.isLibraryListNode(node) && (<any>node).entry.guid;
}
private isSharedFilesNode(node: MinimalNodeEntity): boolean {
return !!node.entry.nodeId;
}
private isRecentListFileNode(node: MinimalNodeEntity): boolean {
return this.isTypeImage(node.entry) && !this.hasAspectNames(node.entry);
}
}

View File

@@ -36,6 +36,8 @@ import { MaterialModule } from '../../material.module';
import { CommentsTabComponent } from './comments-tab/comments-tab.component';
import { InfoDrawerComponent } from './info-drawer.component';
import { MetadataTabComponent } from './metadata-tab/metadata-tab.component';
import { LibraryMetadataTabComponent } from './library-metadata-tab/library-metadata-tab.component';
import { LibraryMetadataFormComponent } from './library-metadata-tab/library-metadata-form.component';
import { VersionsTabComponent } from './versions-tab/versions-tab.component';
export function components() {
@@ -43,7 +45,9 @@ export function components() {
InfoDrawerComponent,
MetadataTabComponent,
CommentsTabComponent,
VersionsTabComponent
VersionsTabComponent,
LibraryMetadataTabComponent,
LibraryMetadataFormComponent
];
}

View File

@@ -0,0 +1,124 @@
<mat-card *ngIf="node">
<mat-card-content *ngIf="!edit">
<div class="mat-form-field mat-form-field-type-mat-input mat-form-field-can-float mat-form-field-should-float adf-full-width">
<div class="mat-form-field-wrapper">
<div class="mat-form-field-flex">
<div class="mat-form-field-infix">
<span class="mat-form-field-label-wrapper">
<span class="mat-form-field-label">
{{ 'LIBRARY.DIALOG.FORM.NAME' | translate }}
</span>
</span>
<span class="mat-input-element">
{{ form.controls.title.value }}
</span>
</div>
</div>
</div>
</div>
<div class="mat-form-field mat-primary mat-form-field-type-mat-input mat-form-field-can-float mat-form-field-should-float adf-full-width">
<div class="mat-form-field-wrapper">
<div class="mat-form-field-flex">
<div class="mat-form-field-infix">
<span class="mat-form-field-label-wrapper">
<span class="mat-form-field-label">
{{ 'LIBRARY.DIALOG.FORM.SITE_ID' | translate }}
</span>
</span>
<span class="mat-input-element">
{{ form.controls.id.value }}
</span>
</div>
</div>
</div>
</div>
<div class="mat-form-field mat-primary mat-form-field-type-mat-input mat-form-field-can-float mat-form-field-should-float adf-full-width">
<div class="mat-form-field-wrapper">
<div class="mat-form-field-flex">
<div class="mat-form-field-infix">
<span class="mat-form-field-label-wrapper">
<span class="mat-form-field-label">
{{ 'LIBRARY.DIALOG.FORM.TYPE' | translate }}
</span>
</span>
<span class="mat-input-element">
{{ (getVisibilityLabel(form.controls.visibility.value)) | translate }}
</span>
</div>
</div>
</div>
</div>
<div class="mat-form-field mat-primary mat-form-field-type-mat-input mat-form-field-can-float mat-form-field-should-float adf-full-width">
<div class="mat-form-field-wrapper">
<div class="mat-form-field-flex">
<div class="mat-form-field-infix">
<span class="mat-form-field-label-wrapper">
<span class="mat-form-field-label">
{{ 'LIBRARY.DIALOG.FORM.DESCRIPTION' | translate }}
</span>
</span>
<span class="mat-input-element">
{{ form.controls.description?.value }}
</span>
</div>
</div>
</div>
</div>
</mat-card-content>
<mat-card-actions align="end" *ngIf="!edit">
<button mat-button color="primary" (click)="toggleEdit()">
{{ 'LIBRARY.DIALOG.EDIT' | translate }}
</button>
</mat-card-actions>
<mat-card-content *ngIf="edit">
<form [formGroup]="form" autocomplete="off">
<mat-form-field class="adf-full-width">
<input matInput cdkFocusInitial required placeholder="{{ 'LIBRARY.DIALOG.FORM.NAME' | translate }}"
formControlName="title">
<mat-error *ngIf="form.controls['title'].hasError('maxlength')">
{{ 'LIBRARY.ERRORS.TITLE_TOO_LONG' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="adf-full-width">
<input matInput placeholder="{{ 'LIBRARY.DIALOG.FORM.SITE_ID' | translate }}" formControlName="id">
</mat-form-field>
<mat-form-field class="adf-full-width">
<mat-select placeholder="{{ 'LIBRARY.DIALOG.FORM.TYPE' | translate }}" formControlName="visibility">
<mat-option [value]="type.value" *ngFor="let type of libraryType">
{{ type.label | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="adf-full-width">
<textarea matInput placeholder="{{ 'LIBRARY.DIALOG.FORM.DESCRIPTION' | translate }}" rows="3"
formControlName="description"></textarea>
<mat-error *ngIf="form.controls['description'].hasError('maxlength')">
{{ 'LIBRARY.ERRORS.DESCRIPTION_TOO_LONG' | translate }}
</mat-error>
</mat-form-field>
</form>
</mat-card-content>
<mat-card-actions align="end" *ngIf="edit && canUpdateLibrary">
<button mat-button color="secondary" (click)="cancel()">
{{ 'LIBRARY.DIALOG.CANCEL' | translate }}
</button>
<button mat-button color="primary" [disabled]="form.invalid" (click)="update()">
{{ 'LIBRARY.DIALOG.UPDATE' | translate }}
</button>
</mat-card-actions>
</mat-card>

View File

@@ -0,0 +1,214 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 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 <http://www.gnu.org/licenses/>.
*/
import { LibraryMetadataFormComponent } from './library-metadata-form.component';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { UpdateLibraryAction } from '../../../store/actions';
import { AppTestingModule } from '../../../testing/app-testing.module';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Site, SiteBody } from 'alfresco-js-api';
describe('LibraryMetadataFormComponent', () => {
let fixture: ComponentFixture<LibraryMetadataFormComponent>;
let component: LibraryMetadataFormComponent;
const storeMock = {
dispatch: jasmine.createSpy('dispatch')
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
declarations: [LibraryMetadataFormComponent],
providers: [{ provide: Store, useValue: storeMock }],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(LibraryMetadataFormComponent);
component = fixture.componentInstance;
});
afterEach(() => {
storeMock.dispatch.calls.reset();
});
it('should initialize form with node data', () => {
const siteEntryModel = {
title: 'libraryTitle',
description: 'description',
visibility: 'PRIVATE'
};
component.node = {
entry: <Site>{
id: 'libraryId',
...siteEntryModel
}
};
fixture.detectChanges();
expect(component.form.value).toEqual(siteEntryModel);
});
it('should update form data when node data changes', () => {
const siteEntryModel = {
title: 'libraryTitle',
description: 'description',
visibility: 'PRIVATE'
};
const newSiteEntryModel = {
title: 'libraryTitle2',
description: 'description2',
visibility: 'PUBLIC'
};
component.node = {
entry: <Site>{
id: 'libraryId',
...siteEntryModel
}
};
fixture.detectChanges();
expect(component.form.value).toEqual(siteEntryModel);
component.node = {
entry: <Site>{
id: 'libraryId',
...newSiteEntryModel
}
};
component.ngOnChanges();
expect(component.form.value).toEqual(newSiteEntryModel);
});
it('should update library node if form is valid', () => {
const siteEntryModel = {
title: 'libraryTitle',
description: 'description',
visibility: 'PRIVATE'
};
component.node = {
entry: <Site>{
id: 'libraryId',
role: 'SiteManager',
...siteEntryModel
}
};
fixture.detectChanges();
component.update();
expect(storeMock.dispatch).toHaveBeenCalledWith(
new UpdateLibraryAction(<SiteBody>siteEntryModel)
);
});
it('should not update library node if it has no permission', () => {
const siteEntryModel = {
title: 'libraryTitle',
description: 'description',
visibility: 'PRIVATE'
};
component.node = {
entry: <Site>{
id: 'libraryId',
role: 'Consumer',
...siteEntryModel
}
};
fixture.detectChanges();
component.update();
expect(storeMock.dispatch).not.toHaveBeenCalledWith(
new UpdateLibraryAction(<SiteBody>siteEntryModel)
);
});
it('should not update library node if form is invalid', () => {
const siteEntryModel = {
title: 'libraryTitle',
description: 'description',
visibility: 'PRIVATE'
};
component.node = {
entry: <Site>{
id: 'libraryId',
role: 'SiteManager',
...siteEntryModel
}
};
fixture.detectChanges();
component.form.controls['title'].setErrors({ maxlength: true });
component.update();
expect(storeMock.dispatch).not.toHaveBeenCalledWith(
new UpdateLibraryAction(<SiteBody>siteEntryModel)
);
});
it('should toggle edit mode', () => {
component.edit = false;
component.toggleEdit();
expect(component.edit).toBe(true);
component.toggleEdit();
expect(component.edit).toBe(false);
});
it('should cancel from changes', () => {
const siteEntryModel = {
title: 'libraryTitle',
description: 'description',
visibility: 'PRIVATE'
};
component.node = {
entry: <Site>{
id: 'libraryId',
...siteEntryModel
}
};
fixture.detectChanges();
expect(component.form.value).toEqual(siteEntryModel);
component.form.controls.title.setValue('libraryTitle-edit');
expect(component.form.value.title).toBe('libraryTitle-edit');
component.cancel();
expect(component.form.value).toEqual(siteEntryModel);
});
});

View File

@@ -0,0 +1,104 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 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 <http://www.gnu.org/licenses/>.
*/
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { SiteEntry } from 'alfresco-js-api';
import { Store } from '@ngrx/store';
import { UpdateLibraryAction } from '../../../store/actions';
import { AppStore } from '../../../store/states/app.state';
@Component({
selector: 'app-library-metadata-form',
templateUrl: './library-metadata-form.component.html'
})
export class LibraryMetadataFormComponent implements OnInit, OnChanges {
@Input()
node: SiteEntry;
edit: boolean;
libraryType = [
{ value: 'PUBLIC', label: 'LIBRARY.VISIBILITY.PUBLIC' },
{ value: 'PRIVATE', label: 'LIBRARY.VISIBILITY.PRIVATE' },
{ value: 'MODERATED', label: 'LIBRARY.VISIBILITY.MODERATED' }
];
form: FormGroup = new FormGroup({
id: new FormControl({ value: '', disabled: true }),
title: new FormControl({ value: '' }, [
Validators.required,
Validators.maxLength(256)
]),
description: new FormControl({ value: '' }, [Validators.maxLength(512)]),
visibility: new FormControl(this.libraryType[0].value)
});
constructor(protected store: Store<AppStore>) {}
get canUpdateLibrary() {
return (
this.node && this.node.entry && this.node.entry.role === 'SiteManager'
);
}
getVisibilityLabel(value) {
return this.libraryType.find(type => type.value === value).label;
}
toggleEdit() {
this.edit = !this.edit;
}
cancel() {
this.updateForm(this.node);
this.toggleEdit();
}
ngOnInit() {
this.updateForm(this.node);
}
ngOnChanges() {
this.updateForm(this.node);
}
update() {
if (this.canUpdateLibrary && this.form.valid) {
this.store.dispatch(new UpdateLibraryAction(this.form.value));
}
}
private updateForm(node: SiteEntry) {
const { entry } = node;
this.form.setValue({
id: entry.id,
title: entry.title,
description: entry.description || '',
visibility: entry.visibility
});
}
}

View File

@@ -0,0 +1,38 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 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 <http://www.gnu.org/licenses/>.
*/
import { Component, Input } from '@angular/core';
import { SiteEntry } from 'alfresco-js-api';
@Component({
selector: 'app-metadata-tab',
template:
'<app-library-metadata-form [node]="node"></app-library-metadata-form>',
host: { class: 'app-metadata-tab' }
})
export class LibraryMetadataTabComponent {
@Input()
node: SiteEntry;
}

View File

@@ -75,5 +75,9 @@
<adf-pagination acaPagination [target]="documentList">
</adf-pagination>
</div>
<div class="sidebar" *ngIf="infoDrawerOpened$ | async">
<aca-info-drawer [node]="selection.library"></aca-info-drawer>
</div>
</app-page-layout-content>
</app-page-layout>

View File

@@ -63,6 +63,10 @@ export class LibrariesComponent extends PageComponent implements OnInit {
})
);
this.subscriptions = this.subscriptions.concat([
this.content.libraryUpdated.subscribe(() => this.documentList.reload())
]);
this.columns = this.extensions.documentListPresets.libraries || [];
}

View File

@@ -35,6 +35,7 @@ import { ToggleInfoDrawerComponent } from '../components/toolbar/toggle-info-dra
import { ToggleFavoriteComponent } from '../components/toolbar/toggle-favorite/toggle-favorite.component';
import { ToggleSharedComponent } from '../components/shared/toggle-shared/toggle-shared.component';
import { MetadataTabComponent } from '../components/info-drawer/metadata-tab/metadata-tab.component';
import { LibraryMetadataTabComponent } from '../components/info-drawer/library-metadata-tab/library-metadata-tab.component';
import { CommentsTabComponent } from '../components/info-drawer/comments-tab/comments-tab.component';
import { VersionsTabComponent } from '../components/info-drawer/versions-tab/versions-tab.component';
import { ExtensionsModule, ExtensionService } from '@alfresco/adf-extensions';
@@ -77,6 +78,7 @@ export class CoreExtensionsModule {
extensions.setComponents({
'app.layout.main': AppLayoutComponent,
'app.components.tabs.metadata': MetadataTabComponent,
'app.components.tabs.library.metadata': LibraryMetadataTabComponent,
'app.components.tabs.comments': CommentsTabComponent,
'app.components.tabs.versions': VersionsTabComponent,
'app.toolbar.toggleInfoDrawer': ToggleInfoDrawerComponent,

View File

@@ -243,7 +243,7 @@ export class AppExtensionService implements RuleContext {
}
getSidebarTabs(): Array<SidebarTabRef> {
return this.sidebar;
return this.sidebar.filter(action => this.filterByRules(<any>action));
}
getComponentById(id: string): Type<{}> {

View File

@@ -219,6 +219,10 @@ export class ContentApiService {
return from(this.api.sitesApi.getSite(siteId, opts));
}
updateLibrary(siteId: string, siteBody: SiteBody): Observable<SiteEntry> {
return from(this.api.sitesApi.updateSite(siteId, siteBody));
}
addFavorite(nodes: Array<MinimalNodeEntity>): Observable<any> {
const payload: FavoriteBody[] = nodes.map(node => {
const { isFolder, nodeId, id } = node.entry;

View File

@@ -49,7 +49,8 @@ import {
Node,
SiteEntry,
DeletedNodesPaging,
PathInfoEntity
PathInfoEntity,
SiteBody
} from 'alfresco-js-api';
import { NodePermissionService } from './node-permission.service';
import { NodeInfo, DeletedNodeInfo, DeleteStatus } from '../store/models';
@@ -80,6 +81,7 @@ export class ContentManagementService {
folderCreated = new Subject<any>();
libraryDeleted = new Subject<string>();
libraryCreated = new Subject<SiteEntry>();
libraryUpdated = new Subject<SiteEntry>();
linksUnshared = new Subject<any>();
favoriteAdded = new Subject<Array<MinimalNodeEntity>>();
favoriteRemoved = new Subject<Array<MinimalNodeEntity>>();
@@ -295,6 +297,22 @@ export class ContentManagementService {
);
}
updateLibrary(siteId: string, siteBody: SiteBody) {
this.contentApi.updateLibrary(siteId, siteBody).subscribe(
(siteEntry: SiteEntry) => {
this.libraryUpdated.next(siteEntry);
this.store.dispatch(
new SnackbarInfoAction('LIBRARY.SUCCESS.LIBRARY_UPDATED')
);
},
() => {
this.store.dispatch(
new SnackbarErrorAction('LIBRARY.ERRORS.LIBRARY_UPDATE_ERROR')
);
}
);
}
async unshareNodes(links: Array<MinimalNodeEntity>) {
const promises = links.map(link =>
this.contentApi.deleteSharedLink(link.entry.id).toPromise()

View File

@@ -34,3 +34,4 @@ export * from './actions/library.actions';
export * from './actions/upload.actions';
export * from './actions/modals.actions';
export * from './actions/repository.actions';
export * from './actions/info-drawer.actions';

View File

@@ -0,0 +1,33 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 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 <http://www.gnu.org/licenses/>.
*/
import { Action } from '@ngrx/store';
export const SET_INFO_DRAWER_STATE = 'SET_INFO_DRAWER_STATE';
export class SetInfoDrawerStateAction implements Action {
readonly type = SET_INFO_DRAWER_STATE;
constructor(public payload: boolean) {}
}

View File

@@ -24,10 +24,12 @@
*/
import { Action } from '@ngrx/store';
import { SiteBody } from 'alfresco-js-api';
export const DELETE_LIBRARY = 'DELETE_LIBRARY';
export const CREATE_LIBRARY = 'CREATE_LIBRARY';
export const NAVIGATE_LIBRARY = 'NAVIGATE_LIBRARY';
export const UPDATE_LIBRARY = 'UPDATE_LIBRARY';
export class DeleteLibraryAction implements Action {
readonly type = DELETE_LIBRARY;
@@ -43,3 +45,7 @@ export class NavigateLibraryAction implements Action {
readonly type = NAVIGATE_LIBRARY;
constructor(public payload?: string) {}
}
export class UpdateLibraryAction implements Action {
readonly type = UPDATE_LIBRARY;
constructor(public payload?: SiteBody) {}
}

View File

@@ -32,7 +32,9 @@ import {
CreateLibraryAction,
CREATE_LIBRARY,
NavigateLibraryAction,
NAVIGATE_LIBRARY
NAVIGATE_LIBRARY,
UPDATE_LIBRARY,
UpdateLibraryAction
} from '../actions';
import { ContentManagementService } from '../../services/content-management.service';
import { Store } from '@ngrx/store';
@@ -40,6 +42,7 @@ import { AppStore } from '../states';
import { appSelection } from '../selectors/app.selectors';
import { ContentApiService } from '../../services/content-api.service';
import { Router } from '@angular/router';
import { SiteBody } from 'alfresco-js-api-node';
@Injectable()
export class LibraryEffects {
@@ -92,4 +95,28 @@ export class LibraryEffects {
}
})
);
@Effect({ dispatch: false })
updateLibrary$ = this.actions$.pipe(
ofType<UpdateLibraryAction>(UPDATE_LIBRARY),
map(action => {
this.store
.select(appSelection)
.pipe(take(1))
.subscribe(selection => {
if (selection && selection.library) {
const { id } = selection.library.entry;
const { title, description, visibility } = action.payload;
const siteBody = <SiteBody>{
title,
description,
visibility
};
this.content.updateLibrary(id, siteBody);
}
});
})
);
}

View File

@@ -37,7 +37,9 @@ import {
SET_CURRENT_FOLDER,
SetCurrentFolderAction,
SET_CURRENT_URL,
SetCurrentUrlAction
SetCurrentUrlAction,
SET_INFO_DRAWER_STATE,
SetInfoDrawerStateAction
} from '../actions';
import {
TOGGLE_INFO_DRAWER,
@@ -76,6 +78,9 @@ export function appReducer(
case TOGGLE_INFO_DRAWER:
newState = updateInfoDrawer(state, <ToggleInfoDrawerAction>action);
break;
case SET_INFO_DRAWER_STATE:
newState = setInfoDrawer(state, <SetInfoDrawerStateAction>action);
break;
case TOGGLE_DOCUMENT_DISPLAY_MODE:
newState = updateDocumentDisplayMode(state, <ToggleDocumentDisplayMode>(
action
@@ -215,6 +220,12 @@ function updateSelectedNodes(
return newState;
}
function setInfoDrawer(state: AppState, action: SetInfoDrawerStateAction) {
const newState = Object.assign({}, state);
newState.infoDrawerOpened = action.payload;
return newState;
}
function updateRepositoryStatus(
state: AppState,
action: SetRepositoryStatusAction

View File

@@ -99,6 +99,14 @@
{ "type": "rule", "value": "app.navigation.isNotTrashcan" }
]
},
{
"id": "app.libraries.toolbar.info",
"type": "core.every",
"parameters": [
{ "type": "rule", "value": "app.selection.notEmpty" },
{ "type": "rule", "value": "app.navigation.isLibraries" }
]
},
{
"id": "app.toolbar.canCopyNode",
"type": "core.every",
@@ -361,6 +369,15 @@
"visible": "app.toolbar.info"
}
},
{
"id": "app.libraries.toolbar.info",
"type": "custom",
"order": 701,
"component": "app.toolbar.toggleInfoDrawer",
"rules": {
"visible": "app.libraries.toolbar.info"
}
},
{
"id": "app.toolbar.more",
"type": "menu",
@@ -827,20 +844,38 @@
"id": "app.sidebar.properties",
"order": 100,
"title": "APP.INFO_DRAWER.TABS.PROPERTIES",
"component": "app.components.tabs.metadata"
"component": "app.components.tabs.metadata",
"rules": {
"visible": "app.navigation.isNotLibraries"
}
},
{
"id": "app.sidebar.comments",
"order": 200,
"title": "APP.INFO_DRAWER.TABS.COMMENTS",
"component": "app.components.tabs.comments"
"component": "app.components.tabs.comments",
"rules": {
"visible": "app.navigation.isNotLibraries"
}
},
{
"id": "app.sidebar.versions",
"order": 300,
"disabled": true,
"title": "APP.INFO_DRAWER.TABS.VERSIONS",
"component": "app.components.tabs.versions"
"component": "app.components.tabs.versions",
"rules": {
"visible": "app.navigation.isNotLibraries"
}
},
{
"id": "test",
"order": 500,
"title": "APP.INFO_DRAWER.TABS.LIBRARY_PROPERTIES",
"component": "app.components.tabs.library.metadata",
"rules": {
"visible": "app.libraries.toolbar.info"
}
}
],
"content-metadata-presets": [

View File

@@ -245,6 +245,7 @@
"TITLE": "Details",
"TABS": {
"PROPERTIES": "Properties",
"LIBRARY_PROPERTIES": "About",
"VERSIONS": "Versions",
"COMMENTS": "Comments"
}
@@ -282,11 +283,14 @@
"DIALOG": {
"CREATE_TITLE": "Create Library",
"CREATE": "Create",
"UPDATE": "Update",
"EDIT": "Edit",
"CANCEL": "Cancel",
"FORM": {
"DESCRIPTION": "Description",
"SITE_ID": "Library ID",
"NAME": "Name"
"NAME": "Name",
"TYPE": "Type"
}
},
"VISIBILITY": {
@@ -301,7 +305,11 @@
"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 numbers and letters only"
"ILLEGAL_CHARACTERS": "Use numbers and letters only",
"LIBRARY_UPDATE_ERROR": "There was an error updating library properties"
},
"SUCCESS": {
"LIBRARY_UPDATED": "Library properties updated"
}
},