diff --git a/demo-shell/src/app.config.json b/demo-shell/src/app.config.json index cf55983c35..d58110379c 100644 --- a/demo-shell/src/app.config.json +++ b/demo-shell/src/app.config.json @@ -119,7 +119,8 @@ ], "include": [ "path", - "allowableOperations" + "allowableOperations", + "properties" ], "sorting": { "options": [ diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts index ac96f3b721..11843d181b 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.spec.ts @@ -18,7 +18,7 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { tick, ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { NodeEntry, Node, SiteEntry, SitePaging, NodePaging, ResultSetPaging } from '@alfresco/js-api'; +import { NodeEntry, Node, SiteEntry, SitePaging, NodePaging, ResultSetPaging, RequestScope } from '@alfresco/js-api'; import { SitesService, setupTestBed, NodesApiService } from '@alfresco/adf-core'; import { of, throwError } from 'rxjs'; import { DropdownBreadcrumbComponent } from '../breadcrumb'; @@ -31,7 +31,7 @@ import { CustomResourcesService } from '../document-list/services/custom-resourc import { NodeEntryEvent, ShareDataRow } from '../document-list'; import { TranslateModule } from '@ngx-translate/core'; import { SearchQueryBuilderService } from '../search'; -import { ContentNodeSelectorService } from './content-node-selector.service'; +import { mockQueryBody } from '../mock/search-query.mock'; const fakeResultSetPaging: ResultSetPaging = { list: { @@ -59,11 +59,9 @@ describe('ContentNodeSelectorPanelComponent', () => { let nodeService: NodesApiService; let sitesService: SitesService; let searchSpy: jasmine.Spy; - let cnSearchSpy: jasmine.Spy; const fakeNodeEntry = new Node({ id: 'fakeId' }); const nodeEntryEvent = new NodeEntryEvent(fakeNodeEntry); let searchQueryBuilderService: SearchQueryBuilderService; - let contentNodeSelectorService: ContentNodeSelectorService; function typeToSearchBox(searchTerm = 'string-to-search') { const searchInput = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-input"]')); @@ -93,11 +91,9 @@ describe('ContentNodeSelectorPanelComponent', () => { nodeService = TestBed.inject(NodesApiService); sitesService = TestBed.inject(SitesService); - contentNodeSelectorService = TestBed.inject(ContentNodeSelectorService); searchQueryBuilderService = component.queryBuilderService; spyOn(nodeService, 'getNode').and.returnValue(of({ id: 'fake-node', path: { elements: [{ nodeType: 'st:site', name: 'fake-site'}] } })); - cnSearchSpy = spyOn(contentNodeSelectorService, 'createQuery').and.callThrough(); searchSpy = spyOn(searchQueryBuilderService, 'execute'); const fakeSite = new SiteEntry({ entry: { id: 'fake-site', guid: 'fake-site', title: 'fake-site', visibility: 'visible' } }); spyOn(sitesService, 'getSite').and.returnValue(of(fakeSite)); @@ -362,32 +358,6 @@ describe('ContentNodeSelectorPanelComponent', () => { let customResourcesService: CustomResourcesService; const entry: Node = { id: 'fakeid'}; - const defaultSearchOptions = (searchTerm, rootNodeId = undefined, skipCount = 0, showFiles = false) => { - - const parentFiltering = rootNodeId ? [{ query: `ANCESTOR:'workspace://SpacesStore/${rootNodeId}'` }] : []; - - const defaultSearchNode: any = { - query: { - query: searchTerm ? `${searchTerm}*` : searchTerm - }, - include: ['path', 'allowableOperations', 'properties'], - paging: { - maxItems: 25, - skipCount: skipCount - }, - filterQueries: [ - { query: `TYPE:'cm:folder'${ showFiles ? " OR TYPE:'cm:content'" : '' }` }, - { query: 'NOT cm:creator:System' }, - ...parentFiltering - ], - scope: { - locations: 'nodes' - } - }; - - return defaultSearchNode; - }; - beforeEach(() => { const documentListService = TestBed.inject(DocumentListService); const expectedDefaultFolderNode = { entry: { path: { elements: [] } } }; @@ -418,23 +388,79 @@ describe('ContentNodeSelectorPanelComponent', () => { fixture.detectChanges(); }); - it('should load the results by calling the search api on search change', fakeAsync(() => { - typeToSearchBox('kakarot'); + it('should the user query get updated when the user types in the search input', fakeAsync(() => { + const updateSpy = spyOn(searchQueryBuilderService, 'update'); + typeToSearchBox('search-term'); tick(debounceSearch); fixture.detectChanges(); - expect(searchSpy).toHaveBeenCalledWith(defaultSearchOptions('kakarot')); + expect(updateSpy).toHaveBeenCalled(); + expect(searchQueryBuilderService.userQuery).toEqual('(search-term)'); + expect(component.searchTerm).toEqual('search-term'); })); - it('should show files in results by calling the search api on search change', fakeAsync(() => { - component.showFilesInResult = true; - typeToSearchBox('kakarot'); + it('should perform a search when the queryBody gets updated and it is defined', async () => { + searchQueryBuilderService.userQuery = 'search-term'; + searchQueryBuilderService.update(); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(searchSpy).toHaveBeenCalledWith(mockQueryBody); + }); + + it('should NOT perform a search and clear the results when the queryBody gets updated and it is NOT defined', async () => { + spyOn(component, 'clearSearch'); + + searchQueryBuilderService.userQuery = ''; + searchQueryBuilderService.update(); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(searchSpy).not.toHaveBeenCalled(); + expect(component.clearSearch).toHaveBeenCalled(); + }); + + it('should reset the search term when clicking the clear icon', () => { + component.searchTerm = 'search-term'; + searchQueryBuilderService.userQuery = 'search-term'; + spyOn(component, 'clearSearch'); + + fixture.detectChanges(); + const clearIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]')); + clearIcon.nativeElement.click(); + + fixture.detectChanges(); + + expect(searchQueryBuilderService.userQuery).toEqual(''); + expect(component.searchTerm).toEqual(''); + expect(component.clearSearch).toHaveBeenCalled(); + }); + + it('should load the results by calling the search api on search change', fakeAsync(() => { + typeToSearchBox('search-term'); tick(debounceSearch); fixture.detectChanges(); - expect(searchSpy).toHaveBeenCalledWith(defaultSearchOptions('kakarot', undefined, 0, true)); + expect(searchSpy).toHaveBeenCalledWith(mockQueryBody); + })); + + it('should the query include the show files filterQuery', fakeAsync(() => { + component.showFilesInResult = true; + typeToSearchBox('search-term'); + + const expectedQueryBody = mockQueryBody; + expectedQueryBody.filterQueries.push({ + query: `TYPE:'cm:folder' OR TYPE:'cm:content'` + }); + + tick(debounceSearch); + fixture.detectChanges(); + + expect(searchSpy).toHaveBeenCalledWith(expectedQueryBody); })); it('should reset the currently chosen node in case of starting a new search', fakeAsync(() => { @@ -453,8 +479,8 @@ describe('ContentNodeSelectorPanelComponent', () => { expect(component.breadcrumbFolderTitle).toBe('My Sites'); }); - it('should call the search api on changing the site selectBox value', fakeAsync(() => { - typeToSearchBox('vegeta'); + it('should perform a search when selecting a site with the correct query', fakeAsync(() => { + typeToSearchBox('search-term'); tick(debounceSearch); @@ -462,40 +488,34 @@ describe('ContentNodeSelectorPanelComponent', () => { component.siteChanged( { entry: { guid: 'namek' } }); + const expectedQueryBody = mockQueryBody; + expectedQueryBody.filterQueries = [ { query: `ANCESTOR:'workspace://SpacesStore/namek'`} ]; + expect(searchSpy.calls.count()).toBe(2, 'Search count should be two after the site change'); - expect(searchSpy.calls.argsFor(1)).toEqual([defaultSearchOptions('vegeta', 'namek')]); - })); - - it('should create the query with the right parameters on changing the site selectbox\'s value', fakeAsync(() => { - typeToSearchBox('vegeta'); - - tick(debounceSearch); - expect(cnSearchSpy.calls.count()).toBe(1); - - component.siteChanged( { entry: { guid: '-sites-' } }); - - expect(cnSearchSpy).toHaveBeenCalled(); - expect(cnSearchSpy.calls.count()).toBe(2); - expect(cnSearchSpy).toHaveBeenCalledWith('vegeta', undefined, 0, 25, [], false); - expect(cnSearchSpy).toHaveBeenCalledWith('vegeta', '-sites-', 0, 25, ['123456testId', '09876543testId'], false); + expect(searchSpy).toHaveBeenCalledWith(expectedQueryBody); })); it('should create the query with the right parameters on changing the site selectBox value from a custom dropdown menu', fakeAsync(() => { component.dropdownSiteList = { list: { entries: [ { entry: { guid: '-sites-' } }, { entry: { guid: 'namek' } }] } }; fixture.detectChanges(); - typeToSearchBox('vegeta'); + typeToSearchBox('search-term'); tick(debounceSearch); - expect(cnSearchSpy.calls.count()).toBe(1); + expect(searchSpy.calls.count()).toBe(1); component.siteChanged( { entry: { guid: '-sites-' } }); - expect(cnSearchSpy).toHaveBeenCalled(); - expect(cnSearchSpy.calls.count()).toBe(2); - expect(cnSearchSpy).toHaveBeenCalledWith('vegeta', undefined, 0, 25, [], false); - expect(cnSearchSpy).toHaveBeenCalledWith('vegeta', '-sites-', 0, 25, ['123456testId', '09876543testId'], false); + const expectedQueryBodyWithSiteChange = mockQueryBody; + expectedQueryBodyWithSiteChange.filterQueries = [ + { query: `ANCESTOR:'workspace://SpacesStore/-sites-' OR ANCESTOR:'workspace://SpacesStore/123456testId' OR ANCESTOR:'workspace://SpacesStore/09876543testId'` } + ]; + + expect(searchSpy).toHaveBeenCalled(); + expect(searchSpy.calls.count()).toBe(2); + expect(searchSpy).toHaveBeenCalledWith(mockQueryBody); + expect(searchSpy).toHaveBeenCalledWith(expectedQueryBodyWithSiteChange); })); it('should get the corresponding node ids on search when a known alias is selected from dropdown', fakeAsync(() => { @@ -601,19 +621,24 @@ describe('ContentNodeSelectorPanelComponent', () => { expect(component.showingSearchResults).toBeFalsy(); }); - it('should the query restrict the search to the currentFolderId in case is defined', () => { + it('should the query restrict the search to the currentFolderId in case is defined', fakeAsync(() => { component.currentFolderId = 'my-root-id'; component.restrictRootToCurrentFolderId = true; component.ngOnInit(); - component.search('search'); + typeToSearchBox('search-term'); + tick(debounceSearch); - expect(cnSearchSpy).toHaveBeenCalledWith('search', 'my-root-id', 0, 25, [], false); - }); + const expectedQueryBody = mockQueryBody; + expectedQueryBody.filterQueries = [ { query: `ANCESTOR:'workspace://SpacesStore/my-root-id'`} ]; + + expect(searchSpy).toHaveBeenCalledWith(expectedQueryBody); + })); it('should emit showingSearch event with true while searching', async () => { spyOn(customResourcesService, 'hasCorrespondingNodeIds').and.returnValue(true); const showingSearchSpy = spyOn(component.showingSearch, 'emit'); - component.search('search'); + + component.queryBuilderService.execute({ query: { query: 'search' } }); triggerSearchResults(fakeResultSetPaging); fixture.detectChanges(); @@ -623,15 +648,16 @@ describe('ContentNodeSelectorPanelComponent', () => { expect(showingSearchSpy).toHaveBeenCalledWith(true); }); - it('should emit showingSearch event with false if you remove search term without clicking on X (icon) icon', async () => { + it('should emit showingSearch event with false if you remove search term without clicking on X (icon) icon', fakeAsync(() => { const showingSearchSpy = spyOn(component.showingSearch, 'emit'); - component.search(''); + typeToSearchBox(''); + tick(debounceSearch); + fixture.detectChanges(); - await fixture.whenStable(); expect(component.showingSearchResults).toBe(false); expect(showingSearchSpy).toHaveBeenCalledWith(false); - }); + })); it('should emit showingResults event with false when clicking on the X (clear) icon', () => { const showingSearchSpy = spyOn(component.showingSearch, 'emit'); @@ -654,7 +680,7 @@ describe('ContentNodeSelectorPanelComponent', () => { it('should emit showingResults event with false if search api fails', async () => { getCorrespondingNodeIdsSpy.and.throwError('Failed'); const showingSearchSpy = spyOn(component.showingSearch, 'emit'); - component.search('search'); + component.queryBuilderService.execute({ query: { query: 'search' } }); triggerSearchResults(fakeResultSetPaging); fixture.detectChanges(); @@ -665,13 +691,17 @@ describe('ContentNodeSelectorPanelComponent', () => { }); it('should the query restrict the search to the site and not to the currentFolderId in case is changed', () => { + component.queryBuilderService.userQuery = 'search-term'; component.currentFolderId = 'my-root-id'; component.restrictRootToCurrentFolderId = true; - component.ngOnInit(); component.siteChanged( { entry: { guid: 'my-site-id' } }); - component.search('search'); - expect(cnSearchSpy).toHaveBeenCalledWith('search', 'my-site-id', 0, 25, [], false); + const expectedQueryBodyWithSiteChange = mockQueryBody; + expectedQueryBodyWithSiteChange.filterQueries = [ + { query: `ANCESTOR:'workspace://SpacesStore/my-site-id'` } + ]; + + expect(searchSpy).toHaveBeenCalledWith(expectedQueryBodyWithSiteChange); }); it('should restrict the breadcrumb to the currentFolderId in case restrictedRoot is true', () => { @@ -689,7 +719,7 @@ describe('ContentNodeSelectorPanelComponent', () => { }); it('should clear the search field, nodes and chosenNode when deleting the search input', fakeAsync (() => { - spyOn(component, 'clear').and.callThrough(); + spyOn(component, 'clearSearch').and.callThrough(); typeToSearchBox('a'); tick(debounceSearch); @@ -703,7 +733,7 @@ describe('ContentNodeSelectorPanelComponent', () => { fixture.detectChanges(); expect(searchSpy.calls.count()).toBe(1, 'no other search has been performed'); - expect(component.clear).toHaveBeenCalled(); + expect(component.clearSearch).toHaveBeenCalled(); expect(component.folderIdToShow).toBe('cat-girl-nuku-nuku', 'back to the folder in which the search was performed'); })); @@ -728,7 +758,6 @@ describe('ContentNodeSelectorPanelComponent', () => { component.siteChanged( { entry: { guid: 'namek' } }); expect(searchSpy.calls.count()).toBe(2); - expect(searchSpy.calls.argsFor(1)).toEqual([defaultSearchOptions('piccolo', 'namek')]); component.clear(); @@ -845,21 +874,19 @@ describe('ContentNodeSelectorPanelComponent', () => { }, 300); }); - it('should reload the original folderId when clearing the search input', async() => { - component.search('mock-type-search'); - - triggerSearchResults(fakeResultSetPaging); + it('should reload the original folderId when clearing the search input', fakeAsync(() => { + typeToSearchBox('search-term'); + tick(debounceSearch); fixture.detectChanges(); - await fixture.whenStable(); expect(component.folderIdToShow).toBe(null); - component.clear(); + typeToSearchBox(''); + tick(debounceSearch); fixture.detectChanges(); - await fixture.whenStable(); expect(component.folderIdToShow).toBe('cat-girl-nuku-nuku'); - }); + })); it('should set the folderIdToShow to the default "currentFolderId" if siteId is undefined', (done) => { component.siteChanged( { entry: { guid: 'Kame-Sennin Muten Roshi' } }); @@ -900,63 +927,50 @@ describe('ContentNodeSelectorPanelComponent', () => { expect(component.searchTerm).toBe(''); expect(component.infiniteScroll).toBeTruthy(); - expect(component.pagination.maxItems).toBe(45); + expect(component.queryBuilderService.paging.maxItems).toBe(45); expect(searchSpy).not.toHaveBeenCalled(); }); - it('should set its loading state to true after search was started', fakeAsync (() => { - component.showingSearchResults = true; - - typeToSearchBox('shenron'); - - tick(debounceSearch); - + it('should set its loading state to true to perform a new search', async() => { + component.prepareDialogForNewSearch(mockQueryBody); fixture.detectChanges(); - - tick(debounceSearch); + await fixture.whenStable(); const spinnerSelector = By.css('[data-automation-id="content-node-selector-search-pagination"] [data-automation-id="adf-infinite-pagination-spinner"]'); const paginationLoading = fixture.debugElement.query(spinnerSelector); + expect(paginationLoading).not.toBeNull(); - })); + }); it('Should infinite pagination target be null when we use it for search ', fakeAsync (() => { component.showingSearchResults = true; - typeToSearchBox('shenron'); - tick(debounceSearch); - fixture.detectChanges(); - tick(debounceSearch); - expect(component.target).toBeNull(); })); - it('Should infinite pagination target be present when search finish', fakeAsync (() => { - component.showingSearchResults = true; - - typeToSearchBox('shenron'); - - tick(debounceSearch); - - fixture.detectChanges(); - - typeToSearchBox(''); - - tick(debounceSearch); - + it('Should infinite pagination target be present when search finish', () => { + triggerSearchResults(fakeResultSetPaging); fixture.detectChanges(); expect(component.target).not.toBeNull(); - })); + }); it('Should infinite pagination target on init be the document list', fakeAsync(() => { component.showingSearchResults = true; expect(component.target).toEqual(component.documentList); })); + + it('Should set the scope to nodes when the component inits', () => { + const expectedScope: RequestScope = { locations: 'nodes' }; + const setScopeSpy = spyOn(component.queryBuilderService, 'setScope'); + component.ngOnInit(); + + expect(setScopeSpy).toHaveBeenCalledWith(expectedScope); + }); }); }); diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts index a34edc581f..03e970666f 100644 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts +++ b/lib/content-services/src/lib/content-node-selector/content-node-selector-panel.component.ts @@ -29,7 +29,6 @@ import { import { HighlightDirective, UserPreferencesService, - PaginationModel, UserPreferenceValues, InfinitePaginationComponent, PaginatedComponent, NodesApiService, @@ -38,11 +37,10 @@ import { FileUploadCompleteEvent } from '@alfresco/adf-core'; import { FormControl } from '@angular/forms'; -import { Node, NodePaging, Pagination, SiteEntry, SitePaging, NodeEntry } from '@alfresco/js-api'; +import { Node, NodePaging, Pagination, SiteEntry, SitePaging, NodeEntry, QueryBody, RequestScope } from '@alfresco/js-api'; import { DocumentListComponent } from '../document-list/components/document-list.component'; import { RowFilter } from '../document-list/data/row-filter.model'; import { ImageResolver } from '../document-list/data/image-resolver.model'; -import { ContentNodeSelectorService } from './content-node-selector.service'; import { debounceTime, takeUntil, scan } from 'rxjs/operators'; import { CustomResourcesService } from '../document-list/services/custom-resources.service'; import { NodeEntryEvent, ShareDataRow } from '../document-list'; @@ -69,14 +67,11 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { DEFAULT_PAGINATION: Pagination = new Pagination({ maxItems: 25, - skipCount: 0, - totalItems: 0, - hasMoreItems: false + skipCount: 0 }); private showSiteList = true; private showSearchField = true; - private showFiles = false; /** If true will restrict the search and breadcrumbs to the currentFolderId */ @Input() @@ -196,7 +191,8 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { @Input() set showFilesInResult(value: boolean) { if (value !== undefined && value !== null) { - this.showFiles = value; + const showFilesQuery = `TYPE:'cm:folder'${value ? " OR TYPE:'cm:content'" : ''}`; + this.queryBuilderService.addFilterQuery(showFilesQuery); } } @@ -234,8 +230,6 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { breadcrumbFolderTitle: string | null = null; startSiteGuid: string | null = null; - pagination: PaginationModel = this.DEFAULT_PAGINATION; - @ViewChild(InfinitePaginationComponent, { static: true }) infinitePaginationComponent: InfinitePaginationComponent; @@ -248,8 +242,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { private onDestroy$ = new Subject(); - constructor(private contentNodeSelectorService: ContentNodeSelectorService, - private customResourcesService: CustomResourcesService, + constructor(private customResourcesService: CustomResourcesService, @Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilderService: SearchQueryBuilderService, private userPreferencesService: UserPreferencesService, private nodesApiService: NodesApiService, @@ -272,7 +265,23 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { debounceTime(this.debounceSearch), takeUntil(this.onDestroy$) ) - .subscribe(searchValue => this.search(searchValue)); + .subscribe(searchValue => { + this.searchTerm = searchValue; + this.queryBuilderService.userQuery = searchValue; + this.queryBuilderService.update(); + }); + + this.queryBuilderService.updated + .pipe(takeUntil(this.onDestroy$)) + .subscribe((queryBody: QueryBody) => { + if (queryBody) { + this.prepareDialogForNewSearch(queryBody); + this.queryBuilderService.execute(queryBody); + } else { + this.resetFolderToShow(); + this.clearSearch(); + } + }); this.queryBuilderService.executed .pipe(takeUntil(this.onDestroy$)) @@ -299,6 +308,8 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { this.breadcrumbTransform = this.breadcrumbTransform ? this.breadcrumbTransform : null; this.isSelectionValid = this.isSelectionValid ? this.isSelectionValid : defaultValidation; this.onFileUploadEvent(); + this.resetPagination(); + this.setSearchScopeToNodes(); } ngOnDestroy() { @@ -365,17 +376,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { this.siteId = chosenSite.entry.guid; this.setTitleIfCustomSite(chosenSite); this.siteChange.emit(chosenSite.entry.title); - this.updateResults(); - } - - /** - * Updates the searchTerm attribute and starts a new search - * - * @param searchTerm string value to search against - */ - search(searchTerm: string): void { - this.searchTerm = searchTerm; - this.updateResults(); + this.queryBuilderService.update(); } /** @@ -393,11 +394,33 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { return folderNode; } + /** + * Prepares the dialog for a new search + */ + prepareDialogForNewSearch(queryBody: QueryBody): void { + this.target = queryBody ? null : this.documentList; + if (this.target) { + this.infinitePaginationComponent.reset(); + } + this.folderIdToShow = null; + this.loadingSearchResults = true; + this.addCorrespondingNodeIdsQuery(); + this.resetChosenNode(); + } + /** * Clear the search input and reset to last folder node in which search was performed */ clear(): void { - this.clearSearch(); + this.searchTerm = ''; + this.queryBuilderService.userQuery = ''; + this.queryBuilderService.update(); + } + + /** + * Resets the folder to be shown with the site selection or the initial landing folder + */ + resetFolderToShow(): void { this.folderIdToShow = this.siteId || this.currentFolderId; } @@ -407,71 +430,52 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { clearSearch() { this.searchTerm = ''; this.nodePaging = null; - this.pagination.maxItems = this.pageSize; + this.resetPagination(); this.resetChosenNode(); this.showingSearchResults = false; this.showingSearch.emit(this.showingSearchResults); } - /** - * Update the result list depending on the criteria - */ - private updateResults(): void { - this.target = this.searchTerm.length > 0 ? null : this.documentList; - - if (this.searchTerm.length === 0) { - this.clear(); - } else { - this.startNewSearch(); - } - } - - /** - * Load the first page of a new search result - */ - private startNewSearch(): void { - this.nodePaging = null; - this.pagination.maxItems = this.pageSize; - if (this.target) { - this.infinitePaginationComponent.reset(); - } - this.chosenNode = null; - this.folderIdToShow = null; - this.querySearch(); - } - - /** - * Perform the call to searchService with the proper parameters - */ - private querySearch(): void { - this.loadingSearchResults = true; + private addCorrespondingNodeIdsQuery() { + let extraParentFiltering = ''; if (this.customResourcesService.hasCorrespondingNodeIds(this.siteId)) { this.customResourcesService.getCorrespondingNodeIds(this.siteId) .subscribe((nodeIds) => { - const query = this.contentNodeSelectorService.createQuery(this.searchTerm, this.siteId, this.pagination.skipCount, this.pagination.maxItems, nodeIds, this.showFiles); - this.queryBuilderService.execute(query); - }, - () => { - this.showSearchResults({ list: { entries: [] } }); - }); + if (nodeIds && 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 query = this.contentNodeSelectorService.createQuery(this.searchTerm, this.siteId, this.pagination.skipCount, this.pagination.maxItems, [], this.showFiles); - this.queryBuilderService.execute(query); + const parentFiltering = this.siteId ? `ANCESTOR:'workspace://SpacesStore/${this.siteId}'` : ''; + this.queryBuilderService.addFilterQuery(parentFiltering); } } + private setSearchScopeToNodes() { + const scope: RequestScope = { + locations: 'nodes' + }; + this.queryBuilderService.setScope(scope); + } + /** * Show the results of the search * * @param results Search results */ - private showSearchResults(nodePaging: NodePaging): void { + private showSearchResults(results: NodePaging): void { this.showingSearchResults = true; this.loadingSearchResults = false; this.showingSearch.emit(this.showingSearchResults); - this.nodePaging = nodePaging; + this.nodePaging = results; } /** @@ -505,14 +509,15 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { /** * Loads the next batch of search results * - * @param event Pagination object + * @param pagination Pagination object */ getNextPageOfSearch(pagination: Pagination): void { this.infiniteScroll = true; - this.pagination = pagination; + this.queryBuilderService.paging.maxItems = pagination.maxItems; + this.queryBuilderService.paging.skipCount = pagination.skipCount; if (this.searchTerm.length > 0) { - this.querySearch(); + this.queryBuilderService.update(); } } @@ -541,8 +546,7 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { */ onCurrentSelection(nodesEntries: NodeEntry[]): void { const validNodesEntity = nodesEntries.filter((node) => this.isSelectionValid(node.entry)); - const nodes: Node[] = validNodesEntity.map((node) => node.entry ); - this.chosenNode = nodes; + this.chosenNode = validNodesEntity.map((node) => node.entry ); } setTitleIfCustomSite(site: SiteEntry) { @@ -578,4 +582,11 @@ export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy { return selectedNodes; } + + private resetPagination(): void { + this.queryBuilderService.paging = { + maxItems: this.pageSize, + skipCount: this.DEFAULT_PAGINATION.skipCount + }; + } } diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector.service.spec.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector.service.spec.ts deleted file mode 100644 index 5b6b858b19..0000000000 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector.service.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -/*! - * @license - * Copyright 2019 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { TestBed } from '@angular/core/testing'; -import { setupTestBed } from '@alfresco/adf-core'; -import { ContentNodeSelectorService } from './content-node-selector.service'; -import { ContentTestingModule } from '../testing/content.testing.module'; -import { TranslateModule } from '@ngx-translate/core'; - -describe('ContentNodeSelectorService', () => { - - let service: ContentNodeSelectorService; - - setupTestBed({ - imports: [ - TranslateModule.forRoot(), - ContentTestingModule - ] - }); - - beforeEach(() => { - service = TestBed.inject(ContentNodeSelectorService); - }); - - it('should have the proper main query for search string', () => { - const queryBody = service.createQuery('nuka cola quantum'); - - expect(queryBody.query).toEqual({ - query: 'nuka cola quantum*' - }); - }); - - it('should make it including the path and allowableOperations', () => { - const queryBody = service.createQuery('nuka cola quantum'); - - expect(queryBody.include).toEqual(['path', 'allowableOperations', 'properties']); - }); - - it('should make the search restricted to nodes only', () => { - const queryBody = service.createQuery('nuka cola quantum'); - - expect(queryBody.scope.locations).toEqual('nodes'); - }); - - it('should set the maxItems and paging properly by parameters', () => { - const queryBody = service.createQuery('nuka cola quantum', null, 10, 100); - - expect(queryBody.paging.maxItems).toEqual(100); - expect(queryBody.paging.skipCount).toEqual(10); - }); - - it('should set the maxItems and paging properly by default', () => { - const queryBody = service.createQuery('nuka cola quantum'); - - expect(queryBody.paging.maxItems).toEqual(25); - expect(queryBody.paging.skipCount).toEqual(0); - }); - - it('should filter the search for folders', () => { - const queryBody = service.createQuery('nuka cola quantum'); - - expect(queryBody.filterQueries).toContain({ query: "TYPE:'cm:folder'" }); - }); - - it('should filter the search for files', () => { - const queryBody = service.createQuery('nuka cola quantum', null, 0, 25, [], true); - - expect(queryBody.filterQueries).toContain({ query: "TYPE:'cm:folder' OR TYPE:'cm:content'" }); - }); - - it('should filter out the "system-base" entries', () => { - const queryBody = service.createQuery('nuka cola quantum'); - - expect(queryBody.filterQueries).toContain({ query: 'NOT cm:creator:System' }); - }); - - it('should filter for the provided ancestor if defined', () => { - const queryBody = service.createQuery('nuka cola quantum', 'diamond-city'); - - expect(queryBody.filterQueries).toContain({ query: 'ANCESTOR:\'workspace://SpacesStore/diamond-city\'' }); - }); - - it('should NOT filter for the ancestor if NOT defined', () => { - const queryBody = service.createQuery('nuka cola quantum'); - - expect(queryBody.filterQueries).not.toContain({ query: 'ANCESTOR:\'workspace://SpacesStore/null\'' }); - }); - - it('should filter for the extra provided ancestors if defined', () => { - const queryBody = service.createQuery('nuka cola quantum', 'diamond-city', 0, 25, ['extra-diamond-city']); - - expect(queryBody.filterQueries).toContain({ query: 'ANCESTOR:\'workspace://SpacesStore/diamond-city\' OR ANCESTOR:\'workspace://SpacesStore/extra-diamond-city\'' }); - }); - - it('should NOT filter for extra ancestors if an empty list of ids is provided', () => { - const queryBody = service.createQuery('nuka cola quantum', 'diamond-city', 0, 25, []); - - expect(queryBody.filterQueries).toContain({ query: 'ANCESTOR:\'workspace://SpacesStore/diamond-city\'' }); - }); - - it('should NOT filter for the extra provided ancestor if it\'s the same as the rootNodeId', () => { - const queryBody = service.createQuery('nuka cola quantum', 'diamond-city', 0, 25, ['diamond-city']); - - expect(queryBody.filterQueries).toContain({ query: 'ANCESTOR:\'workspace://SpacesStore/diamond-city\'' }); - }); -}); diff --git a/lib/content-services/src/lib/content-node-selector/content-node-selector.service.ts b/lib/content-services/src/lib/content-node-selector/content-node-selector.service.ts deleted file mode 100644 index 80299f2b36..0000000000 --- a/lib/content-services/src/lib/content-node-selector/content-node-selector.service.ts +++ /dev/null @@ -1,62 +0,0 @@ -/*! - * @license - * Copyright 2019 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Injectable } from '@angular/core'; -import { QueryBody } from '@alfresco/js-api'; - -/** - * Internal service used by ContentNodeSelector component. - */ -@Injectable({ - providedIn: 'root' -}) -export class ContentNodeSelectorService { - - createQuery(searchTerm: string, rootNodeId: string = null, skipCount: number = 0, maxItems: number = 25, extraNodeIds?: string[], showFiles?: boolean): QueryBody { - let extraParentFiltering = ''; - - if (extraNodeIds && extraNodeIds.length) { - extraNodeIds - .filter((id) => id !== rootNodeId) - .forEach((extraId) => { - extraParentFiltering += ` OR ANCESTOR:'workspace://SpacesStore/${extraId}'`; - }); - } - - const parentFiltering = rootNodeId ? [{ query: `ANCESTOR:'workspace://SpacesStore/${rootNodeId}'${extraParentFiltering}` }] : []; - - return { - query: { - query: `${searchTerm}*` - }, - include: ['path', 'allowableOperations', 'properties'], - paging: { - maxItems: maxItems, - skipCount: skipCount - }, - filterQueries: [ - { query: `TYPE:'cm:folder'${showFiles ? " OR TYPE:'cm:content'" : ''}` }, - { query: 'NOT cm:creator:System' }, - ...parentFiltering - ], - scope: { - locations: 'nodes' - } - }; - - } -} diff --git a/lib/content-services/src/lib/content-node-selector/public-api.ts b/lib/content-services/src/lib/content-node-selector/public-api.ts index 7e02ad4b15..a1f0db75cf 100644 --- a/lib/content-services/src/lib/content-node-selector/public-api.ts +++ b/lib/content-services/src/lib/content-node-selector/public-api.ts @@ -19,7 +19,6 @@ export * from './name-location-cell/name-location-cell.component'; export * from './content-node-selector.component-data.interface'; export * from './content-node-selector-panel.component'; export * from './content-node-selector.component'; -export * from './content-node-selector.service'; export * from './content-node-dialog.service'; export * from './content-node-selector-panel.service'; diff --git a/lib/content-services/src/lib/mock/public-api.ts b/lib/content-services/src/lib/mock/public-api.ts index 151df1f0df..ac5c588d4f 100644 --- a/lib/content-services/src/lib/mock/public-api.ts +++ b/lib/content-services/src/lib/mock/public-api.ts @@ -21,3 +21,4 @@ export * from './search.component.mock'; export * from './search.service.mock'; export * from './search-filter-mock'; export * from './sites-dropdown.component.mock'; +export * from './search-query.mock'; diff --git a/lib/content-services/src/lib/mock/search-query.mock.ts b/lib/content-services/src/lib/mock/search-query.mock.ts new file mode 100644 index 0000000000..20418dfbd3 --- /dev/null +++ b/lib/content-services/src/lib/mock/search-query.mock.ts @@ -0,0 +1,41 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { QueryBody } from '@alfresco/js-api'; + +export const mockQueryBody: QueryBody = { + query: { + query: '(search-term)', + language: 'afts' + }, + include: ['path', 'allowableOperations'], + paging: { + maxItems: 25, + skipCount: 0 + }, + fields: undefined, + filterQueries: [], + facetQueries: null, + facetIntervals: null, + facetFields: null, + sort: [], + scope: { + locations: 'nodes' + }, + highlight: null, + facetFormat: 'V2' +}; diff --git a/lib/content-services/src/lib/search/base-query-builder.service.ts b/lib/content-services/src/lib/search/base-query-builder.service.ts index 6d201ce734..f683d66201 100644 --- a/lib/content-services/src/lib/search/base-query-builder.service.ts +++ b/lib/content-services/src/lib/search/base-query-builder.service.ts @@ -24,7 +24,8 @@ import { RequestFacetField, RequestSortDefinitionInner, ResultSetPaging, - RequestHighlight + RequestHighlight, + RequestScope } from '@alfresco/js-api'; import { SearchCategory } from './search-category.interface'; import { FilterQuery } from './filter-query.interface'; @@ -52,6 +53,7 @@ export abstract class BaseQueryBuilderService { paging: { maxItems?: number; skipCount?: number } = null; sorting: SearchSortingDefinition[] = []; sortingOptions: SearchSortingDefinition[] = []; + private scope: RequestScope; protected userFacetBuckets: { [key: string]: FacetFieldBucket[] } = {}; @@ -193,6 +195,14 @@ export abstract class BaseQueryBuilderService { return null; } + setScope(scope: RequestScope) { + this.scope = scope; + } + + getScope(): RequestScope { + return this.scope; + } + /** * Builds the current query and triggers the `updated` event. */ @@ -265,6 +275,10 @@ export abstract class BaseQueryBuilderService { highlight: this.highlight }; + if (this.scope) { + result['scope'] = this.scope; + } + result['facetFormat'] = 'V2'; return result; } diff --git a/lib/content-services/src/lib/search/search-query-builder.service.spec.ts b/lib/content-services/src/lib/search/search-query-builder.service.spec.ts index ad63444bee..6dc326a7e0 100644 --- a/lib/content-services/src/lib/search/search-query-builder.service.spec.ts +++ b/lib/content-services/src/lib/search/search-query-builder.service.spec.ts @@ -618,4 +618,47 @@ describe('SearchQueryBuilder', () => { builder.execute(); }); + + it('should include contain the path and allowableOperations by default', () => { + const builder = new SearchQueryBuilderService(buildConfig({}), null); + builder.userQuery = 'nuka cola quantum'; + const queryBody = builder.buildQuery(); + + expect(queryBody.include).toEqual(['path', 'allowableOperations']); + }); + + it('should fetch the include config from the app config', () => { + const includeConfig = ['path', 'allowableOperations', 'properties']; + const config: SearchConfiguration = { + include: includeConfig + }; + const builder = new SearchQueryBuilderService(buildConfig(config), null); + builder.userQuery = 'nuka cola quantum'; + const queryBody = builder.buildQuery(); + + expect(queryBody.include).toEqual(includeConfig); + }); + + it('should the query contain the pagination', () => { + const builder = new SearchQueryBuilderService(buildConfig({}), null); + builder.userQuery = 'nuka cola quantum'; + const mockPagination = { + maxItems: 10, + skipCount: 0 + }; + builder.paging = mockPagination; + const queryBody = builder.buildQuery(); + + expect(queryBody.paging).toEqual(mockPagination); + }); + + it('should the query contain the scope in case it is defined', () => { + const builder = new SearchQueryBuilderService(buildConfig({}), null); + const mockScope = { locations: 'mock-location' }; + builder.userQuery = 'nuka cola quantum'; + builder.setScope(mockScope); + const queryBody = builder.buildQuery(); + + expect(queryBody.scope).toEqual(mockScope); + }); }); diff --git a/lib/process-services/src/lib/content-widget/attach-file-widget-dialog.component.ts b/lib/process-services/src/lib/content-widget/attach-file-widget-dialog.component.ts index f5bf4e9c5b..5b42560a73 100644 --- a/lib/process-services/src/lib/content-widget/attach-file-widget-dialog.component.ts +++ b/lib/process-services/src/lib/content-widget/attach-file-widget-dialog.component.ts @@ -19,7 +19,7 @@ import { Component, Inject, ViewEncapsulation, ViewChild } from '@angular/core'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { ExternalAlfrescoApiService, AlfrescoApiService, LoginDialogPanelComponent, SearchService, TranslationService, AuthenticationService, SitesService } from '@alfresco/adf-core'; import { AttachFileWidgetDialogComponentData } from './attach-file-widget-dialog-component.interface'; -import { DocumentListService, ContentNodeSelectorService } from '@alfresco/adf-content-services'; +import { DocumentListService } from '@alfresco/adf-content-services'; import { Node } from '@alfresco/js-api'; @Component({ @@ -31,7 +31,6 @@ import { Node } from '@alfresco/js-api'; AuthenticationService, DocumentListService, SitesService, - ContentNodeSelectorService, SearchService, { provide: AlfrescoApiService, useClass: ExternalAlfrescoApiService} ] })