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 @@ +
+

ASPECT CHOSEN :

+

{{currentResult}}

+
+ + Node Id For Aspects + + + + +
+
+ +
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 @@ +
+

{{title | translate}}

+
{{description | translate}}
+
+ +
+

{{overTableMessage | translate}}

+

{{currentAspectSelection ? currentAspectSelection.length : 0}} + {{'ADF-ASPECT-LIST.DIALOG.SELECTED' | 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}}

+ + + + + + + + + + + + + + + + +
{{'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 @@