[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
This commit is contained in:
Michal Kinas
2026-07-03 16:41:39 +02:00
committed by GitHub
parent 18e602aa85
commit 448e667c52
5 changed files with 106 additions and 27 deletions
@@ -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);
});
});
});
@@ -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');
@@ -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<string[]>();
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;
@@ -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<void> {
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));
}
}
@@ -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;
}
}