From 448e667c52f68bec2b4c33cb9eaad59ec4f08902 Mon Sep 17 00:00:00 2001 From: Michal Kinas <113341662+MichalKinas@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:41:39 +0200 Subject: [PATCH] [MNT-25681] Content node selector panel filtering fixed (#12032) * [MNT-25681] Content node selector panel filtering fixed * [MNT-25681] Small cleanup * [MNT-25681] CR fix --- .../content-node-dialog.service.spec.ts | 4 ++ .../content-node-dialog.service.ts | 6 +- ...de-selector-panel.component-search.spec.ts | 52 +++++++++++++++- .../content-node-selector-panel.component.ts | 60 ++++++++++++------- .../infinite-select-scroll.directive.ts | 11 +++- 5 files changed, 106 insertions(+), 27 deletions(-) diff --git a/lib/content-services/src/lib/content-node-selector/content-node-dialog.service.spec.ts b/lib/content-services/src/lib/content-node-selector/content-node-dialog.service.spec.ts index ddd8c13297..7891130cd0 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-dialog.service.spec.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-dialog.service.spec.ts @@ -206,5 +206,9 @@ describe('ContentNodeDialogService', () => { expect(testContentNodeSelectorComponentData.isSelectionValid(testData.node)).toBe(testData.expected); }); }); + + it('should scope the search to folders by disabling files in the result', () => { + expect(testContentNodeSelectorComponentData.showFilesInResult).toBe(false); + }); }); }); diff --git a/lib/content-services/src/lib/content-node-selector/content-node-dialog.service.ts b/lib/content-services/src/lib/content-node-selector/content-node-dialog.service.ts index 3adf82bfee..c36a5851d1 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-dialog.service.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-dialog.service.ts @@ -154,7 +154,8 @@ export class ContentNodeDialogService { where: '(isFolder=true)', isSelectionValid: this.isCopyMoveSelectionValid.bind(this), excludeSiteContent: excludeSiteContent || ContentNodeDialogService.nonDocumentSiteContent, - select + select, + showFilesInResult: false }; const dialogRef = this.openContentNodeDialog(data, 'adf-content-node-selector-dialog', '630px'); @@ -196,7 +197,8 @@ export class ContentNodeDialogService { imageResolver: this.imageResolver.bind(this), isSelectionValid: this.hasAllowableOperationsOnNodeFolder.bind(this), where: '(isFolder=true)', - select + select, + showFilesInResult: false }; const dialogRef = this.openContentNodeDialog(data, 'adf-content-node-selector-dialog', '630px'); diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel/content-node-selector-panel.component-search.spec.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel/content-node-selector-panel.component-search.spec.ts index f63894769d..587d328a44 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel/content-node-selector-panel.component-search.spec.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel/content-node-selector-panel.component-search.spec.ts @@ -19,7 +19,7 @@ import { DebugElement } from '@angular/core'; import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Node, NodeEntry, NodePaging, RequestScope, ResultSetPaging, SiteEntry, SitePaging, SitePagingList } from '@alfresco/js-api'; -import { of } from 'rxjs'; +import { of, Subject } from 'rxjs'; import { ContentNodeSelectorPanelComponent } from './content-node-selector-panel.component'; import { DocumentListService } from '../../document-list/services/document-list.service'; import { DocumentListComponent } from '../../document-list/components/document-list.component'; @@ -294,6 +294,32 @@ describe('ContentNodeSelectorPanelComponent', () => { expect(searchQueryBuilderService.addFilterQuery).toHaveBeenCalledWith(expectedRequest); })); + it('should remove the previous site filter query when changing the selected site', fakeAsync(() => { + spyOn(searchQueryBuilderService, 'addFilterQuery').and.callThrough(); + spyOn(searchQueryBuilderService, 'removeFilterQuery').and.callThrough(); + typeToSearchBox('search-term'); + tick(debounceSearch); + + component.siteChanged({ entry: { guid: 'namek' } } as SiteEntry); + component.siteChanged({ entry: { guid: 'vegeta' } } as SiteEntry); + + expect(searchQueryBuilderService.removeFilterQuery).toHaveBeenCalledWith(`ANCESTOR:'workspace://SpacesStore/namek'`); + expect(searchQueryBuilderService.addFilterQuery).toHaveBeenCalledWith(`ANCESTOR:'workspace://SpacesStore/vegeta'`); + expect(searchQueryBuilderService.filterQueries).toEqual([{ query: `ANCESTOR:'workspace://SpacesStore/vegeta'` }]); + })); + + it('should browse the selected site folder without searching when no search term is present', fakeAsync(() => { + expect(searchSpy.calls.count()).toBe(0); + + component.siteChanged({ entry: { guid: 'namek' } } as SiteEntry); + tick(debounceSearch); + + expect(searchSpy).not.toHaveBeenCalled(); + expect(component.searchTerm).toBe(''); + expect(component.folderIdToShow).toBe('namek'); + expect(component.showingSearchResults).toBe(false); + })); + it('should create the query with the right parameters on changing the site selectBox value from a custom dropdown menu', fakeAsync(() => { spyOn(searchQueryBuilderService, 'addFilterQuery').and.callThrough(); component.dropdownSiteList = { list: { entries: [{ entry: { guid: '-sites-' } }, { entry: { guid: 'namek' } }] } } as SitePaging; @@ -343,6 +369,29 @@ describe('ContentNodeSelectorPanelComponent', () => { expect(getCorrespondingNodeIdsSpy.calls.mostRecent().args[0]).toEqual('-sites-'); })); + it('should execute the search only after the corresponding node ids filter query is applied for aliases', fakeAsync(() => { + component.documentList.folderNode = { id: 'fakeNodeId', isFolder: true, path: {} } as Node; + spyOn(searchQueryBuilderService, 'addFilterQuery').and.callThrough(); + const nodeIds$ = new Subject(); + getCorrespondingNodeIdsSpy.and.returnValue(nodeIds$.asObservable()); + + typeToSearchBox('vegeta'); + tick(debounceSearch); + + searchSpy.calls.reset(); + + component.siteChanged({ entry: { guid: '-sites-' } } as SiteEntry); + + expect(searchSpy).not.toHaveBeenCalled(); + + nodeIds$.next(['123456testId']); + nodeIds$.complete(); + + const expectedRequest = `ANCESTOR:'workspace://SpacesStore/-sites-' OR ANCESTOR:'workspace://SpacesStore/123456testId'`; + expect(searchQueryBuilderService.addFilterQuery).toHaveBeenCalledWith(expectedRequest); + expect(searchSpy).toHaveBeenCalledWith(false); + })); + it('should NOT get the corresponding node ids on search when NOTHING is selected from dropdown', fakeAsync(() => { component.dropdownSiteList = { list: { entries: [{ entry: { guid: '-sites-' } }, { entry: { guid: 'namek' } }] } } as SitePaging; fixture.detectChanges(); @@ -508,6 +557,7 @@ describe('ContentNodeSelectorPanelComponent', () => { it('should the query restrict the search to the site and not to the currentFolderId in case is changed', async () => { spyOn(searchQueryBuilderService, 'addFilterQuery').and.callThrough(); + component.searchTerm = 'search-term'; searchQueryBuilderService.userQuery = 'search-term*'; component.currentFolderId = 'my-root-id'; component.restrictRootToCurrentFolderId = true; diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel/content-node-selector-panel.component.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel/content-node-selector-panel.component.ts index 007e06f7f2..fa14059d39 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel/content-node-selector-panel.component.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel/content-node-selector-panel.component.ts @@ -40,7 +40,8 @@ import { ImageResolver } from '../../document-list/data/image-resolver.model'; import { CustomResourcesService } from '../../document-list/services/custom-resources.service'; import { ShareDataRow } from '../../document-list/data/share-data-row.model'; import { NodeEntryEvent } from '../../document-list/components/node.event'; -import { debounceTime } from 'rxjs/operators'; +import { debounceTime, map } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; import { ContentNodeSelectorPanelService } from './content-node-selector-panel.service'; import { MatFormFieldModule } from '@angular/material/form-field'; import { TranslatePipe } from '@ngx-translate/core'; @@ -110,6 +111,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit { private showSearchField = true; private showCounter = false; private _emptyList = true; + private lastParentFilterQuery: string | null = null; /** If true will restrict the search and breadcrumbs to the currentFolderId */ @Input() @@ -440,7 +442,12 @@ export class ContentNodeSelectorPanelComponent implements OnInit { this.siteId = chosenSite.entry.guid; this.setTitleIfCustomSite(chosenSite); this.siteChange.emit(chosenSite.entry.title); - this.executeSearch(this.searchTerm); + if (this.searchTerm) { + this.executeSearch(this.searchTerm); + } else { + this.resetFolderToShow(); + this.clearSearch(); + } } /** @@ -467,7 +474,6 @@ export class ContentNodeSelectorPanelComponent implements OnInit { this.folderIdToShow = null; this.preselectedNodes = []; this.loadingSearchResults = true; - this.addCorrespondingNodeIdsQuery(); this.resetChosenNode(); } @@ -499,25 +505,35 @@ export class ContentNodeSelectorPanelComponent implements OnInit { this.showingSearch.emit(this.showingSearchResults); } - private addCorrespondingNodeIdsQuery() { - let extraParentFiltering = ''; - + private addCorrespondingNodeIdsQuery(): Observable { if (this.customResourcesService.hasCorrespondingNodeIds(this.siteId)) { - this.customResourcesService.getCorrespondingNodeIds(this.siteId).subscribe((nodeIds) => { - if (nodeIds?.length) { - nodeIds - .filter((id) => id !== this.siteId) - .forEach((extraId) => { - extraParentFiltering += ` OR ANCESTOR:'workspace://SpacesStore/${extraId}'`; - }); - } - const parentFiltering = this.siteId ? `ANCESTOR:'workspace://SpacesStore/${this.siteId}'${extraParentFiltering}` : ''; - this.queryBuilderService.addFilterQuery(parentFiltering); - }); - } else { - const parentFiltering = this.siteId ? `ANCESTOR:'workspace://SpacesStore/${this.siteId}'` : ''; - this.queryBuilderService.addFilterQuery(parentFiltering); + return this.customResourcesService.getCorrespondingNodeIds(this.siteId).pipe( + map((nodeIds) => { + let extraParentFiltering = ''; + if (nodeIds?.length) { + nodeIds + .filter((id) => id !== this.siteId) + .forEach((extraId) => { + extraParentFiltering += ` OR ANCESTOR:'workspace://SpacesStore/${extraId}'`; + }); + } + const parentFiltering = this.siteId ? `ANCESTOR:'workspace://SpacesStore/${this.siteId}'${extraParentFiltering}` : ''; + this.setParentFilterQuery(parentFiltering); + }) + ); } + + const siteFilter = this.siteId ? `ANCESTOR:'workspace://SpacesStore/${this.siteId}'` : ''; + this.setParentFilterQuery(siteFilter); + return of(undefined); + } + + private setParentFilterQuery(parentFiltering: string) { + if (this.lastParentFilterQuery) { + this.queryBuilderService.removeFilterQuery(this.lastParentFilterQuery); + } + this.lastParentFilterQuery = parentFiltering || null; + this.queryBuilderService.addFilterQuery(parentFiltering); } private setSearchScopeToNodes() { @@ -685,6 +701,8 @@ export class ContentNodeSelectorPanelComponent implements OnInit { this.queryBuilderService.searchMode = 'formula'; const wildcardSuffix = this.queryBuilderService.wildcardsEnabled ? '*' : ''; this.queryBuilderService.userQuery = searchValue.length > 0 ? `(${searchValue}${wildcardSuffix})` : searchValue; - this.queryBuilderService.execute(false); + this.addCorrespondingNodeIdsQuery() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.queryBuilderService.execute(false)); } } diff --git a/lib/core/src/lib/directives/infinite-select-scroll.directive.ts b/lib/core/src/lib/directives/infinite-select-scroll.directive.ts index 353fd5a1ca..aa69c536fc 100644 --- a/lib/core/src/lib/directives/infinite-select-scroll.directive.ts +++ b/lib/core/src/lib/directives/infinite-select-scroll.directive.ts @@ -38,8 +38,12 @@ export class InfiniteSelectScrollDirective implements AfterViewInit { ngAfterViewInit() { this.matSelect.openedChange.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((opened: boolean) => { if (opened) { - this.itemHeightToWaitBeforeLoadNext = this.getItemHeight() * (InfiniteSelectScrollDirective.MAX_ITEMS / 2); - this.matSelect.panel.nativeElement.addEventListener('scroll', (event: Event) => this.handleScrollEvent(event)); + setTimeout(() => { + if (this.matSelect.panel?.nativeElement) { + this.itemHeightToWaitBeforeLoadNext = this.getItemHeight() * (InfiniteSelectScrollDirective.MAX_ITEMS / 2); + this.matSelect.panel.nativeElement.addEventListener('scroll', (event: Event) => this.handleScrollEvent(event)); + } + }); } }); } @@ -56,6 +60,7 @@ export class InfiniteSelectScrollDirective implements AfterViewInit { } private getItemHeight(): number { - return parseFloat(getComputedStyle(this.matSelect.panel.nativeElement).fontSize || '0') * SELECT_ITEM_HEIGHT_EM; + const panelElement = this.matSelect.panel?.nativeElement; + return panelElement ? parseFloat(getComputedStyle(panelElement).fontSize || '0') * SELECT_ITEM_HEIGHT_EM : 0; } }