[ADF-5316] - Content Type (#6602)

* [ADF-5316] - added content type editing for nodes

* [ADF-5316] - added content type editing for nodes

* [ADF-5316] - fix unit test #1

* [ADF-5316] - fix unit test #2

* [ADF-5316] - fix unit test - final

* Removed failing lint word

* [ADF-5316] - added alfresco api real calls

* Build fixed

* [ADF-5316] - fixed second loop trigger on model

* [ADF-5316] - fixed unit tests

* [ADF-5316] - removed unused stream

* [ADF-5316] - fixed package.json

* [ADF-5316] - added missing unit tests

* [ADF-5316] - fixed wrong import

Co-authored-by: Vito Albano <vitoalbano@vitoalbano-mbp-0120.local>
This commit is contained in:
Vito
2021-02-12 09:33:01 +00:00
committed by GitHub
parent 0b66ee8171
commit eb9e555ba9
31 changed files with 981 additions and 33 deletions

View File

@@ -0,0 +1,86 @@
---
Title: Content Type Dialog component
Added: v2.0.0
Status: Active
Last reviewed: 2021-01-20
---
# [Content Type Dialog component](../../../lib/content-services/src/lib/content-type/content-type-dialog.component.ts "Defined in content-type-dialog.component.ts")
Confirm dialog when user changes content type of a node.
## Details
The [Content Type Dialog component](content-type-dialog.component.md) works as a dialog showing a confirm message when the user changes the conten type of a node. It is showing the properties of the new content type selected.
### Showing the dialog
Unlike most components, the [Content Type Dialog component](content-type-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 [`ContentTypeDialogComponentData`](../../../lib/content-services/src/lib/content-type/content-type-metadata.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 ContentTypeDialogComponentData {
title: string;
description: string;
confirmMessage: string;
select: Subject<boolean>;
nodeType?: 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 |
| confirmMessage | `string` | "" | Text that will be showed on the top of properties list accordion |
| select | [`Subject<Node>`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/content-rest-api/docs/Node.md) | | Event emitted when apply button is clicked |
| nodeType | `string` | "" | current prefixed name of the content type selected |
If you don't want to manage the dialog yourself then it is easier to use the
methods of the Content Type Property Service, 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() {
const data: ContentTypeDialogComponentData = {
title: 'CORE.METADATA.CONTENT_TYPE.DIALOG.TITLE',
description: 'CORE.METADATA.CONTENT_TYPE.DIALOG.DESCRIPTION',
confirmMessage: 'CORE.METADATA.CONTENT_TYPE.DIALOG.CONFIRM',
select: select,
nodeType
};
this.dialog.open(
ContentTypeDialogComponent,
{
data, panelClass: 'adf-content-type-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 [`ContentTypeDialogData`](../../../lib/content-services/src/lib/content-type/content-type-metadata.interface.ts) object passed to the dialog.
When the dialog action is selected by clicking, the `data.select` stream will be completed.

View File

@@ -24,11 +24,14 @@ import { setupTestBed, AllowableOperationsEnum } from '@alfresco/adf-core';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { SimpleChange } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { of } from 'rxjs';
describe('ContentMetadataCardComponent', () => {
let component: ContentMetadataCardComponent;
let fixture: ComponentFixture<ContentMetadataCardComponent>;
let contentMetadataService: ContentMetadataService;
let node: Node;
const preset = 'custom-preset';
@@ -41,6 +44,7 @@ describe('ContentMetadataCardComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ContentMetadataCardComponent);
contentMetadataService = TestBed.inject(ContentMetadataService);
component = fixture.componentInstance;
node = <Node> {
aspectNames: [],
@@ -53,6 +57,7 @@ describe('ContentMetadataCardComponent', () => {
component.node = node;
component.preset = preset;
spyOn(contentMetadataService, 'getContentTypeProperty').and.returnValue(of([]));
fixture.detectChanges();
});

View File

@@ -9,7 +9,6 @@
{{ 'CORE.METADATA.BASIC.HEADER' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<adf-card-view
(keydown)="keyDown($event)"
[properties]="basicProperties$ | async"

View File

@@ -58,7 +58,7 @@ describe('ContentMetadataComponent', () => {
node = <Node> {
id: 'node-id',
aspectNames: [],
nodeType: '',
nodeType: 'cm:node',
content: {},
properties: {},
createdByUser: {},
@@ -75,6 +75,7 @@ describe('ContentMetadataComponent', () => {
component.node = node;
component.preset = preset;
spyOn(contentMetadataService, 'getContentTypeProperty').and.returnValue(of([]));
fixture.detectChanges();
});
@@ -169,10 +170,33 @@ describe('ContentMetadataComponent', () => {
saveButton.nativeElement.click();
fixture.detectChanges();
}));
it('should open the confirm dialog when content type is changed', fakeAsync(() => {
component.editable = true;
const property = <CardViewBaseItemModel> { key: 'nodeType', value: 'ft:sbiruli' };
const expectedNode = Object.assign({}, node, { nodeType: 'ft:sbiruli' });
spyOn(contentMetadataService, 'openConfirmDialog').and.returnValue(of(true));
spyOn(nodesApiService, 'updateNode').and.callFake(() => {
return of(expectedNode);
});
updateService.update(property, 'ft:poppoli');
tick(600);
fixture.detectChanges();
tick(100);
const saveButton = fixture.debugElement.query(By.css('[data-automation-id="save-metadata"]'));
saveButton.nativeElement.click();
tick(100);
expect(component.node).toEqual(expectedNode);
expect(contentMetadataService.openConfirmDialog).toHaveBeenCalledWith({nodeType: 'ft:poppoli'});
expect(nodesApiService.updateNode).toHaveBeenCalled();
}));
});
describe('Reseting', () => {
it('should reset changedProperties on reset click', async(async () => {
it('should reset changedProperties on reset click', async () => {
component.changedProperties = { properties: { 'property-key': 'updated-value' } };
component.hasMetadataChanged = true;
component.editable = true;
@@ -189,7 +213,7 @@ describe('ContentMetadataComponent', () => {
fixture.detectChanges();
expect(component.changedProperties).toEqual({});
expect(nodesApiService.updateNode).not.toHaveBeenCalled();
}));
});
});
describe('Properties loading', () => {
@@ -205,6 +229,7 @@ describe('ContentMetadataComponent', () => {
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
expect(contentMetadataService.getContentTypeProperty).toHaveBeenCalledWith(node.nodeType);
expect(contentMetadataService.getBasicProperties).toHaveBeenCalledWith(expectedNode);
});
@@ -221,7 +246,7 @@ describe('ContentMetadataComponent', () => {
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const basicPropertiesComponent = fixture.debugElement.query(By.directive(CardViewComponent)).componentInstance;
expect(basicPropertiesComponent.properties).toBe(expectedProperties);
expect(basicPropertiesComponent.properties.length).toBe(expectedProperties.length);
});
}));

View File

@@ -17,7 +17,7 @@
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Node } from '@alfresco/js-api';
import { Observable, Subject, of } from 'rxjs';
import { Observable, Subject, of, zip } from 'rxjs';
import {
CardViewItem,
NodesApiService,
@@ -30,7 +30,7 @@ import {
} from '@alfresco/adf-core';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { CardViewGroup } from '../../interfaces/content-metadata.interfaces';
import { takeUntil, debounceTime, catchError } from 'rxjs/operators';
import { takeUntil, debounceTime, catchError, map } from 'rxjs/operators';
@Component({
selector: 'adf-content-metadata',
@@ -155,11 +155,18 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
private loadProperties(node: Node) {
if (node) {
this.basicProperties$ = this.contentMetadataService.getBasicProperties(node);
this.basicProperties$ = this.getProperties(node);
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(node, this.preset);
}
}
private getProperties(node: Node) {
const properties$ = this.contentMetadataService.getBasicProperties(node);
const contentTypeProperty$ = this.contentMetadataService.getContentTypeProperty(node.nodeType);
return zip(properties$, contentTypeProperty$)
.pipe(map(([properties, contentTypeProperty]) => [...properties, ...contentTypeProperty]));
}
updateChanges(updatedNodeChanges) {
Object.keys(updatedNodeChanges).map((propertyGroup: string) => {
if (typeof updatedNodeChanges[propertyGroup] === 'object') {
@@ -174,6 +181,16 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
}
saveChanges() {
if (this.hasContentTypeChanged(this.changedProperties)) {
this.contentMetadataService.openConfirmDialog(this.changedProperties).subscribe(() => {
this.updateNode();
});
} else {
this.updateNode();
}
}
private updateNode() {
this.nodesApiService.updateNode(this.node.id, this.changedProperties).pipe(
catchError((err) => {
this.cardViewUpdateService.updateElement(this.targetProperty);
@@ -189,6 +206,10 @@ export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
});
}
private hasContentTypeChanged(changedProperties): boolean {
return !!changedProperties?.nodeType;
}
revertChanges() {
this.changedProperties = {};
this.hasMetadataChanged = false;

View File

@@ -22,6 +22,7 @@ export * from './services/content-metadata.service';
export * from './services/property-descriptors.service';
export * from './services/property-groups-translator.service';
export * from './services/config/content-metadata-config.factory';
export * from './services/content-type-property.service';
export * from './services/config/indifferent-config.service';
export * from './services/config/layout-oriented-config.service';

View File

@@ -18,7 +18,6 @@
import { Injectable } from '@angular/core';
import { Node } from '@alfresco/js-api';
import { CardViewDateItemModel, CardViewTextItemModel, FileSizePipe } from '@alfresco/adf-core';
@Injectable({
providedIn: 'root'
})

View File

@@ -18,17 +18,19 @@
import { AlfrescoApiService, AppConfigService, setupTestBed } from '@alfresco/adf-core';
import { ClassesApi, Node } from '@alfresco/js-api';
import { TestBed } from '@angular/core/testing';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { ContentMetadataService } from './content-metadata.service';
import { of } from 'rxjs';
import { PropertyGroup } from '../interfaces/property-group.interface';
import { TranslateModule } from '@ngx-translate/core';
import { ContentTypePropertiesService } from './content-type-property.service';
import { ContentTestingModule } from '../../testing/content.testing.module';
describe('ContentMetaDataService', () => {
let service: ContentMetadataService;
let classesApi: ClassesApi;
let appConfig: AppConfigService;
let contentPropertyService: ContentTypePropertiesService;
const exifResponse: PropertyGroup = {
name: 'exif:exif',
@@ -64,6 +66,7 @@ describe('ContentMetaDataService', () => {
beforeEach(() => {
service = TestBed.inject(ContentMetadataService);
contentPropertyService = TestBed.inject(ContentTypePropertiesService);
const alfrescoApiService = TestBed.inject(AlfrescoApiService);
classesApi = alfrescoApiService.classesApi;
appConfig = TestBed.inject(AppConfigService);
@@ -89,6 +92,28 @@ describe('ContentMetaDataService', () => {
);
});
it('should return the content type property', () => {
spyOn(contentPropertyService, 'getContentTypeCardItem').and.returnValue(of({ label: 'hello i am a weird content type'}));
service.getContentTypeProperty('fn:fakenode').subscribe(
(res: any) => {
expect(res).toBeDefined();
expect(res).not.toBeNull();
expect(res.label).toBe('hello i am a weird content type');
}
);
});
it('should trigger the opening of the content type dialog', () => {
spyOn(contentPropertyService, 'openContentTypeDialogConfirm').and.returnValue(of());
service.openConfirmDialog('fn:fakenode').subscribe(
() => {
expect(contentPropertyService.openContentTypeDialogConfirm).toHaveBeenCalledWith('fn:fakenode');
}
);
});
describe('AspectOriented preset', () => {
it('should return response with exif property', (done) => {

View File

@@ -25,7 +25,7 @@ import { CardViewGroup, OrganisedPropertyGroup } from '../interfaces/content-met
import { ContentMetadataConfigFactory } from './config/content-metadata-config.factory';
import { PropertyDescriptorsService } from './property-descriptors.service';
import { map, switchMap } from 'rxjs/operators';
import { ContentTypePropertiesService } from './content-type-property.service';
@Injectable({
providedIn: 'root'
})
@@ -36,13 +36,22 @@ export class ContentMetadataService {
constructor(private basicPropertiesService: BasicPropertiesService,
private contentMetadataConfigFactory: ContentMetadataConfigFactory,
private propertyGroupTranslatorService: PropertyGroupTranslatorService,
private propertyDescriptorsService: PropertyDescriptorsService) {
private propertyDescriptorsService: PropertyDescriptorsService,
private contentTypePropertyService: ContentTypePropertiesService) {
}
getBasicProperties(node: Node): Observable<CardViewItem[]> {
return of(this.basicPropertiesService.getProperties(node));
}
getContentTypeProperty(nodeType: string): Observable<CardViewItem[]> {
return this.contentTypePropertyService.getContentTypeCardItem(nodeType);
}
openConfirmDialog(changedProperties): Observable<any> {
return this.contentTypePropertyService.openContentTypeDialogConfirm(changedProperties.nodeType);
}
getGroupedProperties(node: Node, presetName: string = 'default'): Observable<CardViewGroup[]> {
let groupedProperties = of([]);

View File

@@ -0,0 +1,86 @@
/*!
* @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 { Node } from '@alfresco/js-api';
import { TestBed } from '@angular/core/testing';
import { ContentMetadataService } from './content-metadata.service';
import { of } from 'rxjs';
import { ContentTypePropertiesService } from './content-type-property.service';
import { setupTestBed } from 'core';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { TranslateModule } from '@ngx-translate/core';
describe('ContentMetaDataService', () => {
let service: ContentMetadataService;
let contentPropertyService: ContentTypePropertiesService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
ContentTestingModule
]
});
beforeEach(() => {
service = TestBed.inject(ContentMetadataService);
contentPropertyService = TestBed.inject(ContentTypePropertiesService);
});
it('should return all the properties of the node', () => {
const fakeNode: Node = <Node> {
name: 'Node',
id: 'fake-id',
isFile: true,
aspectNames: ['exif:exif'],
createdByUser: {displayName: 'test-user'},
modifiedByUser: {displayName: 'test-user-modified'}
};
service.getBasicProperties(fakeNode).subscribe(
(res) => {
expect(res.length).toEqual(10);
expect(res[0].value).toEqual('Node');
expect(res[1].value).toBeFalsy();
expect(res[2].value).toBe('test-user');
}
);
});
it('should return the content type property', () => {
spyOn(contentPropertyService, 'getContentTypeCardItem').and.returnValue(of({ label: 'hello i am a weird content type'}));
service.getContentTypeProperty('fn:fakenode').subscribe(
(res: any) => {
expect(res).toBeDefined();
expect(res).not.toBeNull();
expect(res.label).toBe('hello i am a weird content type');
}
);
});
it('should trigger the opening of the content type dialog', () => {
spyOn(contentPropertyService, 'openContentTypeDialogConfirm').and.returnValue(of());
service.openConfirmDialog('fn:fakenode').subscribe(
() => {
expect(contentPropertyService.openContentTypeDialogConfirm).toHaveBeenCalledWith('fn:fakenode');
}
);
});
});

View File

@@ -0,0 +1,106 @@
/*!
* @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 { CardViewItem, CardViewSelectItemModel, CardViewSelectItemOption } from '@alfresco/adf-core';
import { Observable, of, Subject, zip } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { ContentTypeDialogComponent } from '../../content-type/content-type-dialog.component';
import { ContentTypeDialogComponentData } from '../../content-type/content-type-metadata.interface';
import { ContentTypeService } from '../../content-type/content-type.service';
import { TypeEntry } from '@alfresco/js-api';
@Injectable({
providedIn: 'root'
})
export class ContentTypePropertiesService {
constructor(private contentTypeService: ContentTypeService, private dialog: MatDialog) {
}
getContentTypeCardItem(nodeType: string): Observable<CardViewItem[]> {
return this.contentTypeService.getContentTypeByPrefix(nodeType).
pipe(
map((contentType) => {
const contentTypesOptions$ = this.getContentTypesAsSelectOption(contentType);
const contentTypeCard = this.buildContentTypeSelectCardModel(contentType.entry.id, contentTypesOptions$);
return [contentTypeCard];
}));
}
private buildContentTypeSelectCardModel(currentValue: string, options$: Observable<CardViewSelectItemOption<string>[]>): CardViewSelectItemModel<string> {
const contentTypeCard = new CardViewSelectItemModel({
label: 'CORE.METADATA.BASIC.CONTENT_TYPE',
value: currentValue,
key: 'nodeType',
editable: true,
options$: options$
});
return contentTypeCard;
}
private getContentTypesAsSelectOption(currentType: TypeEntry): Observable<CardViewSelectItemOption<string>[]> {
const childrenTypes$ = this.contentTypeService.getContentTypeChildren(currentType.entry.id);
return zip(childrenTypes$, of(currentType)).pipe(
distinctUntilChanged(),
map(([contentTypesEntries, currentContentType]) => {
const updatedTypes = this.appendCurrentType(currentContentType, contentTypesEntries);
return updatedTypes.map((contentType) => <CardViewSelectItemOption<string>> { key: contentType.entry.id, label: contentType.entry.title ?? contentType.entry.id});
}));
}
private appendCurrentType(currentType: TypeEntry, contentTypesEntries: TypeEntry[]): TypeEntry[] {
const resultTypes = contentTypesEntries;
if (contentTypesEntries.indexOf(currentType) === -1) {
resultTypes.push(currentType);
}
return resultTypes;
}
openContentTypeDialogConfirm(nodeType): Observable<boolean> {
const select = new Subject<boolean>();
select.subscribe({
complete: this.close.bind(this)
});
const data: ContentTypeDialogComponentData = {
title: 'CORE.METADATA.CONTENT_TYPE.DIALOG.TITLE',
description: 'CORE.METADATA.CONTENT_TYPE.DIALOG.DESCRIPTION',
confirmMessage: 'CORE.METADATA.CONTENT_TYPE.DIALOG.CONFIRM',
select: select,
nodeType
};
this.openDialog(data, 'adf-content-type-dialog', '600px');
return select;
}
close() {
this.dialog.closeAll();
}
private openDialog(data: ContentTypeDialogComponentData, panelClass: string, width: string) {
this.dialog.open(ContentTypeDialogComponent, {
data,
panelClass,
width,
disableClose: true
});
}
}

View File

@@ -0,0 +1,45 @@
<div class="adf-content-type-dialog">
<h2 mat-dialog-title class="adf-content-type-dialog-title" data-automation-id="content-type-dialog-title">{{title |
translate}}</h2>
<mat-dialog-content class="mat-typography" class="adf-content-type-dialog-content"
data-automation-id="content-type-dialog-content">
<h4 data-automation-id="content-type-dialog-description">{{description | translate}}</h4>
<p data-automation-id="content-type-dialog-confirm-message">{{confirmMessage | translate}}</p>
<mat-accordion>
<mat-expansion-panel class="adf-content-type-accordion">
<mat-expansion-panel-header>
<mat-panel-title>
{{'CORE.METADATA.CONTENT_TYPE.DIALOG.VIEW_DETAILS' | translate}}
</mat-panel-title>
</mat-expansion-panel-header>
<table mat-table [dataSource]="currentContentType?.entry?.properties"
*ngIf="currentContentType?.entry?.properties?.length > 0" class="adf-content-type-table">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{'CORE.METADATA.CONTENT_TYPE.DIALOG.PROPERTY.NAME' |
translate}} </th>
<td mat-cell *matCellDef="let property"> {{property.id}} </td>
</ng-container>
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef>
{{'CORE.METADATA.CONTENT_TYPE.DIALOG.PROPERTY.DESCRIPTION' | translate}} </th>
<td mat-cell *matCellDef="let property"> {{property.title}} </td>
</ng-container>
<ng-container matColumnDef="dataType">
<th mat-header-cell *matHeaderCellDef> {{'CORE.METADATA.CONTENT_TYPE.DIALOG.PROPERTY.DATA_TYPE'
| translate}} </th>
<td mat-cell *matCellDef="let property"> {{property.dataType}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="propertyColumns"></tr>
<tr mat-row *matRowDef="let row; columns: propertyColumns;"></tr>
</table>
</mat-expansion-panel>
</mat-accordion>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close
id="conten-type-dialog-actions-cancel">{{'CORE.METADATA.CONTENT_TYPE.DIALOG.CANCEL' | translate }}</button>
<button mat-button class="adf-content-type-dialog-apply-button" id="content-type-dialog-apply-button"
[mat-dialog-close]="true" cdkFocusInitial (click)="onApply()">{{'CORE.METADATA.CONTENT_TYPE.DIALOG.APPLY' |
translate}}</button>
</mat-dialog-actions>
</div>

View File

@@ -0,0 +1,41 @@
@mixin adf-content-type-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 {
&-content-type-dialog {
.mat-expansion-panel {
margin-bottom: 10px;
}
}
&-content-type-accordion {
margin: 10px;
}
&-content-type-dialog-title {
font-size: large;
font-weight: 200;
margin-top: 0;
}
&-content-type-dialog-description {
font-size: small;
line-height: normal;
}
&-content-type-table {
width: 100%;
}
&-content-type-dialog-apply-button {
color: mat-color($primary);
}
}
}

View File

@@ -0,0 +1,158 @@
/*!
* @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 { TranslateModule } from '@ngx-translate/core';
import { of, Subject } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
import { ContentTypeDialogComponent } from './content-type-dialog.component';
import { ContentTypeService } from './content-type.service';
import { ContentTypeDialogComponentData } from './content-type-metadata.interface';
import { TypeEntry } from '@alfresco/js-api';
const elementCustom: TypeEntry = {
entry: {
id: 'ck:pippobaudo',
title: 'PIPPO-BAUDO',
description: 'Doloro reaepgfihawpefih peahfa powfj p[qwofhjaq[ fq[owfj[qowjf[qowfgh[qowh f[qowhfj [qwohf',
parentId: 'cm:content',
properties: [
{
id: 'ck:PropA',
dataType: 'ck:propA',
defaultValue: 'HERE I AM',
description: 'A property',
isMandatory: false,
isMandatoryEnforced: false,
isMultiValued: false,
title: 'PropertyA'
},
{
id: 'ck:PropB',
dataType: 'ck:propB',
defaultValue: 'HERE I AM',
description: 'A property',
isMandatory: false,
isMandatoryEnforced: false,
isMultiValued: false,
title: 'PropertyB'
},
{
id: 'ck:PropC',
dataType: 'ck:propC',
defaultValue: 'HERE I AM',
description: 'A property',
isMandatory: false,
isMandatoryEnforced: false,
isMultiValued: false,
title: 'PropertyC'
}
]
}
};
describe('Content Type Dialog Component', () => {
let fixture: ComponentFixture<ContentTypeDialogComponent>;
let contentTypeService: ContentTypeService;
let data: ContentTypeDialogComponentData;
beforeEach(async () => {
data = <ContentTypeDialogComponentData> {
title: 'Title',
description: 'Description that can be longer or shorter',
nodeType: 'fk:fakeNode',
confirmMessage: 'Do you want to jump? Y/N',
select: new Subject<boolean>()
};
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(() => {
contentTypeService = TestBed.inject(ContentTypeService);
spyOn(contentTypeService, 'getContentTypeByPrefix').and.returnValue(of(elementCustom));
fixture = TestBed.createComponent(ContentTypeDialogComponent);
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
it('should show basic information for the dialog', () => {
const dialogTitle = fixture.nativeElement.querySelector('[data-automation-id="content-type-dialog-title"]');
expect(dialogTitle).not.toBeNull();
expect(dialogTitle.innerText).toBe(data.title);
const description = fixture.nativeElement.querySelector('[data-automation-id="content-type-dialog-description"]');
expect(description).not.toBeNull();
expect(description.innerText).toBe(data.description);
const confirmMessage = fixture.nativeElement.querySelector('[data-automation-id="content-type-dialog-confirm-message"]');
expect(confirmMessage).not.toBeNull();
expect(confirmMessage.innerText).toBe(data.confirmMessage);
});
it('should complete the select stream Cancel button is clicked', (done) => {
data.select.subscribe(() => { }, () => { }, () => done());
const cancelButton: HTMLButtonElement = fixture.nativeElement.querySelector('#conten-type-dialog-actions-cancel');
expect(cancelButton).toBeDefined();
cancelButton.click();
fixture.detectChanges();
});
it('should show the property of the aspect', async () => {
const showPropertyAccordon: HTMLButtonElement = fixture.nativeElement.querySelector('.adf-content-type-accordion .mat-expansion-panel-header');
expect(showPropertyAccordon).toBeDefined();
showPropertyAccordon.click();
fixture.detectChanges();
await fixture.whenStable();
const propertyShowed: NodeList = fixture.nativeElement.querySelectorAll('.adf-content-type-table .mat-row');
expect(propertyShowed.length).toBe(3);
});
it('should emit true when apply is clicked', (done) => {
data.select.subscribe((value) => {
expect(value).toBe(true);
}, () => { }, () => done());
const applyButton: HTMLButtonElement = fixture.nativeElement.querySelector('#content-type-dialog-apply-button');
expect(applyButton).toBeDefined();
applyButton.click();
fixture.detectChanges();
});
});

View File

@@ -0,0 +1,72 @@
/*!
* @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 { TypeEntry } from '@alfresco/js-api';
import { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ContentTypeDialogComponentData } from './content-type-metadata.interface';
import { ContentTypeService } from './content-type.service';
@Component({
selector: 'adf-content-type-dialog',
templateUrl: './content-type-dialog.component.html',
styleUrls: ['./content-type-dialog.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ContentTypeDialogComponent implements OnInit {
title: string;
description: string;
nodeType: string;
confirmMessage: string;
currentContentType: TypeEntry;
propertyColumns: string[] = ['name', 'title', 'dataType'];
constructor(private dialog: MatDialogRef<ContentTypeDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ContentTypeDialogComponentData,
private contentTypeService: ContentTypeService) {
this.title = data.title;
this.description = data.description;
this.confirmMessage = data.confirmMessage;
this.nodeType = data.nodeType;
this.contentTypeService.getContentTypeByPrefix(this.nodeType).subscribe((contentTypeEntry) => {
this.currentContentType = contentTypeEntry;
});
}
ngOnInit() {
this.dialog.backdropClick().subscribe(() => {
this.close();
});
}
close() {
this.data.select.complete();
}
onCancel() {
this.close();
}
onApply() {
this.data.select.next(true);
this.close();
}
}

View File

@@ -0,0 +1,26 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Subject } from 'rxjs';
export interface ContentTypeDialogComponentData {
title: string;
description: string;
confirmMessage: string;
select: Subject<boolean>;
nodeType?: string;
}

View File

@@ -0,0 +1,45 @@
/*!
* @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 { MatTableModule } from '@angular/material/table';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { TranslateModule } from '@ngx-translate/core';
import { MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { ContentTypeDialogComponent } from './content-type-dialog.component';
@NgModule({
imports: [
CommonModule,
MatTableModule,
MatExpansionModule,
MatCheckboxModule,
TranslateModule,
MatDialogModule,
MatButtonModule
],
exports: [
ContentTypeDialogComponent
],
declarations: [
ContentTypeDialogComponent
]
})
export class ContentTypeModule { }

View File

@@ -0,0 +1,66 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TypeEntry } from '@alfresco/js-api';
import { AlfrescoApiService } from 'core';
import { of } from 'rxjs';
import { ContentTypeService } from './content-type.service';
describe('ContentTypeService', () => {
const fakeEntryMock: TypeEntry = {
entry: {
id : 'fake-type-id',
title: 'fake-title',
description: 'optional-fake-description',
parentId: 'cm:parent',
properties: []
}
};
const mockTypesApi = jasmine.createSpyObj('TypesApi', ['getType', 'listTypes']);
const alfrescoApiService: AlfrescoApiService = new AlfrescoApiService(null, null);
const contentTypeService: ContentTypeService = new ContentTypeService(alfrescoApiService);
beforeEach(() => {
spyOnProperty(alfrescoApiService, 'typesApi').and.returnValue(mockTypesApi);
});
it('should get a node type info', (done) => {
mockTypesApi.getType.and.returnValue(of(fakeEntryMock));
contentTypeService.getContentTypeByPrefix('whatever-whenever').subscribe((result) => {
expect(result).toBeDefined();
expect(result).not.toBeNull();
expect(result.entry.id).toBe('fake-type-id');
expect(mockTypesApi.getType).toHaveBeenCalledWith('whatever-whenever');
done();
});
});
it('should get the list of children types', (done) => {
mockTypesApi.listTypes.and.returnValue(of({ list: {entries: [fakeEntryMock]}}));
contentTypeService.getContentTypeChildren('whatever-whenever').subscribe((results: TypeEntry[]) => {
expect(results).toBeDefined();
expect(results).not.toBeNull();
expect(results.length).toBe(1);
expect(results[0].entry.id).toBe('fake-type-id');
expect(mockTypesApi.listTypes).toHaveBeenCalledWith({ where: '(parentIds in (\'whatever-whenever\') and not namespaceUri matches(\'http://www.alfresco.org/model.*\'))' });
done();
});
});
});

View File

@@ -0,0 +1,41 @@
/*!
* @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 { TypeEntry, TypePaging } from '@alfresco/js-api';
import { Injectable } from '@angular/core';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ContentTypeService {
constructor(private alfrescoApiService: AlfrescoApiService) {
}
getContentTypeByPrefix(prefixedType: string): Observable<TypeEntry> {
return from(this.alfrescoApiService.typesApi.getType(prefixedType));
}
getContentTypeChildren(nodeType: string): Observable<TypeEntry[]> {
const opts = {where : `(parentIds in ('${nodeType}') and not namespaceUri matches('http://www.alfresco.org/model.*'))`};
return from(this.alfrescoApiService.typesApi.listTypes(opts)).pipe(
map((result: TypePaging) => result.list.entries)
);
}
}

View File

@@ -0,0 +1,23 @@
/*!
* @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 './content-type.service';
// export * from './content-type.model';
export * from './content-type-metadata.interface';
export * from './content-type-dialog.component';
export * from './content-type.module';

View File

@@ -0,0 +1,18 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './public-api';

View File

@@ -39,6 +39,7 @@ import { FolderDirectiveModule } from './folder-directive/folder-directive.modul
import { ContentMetadataModule } from './content-metadata/content-metadata.module';
import { PermissionManagerModule } from './permission-manager/permission-manager.module';
import { TreeViewModule } from './tree-view/tree-view.module';
import { ContentTypeModule } from './content-type/content-type.module';
@NgModule({
imports: [
@@ -63,7 +64,8 @@ import { TreeViewModule } from './tree-view/tree-view.module';
ContentDirectiveModule,
PermissionManagerModule,
VersionManagerModule,
TreeViewModule
TreeViewModule,
ContentTypeModule
],
providers: [
{
@@ -92,7 +94,8 @@ import { TreeViewModule } from './tree-view/tree-view.module';
ContentDirectiveModule,
PermissionManagerModule,
VersionManagerModule,
TreeViewModule
TreeViewModule,
ContentTypeModule
]
})
export class ContentModule {

View File

@@ -334,7 +334,23 @@
"CREATOR": "Creator",
"CREATED_DATE": "Created Date",
"MODIFIER": "Modifier",
"MODIFIED_DATE": "Modified Date"
"MODIFIED_DATE": "Modified Date",
"CONTENT_TYPE": "Content Type"
},
"CONTENT_TYPE": {
"DIALOG" :{
"TITLE" : "Change content type",
"DESCRIPTION": "Making this change to the content type will permanently add some properties and stored metadata to the document.",
"CONFIRM": "Are you sure you want to change the content type?",
"CANCEL": "CANCEL",
"APPLY": "SAVE CHANGES",
"VIEW_DETAILS": "View details",
"PROPERTY" :{
"NAME" : "Name",
"DESCRIPTION": "Description",
"DATA_TYPE": "Data type"
}
}
},
"ERRORS": {
"GENERIC": "Error updating property",

View File

@@ -28,7 +28,6 @@ export const mockContentModelTextProperty = {
defaultValue: '',
mandatoryEnforced: false,
indexed: false,
facetable: 'FALSE',
indexTokenisationMode: '',
constraints: []
};
@@ -44,7 +43,6 @@ export const mockContentModelDateProperty = {
defaultValue: '',
mandatoryEnforced: false,
indexed: false,
facetable: 'FALSE',
indexTokenisationMode: '',
constraints: []
};

View File

@@ -26,6 +26,7 @@
@import '../permission-manager/components/add-permission/add-permission-panel.component';
@import '../tree-view/components/tree-view.component';
@import '../version-manager/version-comparison.component';
@import '../content-type/content-type-dialog.component';
@mixin adf-content-services-theme($theme) {
@include adf-breadcrumb-theme($theme);
@@ -52,4 +53,5 @@
@include adf-search-filter-theme($theme);
@include adf-search-chip-list-theme($theme);
@include adf-version-comparison-theme($theme);
@include adf-content-type-dialog-theme($theme);
}

View File

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

View File

@@ -15,7 +15,7 @@
<adf-select-filter-input *ngIf="showInputFilter" (change)="onFilterInputChange($event)"></adf-select-filter-input>
<mat-option *ngIf="showNoneOption()">{{ 'CORE.CARDVIEW.NONE' | translate }}</mat-option>
<mat-option *ngFor="let option of getList() | async"
<mat-option *ngFor="let option of list$ | async"
[value]="option.key">
{{ option.label | translate }}
</mat-option>

View File

@@ -18,7 +18,7 @@
import { Component, Input, OnChanges, OnDestroy } from '@angular/core';
import { CardViewSelectItemModel } from '../../models/card-view-selectitem.model';
import { CardViewUpdateService } from '../../services/card-view-update.service';
import { Observable, Subject } from 'rxjs';
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { CardViewSelectItemOption } from '../../interfaces/card-view.interfaces';
import { MatSelectChange } from '@angular/material/select';
import { BaseCardView } from '../base-card-view';
@@ -44,11 +44,13 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
displayEmpty: boolean = true;
value: string | number;
filter: string = '';
filter$: BehaviorSubject<string> = new BehaviorSubject('');
showInputFilter: boolean = false;
private onDestroy$ = new Subject<void>();
list$: Observable<CardViewSelectItemOption<string | number>[]> = null;
constructor(cardViewUpdateService: CardViewUpdateService, private appConfig: AppConfigService) {
super(cardViewUpdateService);
}
@@ -63,10 +65,12 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
.subscribe((options: CardViewSelectItemOption<string>[]) => {
this.showInputFilter = options.length > this.optionsLimit;
});
this.list$ = this.getList();
}
onFilterInputChange(value: string) {
this.filter = value.toString();
this.filter$.next(value.toString());
}
isEditable(): boolean {
@@ -77,12 +81,12 @@ export class CardViewSelectItemComponent extends BaseCardView<CardViewSelectItem
return this.options$ || this.property.options$;
}
getList(): Observable<CardViewSelectItemOption<string>[]> {
return this.getOptions()
getList(): Observable<CardViewSelectItemOption<string | number>[]> {
return combineLatest([this.getOptions(), this.filter$])
.pipe(
map((items: CardViewSelectItemOption<string>[]) => items.filter(
(item: CardViewSelectItemOption<string>) =>
item.label.toLowerCase().includes(this.filter.toLowerCase()))),
map(([items, filter]) => items.filter((item: CardViewSelectItemOption<string>) =>
filter ? item.label.toLowerCase().includes(filter.toLowerCase())
: true)),
takeUntil(this.onDestroy$)
);
}

View File

@@ -26,18 +26,25 @@ export class CardViewSelectItemModel<T> extends CardViewBaseItemModel implements
type: string = 'select';
options$: Observable<CardViewSelectItemOption<T>[]>;
valueFetch$: Observable<string> = null;
constructor(cardViewSelectItemProperties: CardViewSelectItemProperties<T>) {
super(cardViewSelectItemProperties);
this.options$ = cardViewSelectItemProperties.options$;
}
get displayValue() {
return this.options$.pipe(
this.valueFetch$ = this.options$.pipe(
switchMap((options) => {
const option = options.find((o) => o.key === this.value?.toString());
return of(option ? option.label : '');
})
);
}));
}
get displayValue() {
return this.valueFetch$;
}
setValue(value: any) {
this.value = value;
}
}

View File

@@ -199,7 +199,23 @@
"CREATOR": "Creator",
"CREATED_DATE": "Created Date",
"MODIFIER": "Modifier",
"MODIFIED_DATE": "Modified Date"
"MODIFIED_DATE": "Modified Date",
"CONTENT_TYPE": "Content Type"
},
"CONTENT_TYPE": {
"DIALOG" :{
"TITLE" : "Change content type",
"DESCRIPTION": "Making this change to the content type will permanently add some properties and stored metadata to the document.",
"CONFIRM": "Are you sure you want to change the content type?",
"CANCEL": "CANCEL",
"APPLY": "SAVE CHANGES",
"VIEW_DETAILS": "View details",
"PROPERTY" :{
"NAME" : "Name",
"DESCRIPTION": "Description",
"DATA_TYPE": "Data type"
}
}
},
"ACTIONS": {
"EDIT": "Edit",

View File

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