From eb9e555ba95ae4aaaa1d287597ab92e5885c94c9 Mon Sep 17 00:00:00 2001 From: Vito Date: Fri, 12 Feb 2021 09:33:01 +0000 Subject: [PATCH] [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 --- .../content-type-dialog.component.md | 86 ++++++++++ .../content-metadata-card.component.spec.ts | 5 + .../content-metadata.component.html | 1 - .../content-metadata.component.spec.ts | 33 +++- .../content-metadata.component.ts | 27 ++- .../src/lib/content-metadata/public-api.ts | 1 + .../services/basic-properties.service.ts | 1 - .../services/content-metadata.service.spec.ts | 27 ++- .../services/content-metadata.service.ts | 13 +- .../content-type-property.service.spec.ts | 86 ++++++++++ .../services/content-type-property.service.ts | 106 ++++++++++++ .../content-type-dialog.component.html | 45 +++++ .../content-type-dialog.component.scss | 41 +++++ .../content-type-dialog.component.spec.ts | 158 ++++++++++++++++++ .../content-type-dialog.component.ts | 72 ++++++++ .../content-type-metadata.interface.ts | 26 +++ .../lib/content-type/content-type.module.ts | 45 +++++ .../content-type/content-type.service.spec.ts | 66 ++++++++ .../lib/content-type/content-type.service.ts | 41 +++++ .../src/lib/content-type/index.ts | 23 +++ .../src/lib/content-type/public-api.ts | 18 ++ .../src/lib/content.module.ts | 7 +- lib/content-services/src/lib/i18n/en.json | 18 +- .../src/lib/mock/content-model.mock.ts | 2 - .../src/lib/styles/_index.scss | 2 + lib/content-services/src/public-api.ts | 1 + .../card-view-selectitem.component.html | 2 +- .../card-view-selectitem.component.ts | 20 ++- .../models/card-view-selectitem.model.ts | 17 +- lib/core/i18n/en.json | 18 +- lib/core/services/alfresco-api.service.ts | 6 +- 31 files changed, 981 insertions(+), 33 deletions(-) create mode 100644 docs/content-services/components/content-type-dialog.component.md create mode 100644 lib/content-services/src/lib/content-metadata/services/content-type-property.service.spec.ts create mode 100644 lib/content-services/src/lib/content-metadata/services/content-type-property.service.ts create mode 100644 lib/content-services/src/lib/content-type/content-type-dialog.component.html create mode 100644 lib/content-services/src/lib/content-type/content-type-dialog.component.scss create mode 100644 lib/content-services/src/lib/content-type/content-type-dialog.component.spec.ts create mode 100644 lib/content-services/src/lib/content-type/content-type-dialog.component.ts create mode 100644 lib/content-services/src/lib/content-type/content-type-metadata.interface.ts create mode 100644 lib/content-services/src/lib/content-type/content-type.module.ts create mode 100644 lib/content-services/src/lib/content-type/content-type.service.spec.ts create mode 100644 lib/content-services/src/lib/content-type/content-type.service.ts create mode 100644 lib/content-services/src/lib/content-type/index.ts create mode 100644 lib/content-services/src/lib/content-type/public-api.ts diff --git a/docs/content-services/components/content-type-dialog.component.md b/docs/content-services/components/content-type-dialog.component.md new file mode 100644 index 0000000000..30f2547289 --- /dev/null +++ b/docs/content-services/components/content-type-dialog.component.md @@ -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; + 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`](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. diff --git a/lib/content-services/src/lib/content-metadata/components/content-metadata-card/content-metadata-card.component.spec.ts b/lib/content-services/src/lib/content-metadata/components/content-metadata-card/content-metadata-card.component.spec.ts index 8199767323..3ffb3dbfe6 100644 --- a/lib/content-services/src/lib/content-metadata/components/content-metadata-card/content-metadata-card.component.spec.ts +++ b/lib/content-services/src/lib/content-metadata/components/content-metadata-card/content-metadata-card.component.spec.ts @@ -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; + 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 = { aspectNames: [], @@ -53,6 +57,7 @@ describe('ContentMetadataCardComponent', () => { component.node = node; component.preset = preset; + spyOn(contentMetadataService, 'getContentTypeProperty').and.returnValue(of([])); fixture.detectChanges(); }); diff --git a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.html b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.html index 472a41d5ef..9d4bb24f05 100644 --- a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.html +++ b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.html @@ -9,7 +9,6 @@ {{ 'CORE.METADATA.BASIC.HEADER' | translate }} - { 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 = { 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); }); })); diff --git a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.ts b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.ts index 8540a46489..2d47266e83 100644 --- a/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.ts +++ b/lib/content-services/src/lib/content-metadata/components/content-metadata/content-metadata.component.ts @@ -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; diff --git a/lib/content-services/src/lib/content-metadata/public-api.ts b/lib/content-services/src/lib/content-metadata/public-api.ts index 3c563fc2bb..5963f9d74f 100644 --- a/lib/content-services/src/lib/content-metadata/public-api.ts +++ b/lib/content-services/src/lib/content-metadata/public-api.ts @@ -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'; diff --git a/lib/content-services/src/lib/content-metadata/services/basic-properties.service.ts b/lib/content-services/src/lib/content-metadata/services/basic-properties.service.ts index 9a4742f6e7..f867c76061 100644 --- a/lib/content-services/src/lib/content-metadata/services/basic-properties.service.ts +++ b/lib/content-services/src/lib/content-metadata/services/basic-properties.service.ts @@ -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' }) diff --git a/lib/content-services/src/lib/content-metadata/services/content-metadata.service.spec.ts b/lib/content-services/src/lib/content-metadata/services/content-metadata.service.spec.ts index 7256594844..3b2157fc73 100644 --- a/lib/content-services/src/lib/content-metadata/services/content-metadata.service.spec.ts +++ b/lib/content-services/src/lib/content-metadata/services/content-metadata.service.spec.ts @@ -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) => { diff --git a/lib/content-services/src/lib/content-metadata/services/content-metadata.service.ts b/lib/content-services/src/lib/content-metadata/services/content-metadata.service.ts index b78dae3c76..61d2c0d2c6 100644 --- a/lib/content-services/src/lib/content-metadata/services/content-metadata.service.ts +++ b/lib/content-services/src/lib/content-metadata/services/content-metadata.service.ts @@ -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 { return of(this.basicPropertiesService.getProperties(node)); } + getContentTypeProperty(nodeType: string): Observable { + return this.contentTypePropertyService.getContentTypeCardItem(nodeType); + } + + openConfirmDialog(changedProperties): Observable { + return this.contentTypePropertyService.openContentTypeDialogConfirm(changedProperties.nodeType); + } + getGroupedProperties(node: Node, presetName: string = 'default'): Observable { let groupedProperties = of([]); diff --git a/lib/content-services/src/lib/content-metadata/services/content-type-property.service.spec.ts b/lib/content-services/src/lib/content-metadata/services/content-type-property.service.spec.ts new file mode 100644 index 0000000000..caa8c01c14 --- /dev/null +++ b/lib/content-services/src/lib/content-metadata/services/content-type-property.service.spec.ts @@ -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 = { + 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'); + } + ); + }); + +}); diff --git a/lib/content-services/src/lib/content-metadata/services/content-type-property.service.ts b/lib/content-services/src/lib/content-metadata/services/content-type-property.service.ts new file mode 100644 index 0000000000..af34ec994b --- /dev/null +++ b/lib/content-services/src/lib/content-metadata/services/content-type-property.service.ts @@ -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 { + 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[]>): CardViewSelectItemModel { + 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[]> { + 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) => > { 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 { + const select = new Subject(); + 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 + }); + } +} diff --git a/lib/content-services/src/lib/content-type/content-type-dialog.component.html b/lib/content-services/src/lib/content-type/content-type-dialog.component.html new file mode 100644 index 0000000000..ad81b1489a --- /dev/null +++ b/lib/content-services/src/lib/content-type/content-type-dialog.component.html @@ -0,0 +1,45 @@ +
+

{{title | + translate}}

+ +

{{description | translate}}

+

{{confirmMessage | translate}}

+ + + + + {{'CORE.METADATA.CONTENT_TYPE.DIALOG.VIEW_DETAILS' | translate}} + + + + + + + + + + + + + + + + + +
{{'CORE.METADATA.CONTENT_TYPE.DIALOG.PROPERTY.NAME' | + translate}} {{property.id}} + {{'CORE.METADATA.CONTENT_TYPE.DIALOG.PROPERTY.DESCRIPTION' | translate}} {{property.title}} {{'CORE.METADATA.CONTENT_TYPE.DIALOG.PROPERTY.DATA_TYPE' + | translate}} {{property.dataType}}
+
+
+
+ + + + +
diff --git a/lib/content-services/src/lib/content-type/content-type-dialog.component.scss b/lib/content-services/src/lib/content-type/content-type-dialog.component.scss new file mode 100644 index 0000000000..cb22bfc82f --- /dev/null +++ b/lib/content-services/src/lib/content-type/content-type-dialog.component.scss @@ -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); + } + + } +} diff --git a/lib/content-services/src/lib/content-type/content-type-dialog.component.spec.ts b/lib/content-services/src/lib/content-type/content-type-dialog.component.spec.ts new file mode 100644 index 0000000000..3a1e118628 --- /dev/null +++ b/lib/content-services/src/lib/content-type/content-type-dialog.component.spec.ts @@ -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; + let contentTypeService: ContentTypeService; + let data: ContentTypeDialogComponentData; + + beforeEach(async () => { + data = { + title: 'Title', + description: 'Description that can be longer or shorter', + nodeType: 'fk:fakeNode', + confirmMessage: 'Do you want to jump? Y/N', + 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(() => { + 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(); + }); + +}); diff --git a/lib/content-services/src/lib/content-type/content-type-dialog.component.ts b/lib/content-services/src/lib/content-type/content-type-dialog.component.ts new file mode 100644 index 0000000000..e284fe45be --- /dev/null +++ b/lib/content-services/src/lib/content-type/content-type-dialog.component.ts @@ -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, + @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(); + } +} diff --git a/lib/content-services/src/lib/content-type/content-type-metadata.interface.ts b/lib/content-services/src/lib/content-type/content-type-metadata.interface.ts new file mode 100644 index 0000000000..a87e714953 --- /dev/null +++ b/lib/content-services/src/lib/content-type/content-type-metadata.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 ContentTypeDialogComponentData { + title: string; + description: string; + confirmMessage: string; + select: Subject; + nodeType?: string; +} diff --git a/lib/content-services/src/lib/content-type/content-type.module.ts b/lib/content-services/src/lib/content-type/content-type.module.ts new file mode 100644 index 0000000000..9100443101 --- /dev/null +++ b/lib/content-services/src/lib/content-type/content-type.module.ts @@ -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 { } diff --git a/lib/content-services/src/lib/content-type/content-type.service.spec.ts b/lib/content-services/src/lib/content-type/content-type.service.spec.ts new file mode 100644 index 0000000000..b7a1474a85 --- /dev/null +++ b/lib/content-services/src/lib/content-type/content-type.service.spec.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 { 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(); + }); + }); + +}); diff --git a/lib/content-services/src/lib/content-type/content-type.service.ts b/lib/content-services/src/lib/content-type/content-type.service.ts new file mode 100644 index 0000000000..ebc20336d8 --- /dev/null +++ b/lib/content-services/src/lib/content-type/content-type.service.ts @@ -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 { + return from(this.alfrescoApiService.typesApi.getType(prefixedType)); + } + + getContentTypeChildren(nodeType: string): Observable { + 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) + ); + } +} diff --git a/lib/content-services/src/lib/content-type/index.ts b/lib/content-services/src/lib/content-type/index.ts new file mode 100644 index 0000000000..73861a7535 --- /dev/null +++ b/lib/content-services/src/lib/content-type/index.ts @@ -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'; diff --git a/lib/content-services/src/lib/content-type/public-api.ts b/lib/content-services/src/lib/content-type/public-api.ts new file mode 100644 index 0000000000..a7e30cc675 --- /dev/null +++ b/lib/content-services/src/lib/content-type/public-api.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/content.module.ts b/lib/content-services/src/lib/content.module.ts index 8effb43233..d7fb601081 100644 --- a/lib/content-services/src/lib/content.module.ts +++ b/lib/content-services/src/lib/content.module.ts @@ -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 { diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json index 9c74f4175f..7750792086 100644 --- a/lib/content-services/src/lib/i18n/en.json +++ b/lib/content-services/src/lib/i18n/en.json @@ -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", diff --git a/lib/content-services/src/lib/mock/content-model.mock.ts b/lib/content-services/src/lib/mock/content-model.mock.ts index c77b0b7b1a..d30ca8b34d 100644 --- a/lib/content-services/src/lib/mock/content-model.mock.ts +++ b/lib/content-services/src/lib/mock/content-model.mock.ts @@ -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: [] }; diff --git a/lib/content-services/src/lib/styles/_index.scss b/lib/content-services/src/lib/styles/_index.scss index 871593151f..ddae7cb24a 100644 --- a/lib/content-services/src/lib/styles/_index.scss +++ b/lib/content-services/src/lib/styles/_index.scss @@ -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); } diff --git a/lib/content-services/src/public-api.ts b/lib/content-services/src/public-api.ts index 4050290b84..dd609b6412 100644 --- a/lib/content-services/src/public-api.ts +++ b/lib/content-services/src/public-api.ts @@ -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'; diff --git a/lib/core/card-view/components/card-view-selectitem/card-view-selectitem.component.html b/lib/core/card-view/components/card-view-selectitem/card-view-selectitem.component.html index 7eb1c72845..6825ea3857 100644 --- a/lib/core/card-view/components/card-view-selectitem/card-view-selectitem.component.html +++ b/lib/core/card-view/components/card-view-selectitem/card-view-selectitem.component.html @@ -15,7 +15,7 @@ {{ 'CORE.CARDVIEW.NONE' | translate }} - {{ option.label | translate }} diff --git a/lib/core/card-view/components/card-view-selectitem/card-view-selectitem.component.ts b/lib/core/card-view/components/card-view-selectitem/card-view-selectitem.component.ts index bb490cfadc..f5a50c711d 100644 --- a/lib/core/card-view/components/card-view-selectitem/card-view-selectitem.component.ts +++ b/lib/core/card-view/components/card-view-selectitem/card-view-selectitem.component.ts @@ -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 = new BehaviorSubject(''); showInputFilter: boolean = false; private onDestroy$ = new Subject(); + list$: Observable[]> = null; + constructor(cardViewUpdateService: CardViewUpdateService, private appConfig: AppConfigService) { super(cardViewUpdateService); } @@ -63,10 +65,12 @@ export class CardViewSelectItemComponent extends BaseCardView[]) => { 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[]> { - return this.getOptions() + getList(): Observable[]> { + return combineLatest([this.getOptions(), this.filter$]) .pipe( - map((items: CardViewSelectItemOption[]) => items.filter( - (item: CardViewSelectItemOption) => - item.label.toLowerCase().includes(this.filter.toLowerCase()))), + map(([items, filter]) => items.filter((item: CardViewSelectItemOption) => + filter ? item.label.toLowerCase().includes(filter.toLowerCase()) + : true)), takeUntil(this.onDestroy$) ); } diff --git a/lib/core/card-view/models/card-view-selectitem.model.ts b/lib/core/card-view/models/card-view-selectitem.model.ts index 38d40472a1..f1f478f894 100644 --- a/lib/core/card-view/models/card-view-selectitem.model.ts +++ b/lib/core/card-view/models/card-view-selectitem.model.ts @@ -26,18 +26,25 @@ export class CardViewSelectItemModel extends CardViewBaseItemModel implements type: string = 'select'; options$: Observable[]>; + valueFetch$: Observable = null; + constructor(cardViewSelectItemProperties: CardViewSelectItemProperties) { 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; } } diff --git a/lib/core/i18n/en.json b/lib/core/i18n/en.json index 9ee3742ee2..50adbc8d0c 100644 --- a/lib/core/i18n/en.json +++ b/lib/core/i18n/en.json @@ -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", diff --git a/lib/core/services/alfresco-api.service.ts b/lib/core/services/alfresco-api.service.ts index a9f8c13ded..cd38ebc9e0 100644 --- a/lib/core/services/alfresco-api.service.ts +++ b/lib/core/services/alfresco-api.service.ts @@ -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) {