From 716ce228ce56e74f23bcde52a2af2654ee579859 Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Tue, 13 Jan 2026 14:04:04 +0000 Subject: [PATCH] ACS-8772: Migrate node tooltip to Angular signals (#11498) --- docs/README.md | 1 - docs/core/pipes/node-name-tooltip.pipe.md | 34 --- docs/versionIndex.md | 1 - .../dropdown-breadcrumb.component.ts | 4 +- .../src/lib/content.module.ts | 3 - .../components/document-list.component.ts | 2 +- .../library-name-column.component.spec.ts | 86 ------ .../library-name-column.component.ts | 32 +- .../library-role-column.component.spec.ts | 42 ++- .../library-role-column.component.ts | 48 ++- .../name-column/name-column.component.spec.ts | 164 +++++++++- .../name-column/name-column.component.ts | 20 +- .../trashcan-name-column.component.ts | 39 +-- .../src/lib/document-list/public-api.ts | 3 + .../utils/node-tooltip.utils.spec.ts | 289 ++++++++++++++++++ .../document-list/utils/node-tooltip.utils.ts | 144 +++++++++ .../src/lib/pipes/content-pipe.module.ts | 31 -- lib/content-services/src/lib/pipes/index.ts | 18 -- .../lib/pipes/node-name-tooltip.pipe.spec.ts | 144 --------- .../src/lib/pipes/node-name-tooltip.pipe.ts | 76 ----- .../src/lib/pipes/public-api.ts | 19 -- .../search-date-range.component.ts | 3 +- .../search-properties.component.ts | 7 +- lib/content-services/src/public-api.ts | 1 - .../auth/oidc/oidc-authentication.service.ts | 25 +- .../card-view-boolitem.component.ts | 2 +- .../models/card-view-arrayitem.model.ts | 2 +- .../models/card-view-boolitem.model.ts | 2 +- .../models/card-view-mapitem.model.ts | 2 +- .../datatable-cell.component.ts | 4 +- .../widgets/core/container.model.ts | 2 +- .../widgets/core/form-field.model.ts | 12 +- .../src/lib/models/task-list-sorting.model.ts | 6 +- .../src/lib/people/mock/people-cloud.mock.ts | 2 +- .../models/process-list-sorting.model.ts | 8 +- .../task-list/task-list-cloud.component.ts | 2 +- nx.json | 1 + package.json | 2 +- 38 files changed, 705 insertions(+), 578 deletions(-) delete mode 100644 docs/core/pipes/node-name-tooltip.pipe.md delete mode 100644 lib/content-services/src/lib/document-list/components/library-name-column/library-name-column.component.spec.ts create mode 100644 lib/content-services/src/lib/document-list/utils/node-tooltip.utils.spec.ts create mode 100644 lib/content-services/src/lib/document-list/utils/node-tooltip.utils.ts delete mode 100644 lib/content-services/src/lib/pipes/content-pipe.module.ts delete mode 100644 lib/content-services/src/lib/pipes/index.ts delete mode 100644 lib/content-services/src/lib/pipes/node-name-tooltip.pipe.spec.ts delete mode 100644 lib/content-services/src/lib/pipes/node-name-tooltip.pipe.ts delete mode 100644 lib/content-services/src/lib/pipes/public-api.ts diff --git a/docs/README.md b/docs/README.md index 5a34474b58..30e6b7d1c0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -178,7 +178,6 @@ A collection of Angular components for generic use. | [Full name pipe](core/pipes/full-name.pipe.md) | Joins the first and last name properties from a `UserLike` object into a single string. | [Source](../lib/core/src/lib/pipes/full-name.pipe.ts) | | [Localized Date pipe](core/pipes/localized-date.pipe.md) | Converts a date to a given format and locale. | [Source](../lib/core/src/lib/pipes/localized-date.pipe.ts) | | [Multi Value Pipe](core/pipes/multi-value.pipe.md) | Takes an array of strings and turns it into one string where items are separated by a separator. The default separator applied to the list is the comma , however, you can set your own separator in the params of the pipe. | [Source](../lib/core/src/lib/pipes/multi-value.pipe.ts) | -| [Node Name Tooltip pipe](core/pipes/node-name-tooltip.pipe.md) | Formats the tooltip for a Node. | [Source](../lib/content-services/src/lib/pipes/node-name-tooltip.pipe.ts) | | [Text Highlight pipe](core/pipes/text-highlight.pipe.md) | Adds highlighting to words or sections of text that match a search string. | [Source](../lib/core/src/lib/pipes/text-highlight.pipe.ts) | | [Time Ago pipe](core/pipes/time-ago.pipe.md) | Converts a recent past date into a number of days ago. | [Source](../lib/core/src/lib/pipes/time-ago.pipe.ts) | | [User Initial pipe](core/pipes/user-initial.pipe.md) | Takes the name fields of a `UserLike` object and extracts and formats the initials. | [Source](../lib/core/src/lib/pipes/user-initial.pipe.ts) | diff --git a/docs/core/pipes/node-name-tooltip.pipe.md b/docs/core/pipes/node-name-tooltip.pipe.md deleted file mode 100644 index f19d0c12d9..0000000000 --- a/docs/core/pipes/node-name-tooltip.pipe.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -Title: Node Name Tooltip pipe -Added: v2.0.0 -Status: Active ---- - -# [Node Name Tooltip pipe](../../../lib/content-services/src/lib/pipes/node-name-tooltip.pipe.ts "Defined in node-name-tooltip.pipe.ts") - -Formats the tooltip for a [`Node`](https://github.com/Alfresco/alfresco-js-api/blob/develop/src/api/content-rest-api/docs/Node.md). - -## Basic Usage - - - -```html - - - {{ value }} - - -``` - - - -## Details - -The tooltip is formatted according to the following rules: - -- if the _title_ and _description_ are missing, then the tooltip shows the _name_; -- if the _title_ is missing, then the tooltip shows the _name_ and _description_; -- if the _description_ is missing, then the tooltip shows the _name_ and _title_; -- if _name_ and _title_, _name_ and _description_, or _title_ and _description_ are the same, then only a single line is displayed. diff --git a/docs/versionIndex.md b/docs/versionIndex.md index 041ccda419..dd527b061c 100644 --- a/docs/versionIndex.md +++ b/docs/versionIndex.md @@ -605,7 +605,6 @@ backend services have been tested with each released version of ADF. - [Logout directive](core/directives/logout.directive.md) - [Node delete directive](content-services/directives/node-delete.directive.md) - [Node favorite directive](content-services/directives/node-favorite.directive.md) -- [Node name tooltip pipe](core/pipes/node-name-tooltip.pipe.md) - [Node permission service](content-services/services/node-permission.service.md) - [Node restore directive](content-services/directives/node-restore.directive.md) - [Nodes api service](core/services/nodes-api.service.md) diff --git a/lib/content-services/src/lib/breadcrumb/dropdown-breadcrumb.component.ts b/lib/content-services/src/lib/breadcrumb/dropdown-breadcrumb.component.ts index 3af9adc3bb..8046cb139c 100644 --- a/lib/content-services/src/lib/breadcrumb/dropdown-breadcrumb.component.ts +++ b/lib/content-services/src/lib/breadcrumb/dropdown-breadcrumb.component.ts @@ -33,10 +33,10 @@ import { IconModule } from '@alfresco/adf-core'; }) export class DropdownBreadcrumbComponent extends BreadcrumbComponent implements OnChanges { @ViewChild('dropdown') - dropdown: MatSelect; + declare dropdown: MatSelect; currentNode: PathElement; - previousNodes: PathElement[]; + declare previousNodes: PathElement[]; /** * Calculate the current and previous nodes from the route array diff --git a/lib/content-services/src/lib/content.module.ts b/lib/content-services/src/lib/content.module.ts index 84df9a9518..fcf5b3b2ee 100644 --- a/lib/content-services/src/lib/content.module.ts +++ b/lib/content-services/src/lib/content.module.ts @@ -32,7 +32,6 @@ import { CONTENT_PERMISSION_MANAGER_DIRECTIVES } from './permission-manager/perm import { ASPECT_LIST_DIRECTIVES } from './aspect-list/aspect-list.module'; import { versionCompatibilityFactory } from './version-compatibility/version-compatibility-factory'; import { VersionCompatibilityService } from './version-compatibility/version-compatibility.service'; -import { CONTENT_PIPES } from './pipes/content-pipe.module'; import { contentAuthLoaderFactory } from './auth-loader/content-auth-loader-factory'; import { ContentAuthLoaderService } from './auth-loader/content-auth-loader.service'; import { CategoriesManagementComponent } from './category'; @@ -55,7 +54,6 @@ import { AlfrescoApiLoaderService, createAlfrescoApiInstance } from './api-facto MaterialModule, MatDatetimepickerModule, MatNativeDatetimeModule, - ...CONTENT_PIPES, ...CONTENT_TAG_DIRECTIVES, ...CONTENT_DIALOG_DIRECTIVES, ...CONTENT_SEARCH_DIRECTIVES, @@ -82,7 +80,6 @@ import { AlfrescoApiLoaderService, createAlfrescoApiInstance } from './api-facto providers: [provideTranslations('adf-content-services', 'assets/adf-content-services')], exports: [ MaterialModule, - ...CONTENT_PIPES, ...CONTENT_TAG_DIRECTIVES, ...DOCUMENT_LIST_DIRECTIVES, ...CONTENT_UPLOAD_DIRECTIVES, diff --git a/lib/content-services/src/lib/document-list/components/document-list.component.ts b/lib/content-services/src/lib/document-list/components/document-list.component.ts index 3b9415e007..ae83800fc8 100644 --- a/lib/content-services/src/lib/document-list/components/document-list.component.ts +++ b/lib/content-services/src/lib/document-list/components/document-list.component.ts @@ -125,7 +125,7 @@ export class DocumentListComponent extends DataTableSchema implements OnInit, On DEFAULT_SORTING: DataSorting[] = [new DataSorting('name', 'asc'), new DataSorting('isFolder', 'desc')]; @ContentChild(DataColumnListComponent) - columnList: DataColumnListComponent; + declare columnList: DataColumnListComponent; @ContentChild(CustomLoadingContentTemplateDirective) customLoadingContent: CustomLoadingContentTemplateDirective; diff --git a/lib/content-services/src/lib/document-list/components/library-name-column/library-name-column.component.spec.ts b/lib/content-services/src/lib/document-list/components/library-name-column/library-name-column.component.spec.ts deleted file mode 100644 index d415bc749e..0000000000 --- a/lib/content-services/src/lib/document-list/components/library-name-column/library-name-column.component.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -/*! - * @license - * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { TestBed, ComponentFixture } from '@angular/core/testing'; -import { LibraryNameColumnComponent } from './library-name-column.component'; - -describe('LibraryNameColumnComponent', () => { - let fixture: ComponentFixture; - let component: LibraryNameColumnComponent; - let node; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [LibraryNameColumnComponent] - }); - node = { - id: 'nodeId', - path: { - elements: [] - } - }; - fixture = TestBed.createComponent(LibraryNameColumnComponent); - component = fixture.componentInstance; - }); - - describe('makeLibraryTooltip()', () => { - it('maps tooltip to description', () => { - node.description = 'description'; - const tooltip = component.makeLibraryTooltip(node); - - expect(tooltip).toBe(node.description); - }); - - it('maps tooltip to description', () => { - node.title = 'title'; - const tooltip = component.makeLibraryTooltip(node); - - expect(tooltip).toBe(node.title); - }); - - it('sets tooltip to empty string', () => { - const tooltip = component.makeLibraryTooltip(node); - - expect(tooltip).toBe(''); - }); - }); - - describe('makeLibraryTitle()', () => { - it('sets title with id when duplicate nodes title exists in list', () => { - node.title = 'title'; - - const rows = [{ node: { entry: { id: 'some-id', title: 'title' } } }] as any[]; - - const title = component.makeLibraryTitle(node, rows); - expect(title).toContain('nodeId'); - }); - - it('sets title when no duplicate nodes title exists in list', () => { - node.title = 'title'; - - const rows = [ - { - node: { entry: { id: 'some-id', title: 'title-some-id' } } - } - ] as any[]; - - const title = component.makeLibraryTitle(node, rows); - - expect(title).toBe('title'); - }); - }); -}); diff --git a/lib/content-services/src/lib/document-list/components/library-name-column/library-name-column.component.ts b/lib/content-services/src/lib/document-list/components/library-name-column/library-name-column.component.ts index cc1176b4f8..e3383d6b88 100644 --- a/lib/content-services/src/lib/document-list/components/library-name-column/library-name-column.component.ts +++ b/lib/content-services/src/lib/document-list/components/library-name-column/library-name-column.component.ts @@ -16,13 +16,14 @@ */ import { ChangeDetectionStrategy, Component, DestroyRef, ElementRef, inject, Input, OnInit, ViewEncapsulation } from '@angular/core'; -import { NodeEntry, Site } from '@alfresco/js-api'; +import { NodeEntry } from '@alfresco/js-api'; import { ShareDataRow } from '../../data/share-data-row.model'; import { NodesApiService } from '../../../common/services/nodes-api.service'; import { BehaviorSubject } from 'rxjs'; import { AsyncPipe } from '@angular/common'; import { TranslatePipe } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { NodeTooltipUtils } from '../../utils/node-tooltip.utils'; @Component({ selector: 'adf-library-name-column', @@ -62,7 +63,10 @@ export class LibraryNameColumnComponent implements OnInit { private readonly destroyRef = inject(DestroyRef); - constructor(private element: ElementRef, private nodesApiService: NodesApiService) {} + constructor( + private element: ElementRef, + private nodesApiService: NodesApiService + ) {} ngOnInit() { this.updateValue(); @@ -84,8 +88,9 @@ export class LibraryNameColumnComponent implements OnInit { this.node = this.context.row.node; const rows: Array = this.context.data.rows || []; if (this.node?.entry) { - this.displayText$.next(this.makeLibraryTitle(this.node.entry as any, rows)); - this.displayTooltip$.next(this.makeLibraryTooltip(this.node.entry)); + const allEntries = rows.map((row: ShareDataRow) => row.node.entry); + this.displayText$.next(NodeTooltipUtils.getLibraryTitle(this.node.entry, allEntries)); + this.displayTooltip$.next(NodeTooltipUtils.getLibraryTooltip(this.node)); } } @@ -99,23 +104,4 @@ export class LibraryNameColumnComponent implements OnInit { }) ); } - - makeLibraryTooltip(library: any): string { - const { description, title } = library; - - return description || title || ''; - } - - makeLibraryTitle(library: Site, rows: Array): string { - const entries = rows.map((row: ShareDataRow) => row.node.entry); - const { title, id } = library; - - let isDuplicate = false; - - if (entries) { - isDuplicate = entries.some((entry: any) => entry.id !== id && entry.title === title); - } - - return isDuplicate ? `${title} (${id})` : `${title}`; - } } diff --git a/lib/content-services/src/lib/document-list/components/library-role-column/library-role-column.component.spec.ts b/lib/content-services/src/lib/document-list/components/library-role-column/library-role-column.component.spec.ts index 11fb4fdb4f..80175a2692 100644 --- a/lib/content-services/src/lib/document-list/components/library-role-column/library-role-column.component.spec.ts +++ b/lib/content-services/src/lib/document-list/components/library-role-column/library-role-column.component.spec.ts @@ -35,11 +35,10 @@ describe('LibraryRoleColumnComponent', () => { row: { node: { entry: { role: 'SiteManager' } } } }; - let value = ''; - component.displayText$.subscribe((val) => (value = val)); - + component.ngOnInit(); fixture.detectChanges(); - expect(value).toBe('LIBRARY.ROLE.MANAGER'); + + expect(component.displayText()).toBe('LIBRARY.ROLE.MANAGER'); }); it('should render Collaborator', () => { @@ -47,11 +46,10 @@ describe('LibraryRoleColumnComponent', () => { row: { node: { entry: { role: 'SiteCollaborator' } } } }; - let value = ''; - component.displayText$.subscribe((val) => (value = val)); - + component.ngOnInit(); fixture.detectChanges(); - expect(value).toBe('LIBRARY.ROLE.COLLABORATOR'); + + expect(component.displayText()).toBe('LIBRARY.ROLE.COLLABORATOR'); }); it('should render Contributor', () => { @@ -59,11 +57,10 @@ describe('LibraryRoleColumnComponent', () => { row: { node: { entry: { role: 'SiteContributor' } } } }; - let value = ''; - component.displayText$.subscribe((val) => (value = val)); - + component.ngOnInit(); fixture.detectChanges(); - expect(value).toBe('LIBRARY.ROLE.CONTRIBUTOR'); + + expect(component.displayText()).toBe('LIBRARY.ROLE.CONTRIBUTOR'); }); it('should render Consumer', () => { @@ -71,11 +68,10 @@ describe('LibraryRoleColumnComponent', () => { row: { node: { entry: { role: 'SiteConsumer' } } } }; - let value = ''; - component.displayText$.subscribe((val) => (value = val)); - + component.ngOnInit(); fixture.detectChanges(); - expect(value).toBe('LIBRARY.ROLE.CONSUMER'); + + expect(component.displayText()).toBe('LIBRARY.ROLE.CONSUMER'); }); it('should not render text for unknown', () => { @@ -83,11 +79,10 @@ describe('LibraryRoleColumnComponent', () => { row: { node: { entry: { role: 'ROLE' } } } }; - let value = ''; - component.displayText$.subscribe((val) => (value = val)); - + component.ngOnInit(); fixture.detectChanges(); - expect(value).toBe('LIBRARY.ROLE.NONE'); + + expect(component.displayText()).toBe('LIBRARY.ROLE.NONE'); }); it('should take role from obj when node entry role is not provided', () => { @@ -98,10 +93,9 @@ describe('LibraryRoleColumnComponent', () => { } }; - let value = ''; - component.displayText$.subscribe((val) => (value = val)); - + component.ngOnInit(); fixture.detectChanges(); - expect(value).toBe('LIBRARY.ROLE.MANAGER'); + + expect(component.displayText()).toBe('LIBRARY.ROLE.MANAGER'); }); }); diff --git a/lib/content-services/src/lib/document-list/components/library-role-column/library-role-column.component.ts b/lib/content-services/src/lib/document-list/components/library-role-column/library-role-column.component.ts index a6ce11f193..b2e44f5590 100644 --- a/lib/content-services/src/lib/document-list/components/library-role-column/library-role-column.component.ts +++ b/lib/content-services/src/lib/document-list/components/library-role-column/library-role-column.component.ts @@ -15,21 +15,19 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, DestroyRef, inject, Input, OnInit, ViewEncapsulation } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject, Input, OnInit, signal, ViewEncapsulation } from '@angular/core'; import { Site } from '@alfresco/js-api'; import { ShareDataRow } from '../../data/share-data-row.model'; import { NodesApiService } from '../../../common/services/nodes-api.service'; -import { AsyncPipe } from '@angular/common'; import { TranslatePipe } from '@ngx-translate/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'adf-library-role-column', - imports: [AsyncPipe, TranslatePipe], + imports: [TranslatePipe], template: ` - - {{ displayText$ | async | translate }} + + {{ displayText() | translate }} `, changeDetection: ChangeDetectionStrategy.OnPush, @@ -40,7 +38,23 @@ export class LibraryRoleColumnComponent implements OnInit { @Input({ required: true }) context: any; - displayText$ = new BehaviorSubject(''); + private readonly role = signal(undefined); + + readonly displayText = computed(() => { + const roleValue = this.role(); + switch (roleValue) { + case Site.RoleEnum.SiteManager: + return 'LIBRARY.ROLE.MANAGER'; + case Site.RoleEnum.SiteCollaborator: + return 'LIBRARY.ROLE.COLLABORATOR'; + case Site.RoleEnum.SiteContributor: + return 'LIBRARY.ROLE.CONTRIBUTOR'; + case Site.RoleEnum.SiteConsumer: + return 'LIBRARY.ROLE.CONSUMER'; + default: + return 'LIBRARY.ROLE.NONE'; + } + }); private readonly destroyRef = inject(DestroyRef); @@ -63,23 +77,7 @@ export class LibraryRoleColumnComponent implements OnInit { } protected updateValue() { - const role = this.context.row.node?.entry.role ?? this.context.row.obj.role; - switch (role) { - case Site.RoleEnum.SiteManager: - this.displayText$.next('LIBRARY.ROLE.MANAGER'); - break; - case Site.RoleEnum.SiteCollaborator: - this.displayText$.next('LIBRARY.ROLE.COLLABORATOR'); - break; - case Site.RoleEnum.SiteContributor: - this.displayText$.next('LIBRARY.ROLE.CONTRIBUTOR'); - break; - case Site.RoleEnum.SiteConsumer: - this.displayText$.next('LIBRARY.ROLE.CONSUMER'); - break; - default: - this.displayText$.next('LIBRARY.ROLE.NONE'); - break; - } + const roleValue = this.context.row.node?.entry.role ?? this.context.row.obj.role; + this.role.set(roleValue); } } diff --git a/lib/content-services/src/lib/document-list/components/name-column/name-column.component.spec.ts b/lib/content-services/src/lib/document-list/components/name-column/name-column.component.spec.ts index 9177295d51..0bfee3bedd 100644 --- a/lib/content-services/src/lib/document-list/components/name-column/name-column.component.spec.ts +++ b/lib/content-services/src/lib/document-list/components/name-column/name-column.component.spec.ts @@ -18,8 +18,13 @@ import { NameColumnComponent } from './name-column.component'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { skip } from 'rxjs/operators'; +import { NodeEntry } from '@alfresco/js-api'; describe('NameColumnComponent', () => { + const nodeName = 'node-name'; + const nodeTitle = 'node-title'; + const nodeDescription = 'node-description'; + let fixture: ComponentFixture; let context: any; let component: NameColumnComponent; @@ -42,22 +47,159 @@ describe('NameColumnComponent', () => { component.context = context; }); - it('should set the display value based on default key', (done) => { - component.displayText$.pipe(skip(1)).subscribe((value) => { - expect(value).toBe('name'); - done(); + describe('Display Text', () => { + it('should set the display value based on default key', (done) => { + component.displayText$.pipe(skip(1)).subscribe((value) => { + expect(value).toBe('name'); + done(); + }); + + component.ngOnInit(); }); - component.ngOnInit(); + it('should set the display value based on the custom key', (done) => { + component.key = 'title'; + component.displayText$.pipe(skip(1)).subscribe((value) => { + expect(value).toBe('title'); + done(); + }); + + component.ngOnInit(); + }); }); - it('should set the display value based on the custom key', (done) => { - component.key = 'title'; - component.displayText$.pipe(skip(1)).subscribe((value) => { - expect(value).toBe('title'); - done(); + describe('Tooltip Logic', () => { + it('should return null when missing node', () => { + component.context.row.node = null; + component.ngOnInit(); + expect(component.tooltip()).toBe(null); }); - component.ngOnInit(); + it('should return null when missing node entry', () => { + component.context.row.node = {} as any; + component.ngOnInit(); + expect(component.tooltip()).toBe(null); + }); + + it('should use title and description when all fields present', () => { + const node: any = { + entry: { + name: nodeName, + properties: { + 'cm:title': nodeTitle, + 'cm:description': nodeDescription + } + } + }; + component.context.row.node = node; + component.ngOnInit(); + const tooltip = component.tooltip(); + expect(tooltip).toBe(`${nodeTitle}\n${nodeDescription}`); + }); + + it('should use name when other properties are missing', () => { + const node = { + entry: { + name: nodeName + } + } as NodeEntry; + component.context.row.node = node; + component.ngOnInit(); + const tooltip = component.tooltip(); + expect(tooltip).toBe(nodeName); + }); + + it('should display name when title and description are missing', () => { + const node: any = { + entry: { + name: nodeName, + properties: {} + } + }; + component.context.row.node = node; + component.ngOnInit(); + const tooltip = component.tooltip(); + expect(tooltip).toBe(nodeName); + }); + + it('should use name and description when title is missing', () => { + const node: any = { + entry: { + name: nodeName, + properties: { + 'cm:title': null, + 'cm:description': nodeDescription + } + } + }; + component.context.row.node = node; + component.ngOnInit(); + const tooltip = component.tooltip(); + expect(tooltip).toBe(`${nodeName}\n${nodeDescription}`); + }); + + it('should use name and title when description is missing', () => { + const node: any = { + entry: { + name: nodeName, + properties: { + 'cm:title': nodeTitle, + 'cm:description': null + } + } + }; + component.context.row.node = node; + component.ngOnInit(); + const tooltip = component.tooltip(); + expect(tooltip).toBe(`${nodeName}\n${nodeTitle}`); + }); + + it('should use name if name and description are the same', () => { + const node: any = { + entry: { + name: nodeName, + properties: { + 'cm:title': null, + 'cm:description': nodeName + } + } + }; + component.context.row.node = node; + component.ngOnInit(); + const tooltip = component.tooltip(); + expect(tooltip).toBe(nodeName); + }); + + it('should use name if name and title are the same', () => { + const node: any = { + entry: { + name: nodeName, + properties: { + 'cm:title': nodeName, + 'cm:description': null + } + } + }; + component.context.row.node = node; + component.ngOnInit(); + const tooltip = component.tooltip(); + expect(tooltip).toBe(nodeName); + }); + + it('should use name if all values are the same', () => { + const node: any = { + entry: { + name: nodeName, + properties: { + 'cm:title': nodeName, + 'cm:description': nodeName + } + } + }; + component.context.row.node = node; + component.ngOnInit(); + const tooltip = component.tooltip(); + expect(tooltip).toBe(nodeName); + }); }); }); diff --git a/lib/content-services/src/lib/document-list/components/name-column/name-column.component.ts b/lib/content-services/src/lib/document-list/components/name-column/name-column.component.ts index 9b01179523..e0fc759824 100644 --- a/lib/content-services/src/lib/document-list/components/name-column/name-column.component.ts +++ b/lib/content-services/src/lib/document-list/components/name-column/name-column.component.ts @@ -15,19 +15,19 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, DestroyRef, ElementRef, inject, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, DestroyRef, ElementRef, inject, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { NodeEntry } from '@alfresco/js-api'; import { BehaviorSubject } from 'rxjs'; import { NodesApiService } from '../../../common/services/nodes-api.service'; import { ShareDataRow } from '../../data/share-data-row.model'; import { AsyncPipe } from '@angular/common'; import { TranslatePipe } from '@ngx-translate/core'; -import { NodeNameTooltipPipe } from '../../../pipes/node-name-tooltip.pipe'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { NodeTooltipUtils } from '../../utils/node-tooltip.utils'; @Component({ selector: 'adf-name-column', - imports: [AsyncPipe, TranslatePipe, NodeNameTooltipPipe], + imports: [AsyncPipe, TranslatePipe], template: ` (''); node: NodeEntry; + readonly tooltip = computed(() => NodeTooltipUtils.getNodeTooltip(this.node)); + private readonly destroyRef = inject(DestroyRef); - constructor(private element: ElementRef, private nodesApiService: NodesApiService) {} + constructor( + private element: ElementRef, + private nodesApiService: NodesApiService + ) {} ngOnInit() { this.updateValue(); @@ -85,8 +91,8 @@ export class NameColumnComponent implements OnInit { this.node = this.context.row.node; if (this.node?.entry) { - const displayText = this.context.row.getValue(this.key); - this.displayText$.next(displayText || this.node.entry.id); + const displayValue = this.context.row.getValue(this.key); + this.displayText$.next(displayValue || this.node.entry.id); } } diff --git a/lib/content-services/src/lib/document-list/components/trashcan-name-column/trashcan-name-column.component.ts b/lib/content-services/src/lib/document-list/components/trashcan-name-column/trashcan-name-column.component.ts index 3b3b90ca86..3a80b21ea4 100644 --- a/lib/content-services/src/lib/document-list/components/trashcan-name-column/trashcan-name-column.component.ts +++ b/lib/content-services/src/lib/document-list/components/trashcan-name-column/trashcan-name-column.component.ts @@ -15,36 +15,29 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { NodeEntry } from '@alfresco/js-api'; import { ShareDataRow } from '../../data/share-data-row.model'; -import { CommonModule } from '@angular/common'; -import { NodeNameTooltipPipe } from '../../../pipes/node-name-tooltip.pipe'; +import { NodeTooltipUtils } from '../../utils/node-tooltip.utils'; @Component({ selector: 'adf-trashcan-name-column', - imports: [CommonModule, NodeNameTooltipPipe], - template: ` - - {{ displayText }} - - - {{ displayText }} - - `, + template: `{{ displayText }}`, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { class: 'adf-datatable-content-cell adf-trashcan-name-column' } }) export class TrashcanNameColumnComponent implements OnInit { @Input({ required: true }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any context: any; isLibrary = false; displayText: string; - displayTooltip: string; node: NodeEntry; + readonly tooltip = computed(() => (this.isLibrary ? NodeTooltipUtils.getLibraryTooltip(this.node) : NodeTooltipUtils.getNodeTooltip(this.node))); + ngOnInit() { this.node = this.context.row.node; const rows: Array = this.context.data.rows || []; @@ -53,27 +46,11 @@ export class TrashcanNameColumnComponent implements OnInit { this.isLibrary = this.node.entry.nodeType === 'st:site'; if (this.isLibrary) { - const { properties } = this.node.entry; - - this.displayText = this.makeLibraryTitle(this.node.entry, rows); - this.displayTooltip = properties['cm:description'] || properties['cm:title']; + const allEntries = rows.map((row) => row.node.entry); + this.displayText = NodeTooltipUtils.getLibraryTitle(this.node.entry, allEntries); } else { this.displayText = this.node.entry.name || this.node.entry.id; } } } - - makeLibraryTitle(library: any, rows: Array): string { - const entries = rows.map((r: ShareDataRow) => r.node.entry); - const { id } = library; - const title = library.properties['cm:title']; - - let isDuplicate = false; - - if (entries) { - isDuplicate = entries.some((entry: any) => entry.id !== id && entry.properties['cm:title'] === title); - } - - return isDuplicate ? `${library.properties['cm:title']} (${library.name})` : `${library.properties['cm:title']}`; - } } diff --git a/lib/content-services/src/lib/document-list/public-api.ts b/lib/content-services/src/lib/document-list/public-api.ts index 6694b4b950..8fec33d7ba 100644 --- a/lib/content-services/src/lib/document-list/public-api.ts +++ b/lib/content-services/src/lib/document-list/public-api.ts @@ -49,4 +49,7 @@ export * from './models/node-action.enum'; export * from './interfaces/document-list-loader.interface'; +// utils +export * from './utils/node-tooltip.utils'; + export * from './document-list.module'; diff --git a/lib/content-services/src/lib/document-list/utils/node-tooltip.utils.spec.ts b/lib/content-services/src/lib/document-list/utils/node-tooltip.utils.spec.ts new file mode 100644 index 0000000000..2049382d7b --- /dev/null +++ b/lib/content-services/src/lib/document-list/utils/node-tooltip.utils.spec.ts @@ -0,0 +1,289 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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 { NodeEntry } from '@alfresco/js-api'; +import { NodeTooltipUtils } from './node-tooltip.utils'; + +describe('NodeTooltipUtils', () => { + const nodeName = 'node-name'; + const nodeTitle = 'node-title'; + const nodeDescription = 'node-description'; + + describe('getNodeTooltip', () => { + it('should return null when node is null', () => { + const result = NodeTooltipUtils.getNodeTooltip(null as any); + expect(result).toBe(null); + }); + + it('should return null when node entry is missing', () => { + const node = {} as NodeEntry; + const result = NodeTooltipUtils.getNodeTooltip(node); + expect(result).toBe(null); + }); + + it('should return name only when title and description are missing', () => { + const node: any = { + entry: { + name: nodeName + } + }; + const result = NodeTooltipUtils.getNodeTooltip(node); + expect(result).toBe(nodeName); + }); + + it('should return name only when properties object is empty', () => { + const node: any = { + entry: { + name: nodeName, + properties: {} + } + }; + const result = NodeTooltipUtils.getNodeTooltip(node); + expect(result).toBe(nodeName); + }); + + it('should return title and description when both are present', () => { + const node: any = { + entry: { + name: nodeName, + properties: { + 'cm:title': nodeTitle, + 'cm:description': nodeDescription + } + } + }; + const result = NodeTooltipUtils.getNodeTooltip(node); + expect(result).toBe(`${nodeTitle}\n${nodeDescription}`); + }); + + it('should return name and title when only title is present', () => { + const node: any = { + entry: { + name: nodeName, + properties: { + 'cm:title': nodeTitle + } + } + }; + const result = NodeTooltipUtils.getNodeTooltip(node); + expect(result).toBe(`${nodeName}\n${nodeTitle}`); + }); + + it('should return name and description when only description is present', () => { + const node: any = { + entry: { + name: nodeName, + properties: { + 'cm:description': nodeDescription + } + } + }; + const result = NodeTooltipUtils.getNodeTooltip(node); + expect(result).toBe(`${nodeName}\n${nodeDescription}`); + }); + + it('should remove case-insensitive duplicates', () => { + const node: any = { + entry: { + name: 'Same-Name', + properties: { + 'cm:title': 'same-name' + } + } + }; + const result = NodeTooltipUtils.getNodeTooltip(node); + expect(result).toBe('Same-Name'); + }); + + it('should preserve order when removing duplicates', () => { + const node: any = { + entry: { + name: 'First', + properties: { + 'cm:title': 'Second', + 'cm:description': 'FIRST' + } + } + }; + const result = NodeTooltipUtils.getNodeTooltip(node); + // When both title and description exist, shows "title\ndescription" + // 'FIRST' is a case-insensitive duplicate of 'First', but since we're using title+description logic, + // the duplicate check happens between 'Second' and 'FIRST', so 'FIRST' remains + expect(result).toBe('Second\nFIRST'); + }); + }); + + describe('getLibraryTooltip', () => { + it('should return empty string when node is null', () => { + const result = NodeTooltipUtils.getLibraryTooltip(null as any); + expect(result).toBe(''); + }); + + it('should return empty string when node entry is missing', () => { + const node = {} as NodeEntry; + const result = NodeTooltipUtils.getLibraryTooltip(node); + expect(result).toBe(''); + }); + + it('should return description when available', () => { + const node: any = { + entry: { + properties: { + 'cm:title': 'Library Title', + 'cm:description': 'Library Description' + } + } + }; + const result = NodeTooltipUtils.getLibraryTooltip(node); + expect(result).toBe('Library Description'); + }); + + it('should return title when description is not available', () => { + const node: any = { + entry: { + properties: { + 'cm:title': 'Library Title' + } + } + }; + const result = NodeTooltipUtils.getLibraryTooltip(node); + expect(result).toBe('Library Title'); + }); + + it('should return empty string when neither description nor title are available', () => { + const node: any = { + entry: { + properties: {} + } + }; + const result = NodeTooltipUtils.getLibraryTooltip(node); + expect(result).toBe(''); + }); + + it('should handle direct properties (description and title)', () => { + const node: any = { + entry: { + description: 'Direct Description', + title: 'Direct Title' + } + }; + const result = NodeTooltipUtils.getLibraryTooltip(node); + expect(result).toBe('Direct Description'); + }); + + it('should prefer direct description over cm:description', () => { + const node: any = { + entry: { + description: 'Direct Description', + properties: { + 'cm:description': 'CM Description' + } + } + }; + const result = NodeTooltipUtils.getLibraryTooltip(node); + expect(result).toBe('Direct Description'); + }); + + it('should fall back to cm:description when direct description is not available', () => { + const node: any = { + entry: { + properties: { + 'cm:description': 'CM Description' + } + } + }; + const result = NodeTooltipUtils.getLibraryTooltip(node); + expect(result).toBe('CM Description'); + }); + }); + + describe('getLibraryTitle', () => { + it('should return empty string when library is null', () => { + const result = NodeTooltipUtils.getLibraryTitle(null, []); + expect(result).toBe(''); + }); + + it('should return title when no duplicates exist', () => { + const library = { id: 'lib1', title: 'Library Title' }; + const allEntries = [ + { id: 'lib2', title: 'Other Title' }, + { id: 'lib3', title: 'Another Title' } + ]; + const result = NodeTooltipUtils.getLibraryTitle(library, allEntries); + expect(result).toBe('Library Title'); + }); + + it('should append ID when duplicate title exists', () => { + const library = { id: 'lib1', title: 'Duplicate Title' }; + const allEntries = [ + { id: 'lib2', title: 'Duplicate Title' }, + { id: 'lib3', title: 'Other Title' } + ]; + const result = NodeTooltipUtils.getLibraryTitle(library, allEntries); + expect(result).toBe('Duplicate Title (lib1)'); + }); + + it('should append name when duplicate title exists and name is available', () => { + const library = { id: 'lib1', name: 'library-name', title: 'Duplicate Title' }; + const allEntries = [{ id: 'lib2', title: 'Duplicate Title' }]; + const result = NodeTooltipUtils.getLibraryTitle(library, allEntries); + expect(result).toBe('Duplicate Title (library-name)'); + }); + + it('should handle cm:title properties', () => { + const library = { + id: 'lib1', + name: 'library-name', + properties: { 'cm:title': 'CM Title' } + }; + const allEntries = [{ id: 'lib2', properties: { 'cm:title': 'CM Title' } }]; + const result = NodeTooltipUtils.getLibraryTitle(library, allEntries); + expect(result).toBe('CM Title (library-name)'); + }); + + it('should not append suffix when no duplicate exists', () => { + const library = { id: 'lib1', name: 'library-name', title: 'Unique Title' }; + const allEntries = [{ id: 'lib2', title: 'Other Title' }]; + const result = NodeTooltipUtils.getLibraryTitle(library, allEntries); + expect(result).toBe('Unique Title'); + }); + + it('should return name when title is missing', () => { + const library = { id: 'lib1', name: 'library-name' }; + const allEntries = []; + const result = NodeTooltipUtils.getLibraryTitle(library, allEntries); + // Returns name when available, otherwise id + expect(result).toBe('library-name'); + }); + + it('should handle empty entries array', () => { + const library = { id: 'lib1', title: 'Library Title' }; + const result = NodeTooltipUtils.getLibraryTitle(library, []); + expect(result).toBe('Library Title'); + }); + + it('should not consider same library as duplicate', () => { + const library = { id: 'lib1', title: 'Library Title' }; + const allEntries = [ + { id: 'lib1', title: 'Library Title' }, + { id: 'lib2', title: 'Other Title' } + ]; + const result = NodeTooltipUtils.getLibraryTitle(library, allEntries); + expect(result).toBe('Library Title'); + }); + }); +}); diff --git a/lib/content-services/src/lib/document-list/utils/node-tooltip.utils.ts b/lib/content-services/src/lib/document-list/utils/node-tooltip.utils.ts new file mode 100644 index 0000000000..ee13ff2d78 --- /dev/null +++ b/lib/content-services/src/lib/document-list/utils/node-tooltip.utils.ts @@ -0,0 +1,144 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * 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 { NodeEntry } from '@alfresco/js-api'; + +/** + * Utility class for generating node and library tooltips based on node properties + */ +export class NodeTooltipUtils { + /** + * Generates a tooltip string for a node based on its name, title, and description properties. + * The tooltip logic follows these rules: + * - If both title and description exist: shows "title\ndescription" + * - If only title exists: shows "name\ntitle" + * - If only description exists: shows "name\ndescription" + * - If neither exists: shows "name" + * - Removes case-insensitive duplicates while preserving order + * + * @param node - The node entry to generate tooltip for + * @returns The tooltip string with newline-separated lines, or null if node is invalid + */ + static getNodeTooltip(node: NodeEntry): string | null { + if (!node?.entry) { + return null; + } + + const { + entry: { properties, name } + } = node; + + const title = properties?.['cm:title']; + const description = properties?.['cm:description']; + + // Build lines array based on available properties + const lines: string[] = []; + + // Determine first line: title if available and different from name, otherwise name + if (title && description) { + lines.push(title, description); + } else if (title) { + lines.push(name, title); + } else if (description) { + lines.push(name, description); + } else { + lines.push(name); + } + + // Remove case-insensitive duplicates while preserving order + return this.removeDuplicates(lines).join('\n'); + } + + /** + * Generates a tooltip string for a library (site) node. + * Returns description if available, otherwise title, otherwise empty string. + * + * @param node - The node entry to generate tooltip for + * @returns The tooltip string, or empty string if no description or title is available + */ + static getLibraryTooltip(node: NodeEntry): string { + if (!node?.entry) { + return ''; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { properties, description, title } = node.entry as any; + + // Check both direct properties and cm: properties for compatibility + const desc = description || properties?.['cm:description']; + const ttl = title || properties?.['cm:title']; + + return desc || ttl || ''; + } + + /** + * Generates a display title for a library (site) node. + * If there are duplicate titles in the list, appends the library ID/name in parentheses. + * + * @param library - The library entry object + * @param allEntries - Array of all entries to check for duplicates + * @returns The display title, with ID/name appended if duplicate exists + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static getLibraryTitle(library: any, allEntries: any[]): string { + if (!library) { + return ''; + } + + // Support both direct properties and cm: properties + const libraryId = library.id; + const libraryName = library.name; + const libraryTitle = library.title || library.properties?.['cm:title']; + + if (!libraryTitle) { + return libraryName || libraryId || ''; + } + + // Check if there are duplicate titles in the list + let isDuplicate = false; + + if (allEntries && allEntries.length > 0) { + isDuplicate = allEntries.some((entry) => { + const entryId = entry.id; + const entryTitle = entry.title || entry.properties?.['cm:title']; + return entryId !== libraryId && entryTitle === libraryTitle; + }); + } + + // If duplicate, append the ID or name in parentheses + const suffix = libraryName || libraryId; + return isDuplicate && suffix ? `${libraryTitle} (${suffix})` : libraryTitle; + } + + /** + * Removes case-insensitive duplicate strings from an array while preserving order + * + * @param lines - Array of strings to deduplicate + * @returns Array with duplicates removed + */ + private static removeDuplicates(lines: string[]): string[] { + const seen = new Set(); + return lines.filter((line) => { + const lowerLine = line.toLowerCase(); + if (seen.has(lowerLine)) { + return false; + } + seen.add(lowerLine); + return true; + }); + } +} diff --git a/lib/content-services/src/lib/pipes/content-pipe.module.ts b/lib/content-services/src/lib/pipes/content-pipe.module.ts deleted file mode 100644 index 3ae4bec186..0000000000 --- a/lib/content-services/src/lib/pipes/content-pipe.module.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*! - * @license - * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * 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 { NgModule } from '@angular/core'; -import { NodeNameTooltipPipe } from './node-name-tooltip.pipe'; - -export const CONTENT_PIPES = [NodeNameTooltipPipe] as const; - -/** - * @deprecated Use the individual pipe modules instead. - */ -@NgModule({ - imports: [...CONTENT_PIPES], - providers: [...CONTENT_PIPES], - exports: [...CONTENT_PIPES] -}) -export class ContentPipeModule {} diff --git a/lib/content-services/src/lib/pipes/index.ts b/lib/content-services/src/lib/pipes/index.ts deleted file mode 100644 index f22da90e09..0000000000 --- a/lib/content-services/src/lib/pipes/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * @license - * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * 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/pipes/node-name-tooltip.pipe.spec.ts b/lib/content-services/src/lib/pipes/node-name-tooltip.pipe.spec.ts deleted file mode 100644 index 95b3dd34a3..0000000000 --- a/lib/content-services/src/lib/pipes/node-name-tooltip.pipe.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -/*! - * @license - * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * 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 { NodeEntry } from '@alfresco/js-api'; -import { NodeNameTooltipPipe } from './node-name-tooltip.pipe'; - -describe('NodeNameTooltipPipe', () => { - const nodeName = 'node-name'; - const nodeTitle = 'node-title'; - const nodeDescription = 'node-description'; - - let pipe: NodeNameTooltipPipe; - - beforeEach(() => { - pipe = new NodeNameTooltipPipe(); - }); - - it('should not transform when missing node', () => { - expect(pipe.transform(null)).toBe(null); - }); - - it('should not transform when missing node entry', () => { - expect(pipe.transform({} as any)).toBe(null); - }); - - it('should use title and description when all fields present', () => { - const node: any = { - entry: { - name: nodeName, - properties: { - 'cm:title': nodeTitle, - 'cm:description': nodeDescription - } - } - }; - const tooltip = pipe.transform(node); - expect(tooltip).toBe(`${nodeTitle}\n${nodeDescription}`); - }); - - it('should use name when other properties are missing', () => { - const node = { - entry: { - name: nodeName - } - } as NodeEntry; - const tooltip = pipe.transform(node); - expect(tooltip).toBe(nodeName); - }); - - it('should display name when title and description are missing', () => { - const node: any = { - entry: { - name: nodeName, - properties: {} - } - }; - const tooltip = pipe.transform(node); - expect(tooltip).toBe(nodeName); - }); - - it('should use name and description when title is missing', () => { - const node: any = { - entry: { - name: nodeName, - properties: { - 'cm:title': null, - 'cm:description': nodeDescription - } - } - }; - const tooltip = pipe.transform(node); - expect(tooltip).toBe(`${nodeName}\n${nodeDescription}`); - }); - - it('should use name and title when description is missing', () => { - const node: any = { - entry: { - name: nodeName, - properties: { - 'cm:title': nodeTitle, - 'cm:description': null - } - } - }; - const tooltip = pipe.transform(node); - expect(tooltip).toBe(`${nodeName}\n${nodeTitle}`); - }); - - it('should use name if name and description are the same', () => { - const node: any = { - entry: { - name: nodeName, - properties: { - 'cm:title': null, - 'cm:description': nodeName - } - } - }; - const tooltip = pipe.transform(node); - expect(tooltip).toBe(nodeName); - }); - - it('should use name if name and title are the same', () => { - const node: any = { - entry: { - name: nodeName, - properties: { - 'cm:title': nodeName, - 'cm:description': null - } - } - }; - const tooltip = pipe.transform(node); - expect(tooltip).toBe(nodeName); - }); - - it('should use name if all values are the same', () => { - const node: any = { - entry: { - name: nodeName, - properties: { - 'cm:title': nodeName, - 'cm:description': nodeName - } - } - }; - const tooltip = pipe.transform(node); - expect(tooltip).toBe(nodeName); - }); -}); diff --git a/lib/content-services/src/lib/pipes/node-name-tooltip.pipe.ts b/lib/content-services/src/lib/pipes/node-name-tooltip.pipe.ts deleted file mode 100644 index 5db4959969..0000000000 --- a/lib/content-services/src/lib/pipes/node-name-tooltip.pipe.ts +++ /dev/null @@ -1,76 +0,0 @@ -/*! - * @license - * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * 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 { Pipe, PipeTransform } from '@angular/core'; -import { NodeEntry } from '@alfresco/js-api'; - -@Pipe({ - name: 'adfNodeNameTooltip' -}) -export class NodeNameTooltipPipe implements PipeTransform { - transform(node: NodeEntry): string { - if (node) { - return this.getNodeTooltip(node); - } - return null; - } - - private containsLine(lines: string[], line: string): boolean { - return lines.some((item: string) => item.toLowerCase() === line.toLowerCase()); - } - - private removeDuplicateLines(lines: string[]): string[] { - const reducer = (acc: string[], line: string): string[] => { - if (!this.containsLine(acc, line)) { - acc.push(line); - } - return acc; - }; - - return lines.reduce(reducer, []); - } - - private getNodeTooltip(node: NodeEntry): string { - if (!node?.entry) { - return null; - } - - const { - entry: { properties, name } - } = node; - const lines = [name]; - - if (properties) { - const { 'cm:title': title, 'cm:description': description } = properties; - - if (title && description) { - lines[0] = title; - lines[1] = description; - } - - if (title) { - lines[1] = title; - } - - if (description) { - lines[1] = description; - } - } - - return this.removeDuplicateLines(lines).join(`\n`); - } -} diff --git a/lib/content-services/src/lib/pipes/public-api.ts b/lib/content-services/src/lib/pipes/public-api.ts deleted file mode 100644 index c6280cde7e..0000000000 --- a/lib/content-services/src/lib/pipes/public-api.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*! - * @license - * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. - * - * 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 './node-name-tooltip.pipe'; -export * from './content-pipe.module'; diff --git a/lib/content-services/src/lib/search/components/search-date-range-tabbed/search-date-range/search-date-range.component.ts b/lib/content-services/src/lib/search/components/search-date-range-tabbed/search-date-range/search-date-range.component.ts index a338a5842c..da368594a4 100644 --- a/lib/content-services/src/lib/search/components/search-date-range-tabbed/search-date-range/search-date-range.component.ts +++ b/lib/content-services/src/lib/search/components/search-date-range-tabbed/search-date-range/search-date-range.component.ts @@ -79,6 +79,8 @@ export class SearchDateRangeComponent implements OnInit { @Output() valid = new EventEmitter(); + private readonly formBuilder = inject(FormBuilder); + form = this.formBuilder.group({ dateRangeType: DateRangeType.ANY, inLastValueType: InLastDateType.DAYS, @@ -96,7 +98,6 @@ export class SearchDateRangeComponent implements OnInit { private readonly destroyRef = inject(DestroyRef); constructor( - private formBuilder: FormBuilder, private userPreferencesService: UserPreferencesService, private dateAdapter: DateAdapter, @Inject(MAT_DATE_FORMATS) private dateFormatConfig: MatDateFormats diff --git a/lib/content-services/src/lib/search/components/search-properties/search-properties.component.ts b/lib/content-services/src/lib/search/components/search-properties/search-properties.component.ts index 222e24e5fc..c541d1a02c 100644 --- a/lib/content-services/src/lib/search/components/search-properties/search-properties.component.ts +++ b/lib/content-services/src/lib/search/components/search-properties/search-properties.component.ts @@ -59,6 +59,8 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear autocompleteOptions: AutocompleteOption[] = []; preselectedOptions: AutocompleteOption[] = []; + private readonly formBuilder = inject(FormBuilder); + private _form = this.formBuilder.nonNullable.group({ fileSizeOperator: FileSizeOperator.AT_LEAST, fileSize: undefined, @@ -106,10 +108,7 @@ export class SearchPropertiesComponent implements OnInit, AfterViewChecked, Sear private readonly destroyRef = inject(DestroyRef); - constructor( - private readonly formBuilder: FormBuilder, - private readonly translateService: TranslateService - ) {} + constructor(private readonly translateService: TranslateService) {} ngOnInit() { if (this.settings) { diff --git a/lib/content-services/src/public-api.ts b/lib/content-services/src/public-api.ts index 3762d1f1b4..0d1542b78a 100644 --- a/lib/content-services/src/public-api.ts +++ b/lib/content-services/src/public-api.ts @@ -36,7 +36,6 @@ export * from './lib/node-comments/index'; export * from './lib/new-version-uploader'; export * from './lib/interfaces/index'; export * from './lib/version-compatibility/index'; -export * from './lib/pipes/index'; export * from './lib/common/index'; export * from './lib/tree/index'; export * from './lib/category/index'; diff --git a/lib/core/src/lib/auth/oidc/oidc-authentication.service.ts b/lib/core/src/lib/auth/oidc/oidc-authentication.service.ts index 9809b7a676..0e413fef35 100644 --- a/lib/core/src/lib/auth/oidc/oidc-authentication.service.ts +++ b/lib/core/src/lib/auth/oidc/oidc-authentication.service.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { OAuthService, OAuthStorage } from 'angular-oauth2-oidc'; import { Observable, defer, EMPTY, combineLatest } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; @@ -33,17 +33,7 @@ import { HttpHeaders } from '@angular/common/http'; providedIn: 'root' }) export class OidcAuthenticationService extends BaseAuthenticationService { - constructor( - appConfig: AppConfigService, - cookie: CookieService, - private jwtHelperService: JwtHelperService, - private authStorage: OAuthStorage, - private oauthService: OAuthService, - private readonly authConfig: AuthConfigService, - private readonly auth: AuthService - ) { - super(appConfig, cookie); - } + private readonly auth = inject(AuthService); /** * Observable that determines whether an SSO login should be performed. @@ -56,6 +46,17 @@ export class OidcAuthenticationService extends BaseAuthenticationService { map(([authenticated, isDiscoveryDocumentLoaded]) => !authenticated && isDiscoveryDocumentLoaded) ); + constructor( + appConfig: AppConfigService, + cookie: CookieService, + private jwtHelperService: JwtHelperService, + private authStorage: OAuthStorage, + private oauthService: OAuthService, + private readonly authConfig: AuthConfigService + ) { + super(appConfig, cookie); + } + isLoggedIn(): boolean { return this.oauthService.hasValidAccessToken() && this.oauthService.hasValidIdToken(); } diff --git a/lib/core/src/lib/card-view/components/card-view-boolitem/card-view-boolitem.component.ts b/lib/core/src/lib/card-view/components/card-view-boolitem/card-view-boolitem.component.ts index bc2a33ad0f..3c7e4959b6 100644 --- a/lib/core/src/lib/card-view/components/card-view-boolitem/card-view-boolitem.component.ts +++ b/lib/core/src/lib/card-view/components/card-view-boolitem/card-view-boolitem.component.ts @@ -39,7 +39,7 @@ import { MatError } from '@angular/material/form-field'; }) export class CardViewBoolItemComponent extends BaseCardView { @Input() - editable: boolean; + declare editable: boolean; private _error: string; diff --git a/lib/core/src/lib/card-view/models/card-view-arrayitem.model.ts b/lib/core/src/lib/card-view/models/card-view-arrayitem.model.ts index 3f70fbdcc3..ac7fb3eb98 100644 --- a/lib/core/src/lib/card-view/models/card-view-arrayitem.model.ts +++ b/lib/core/src/lib/card-view/models/card-view-arrayitem.model.ts @@ -28,7 +28,7 @@ export interface CardViewArrayItem { export class CardViewArrayItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel { type: string = 'array'; - value: Observable; + declare value: Observable; noOfItemsToDisplay: number; constructor(cardViewArrayItemProperties: CardViewArrayItemProperties) { diff --git a/lib/core/src/lib/card-view/models/card-view-boolitem.model.ts b/lib/core/src/lib/card-view/models/card-view-boolitem.model.ts index fcd9a726fe..426cfce346 100644 --- a/lib/core/src/lib/card-view/models/card-view-boolitem.model.ts +++ b/lib/core/src/lib/card-view/models/card-view-boolitem.model.ts @@ -23,7 +23,7 @@ import { CardViewBoolItemProperties } from '../interfaces/card-view.interfaces'; export class CardViewBoolItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel { type = 'bool'; value = false; - default: boolean; + declare default: boolean; constructor(cardViewBoolItemProperties: CardViewBoolItemProperties) { super(cardViewBoolItemProperties); diff --git a/lib/core/src/lib/card-view/models/card-view-mapitem.model.ts b/lib/core/src/lib/card-view/models/card-view-mapitem.model.ts index b3b5baf2b1..e7e803171b 100644 --- a/lib/core/src/lib/card-view/models/card-view-mapitem.model.ts +++ b/lib/core/src/lib/card-view/models/card-view-mapitem.model.ts @@ -21,7 +21,7 @@ import { CardViewBaseItemModel } from './card-view-baseitem.model'; export class CardViewMapItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel { type = 'map'; - value: Map; + declare value: Map; get displayValue() { if (this.value && this.value.size > 0) { diff --git a/lib/core/src/lib/datatable/components/datatable-cell/datatable-cell.component.ts b/lib/core/src/lib/datatable/components/datatable-cell/datatable-cell.component.ts index 4c23659ecf..43eab9045c 100644 --- a/lib/core/src/lib/datatable/components/datatable-cell/datatable-cell.component.ts +++ b/lib/core/src/lib/datatable/components/datatable-cell/datatable-cell.component.ts @@ -21,7 +21,7 @@ import { DataRow } from '../../data/data-row.model'; import { DataTableAdapter } from '../../data/datatable-adapter'; import { BehaviorSubject } from 'rxjs'; import { DataTableService } from '../../services/datatable.service'; -import { CommonModule } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { ClipboardDirective } from '../../../clipboard/clipboard.directive'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { TruncatePipe } from '../../../pipes/truncate.pipe'; @@ -29,7 +29,7 @@ import { UserPreferencesService } from '../../../common/services/user-preference @Component({ selector: 'adf-datatable-cell', - imports: [CommonModule, ClipboardDirective, TruncatePipe], + imports: [ClipboardDirective, TruncatePipe, AsyncPipe], changeDetection: ChangeDetectionStrategy.OnPush, template: ` @let value = value$ | async; diff --git a/lib/core/src/lib/form/components/widgets/core/container.model.ts b/lib/core/src/lib/form/components/widgets/core/container.model.ts index 0a103c95a0..ac3ca3b1ad 100644 --- a/lib/core/src/lib/form/components/widgets/core/container.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/container.model.ts @@ -23,7 +23,7 @@ import { ContainerColumnModel } from './container-column.model'; import { FormFieldTypes } from './form-field-types'; export class ContainerModel extends FormWidgetModel { - field: FormFieldModel; + declare field: FormFieldModel; readonly columns: ContainerColumnModel[] = []; isExpanded: boolean = true; diff --git a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts index 1539d75c16..b3e0249f6c 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts @@ -52,12 +52,12 @@ export class FormFieldModel extends FormWidgetModel { private readonly defaultEmptyOptionName = 'Choose one...'; // model members - fieldType: string; - id: string; - name: string; - type: string; + declare fieldType: string; + declare id: string; + declare name: string; + declare type: string; overrideId: boolean; - tab: string; + declare tab: string; rowspan: number = 1; colspan: number = 1; placeholder: string = null; @@ -98,7 +98,7 @@ export class FormFieldModel extends FormWidgetModel { variableConfig: VariableConfig; schemaDefinition: DataColumn[]; externalProperty?: string; - style?: string; + declare style?: string; parent?: RepeatableSectionModel; // container model members diff --git a/lib/process-services-cloud/src/lib/models/task-list-sorting.model.ts b/lib/process-services-cloud/src/lib/models/task-list-sorting.model.ts index 1ff3f39a73..592975ada3 100644 --- a/lib/process-services-cloud/src/lib/models/task-list-sorting.model.ts +++ b/lib/process-services-cloud/src/lib/models/task-list-sorting.model.ts @@ -28,14 +28,14 @@ export class TaskListCloudSortingModel { } export class TaskListRequestSortingModel extends TaskListCloudSortingModel { - orderBy: string; - direction: string; + declare orderBy: string; + declare direction: string; isFieldProcessVariable: boolean; processVariableData?: { processDefinitionKey: string; type: string; - } + }; constructor(obj: TaskListRequestSortingModel) { super(obj); diff --git a/lib/process-services-cloud/src/lib/people/mock/people-cloud.mock.ts b/lib/process-services-cloud/src/lib/people/mock/people-cloud.mock.ts index ba8dcd10e3..bbbda5207e 100644 --- a/lib/process-services-cloud/src/lib/people/mock/people-cloud.mock.ts +++ b/lib/process-services-cloud/src/lib/people/mock/people-cloud.mock.ts @@ -51,7 +51,7 @@ export const mockPreselectedFoodUsers = [ @Injectable() export class IdentityUserServiceMock extends IdentityUserService { - queryParams: { search: string; application?: string; roles?: string[]; groups?: string[] }; + declare queryParams: { search: string; application?: string; roles?: string[]; groups?: string[] }; getCurrentUserInfo(): IdentityUserModel { return mockKielbasaSausage; diff --git a/lib/process-services-cloud/src/lib/process/process-list/models/process-list-sorting.model.ts b/lib/process-services-cloud/src/lib/process/process-list/models/process-list-sorting.model.ts index 1bce1642eb..8adf920ed5 100644 --- a/lib/process-services-cloud/src/lib/process/process-list/models/process-list-sorting.model.ts +++ b/lib/process-services-cloud/src/lib/process/process-list/models/process-list-sorting.model.ts @@ -18,7 +18,7 @@ export class ProcessListCloudSortingModel { orderBy: string; direction: string; - constructor(obj: any) { + constructor(obj: any) { if (obj) { this.orderBy = obj.orderBy; this.direction = obj.direction; @@ -26,14 +26,14 @@ export class ProcessListCloudSortingModel { } } export class ProcessListRequestSortingModel extends ProcessListCloudSortingModel { - orderBy: string; - direction: string; + declare orderBy: string; + declare direction: string; isFieldProcessVariable: boolean; processVariableData?: { processDefinitionKey: string; type: string; - } + }; constructor(obj: ProcessListRequestSortingModel) { super(obj); diff --git a/lib/process-services-cloud/src/lib/task/task-list/components/task-list/task-list-cloud.component.ts b/lib/process-services-cloud/src/lib/task/task-list/components/task-list/task-list-cloud.component.ts index 730a6f6071..b9b4ecf538 100644 --- a/lib/process-services-cloud/src/lib/task/task-list/components/task-list/task-list-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/task/task-list/components/task-list/task-list-cloud.component.ts @@ -260,7 +260,7 @@ export class TaskListCloudComponent extends BaseTaskListCloudComponent(false); isLoading$ = combineLatest([this.isLoadingPreferences$, this.isReloadingSubject$]).pipe( diff --git a/nx.json b/nx.json index 54b012420c..48d436f790 100644 --- a/nx.json +++ b/nx.json @@ -18,6 +18,7 @@ }, "tasksRunnerOptions": { "default": { + "runner": "nx/tasks-runners/default", "options": { "runtimeCacheInputs": ["node -v"] } diff --git a/package.json b/package.json index 1e65b27c39..e503951fb5 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "8.3.0", "author": "Hyland Software, Inc. and its affiliates", "scripts": { - "prepare": "husky install", + "prepare": "husky", "bundle:js-api": "nx run js-api:bundle", "bundle:cli": "nx run cli:bundle", "test:affected": "nx affected:test",