[ADF-5305] - Added aspect list component (#6549)

* [ADF-5305] - Creation of aspect list and aspect list dialog components

* [ADF-5305] - unit test for aspect list

* [ADF-5305] - added filtering for aspects

* [ADF-5305] - enabling tests

* [ADF-5305] - added filtering and unit test

* [ADF-5305] - added context action to demo shell

* [ADF-5305] - added button on metadata card for opening aspects

* [ADF-5305] - fixed unit test for filtering aspects

* [ADF-5305] - added documentation

* [ADF-5305] - fixed lint

* [ADF-5305] - Updated the js-api calls

* [ADF-5305] - Removed circle dependency

* [ADF-5305] - Simplified code

* [ADF-5305] - revert changes on package.json

* [ADF-5305] - removed extra cspell word

* [ADF-5305] - added filtering on aspect list service

* [ADF-5305] - fix unit test for aspect service

* [ADF-5305] - reverted changes to package-loc

* [ADF-5305] - removed unused changes

* [ADF-5305] - attempt to fix PR #§

* [ADF-5305] - attempt to fix PR #2

* [ADF-5305] - attempt to fix PR #3

* [ADF-5305] - attempt to fix PR #4

* [ADF-5305] - attempt to fix PR #5

Co-authored-by: Vito Albano <vitoalbano@vitoalbano-mbp-0120.local>
This commit is contained in:
Vito
2021-02-17 11:13:35 +00:00
committed by GitHub
parent f7f80bc013
commit e62c752f1f
38 changed files with 1690 additions and 11 deletions

View File

@@ -94,6 +94,7 @@
"SEARCH_SERVICE_APPROACH": "Check this to disable the input property and configure using the service",
"HEADER_DATA": "Header Data",
"TREE_VIEW": "Tree View",
"EXPAND_LIST": "Expandable item list",
"ICONS": "Icons",
"PEOPLE_GROUPS_CLOUD": "People/Group Cloud",
"TASK_HEADER_CLOUD": {
@@ -166,6 +167,7 @@
},
"ACTIONS": {
"VERSIONS": "Manage versions",
"ASPECTS": "Update Aspects",
"LOCK": "Lock",
"METADATA": "Info",
"DOWNLOAD": "Download",

View File

@@ -1197,5 +1197,13 @@
{
"name": "subprocessapp"
}
]
],
"aspect-visible": {
"default" : ["cm:generalclassifiable", "cm:complianceable",
"cm:dublincore", "cm:effectivity", "cm:summarizable",
"cm:versionable", "cm:templatable","cm:emailed", "emailserver:aliasable",
"cm:taggable", "app:inlineeditable", "cm:geographic", "exif:exif",
"audio:audio", "cm:indexControl", "dp:restrictable", "smf:customConfigSmartFolder", "smf:systemConfigSmartFolder"],
"ai": ["ai:products", "ai:dates", "ai:places", "ai:events", "ai:organizations", "ai:people", "ai:things", "ai:quantities", "ai:creativeWorks", "ai:labels", "ai:textLines"]
}
}

View File

@@ -93,6 +93,7 @@ import {
CustomEditorComponent,
CustomWidgetComponent
} from './components/cloud/custom-form-components/custom-editor.component';
import { AspectListSampleComponent } from './components/aspect-list-sample/aspect-list-sample.component';
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
@@ -180,6 +181,7 @@ registerLocaleData(localeSv);
DemoErrorComponent,
FormLoadingComponent,
TreeViewSampleComponent,
AspectListSampleComponent,
CloudLayoutComponent,
AppsCloudDemoComponent,
TasksCloudDemoComponent,

View File

@@ -54,6 +54,7 @@ import { TaskHeaderCloudDemoComponent } from './components/cloud/task-header-clo
import { FilteredSearchComponent } from './components/files/filtered-search.component';
import { ProcessCloudLayoutComponent } from './components/cloud/process-cloud-layout.component';
import { ServiceTaskListCloudDemoComponent } from './components/cloud/service-task-list-cloud-demo.component';
import { AspectListSampleComponent } from './components/aspect-list-sample/aspect-list-sample.component';
export const appRoutes: Routes = [
{ path: 'login', loadChildren: () => import('./components/login/login.module').then(m => m.AppLoginModule) },
@@ -413,6 +414,11 @@ export const appRoutes: Routes = [
component: TreeViewSampleComponent,
canActivate: [AuthGuardEcm]
},
{
path: 'expandable-list',
component: AspectListSampleComponent,
canActivate: [AuthGuardEcm]
},
{
path: 'about',
loadChildren: () => import('./components/about/about.module').then(m => m.AppAboutModule)

View File

@@ -86,6 +86,7 @@ export class AppLayoutComponent implements OnInit, OnDestroy {
/* cspell:disable-next-line */
{ href: '/overlay-viewer', icon: 'pageview', title: 'APP_LAYOUT.OVERLAY_VIEWER' },
{ href: '/treeview', icon: 'nature', title: 'APP_LAYOUT.TREE_VIEW' },
{ href: '/expandable-list', icon: 'hot_tub', title: 'APP_LAYOUT.EXPAND_LIST' },
{ href: '/icons', icon: 'tag_faces', title: 'APP_LAYOUT.ICONS' },
{ href: '/about', icon: 'info_outline', title: 'APP_LAYOUT.ABOUT' }
];

View File

@@ -0,0 +1,18 @@
<div class="example-button-container">
<p> ASPECT CHOSEN :</p>
<p>{{currentResult}}</p>
<br>
<mat-form-field class="example-almost-full-width">
<mat-label>Node Id For Aspects</mat-label>
<input matInput placeholder="Node Id" [(ngModel)]="currentNodeId">
</mat-form-field>
<button mat-raised-button color="primary" aria-label="Click to show the list" (click)="showAspectForNode()">
Show/Hide List
</button>
<adf-aspect-list [nodeId]="currentNodeId" *ngIf="isShowed" (valueChanged)="onValueChanged($event)"></adf-aspect-list>
</div>
<div>
<button mat-fab color="primary" aria-label="Open dialog" (click)="openAspectDialog()">
Dialog
</button>
</div>

View File

@@ -0,0 +1,7 @@
.example-button-container {
width: 90%;
}
.example-almost-full-width {
width: 70%;
}

View File

@@ -0,0 +1,47 @@
/*!
* @license
* Copyright 2019 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 { AspectListService } from '@alfresco/adf-content-services';
import { Component } from '@angular/core';
@Component({
selector: 'app-expandable-menu',
templateUrl: 'aspect-list-sample.component.html',
styleUrls: ['aspect-list-sample.component.scss']
})
export class AspectListSampleComponent {
currentNodeId: string = '';
isShowed: boolean = false;
currentResult: string[] = [];
constructor(private aspectListService: AspectListService) { }
showAspectForNode() {
this.isShowed = !this.isShowed;
}
openAspectDialog() {
this.aspectListService.openAspectListDialog(this.currentNodeId).subscribe((result) => this.currentResult = Array.from(result));
}
onValueChanged(aspects) {
this.currentResult = Array.from(aspects);
}
}

View File

@@ -437,6 +437,12 @@
handler="lock"
title="DOCUMENT_LIST.ACTIONS.LOCK">
</content-action>
<content-action
icon="beach_access"
target="document"
title="DOCUMENT_LIST.ACTIONS.ASPECTS"
(execute)="onAspectUpdate($event)">
</content-action>
</content-actions>
</adf-document-list>
</div>

View File

@@ -26,7 +26,8 @@ import {
PaginationComponent, FormValues, DisplayMode, ShowHeaderMode, InfinitePaginationComponent,
SharedLinksApiService,
FormRenderingService,
FileUploadEvent
FileUploadEvent,
NodesApiService
} from '@alfresco/adf-core';
import {
@@ -36,7 +37,8 @@ import {
ConfirmDialogComponent,
LibraryDialogComponent,
ContentMetadataService,
FilterSearch
FilterSearch,
AspectListService
} from '@alfresco/adf-content-services';
import { SelectAppsDialogComponent, ProcessFormRenderingService } from '@alfresco/adf-process-services';
@@ -228,7 +230,9 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy {
public authenticationService: AuthenticationService,
public alfrescoApiService: AlfrescoApiService,
private contentMetadataService: ContentMetadataService,
private sharedLinksApiService: SharedLinksApiService) {
private sharedLinksApiService: SharedLinksApiService,
private aspectListService: AspectListService,
private nodeService: NodesApiService) {
}
showFile(event) {
@@ -467,6 +471,14 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy {
}
}
onAspectUpdate(event: any) {
this.aspectListService.openAspectListDialog(event.value.entry.id).subscribe((aspectList) => {
this.nodeService.updateNode(event.value.entry.id, {aspectNames : [...aspectList]}).subscribe(() => {
this.openSnackMessageInfo('Node Aspects Updated');
});
});
}
onManageMetadata(event: any) {
const contentEntry = event.value.entry;
const displayEmptyMetadata = this.displayEmptyMetadata;

View File

@@ -0,0 +1,94 @@
---
Title: Aspect List Dialog component
Added: v2.0.0
Status: Active
Last reviewed: 2021-01-20
---
# [Aspect List Dialog component](../../../lib/content-services/src/lib/aspect-list/aspect-list-dialog.component.ts "Defined in aspect-list-dialog.component.ts")
Allows a user to choose aspects for a node.
## Details
The [Aspect List Dialog component](aspect-list-dialog.component.md) works as a dialog showing the list of aspects available.
It is possible to filter the aspect showed via the app.config.json.
### Showing the dialog
Unlike most components, the [Aspect List Dialog component](aspect-list-dialog.component.md) is typically shown in a dialog box
rather than the main page and you are responsible for opening the dialog yourself. You can use the
[Angular Material Dialog](https://material.angular.io/components/dialog/overview) for this,
as shown in the usage example. ADF provides the [`AspectListDialogComponentData`](../../../lib/content-services/src/lib/aspect-list/aspect-list-dialog-data.interface.ts) interface
to work with the Dialog's
[data option](https://material.angular.io/components/dialog/overview#sharing-data-with-the-dialog-component-):
```ts
export interface AspectListDialogComponentData {
title: string;
description: string;
overTableMessage: string;
select: Subject<string[]>;
nodeId?: string;
}
```
The properties are described in the table below:
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| title | `string` | "" | Dialog title |
| description | `string` | "" | Text to appear as description under the dialog title |
| overTableMessage | `string` | "" | Text that will be showed on the top of the aspect list table |
| select | [`Subject<Node>`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/content-rest-api/docs/Node.md) | | Event emitted with the current node selection when the dialog closes |
| nodeId | `string` | "" | Identifier of a node to apply aspects to. |
If you don't want to manage the dialog yourself then it is easier to use the
[Aspect List component](aspect-list.component.md), or the
methods of the [Aspect List service](../services/aspect-list.service.md), which create
the dialog for you.
### Usage example
```ts
import { MatDialog } from '@angular/material/dialog';
import { AspectListDialogComponentData, AspectListDialogComponent} from '@adf/content-services'
import { Subject } from 'rxjs/Subject';
...
constructor(dialog: MatDialog ... ) {}
openSelectorDialog() {
data: AspectListDialogComponentData = {
title: "Choose an item",
description: "Choose",
overTableMessage: "Over Table Message",
nodeId: currentNodeID,
select: new Subject<Node[]>()
};
this.dialog.open(
AspectListDialogComponent,
{
data, panelClass: 'adf-aspect-list-dialog',
width: '630px'
}
);
data.select.subscribe((selections: Node[]) => {
// Use or store selection...
},
(error)=>{
//your error handling
},
()=>{
//action called when an action or cancel is clicked on the dialog
this.dialog.closeAll();
});
}
```
All the results will be streamed to the select [subject](http://reactivex.io/rxjs/manual/overview.html#subject) present in the [`AspectListDialogComponentData`](../../../lib/content-services/src/lib/aspect-list/aspect-list-dialog-data.interface.ts) object passed to the dialog.
When the dialog action is selected by clicking, the `data.select` stream will be completed.
## See also
- [Aspect list component](aspect-list.component.md)

View File

@@ -0,0 +1,44 @@
---
Title: Aspect List component
Added: v2.0.0
Status: Active
Last reviewed: 2021-01-20
---
# [Aspect List component](../../../lib/content-services/src/lib/aspect-list/aspect-list.component.ts "Defined in aspect-list.component.ts")
This component will show in an expandable row list with checkboxes all the aspect of a node, if a node id is given, or otherwise a complete list.
The aspect are filtered via the app.config.json in this way :
```json
"aspect-visible": {
"default" : ["as:aspectThatWillBeShowedIfPresent"]
}
```
## Basic Usage
```html
<adf-aspect-list (valueChanged)="onValueChanged($event)" [nodeId]="nodeId">
</adf-aspect-list>
```
## Class members
### Properties
| Name | Type | Default value | Description |
| --- | --- | --- | --- |
| nodeId | `string` | | Identifier of a node to apply likes to. |
### Events
| Name | Type | Description |
| --- | --- | --- |
| valueChanged | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<string>` | Emitted every time the user select a new aspect. |
## See also
* [Aspect List Dialog component](rating.component.md)
* [Aspect List service](../services/rating.service.md)
* [Node Aspect service](../services/rating.service.md)

View File

@@ -0,0 +1,26 @@
/*!
* @license
* Copyright 2019 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 { Subject } from 'rxjs';
export interface AspectListDialogComponentData {
title: string;
description: string;
overTableMessage: string;
select: Subject<string[]>;
nodeId?: string;
}

View File

@@ -0,0 +1,39 @@
<header mat-dialog-title data-automation-id="aspect-list-dialog-title" class="adf-aspect-list-dialog">
<h4 class="adf-aspect-list-dialog-title">{{title | translate}}</h4>
<div class="adf-aspect-list-dialog-description">{{description | translate}}</div>
</header>
<div class="adf-aspect-list-dialog-information">
<p id="aspect-list-dialog-over-table-message">{{overTableMessage | translate}}</p>
<p id="aspect-list-dialog-counter">{{currentAspectSelection ? currentAspectSelection.length : 0}}
{{'ADF-ASPECT-LIST.DIALOG.SELECTED' | translate}}</p>
</div>
<mat-dialog-content class="adf-aspect-dialog-content">
<adf-aspect-list #aspectList (valueChanged)="onValueChanged($event)" [nodeId]="currentNodeId">
</adf-aspect-list>
</mat-dialog-content>
<mat-dialog-actions>
<div>
<button mat-button (click)="aspectList.reset()" id="aspect-list-dialog-actions-reset"
data-automation-id="aspect-list-dialog-actions-reset">{{
'ADF-ASPECT-LIST.DIALOG.RESET' | translate }}
</button>
<button mat-button (click)="aspectList.clear()" id="aspect-list-dialog-actions-clear"
data-automation-id="aspect-list-dialog-actions-clear">{{
'ADF-ASPECT-LIST.DIALOG.CLEAR' | translate }}
</button>
</div>
<div>
<button mat-button (click)="onCancel()" id="aspect-list-dialog-actions-cancel"
data-automation-id="aspect-list-dialog-actions-cancel">{{
'ADF-ASPECT-LIST.DIALOG.CANCEL' | translate }}
</button>
<button mat-button (click)="onApply()" id="aspect-list-dialog-actions-apply"
data-automation-id="aspect-list-dialog-actions-apply">{{
'ADF-ASPECT-LIST.DIALOG.APPLY' | translate }}
</button>
</div>
</mat-dialog-actions>

View File

@@ -0,0 +1,55 @@
@mixin adf-aspect-list-dialog-theme($theme) {
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
.adf {
&-aspect-list-dialog-title {
font-size: large;
font-weight: 200;
margin-top: 0;
}
&-aspect-list-dialog-description {
font-size: small;
line-height: normal;
}
&-aspect-list-dialog-information {
display: flex;
justify-content: space-between;
padding-left: 5px;
padding-right: 5px;
font-size: small;
}
&-aspect-list-dialog {
.mat-dialog-actions {
justify-content: space-between;
}
}
&-aspect-dialog-content {
padding-top: 3px;
.adf-aspect-property-table {
.mat-cell {
font-size: smaller;
}
.mat-column-name {
width: 30%;
}
}
}
}
}

View File

@@ -0,0 +1,272 @@
/*!
* @license
* Copyright 2019 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 { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AspectListDialogComponent } from './aspect-list-dialog.component';
import { TranslateModule } from '@ngx-translate/core';
import { of, Subject } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
import { AspectListDialogComponentData } from './aspect-list-dialog-data.interface';
import { NodesApiService } from 'core';
import { AspectListService } from './aspect-list.service';
import { delay } from 'rxjs/operators';
import { AspectEntry } from '@alfresco/js-api';
const aspectListMock: AspectEntry[] = [{
entry: {
parentId: 'frs:aspectZero',
id: 'frs:AspectOne',
description: 'First Aspect with random description',
title: 'FirstAspect',
properties: [
{
id: 'channelPassword',
title: 'The authenticated channel password',
dataType: 'd:encrypted'
},
{
id: 'channelUsername',
title: 'The authenticated channel username',
dataType: 'd:encrypted'
}
]
}
},
{
entry: {
parentId: 'frs:AspectZer',
id: 'frs:SecondAspect',
description: 'Second Aspect description',
title: 'SecondAspect',
properties: [
{
id: 'assetId',
title: 'Published Asset Id',
dataType: 'd:text'
},
{
id: 'assetUrl',
title: 'Published Asset URL',
dataType: 'd:text'
}
]
}
}];
describe('AspectListDialogComponent', () => {
let fixture: ComponentFixture<AspectListDialogComponent>;
let aspectListService: AspectListService;
let nodeService: NodesApiService;
let data: AspectListDialogComponentData;
describe('Without passing node id', () => {
beforeEach(async () => {
data = <AspectListDialogComponentData> {
title: 'Title',
description: 'Description that can be longer or shorter',
overTableMessage: 'Over here',
select: new Subject<string[]>()
};
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
ContentTestingModule,
MatDialogModule
],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
{
provide: MatDialogRef,
useValue: {
keydownEvents: () => of(null),
backdropClick: () => of(null),
close: jasmine.createSpy('close')
}
}
]
}).compileComponents();
});
beforeEach(() => {
aspectListService = TestBed.inject(AspectListService);
spyOn(aspectListService, 'getAspects').and.returnValue(of(aspectListMock));
fixture = TestBed.createComponent(AspectListDialogComponent);
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
it('should show 4 actions : CLEAR, RESET, CANCEL and APPLY', () => {
expect(fixture.nativeElement.querySelector('#aspect-list-dialog-actions-reset')).not.toBeNull();
expect(fixture.nativeElement.querySelector('#aspect-list-dialog-actions-reset')).toBeDefined();
expect(fixture.nativeElement.querySelector('#aspect-list-dialog-actions-clear')).not.toBeNull();
expect(fixture.nativeElement.querySelector('#aspect-list-dialog-actions-clear')).toBeDefined();
expect(fixture.nativeElement.querySelector('#aspect-list-dialog-actions-cancel')).not.toBeNull();
expect(fixture.nativeElement.querySelector('#aspect-list-dialog-actions-cancel')).toBeDefined();
expect(fixture.nativeElement.querySelector('#aspect-list-dialog-actions-apply')).not.toBeNull();
expect(fixture.nativeElement.querySelector('#aspect-list-dialog-actions-apply')).toBeDefined();
});
it('should show basic information for the dialog', () => {
const dialogTitle = fixture.nativeElement.querySelector('[data-automation-id="aspect-list-dialog-title"] .adf-aspect-list-dialog-title');
expect(dialogTitle).not.toBeNull();
expect(dialogTitle.innerText).toBe(data.title);
const dialogDescription = fixture.nativeElement.querySelector('[data-automation-id="aspect-list-dialog-title"] .adf-aspect-list-dialog-description');
expect(dialogDescription).not.toBeNull();
expect(dialogDescription.innerText).toBe(data.description);
const overTableMessage = fixture.nativeElement.querySelector('#aspect-list-dialog-over-table-message');
expect(overTableMessage).not.toBeNull();
expect(overTableMessage.innerText).toBe(data.overTableMessage);
const selectionCounter = fixture.nativeElement.querySelector('#aspect-list-dialog-counter');
expect(selectionCounter).not.toBeNull();
expect(selectionCounter.innerText).toBe('0 ADF-ASPECT-LIST.DIALOG.SELECTED');
});
it('should update the counter when an option is selcted and unselected', async () => {
const firstAspectCheckbox: HTMLInputElement = fixture.nativeElement.querySelector('#aspect-list-0-check-input');
expect(firstAspectCheckbox).toBeDefined();
expect(firstAspectCheckbox).not.toBeNull();
let selectionCounter = fixture.nativeElement.querySelector('#aspect-list-dialog-counter');
expect(selectionCounter).not.toBeNull();
expect(selectionCounter.innerText).toBe('0 ADF-ASPECT-LIST.DIALOG.SELECTED');
firstAspectCheckbox.click();
fixture.detectChanges();
await fixture.whenStable();
selectionCounter = fixture.nativeElement.querySelector('#aspect-list-dialog-counter');
expect(selectionCounter).not.toBeNull();
expect(selectionCounter.innerText).toBe('1 ADF-ASPECT-LIST.DIALOG.SELECTED');
firstAspectCheckbox.click();
fixture.detectChanges();
await fixture.whenStable();
selectionCounter = fixture.nativeElement.querySelector('#aspect-list-dialog-counter');
expect(selectionCounter).not.toBeNull();
expect(selectionCounter.innerText).toBe('0 ADF-ASPECT-LIST.DIALOG.SELECTED');
});
it('should reset to the node values when Reset button is clicked', async () => {
let firstAspectCheckbox: HTMLInputElement = fixture.nativeElement.querySelector('#aspect-list-0-check-input');
expect(firstAspectCheckbox).toBeDefined();
expect(firstAspectCheckbox).not.toBeNull();
firstAspectCheckbox.click();
fixture.detectChanges();
await fixture.whenStable();
const resetButton: HTMLButtonElement = fixture.nativeElement.querySelector('#aspect-list-dialog-actions-reset');
expect(resetButton).toBeDefined();
expect(firstAspectCheckbox.checked).toBeTruthy();
resetButton.click();
fixture.detectChanges();
await fixture.whenStable();
firstAspectCheckbox = fixture.nativeElement.querySelector('#aspect-list-0-check-input');
expect(firstAspectCheckbox.checked).toBeFalsy();
});
it('should clear all the value when Clear button is clicked', async () => {
let firstAspectCheckbox: HTMLInputElement = fixture.nativeElement.querySelector('#aspect-list-0-check-input');
expect(firstAspectCheckbox).toBeDefined();
expect(firstAspectCheckbox).not.toBeNull();
firstAspectCheckbox.click();
fixture.detectChanges();
await fixture.whenStable();
const clearButton: HTMLButtonElement = fixture.nativeElement.querySelector('#aspect-list-dialog-actions-clear');
expect(clearButton).toBeDefined();
expect(firstAspectCheckbox.checked).toBeTruthy();
clearButton.click();
fixture.detectChanges();
await fixture.whenStable();
firstAspectCheckbox = fixture.nativeElement.querySelector('#aspect-list-0-check-input');
expect(firstAspectCheckbox.checked).toBeFalsy();
});
it('should complete the select stream Cancel button is clicked', (done) => {
data.select.subscribe(() => { }, () => { }, () => done());
const cancelButton: HTMLButtonElement = fixture.nativeElement.querySelector('#aspect-list-dialog-actions-cancel');
expect(cancelButton).toBeDefined();
cancelButton.click();
fixture.detectChanges();
});
});
describe('Passing the node id', () => {
beforeEach(async () => {
data = <AspectListDialogComponentData> {
title: 'Title',
description: 'Description that can be longer or shorter',
overTableMessage: 'Over here',
select: new Subject<string[]>(),
nodeId: 'fake-node-id'
};
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
ContentTestingModule,
MatDialogModule
],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
{
provide: MatDialogRef,
useValue: {
close: jasmine.createSpy('close'),
keydownEvents: () => of(null),
backdropClick: () => of(null)
}
}
]
});
await TestBed.compileComponents();
});
beforeEach(async () => {
aspectListService = TestBed.inject(AspectListService);
nodeService = TestBed.inject(NodesApiService);
spyOn(aspectListService, 'getAspects').and.returnValue(of(aspectListMock));
spyOn(aspectListService, 'getVisibleAspects').and.returnValue(['frs:AspectOne']);
spyOn(nodeService, 'getNode').and.returnValue(of({ id: 'fake-node-id', aspectNames: ['frs:AspectOne'] }).pipe(delay(0)));
fixture = TestBed.createComponent(AspectListDialogComponent);
fixture.componentInstance.data.select = new Subject<string[]>();
fixture.detectChanges();
await fixture.whenStable();
});
afterEach(() => {
fixture.destroy();
});
it('should show checked the current aspects of the node', async () => {
fixture.detectChanges();
await fixture.whenRenderingDone();
const firstAspectCheckbox: HTMLInputElement = fixture.nativeElement.querySelector('#aspect-list-0-check-input');
expect(firstAspectCheckbox).toBeDefined();
expect(firstAspectCheckbox).not.toBeNull();
expect(firstAspectCheckbox.checked).toBeTruthy();
});
});
});

View File

@@ -0,0 +1,66 @@
/*!
* @license
* Copyright 2019 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 { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AspectListDialogComponentData } from './aspect-list-dialog-data.interface';
@Component({
selector: 'adf-aspect-list-dialog',
templateUrl: './aspect-list-dialog.component.html',
styleUrls: ['./aspect-list-dialog.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AspectListDialogComponent implements OnInit {
title: string;
description: string;
currentNodeId: string;
overTableMessage: string;
currentAspectSelection: string[] = [];
constructor(private dialog: MatDialogRef<AspectListDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: AspectListDialogComponentData) {
this.title = data.title;
this.description = data.description;
this.overTableMessage = data.overTableMessage;
this.currentNodeId = data.nodeId;
}
ngOnInit() {
this.dialog.backdropClick().subscribe(() => {
this.close();
});
}
onValueChanged(aspectList: string[]) {
this.currentAspectSelection = aspectList;
}
close() {
this.data.select.complete();
}
onCancel() {
this.close();
}
onApply() {
this.data.select.next(this.currentAspectSelection);
this.close();
}
}

View File

@@ -0,0 +1,38 @@
<div id="aspect-list-container" class="adf-aspect-list-container">
<mat-accordion class="adf-accordion-aspect-list">
<mat-expansion-panel *ngFor="let aspect of (aspects$ | async); let colIndex = index" [id]="'aspect-list-'+aspect?.entry?.title">
<mat-expansion-panel-header [id]="'aspect-list-'+aspect?.entry?.title+'header'">
<mat-panel-title>
<mat-checkbox class="adf-aspect-list-check-button" [id]="'aspect-list-'+colIndex+'-check'"
[checked]="nodeAspects?.includes(aspect?.entry?.id)"
(click)="onCheckBoxClick($event)"
(change)="onChange($event, aspect?.entry?.id)">
</mat-checkbox>
<p class="adf-aspect-list-element-title">{{aspect?.entry?.title}}</p>
</mat-panel-title>
<mat-panel-description [id]="'aspect-list-'+colIndex+'-title'"
[matTooltip]="aspect?.entry?.title">
{{aspect?.entry?.title}}
</mat-panel-description>
</mat-expansion-panel-header>
<p class="adf-property-paragraph" [id]="'aspect-list-'+colIndex+'-description'"> {{aspect?.entry?.description}}</p>
<table mat-table [dataSource]="aspect?.entry?.properties" *ngIf="aspect?.entry?.properties?.length > 0" class="adf-aspect-property-table" [id]="'aspect-list-'+colIndex+'-properties-table'">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{'ADF-ASPECT-LIST.PROPERTY_NAME' | translate}} </th>
<td mat-cell *matCellDef="let property"> {{property.id}} </td>
</ng-container>
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef> {{'ADF-ASPECT-LIST.DESCRIPTION' | translate}} </th>
<td mat-cell *matCellDef="let property"> {{property.title}} </td>
</ng-container>
<ng-container matColumnDef="dataType">
<th mat-header-cell *matHeaderCellDef> {{'ADF-ASPECT-LIST.DATA_TYPE' | translate}} </th>
<td mat-cell *matCellDef="let property"> {{property.dataType}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="propertyColumns"></tr>
<tr mat-row *matRowDef="let row; columns: propertyColumns;"></tr>
</table>
</mat-expansion-panel>
</mat-accordion>
</div>

View File

@@ -0,0 +1,72 @@
@mixin adf-aspect-list-theme($theme) {
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
.adf {
&-aspect-list-container {
padding-top: 3px;
max-height: 400px;
overflow: auto;
.adf-aspect-list-check-button {
margin-right: 5px;
align-items: center;
display: flex;
}
.adf-aspect-list-element-title {
display: flex;
align-items: center;
}
.adf-accordion-aspect-list {
.mat-expansion-panel-spacing {
margin: 0;
}
.mat-expansion-panel-header {
font-size: smaller;
border-right-style: inset;
border-left-style: outset;
}
.mat-expansion-panel-header-title {
flex: 1 1 0;
}
.mat-expansion-panel-header-description {
justify-content: flex-start;
align-items: center;
flex: 1 1 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
&-aspect-property-table {
width: 100%;
.mat-column-name {
width: 15%;
}
.mat-column-description {
width: 65%;
}
.mat-column-type {
width: 20%;
padding-left: 10px;
}
}
}
}

View File

@@ -0,0 +1,221 @@
/*!
* @license
* Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { NodesApiService, setupTestBed } from '@alfresco/adf-core';
import { ContentTestingModule } from '../testing/content.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { AspectListComponent } from './aspect-list.component';
import { AspectListService } from './aspect-list.service';
import { of } from 'rxjs';
import { AspectEntry } from '@alfresco/js-api';
const aspectListMock: AspectEntry[] = [{
entry: {
parentId: 'frs:aspectZero',
id: 'frs:AspectOne',
description: 'First Aspect with random description',
title: 'FirstAspect',
properties: [
{
id: 'channelPassword',
title: 'The authenticated channel password',
dataType: 'd:propA'
},
{
id: 'channelUsername',
title: 'The authenticated channel username',
dataType: 'd:propB'
}
]
}
},
{
entry: {
parentId: 'frs:AspectZer',
id: 'frs:SecondAspect',
description: 'Second Aspect description',
title: 'SecondAspect',
properties: [
{
id: 'assetId',
title: 'Published Asset Id',
dataType: 'd:text'
},
{
id: 'assetUrl',
title: 'Published Asset URL',
dataType: 'd:text'
}
]
}
}];
describe('AspectListComponent', () => {
let component: AspectListComponent;
let fixture: ComponentFixture<AspectListComponent>;
let aspectListService: AspectListService;
let nodeService: NodesApiService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
ContentTestingModule
],
providers: [AspectListService]
});
describe('When passing a node id', () => {
beforeEach(() => {
fixture = TestBed.createComponent(AspectListComponent);
component = fixture.componentInstance;
aspectListService = TestBed.inject(AspectListService);
spyOn(aspectListService, 'getAspects').and.returnValue(of(aspectListMock));
spyOn(aspectListService, 'getVisibleAspects').and.returnValue(['frs:AspectOne']);
nodeService = TestBed.inject(NodesApiService);
spyOn(nodeService, 'getNode').and.returnValue(of({ id: 'fake-node-id', aspectNames: ['frs:AspectOne'] }));
component.nodeId = 'fake-node-id';
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
it('should show all the aspects', () => {
const firstElement = fixture.nativeElement.querySelector('#aspect-list-FirstAspect');
const secondElement = fixture.nativeElement.querySelector('#aspect-list-SecondAspect');
expect(firstElement).not.toBeNull();
expect(firstElement).toBeDefined();
expect(secondElement).not.toBeNull();
expect(secondElement).toBeDefined();
});
it('should show the details when a row is clicked', () => {
const firstElement = fixture.nativeElement.querySelector('#aspect-list-FirstAspect');
firstElement.click();
fixture.detectChanges();
const firstElementDesc = fixture.nativeElement.querySelector('#aspect-list-0-description');
expect(firstElementDesc).not.toBeNull();
expect(firstElementDesc).toBeDefined();
const firstElementPropertyTable = fixture.nativeElement.querySelector('#aspect-list-0-properties-table');
expect(firstElementPropertyTable).not.toBeNull();
expect(firstElementPropertyTable).toBeDefined();
const nameProperties = fixture.nativeElement.querySelectorAll('#aspect-list-0-properties-table tbody .mat-column-name');
expect(nameProperties[0]).not.toBeNull();
expect(nameProperties[0]).toBeDefined();
expect(nameProperties[0].innerText).toBe('channelPassword');
expect(nameProperties[1]).not.toBeNull();
expect(nameProperties[1]).toBeDefined();
expect(nameProperties[1].innerText).toBe('channelUsername');
const titleProperties = fixture.nativeElement.querySelectorAll('#aspect-list-0-properties-table tbody .mat-column-title');
expect(titleProperties[0]).not.toBeNull();
expect(titleProperties[0]).toBeDefined();
expect(titleProperties[0].innerText).toBe('The authenticated channel password');
expect(titleProperties[1]).not.toBeNull();
expect(titleProperties[1]).toBeDefined();
expect(titleProperties[1].innerText).toBe('The authenticated channel username');
const dataTypeProperties = fixture.nativeElement.querySelectorAll('#aspect-list-0-properties-table tbody .mat-column-dataType');
expect(dataTypeProperties[0]).not.toBeNull();
expect(dataTypeProperties[0]).toBeDefined();
expect(dataTypeProperties[0].innerText).toBe('d:propA');
expect(dataTypeProperties[1]).not.toBeNull();
expect(dataTypeProperties[1]).toBeDefined();
expect(dataTypeProperties[1].innerText).toBe('d:propB');
});
it('should show as checked the node properties', () => {
const firstAspectCheckbox: HTMLInputElement = fixture.nativeElement.querySelector('#aspect-list-0-check-input');
expect(firstAspectCheckbox).toBeDefined();
expect(firstAspectCheckbox).not.toBeNull();
expect(firstAspectCheckbox.checked).toBeTruthy();
});
it('should remove aspects unchecked', (done) => {
const secondElement = fixture.nativeElement.querySelector('#aspect-list-1-check-input');
expect(secondElement).toBeDefined();
expect(secondElement).not.toBeNull();
expect(secondElement.checked).toBeFalsy();
secondElement.click();
fixture.detectChanges();
expect(component.nodeAspects.length).toBe(2);
expect(component.nodeAspects[1]).toBe('frs:SecondAspect');
component.valueChanged.subscribe((aspects) => {
expect(aspects.length).toBe(1);
expect(aspects[0]).toBe('frs:AspectOne');
done();
});
secondElement.click();
fixture.detectChanges();
});
it('should reset the properties on reset', (done) => {
const secondElement = fixture.nativeElement.querySelector('#aspect-list-1-check-input');
expect(secondElement).toBeDefined();
expect(secondElement).not.toBeNull();
expect(secondElement.checked).toBeFalsy();
secondElement.click();
fixture.detectChanges();
expect(component.nodeAspects.length).toBe(2);
component.valueChanged.subscribe((aspects) => {
expect(aspects.length).toBe(1);
done();
});
component.reset();
});
it('should clear all the properties on clear', (done) => {
expect(component.nodeAspects.length).toBe(1);
component.valueChanged.subscribe((aspects) => {
expect(aspects.length).toBe(0);
done();
});
component.clear();
});
});
describe('When no node id is passed', () => {
beforeEach(() => {
fixture = TestBed.createComponent(AspectListComponent);
component = fixture.componentInstance;
aspectListService = TestBed.inject(AspectListService);
spyOn(aspectListService, 'getAspects').and.returnValue(of(aspectListMock));
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
it('should show all the aspects', () => {
const firstElement = fixture.nativeElement.querySelector('#aspect-list-FirstAspect');
const secondElement = fixture.nativeElement.querySelector('#aspect-list-SecondAspect');
expect(firstElement).not.toBeNull();
expect(firstElement).toBeDefined();
expect(secondElement).not.toBeNull();
expect(secondElement).toBeDefined();
});
});
});

View File

@@ -0,0 +1,99 @@
/*!
* @license
* Copyright 2019 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 { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { NodesApiService } from '@alfresco/adf-core';
import { Observable, Subject } from 'rxjs';
import { concatMap, takeUntil, tap } from 'rxjs/operators';
import { AspectListService } from './aspect-list.service';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { AspectEntry } from '@alfresco/js-api';
@Component({
selector: 'adf-aspect-list',
templateUrl: './aspect-list.component.html',
styleUrls: ['./aspect-list.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AspectListComponent implements OnInit, OnDestroy {
/** Node Id of the node that we want to update */
@Input()
nodeId: string = '';
/** Emitted every time the user select a new aspect */
@Output()
valueChanged: EventEmitter<string[]> = new EventEmitter<string[]>();
propertyColumns: string[] = ['name', 'title', 'dataType'];
aspects$: Observable<AspectEntry[]> = null;
nodeAspects: string[] = [];
nodeAspectStatus: string[] = null;
private onDestroy$ = new Subject<boolean>();
constructor(private aspectListService: AspectListService, private nodeApiService: NodesApiService) {
}
ngOnDestroy(): void {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
ngOnInit(): void {
if (this.nodeId) {
this.aspects$ = this.nodeApiService.getNode(this.nodeId).pipe(
tap((node) => {
this.nodeAspects = node.aspectNames.filter((aspect) => this.aspectListService.getVisibleAspects().includes(aspect));
this.nodeAspectStatus = Array.from(node.aspectNames);
this.valueChanged.emit(this.nodeAspects);
}),
concatMap(() => this.aspectListService.getAspects()),
takeUntil(this.onDestroy$));
} else {
this.aspects$ = this.aspectListService.getAspects()
.pipe(takeUntil(this.onDestroy$));
}
}
onCheckBoxClick(event: Event) {
event.stopImmediatePropagation();
}
onChange(change: MatCheckboxChange, prefixedName: string) {
if (change.checked) {
this.nodeAspects.push(prefixedName);
} else {
this.nodeAspects.splice(this.nodeAspects.indexOf(prefixedName), 1);
}
this.valueChanged.emit(this.nodeAspects);
}
reset() {
if (this.nodeAspectStatus && this.nodeAspectStatus.length > 0) {
this.nodeAspects.splice(0, this.nodeAspects.length, ...this.nodeAspectStatus);
this.valueChanged.emit(this.nodeAspects);
} else {
this.clear();
}
}
clear() {
this.nodeAspects = [];
this.valueChanged.emit(this.nodeAspects);
}
}

View File

@@ -0,0 +1,52 @@
/*!
* @license
* Copyright 2019 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 { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { AspectListComponent } from './aspect-list.component';
import { MatTableModule } from '@angular/material/table';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { PipeModule } from '@alfresco/adf-core';
import { TranslateModule } from '@ngx-translate/core';
import { MatDialogModule } from '@angular/material/dialog';
import { AspectListDialogComponent } from './aspect-list-dialog.component';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
@NgModule({
imports: [
CommonModule,
MatTableModule,
MatExpansionModule,
MatCheckboxModule,
PipeModule,
TranslateModule,
MatDialogModule,
MatButtonModule,
MatTooltipModule
],
exports: [
AspectListComponent,
AspectListDialogComponent
],
declarations: [
AspectListComponent,
AspectListDialogComponent
]
})
export class AspectListModule { }

View File

@@ -0,0 +1,166 @@
/*!
* @license
* Copyright 2019 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 { AspectEntry, AspectPaging } from '@alfresco/js-api';
import { async, TestBed } from '@angular/core/testing';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
import { AlfrescoApiService, AppConfigService, setupTestBed } from 'core';
import { of, Subject } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
import { AspectListService } from './aspect-list.service';
const aspectListMock: AspectEntry[] = [{
entry: {
parentId: 'frs:aspectZero',
id: 'frs:AspectOne',
description: 'First Aspect with random description',
title: 'FirstAspect',
properties: [
{
id: 'channelPassword',
title: 'The authenticated channel password',
dataType: 'd:propA'
},
{
id: 'channelUsername',
title: 'The authenticated channel username',
dataType: 'd:propB'
}
]
}
},
{
entry: {
parentId: 'frs:AspectZer',
id: 'frs:SecondAspect',
description: 'Second Aspect description',
title: 'SecondAspect',
properties: [
{
id: 'assetId',
title: 'Published Asset Id',
dataType: 'd:text'
},
{
id: 'assetUrl',
title: 'Published Asset URL',
dataType: 'd:text'
}
]
}
}];
const customAspectListMock: AspectEntry[] = [{
entry: {
parentId: 'frs:aspectZero',
id: 'frs:AspectCustom',
description: 'First Aspect with random description',
title: 'FirstAspect',
properties: [
{
id: 'channelPassword',
title: 'The authenticated channel password',
dataType: 'd:propA'
},
{
id: 'channelUsername',
title: 'The authenticated channel username',
dataType: 'd:propB'
}
]
}
}];
const listAspectResp: AspectPaging = {
list : {
entries: aspectListMock
}
};
const customListAspectResp: AspectPaging = {
list : {
entries: customAspectListMock
}
};
describe('AspectListService', () => {
describe('should open the dialog', () => {
let service: AspectListService;
let materialDialog: MatDialog;
let spyOnDialogOpen: jasmine.Spy;
let spyOnDialogClose: jasmine.Spy;
const afterOpenObservable: Subject<any> = new Subject();
setupTestBed({
imports: [
TranslateModule.forRoot(),
ContentTestingModule,
MatDialogModule
]
});
beforeEach(() => {
service = TestBed.inject(AspectListService);
materialDialog = TestBed.inject(MatDialog);
spyOnDialogOpen = spyOn(materialDialog, 'open').and.returnValue({
afterOpen: () => afterOpenObservable,
afterClosed: () => of({}),
componentInstance: {
error: new Subject<any>()
}
});
spyOnDialogClose = spyOn(materialDialog, 'closeAll');
});
it('should open the aspect list dialog', () => {
service.openAspectListDialog();
expect(spyOnDialogOpen).toHaveBeenCalled();
});
it('should close the dialog', () => {
service.close();
expect(spyOnDialogClose).toHaveBeenCalled();
});
});
describe('should fetch the list of the aspects', () => {
let service: AspectListService;
const appConfigService: AppConfigService = new AppConfigService(null);
const aspectTypesApi = jasmine.createSpyObj('AspectsApi', ['listAspects']);
const apiService: AlfrescoApiService = new AlfrescoApiService(null, null);
beforeEach(() => {
spyOn(appConfigService, 'get').and.returnValue({ 'default': ['frs:AspectOne'] });
spyOnProperty(apiService, 'aspectsApi').and.returnValue(aspectTypesApi);
service = new AspectListService(apiService, appConfigService, null);
});
it('should get the list of only available aspects', async(() => {
aspectTypesApi.listAspects.and.returnValues(of(listAspectResp), of(customListAspectResp));
service.getAspects().subscribe((list) => {
expect(list.length).toBe(2);
expect(list[0].entry.id).toBe('frs:AspectOne');
expect(list[1].entry.id).toBe('frs:AspectCustom');
});
}));
});
});

View File

@@ -0,0 +1,112 @@
/*!
* @license
* Copyright 2019 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 { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
import { from, Observable, Subject, zip } from 'rxjs';
import { AspectListDialogComponentData } from './aspect-list-dialog-data.interface';
import { AspectListDialogComponent } from './aspect-list-dialog.component';
import { map } from 'rxjs/operators';
import { AspectEntry, AspectPaging } from '@alfresco/js-api';
@Injectable({
providedIn: 'root'
})
export class AspectListService {
constructor(private alfrescoApiService: AlfrescoApiService,
private appConfigService: AppConfigService, private dialog: MatDialog) {
}
getAspects(): Observable<AspectEntry[]> {
const visibleAspectList = this.getVisibleAspects();
const standardAspects$ = this.getStandardAspects(visibleAspectList);
const customAspects$ = this.getCustomAspects();
return zip(standardAspects$, customAspects$).pipe(
map(([standardAspectList, customAspectList]) => [...standardAspectList, ...customAspectList])
);
}
getStandardAspects(whiteList: string[]): Observable<AspectEntry[]> {
const where = `(modelIds in ('cm:contentmodel', 'emailserver:emailserverModel', 'smf:smartFolder', 'app:applicationmodel' ))`;
return from(this.alfrescoApiService.aspectsApi.listAspects(where))
.pipe(
map((result: AspectPaging) => this.filterAspectByConfig(whiteList, result?.list?.entries))
);
}
getCustomAspects(): Observable<AspectEntry[]> {
const where = `(not namespaceUri matches('http://www.alfresco.*')`;
return from(this.alfrescoApiService.aspectsApi.listAspects(where))
.pipe(
map((result: AspectPaging) => result?.list?.entries)
);
}
private filterAspectByConfig(visibleAspectList: string[], aspectEntries: AspectEntry[]): AspectEntry[] {
let result = aspectEntries ? aspectEntries : [];
if (visibleAspectList?.length > 0 && aspectEntries) {
result = aspectEntries.filter((value) => {
return visibleAspectList.includes(value?.entry?.id);
});
}
return result;
}
getVisibleAspects(): string[] {
let visibleAspectList: string[] = [];
const aspectVisibleConfig = this.appConfigService.get('aspect-visible');
if (aspectVisibleConfig) {
for (const aspectGroup of Object.keys(aspectVisibleConfig)) {
visibleAspectList = visibleAspectList.concat(aspectVisibleConfig[aspectGroup]);
}
}
return visibleAspectList;
}
openAspectListDialog(nodeId?: string): Observable<string[]> {
const select = new Subject<string[]>();
select.subscribe({
complete: this.close.bind(this)
});
const data: AspectListDialogComponentData = {
title: 'ADF-ASPECT-LIST.DIALOG.TITLE',
description: 'ADF-ASPECT-LIST.DIALOG.DESCRIPTION',
overTableMessage: 'ADF-ASPECT-LIST.DIALOG.OVER-TABLE-MESSAGE',
select,
nodeId
};
this.openDialog(data, 'adf-aspect-list-dialog', '750px');
return select;
}
private openDialog(data: AspectListDialogComponentData, panelClass: string, width: string) {
this.dialog.open(AspectListDialogComponent, {
data,
panelClass,
width,
disableClose: true
});
}
close() {
this.dialog.closeAll();
}
}

View File

@@ -0,0 +1,18 @@
/*!
* @license
* Copyright 2019 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.
*/
export * from './public-api';

View File

@@ -0,0 +1,74 @@
/*!
* @license
* Copyright 2019 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 { TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { AlfrescoApiService, NodesApiService, setupTestBed } from 'core';
import { of } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
import { AspectListService } from './aspect-list.service';
import { NodeAspectService } from './node-aspect.service';
describe('NodeAspectService', () => {
let aspectListService: AspectListService;
let nodeAspectService: NodeAspectService;
let nodeApiService: NodesApiService;
let alfrescoApiService: AlfrescoApiService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
ContentTestingModule
]
});
beforeEach(() => {
aspectListService = TestBed.inject(AspectListService);
nodeAspectService = TestBed.inject(NodeAspectService);
nodeApiService = TestBed.inject(NodesApiService);
alfrescoApiService = TestBed.inject(AlfrescoApiService);
});
it('should open the aspect list dialog', () => {
spyOn(aspectListService, 'openAspectListDialog').and.returnValue(of([]));
spyOn(nodeApiService, 'updateNode').and.returnValue(of({}));
nodeAspectService.updateNodeAspects('fake-node-id');
expect(aspectListService.openAspectListDialog).toHaveBeenCalledWith('fake-node-id');
});
it('should update the node when the aspect dialog apply the changes', () => {
const expectedParameters = { aspectNames: ['a', 'b', 'c'] };
spyOn(aspectListService, 'openAspectListDialog').and.returnValue(of(['a', 'b', 'c']));
spyOn(nodeApiService, 'updateNode').and.returnValue(of({}));
nodeAspectService.updateNodeAspects('fake-node-id');
expect(nodeApiService.updateNode).toHaveBeenCalledWith('fake-node-id', expectedParameters);
});
it('should send and update node event once the node has been updated', (done) => {
alfrescoApiService.nodeUpdated.subscribe((nodeUpdated) => {
expect(nodeUpdated.id).toBe('fake-node-id');
expect(nodeUpdated.aspectNames).toEqual(['a', 'b', 'c']);
done();
});
const fakeNode = { id: 'fake-node-id', aspectNames: ['a', 'b', 'c'] };
spyOn(aspectListService, 'openAspectListDialog').and.returnValue(of(['a', 'b', 'c']));
spyOn(nodeApiService, 'updateNode').and.returnValue(of(fakeNode));
nodeAspectService.updateNodeAspects('fake-node-id');
});
});

View File

@@ -0,0 +1,39 @@
/*!
* @license
* Copyright 2019 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 { Injectable } from '@angular/core';
import { AlfrescoApiService, NodesApiService } from '@alfresco/adf-core';
import { AspectListService } from './aspect-list.service';
@Injectable({
providedIn: 'root'
})
export class NodeAspectService {
constructor(private alfrescoApiService: AlfrescoApiService,
private nodesApiService: NodesApiService,
private aspectListService: AspectListService) {
}
updateNodeAspects(nodeId: string) {
this.aspectListService.openAspectListDialog(nodeId).subscribe((aspectList) => {
this.nodesApiService.updateNode(nodeId, { aspectNames: [...aspectList] }).subscribe((updatedNode) => {
this.alfrescoApiService.nodeUpdated.next(updatedNode);
});
});
}
}

View File

@@ -0,0 +1,25 @@
/*!
* @license
* Copyright 2019 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.
*/
export * from './aspect-list.component';
export * from './aspect-list-dialog.component';
export * from './aspect-list.service';
export * from './node-aspect.service';
export * from './aspect-list-dialog-data.interface';
export * from './aspect-list.module';

View File

@@ -13,6 +13,14 @@
</mat-card-content>
<mat-card-footer class="adf-content-metadata-card-footer" fxLayout="row" fxLayoutAlign="space-between stretch">
<div>
<button *ngIf="!readOnly && hasAllowableOperations()"
mat-icon-button
(click)="openAspectDialog()"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT_ASPECTS' | translate"
[attr.aria-label]="'CORE.METADATA.ACCESSIBILITY.EDIT_ASPECTS' | translate"
data-automation-id="meta-data-card-edit-aspect">
<mat-icon>menu</mat-icon>
</button>
<button *ngIf="!readOnly && hasAllowableOperations()"
mat-icon-button
(click)="toggleEdit()"

View File

@@ -24,6 +24,7 @@ import { setupTestBed, AllowableOperationsEnum } from '@alfresco/adf-core';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { SimpleChange } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { NodeAspectService } from 'content-services/src/lib/aspect-list';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { of } from 'rxjs';
@@ -34,6 +35,7 @@ describe('ContentMetadataCardComponent', () => {
let contentMetadataService: ContentMetadataService;
let node: Node;
const preset = 'custom-preset';
let nodeAspectService: NodeAspectService = null;
setupTestBed({
imports: [
@@ -57,6 +59,7 @@ describe('ContentMetadataCardComponent', () => {
component.node = node;
component.preset = preset;
nodeAspectService = TestBed.inject(NodeAspectService);
spyOn(contentMetadataService, 'getContentTypeProperty').and.returnValue(of([]));
fixture.detectChanges();
});
@@ -211,4 +214,18 @@ describe('ContentMetadataCardComponent', () => {
component.ngOnChanges({ displayAspect });
expect(component.expanded).toBeTruthy();
});
it('should call the aspect dialog when edit aspect is clicked', () => {
component.editable = true;
component.node.id = 'fake-node-id';
component.node.allowableOperations = [AllowableOperationsEnum.UPDATE];
spyOn(nodeAspectService, 'updateNodeAspects').and.stub();
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('[data-automation-id="meta-data-card-edit-aspect"]'));
button.triggerEventHandler('click', {});
fixture.detectChanges();
expect(nodeAspectService.updateNodeAspects).toHaveBeenCalledWith('fake-node-id');
});
});

View File

@@ -18,7 +18,7 @@
import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Node } from '@alfresco/js-api';
import { ContentService, AllowableOperationsEnum } from '@alfresco/adf-core';
import { NodeAspectService } from '../../../aspect-list/node-aspect.service';
@Component({
selector: 'adf-content-metadata-card',
templateUrl: './content-metadata-card.component.html',
@@ -27,6 +27,7 @@ import { ContentService, AllowableOperationsEnum } from '@alfresco/adf-core';
host: { 'class': 'adf-content-metadata-card' }
})
export class ContentMetadataCardComponent implements OnChanges {
/** (required) The node entity to fetch metadata about */
@Input()
node: Node;
@@ -80,7 +81,7 @@ export class ContentMetadataCardComponent implements OnChanges {
expanded: boolean;
constructor(private contentService: ContentService) {
constructor(private contentService: ContentService, private nodeAspectService: NodeAspectService) {
}
ngOnChanges(changes: SimpleChanges): void {
@@ -104,4 +105,8 @@ export class ContentMetadataCardComponent implements OnChanges {
hasAllowableOperations() {
return this.contentService.hasAllowableOperations(this.node, AllowableOperationsEnum.UPDATE);
}
openAspectDialog() {
this.nodeAspectService.updateNodeAspects(this.node.id);
}
}

View File

@@ -40,6 +40,7 @@ import { ContentMetadataModule } from './content-metadata/content-metadata.modul
import { PermissionManagerModule } from './permission-manager/permission-manager.module';
import { TreeViewModule } from './tree-view/tree-view.module';
import { ContentTypeModule } from './content-type/content-type.module';
import { AspectListModule } from './aspect-list/aspect-list.module';
@NgModule({
imports: [
@@ -65,7 +66,8 @@ import { ContentTypeModule } from './content-type/content-type.module';
PermissionManagerModule,
VersionManagerModule,
TreeViewModule,
ContentTypeModule
ContentTypeModule,
AspectListModule
],
providers: [
{
@@ -95,6 +97,7 @@ import { ContentTypeModule } from './content-type/content-type.module';
PermissionManagerModule,
VersionManagerModule,
TreeViewModule,
AspectListModule,
ContentTypeModule
]
})

View File

@@ -459,5 +459,20 @@
"ACCESSIBILITY": {
"ARIA_LABEL": "Open {{ name }}"
}
},
"ADF-ASPECT-LIST" : {
"PROPERTY_NAME": "Property Name",
"DESCRIPTION": "Description",
"DATA_TYPE": "Data Type",
"DIALOG" : {
"TITLE" : "Customize Properties",
"DESCRIPTION": "Manage the properties associated with selected file(s). Choose from property aspects listed below, to expose and apply additional metadata and funcitonality",
"RESET": "Reset",
"CLEAR": "Clear",
"CANCEL": "Cancel",
"APPLY": "Apply",
"OVER-TABLE-MESSAGE" : "Select property aspects",
"SELECTED": "Selected"
}
}
}

View File

@@ -27,6 +27,8 @@
@import '../tree-view/components/tree-view.component';
@import '../version-manager/version-comparison.component';
@import '../content-type/content-type-dialog.component';
@import '../aspect-list/aspect-list.component';
@import '../aspect-list//aspect-list-dialog.component';
@mixin adf-content-services-theme($theme) {
@include adf-breadcrumb-theme($theme);
@@ -54,4 +56,6 @@
@include adf-search-chip-list-theme($theme);
@include adf-version-comparison-theme($theme);
@include adf-content-type-dialog-theme($theme);
@include adf-aspect-list-theme($theme);
@include adf-aspect-list-dialog-theme($theme);
}

View File

@@ -33,6 +33,7 @@ export * from './lib/permission-manager/index';
export * from './lib/content-node-share/index';
export * from './lib/tree-view/index';
export * from './lib/group/index';
export * from './lib/aspect-list/index';
export * from './lib/content-type/index';
export * from './lib/content.module';

View File

@@ -60,7 +60,6 @@ import { DirectionalityConfigService } from './services/directionality-config.se
import { SearchTextModule } from './search-text/search-text-input.module';
import { versionCompatibilityFactory } from './services/version-compatibility-factory';
import { VersionCompatibilityService } from './services/version-compatibility.service';
@NgModule({
imports: [
TranslateModule,

View File

@@ -223,12 +223,14 @@
"CANCEL": "Cancel",
"CLEAR": "Clear",
"TOGGLE": "Toggle value",
"COPY_TO_CLIPBOARD": "Double click to copy value"
"COPY_TO_CLIPBOARD": "Double click to copy value",
"EDIT_ASPECTS": "Aspect Edit"
},
"ACCESSIBILITY": {
"EDIT": "Edit button",
"DATEPICKER": "Use the arrow keys to navigate between dates. Up and down move to the next or previous week but on the same day. Left and right move to the next or previous day. Press Enter or Return to select a date.",
"COPY_TO_CLIPBOARD_MESSAGE": "Value copied to clipboard"
"COPY_TO_CLIPBOARD_MESSAGE": "Value copied to clipboard",
"EDIT_ASPECTS": "Aspect Edit"
}
},
"SEARCH": {

View File

@@ -23,7 +23,7 @@ import {
SearchApi,
Node,
GroupsApi,
AlfrescoApiCompatibility, AlfrescoApiConfig, TypesApi
AlfrescoApiCompatibility, AlfrescoApiConfig, AspectsApi, TypesApi
} from '@alfresco/js-api';
import { AppConfigService, AppConfigValues } from '../app-config/app-config.service';
import { Subject, Observable, BehaviorSubject } from 'rxjs';
@@ -102,6 +102,10 @@ export class AlfrescoApiService {
return new GroupsApi(this.getInstance());
}
get aspectsApi(): AspectsApi {
return new AspectsApi(this.getInstance());
}
get typesApi(): TypesApi {
return new TypesApi(this.getInstance());
}