diff --git a/demo-shell/resources/i18n/en.json b/demo-shell/resources/i18n/en.json
index 309aec7de9..37a95b6ae3 100644
--- a/demo-shell/resources/i18n/en.json
+++ b/demo-shell/resources/i18n/en.json
@@ -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",
diff --git a/demo-shell/src/app.config.json b/demo-shell/src/app.config.json
index 26de275cf1..bec0c48f06 100644
--- a/demo-shell/src/app.config.json
+++ b/demo-shell/src/app.config.json
@@ -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"]
+ }
}
diff --git a/demo-shell/src/app/app.module.ts b/demo-shell/src/app/app.module.ts
index d6f92cb91d..e5124a69bf 100644
--- a/demo-shell/src/app/app.module.ts
+++ b/demo-shell/src/app/app.module.ts
@@ -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,
diff --git a/demo-shell/src/app/app.routes.ts b/demo-shell/src/app/app.routes.ts
index dddb772de6..c7e5caff1e 100644
--- a/demo-shell/src/app/app.routes.ts
+++ b/demo-shell/src/app/app.routes.ts
@@ -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)
diff --git a/demo-shell/src/app/components/app-layout/app-layout.component.ts b/demo-shell/src/app/components/app-layout/app-layout.component.ts
index 76faedde67..4084b4ce08 100644
--- a/demo-shell/src/app/components/app-layout/app-layout.component.ts
+++ b/demo-shell/src/app/components/app-layout/app-layout.component.ts
@@ -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' }
];
diff --git a/demo-shell/src/app/components/aspect-list-sample/aspect-list-sample.component.html b/demo-shell/src/app/components/aspect-list-sample/aspect-list-sample.component.html
new file mode 100644
index 0000000000..d247df3dbf
--- /dev/null
+++ b/demo-shell/src/app/components/aspect-list-sample/aspect-list-sample.component.html
@@ -0,0 +1,18 @@
+
+
+
+ Dialog
+
+
diff --git a/demo-shell/src/app/components/aspect-list-sample/aspect-list-sample.component.scss b/demo-shell/src/app/components/aspect-list-sample/aspect-list-sample.component.scss
new file mode 100644
index 0000000000..6d633f50f3
--- /dev/null
+++ b/demo-shell/src/app/components/aspect-list-sample/aspect-list-sample.component.scss
@@ -0,0 +1,7 @@
+.example-button-container {
+ width: 90%;
+}
+
+.example-almost-full-width {
+ width: 70%;
+}
diff --git a/demo-shell/src/app/components/aspect-list-sample/aspect-list-sample.component.ts b/demo-shell/src/app/components/aspect-list-sample/aspect-list-sample.component.ts
new file mode 100644
index 0000000000..3b8c2b56eb
--- /dev/null
+++ b/demo-shell/src/app/components/aspect-list-sample/aspect-list-sample.component.ts
@@ -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);
+ }
+
+}
diff --git a/demo-shell/src/app/components/files/files.component.html b/demo-shell/src/app/components/files/files.component.html
index 8e9331edbc..0950e42f8d 100644
--- a/demo-shell/src/app/components/files/files.component.html
+++ b/demo-shell/src/app/components/files/files.component.html
@@ -437,6 +437,12 @@
handler="lock"
title="DOCUMENT_LIST.ACTIONS.LOCK">
+
+
diff --git a/demo-shell/src/app/components/files/files.component.ts b/demo-shell/src/app/components/files/files.component.ts
index d94442e35d..36053f38db 100644
--- a/demo-shell/src/app/components/files/files.component.ts
+++ b/demo-shell/src/app/components/files/files.component.ts
@@ -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;
diff --git a/docs/content-services/components/aspect-list-dialog.component.md b/docs/content-services/components/aspect-list-dialog.component.md
new file mode 100644
index 0000000000..0dfbfb8886
--- /dev/null
+++ b/docs/content-services/components/aspect-list-dialog.component.md
@@ -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;
+ 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`](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()
+ };
+
+ 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)
diff --git a/docs/content-services/components/aspect-list.component.md b/docs/content-services/components/aspect-list.component.md
new file mode 100644
index 0000000000..127de32b12
--- /dev/null
+++ b/docs/content-services/components/aspect-list.component.md
@@ -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
+
+
+```
+
+## 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)`` | 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)
diff --git a/lib/content-services/src/lib/aspect-list/aspect-list-dialog-data.interface.ts b/lib/content-services/src/lib/aspect-list/aspect-list-dialog-data.interface.ts
new file mode 100644
index 0000000000..a11cc55355
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/aspect-list-dialog-data.interface.ts
@@ -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;
+ nodeId?: string;
+}
diff --git a/lib/content-services/src/lib/aspect-list/aspect-list-dialog.component.html b/lib/content-services/src/lib/aspect-list/aspect-list-dialog.component.html
new file mode 100644
index 0000000000..54ec904ea1
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/aspect-list-dialog.component.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+ {{
+ 'ADF-ASPECT-LIST.DIALOG.RESET' | translate }}
+
+
+ {{
+ 'ADF-ASPECT-LIST.DIALOG.CLEAR' | translate }}
+
+
+
+ {{
+ 'ADF-ASPECT-LIST.DIALOG.CANCEL' | translate }}
+
+
+ {{
+ 'ADF-ASPECT-LIST.DIALOG.APPLY' | translate }}
+
+
+
diff --git a/lib/content-services/src/lib/aspect-list/aspect-list-dialog.component.scss b/lib/content-services/src/lib/aspect-list/aspect-list-dialog.component.scss
new file mode 100644
index 0000000000..5a012d05b3
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/aspect-list-dialog.component.scss
@@ -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%;
+ }
+ }
+ }
+
+
+ }
+
+
+}
diff --git a/lib/content-services/src/lib/aspect-list/aspect-list-dialog.component.spec.ts b/lib/content-services/src/lib/aspect-list/aspect-list-dialog.component.spec.ts
new file mode 100644
index 0000000000..b09d6e6fe6
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/aspect-list-dialog.component.spec.ts
@@ -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;
+ let aspectListService: AspectListService;
+ let nodeService: NodesApiService;
+ let data: AspectListDialogComponentData;
+
+ describe('Without passing node id', () => {
+
+ beforeEach(async () => {
+ data = {
+ title: 'Title',
+ description: 'Description that can be longer or shorter',
+ overTableMessage: 'Over here',
+ select: new Subject()
+ };
+
+ 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 = {
+ title: 'Title',
+ description: 'Description that can be longer or shorter',
+ overTableMessage: 'Over here',
+ select: new Subject(),
+ 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();
+ 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();
+ });
+ });
+
+});
diff --git a/lib/content-services/src/lib/aspect-list/aspect-list-dialog.component.ts b/lib/content-services/src/lib/aspect-list/aspect-list-dialog.component.ts
new file mode 100644
index 0000000000..fb2bac4c35
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/aspect-list-dialog.component.ts
@@ -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,
+ @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();
+ }
+}
diff --git a/lib/content-services/src/lib/aspect-list/aspect-list.component.html b/lib/content-services/src/lib/aspect-list/aspect-list.component.html
new file mode 100644
index 0000000000..e90a4fd387
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/aspect-list.component.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+ {{aspect?.entry?.title}}
+
+
+ {{aspect?.entry?.title}}
+
+
+ {{aspect?.entry?.description}}
+
+ 0" class="adf-aspect-property-table" [id]="'aspect-list-'+colIndex+'-properties-table'">
+
+ {{'ADF-ASPECT-LIST.PROPERTY_NAME' | translate}}
+ {{property.id}}
+
+
+ {{'ADF-ASPECT-LIST.DESCRIPTION' | translate}}
+ {{property.title}}
+
+
+ {{'ADF-ASPECT-LIST.DATA_TYPE' | translate}}
+ {{property.dataType}}
+
+
+
+
+
+
+
diff --git a/lib/content-services/src/lib/aspect-list/aspect-list.component.scss b/lib/content-services/src/lib/aspect-list/aspect-list.component.scss
new file mode 100644
index 0000000000..5632bd12f8
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/aspect-list.component.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/lib/content-services/src/lib/aspect-list/aspect-list.component.spec.ts b/lib/content-services/src/lib/aspect-list/aspect-list.component.spec.ts
new file mode 100644
index 0000000000..c0ee0c2b60
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/aspect-list.component.spec.ts
@@ -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;
+ 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();
+ });
+ });
+
+});
diff --git a/lib/content-services/src/lib/aspect-list/aspect-list.component.ts b/lib/content-services/src/lib/aspect-list/aspect-list.component.ts
new file mode 100644
index 0000000000..3073bf3b73
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/aspect-list.component.ts
@@ -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 = new EventEmitter();
+
+ propertyColumns: string[] = ['name', 'title', 'dataType'];
+ aspects$: Observable = null;
+ nodeAspects: string[] = [];
+ nodeAspectStatus: string[] = null;
+
+ private onDestroy$ = new Subject();
+
+ 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);
+ }
+}
diff --git a/lib/content-services/src/lib/aspect-list/aspect-list.module.ts b/lib/content-services/src/lib/aspect-list/aspect-list.module.ts
new file mode 100644
index 0000000000..dce4bab26a
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/aspect-list.module.ts
@@ -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 { }
diff --git a/lib/content-services/src/lib/aspect-list/aspect-list.service.spec.ts b/lib/content-services/src/lib/aspect-list/aspect-list.service.spec.ts
new file mode 100644
index 0000000000..437eb5cf44
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/aspect-list.service.spec.ts
@@ -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 = 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()
+ }
+ });
+ 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');
+ });
+ }));
+ });
+
+});
diff --git a/lib/content-services/src/lib/aspect-list/aspect-list.service.ts b/lib/content-services/src/lib/aspect-list/aspect-list.service.ts
new file mode 100644
index 0000000000..79d9a3852a
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/aspect-list.service.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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 {
+ const select = new Subject();
+ 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();
+ }
+}
diff --git a/lib/content-services/src/lib/aspect-list/index.ts b/lib/content-services/src/lib/aspect-list/index.ts
new file mode 100644
index 0000000000..a7e30cc675
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/index.ts
@@ -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';
diff --git a/lib/content-services/src/lib/aspect-list/node-aspect.service.spec.ts b/lib/content-services/src/lib/aspect-list/node-aspect.service.spec.ts
new file mode 100644
index 0000000000..b60c6e1471
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/node-aspect.service.spec.ts
@@ -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');
+ });
+
+});
diff --git a/lib/content-services/src/lib/aspect-list/node-aspect.service.ts b/lib/content-services/src/lib/aspect-list/node-aspect.service.ts
new file mode 100644
index 0000000000..32fa1ca6be
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/node-aspect.service.ts
@@ -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);
+ });
+ });
+ }
+}
diff --git a/lib/content-services/src/lib/aspect-list/public-api.ts b/lib/content-services/src/lib/aspect-list/public-api.ts
new file mode 100644
index 0000000000..3c12160357
--- /dev/null
+++ b/lib/content-services/src/lib/aspect-list/public-api.ts
@@ -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';
diff --git a/lib/content-services/src/lib/content-metadata/components/content-metadata-card/content-metadata-card.component.html b/lib/content-services/src/lib/content-metadata/components/content-metadata-card/content-metadata-card.component.html
index 54154e9e63..2591517d1c 100644
--- a/lib/content-services/src/lib/content-metadata/components/content-metadata-card/content-metadata-card.component.html
+++ b/lib/content-services/src/lib/content-metadata/components/content-metadata-card/content-metadata-card.component.html
@@ -13,6 +13,14 @@