diff --git a/docs/process-services-cloud/components/process-list-cloud.component.md b/docs/process-services-cloud/components/process-list-cloud.component.md index 9f329eb514..e5257dd17e 100644 --- a/docs/process-services-cloud/components/process-list-cloud.component.md +++ b/docs/process-services-cloud/components/process-list-cloud.component.md @@ -80,6 +80,10 @@ when the process list is empty: | stickyHeader | `boolean` | false | Toggles the sticky header mode. | | suspendedFrom | `string` | "" | Filter the processes. Display only process with suspendedFrom equal to the supplied date. | | suspendedTo | `string` | "" | Filter the processes. Display only process with suspendedTo equal to the supplied date. | +| names | `string[]` | [] | Filter the processes. Display only processes with names matching any of the supplied strings. This input will be used only if `PROCESS_SEARCH_API_METHOD_TOKEN` is provided with `POST` value. | +initiators | `string[]` | [] | Filter the processes. Display only processes started by any of the users whose usernames are present in the array. This input will be used only if `PROCESS_SEARCH_API_METHOD_TOKEN` is provided with `POST` value. | +| appVersions | `string[]` | [] | Filter the processes. Display only processes present in any of the specified app versions. This input will be used only if `PROCESS_SEARCH_API_METHOD_TOKEN` is provided with `POST` value. | +| statuses | `string[]` | [] | Filter the processes. Display only processes with provided statuses. This input will be used only if `PROCESS_SEARCH_API_METHOD_TOKEN` is provided with `POST` value. | ### Events diff --git a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters-cloud.component.spec.ts index d3dccf5875..abf5816c4f 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters-cloud.component.spec.ts @@ -21,7 +21,7 @@ import { of, throwError } from 'rxjs'; import { ProcessFilterCloudService } from '../services/process-filter-cloud.service'; import { ProcessFiltersCloudComponent } from './process-filters-cloud.component'; import { By } from '@angular/platform-browser'; -import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service'; +import { PROCESS_FILTERS_SERVICE_TOKEN, PROCESS_SEARCH_API_METHOD_TOKEN } from '../../../services/cloud-token.service'; import { LocalPreferenceCloudService } from '../../../services/local-preference-cloud.service'; import { mockProcessFilters } from '../mock/process-filters-cloud.mock'; import { AppConfigService, AppConfigServiceMock, NoopTranslateModule } from '@alfresco/adf-core'; @@ -44,16 +44,20 @@ describe('ProcessFiltersCloudComponent', () => { let getProcessFiltersSpy: jasmine.Spy; let getProcessNotificationSubscriptionSpy: jasmine.Spy; - beforeEach(() => { + const configureTestingModule = (providers: any[]) => { TestBed.configureTestingModule({ imports: [NoopTranslateModule, NoopAnimationsModule, MatListModule], providers: [ { provide: PROCESS_FILTERS_SERVICE_TOKEN, useClass: LocalPreferenceCloudService }, { provide: AppConfigService, useClass: AppConfigServiceMock }, - { provide: ProcessListCloudService, useValue: { getProcessCounter: () => of(10) } }, + { provide: ProcessListCloudService, useValue: { + getProcessCounter: () => of(10), + getProcessListCounter: () => of(10) + }}, { provide: ProcessFilterCloudService, useValue: ProcessFilterCloudServiceMock }, NotificationCloudService, - ApolloModule + ApolloModule, + ...providers ] }); fixture = TestBed.createComponent(ProcessFiltersCloudComponent); @@ -62,400 +66,580 @@ describe('ProcessFiltersCloudComponent', () => { processFilterService = TestBed.inject(ProcessFilterCloudService); getProcessFiltersSpy = spyOn(processFilterService, 'getProcessFilters').and.returnValue(of(mockProcessFilters)); getProcessNotificationSubscriptionSpy = spyOn(processFilterService, 'getProcessNotificationSubscription').and.returnValue(of([])); - }); + }; afterEach(() => { fixture.destroy(); }); - it('should attach specific icon for each filter if hasIcon is true', async () => { - const change = new SimpleChange(undefined, 'my-app-1', true); - component.ngOnChanges({ appName: change }); - - fixture.detectChanges(); - await fixture.whenStable(); - - component.showIcons = true; - - fixture.detectChanges(); - await fixture.whenStable(); + describe('PROCESS_SEARCH_API_METHOD_TOKEN injected with GET value', () => { + beforeEach(() => { + configureTestingModule([{ provide: PROCESS_SEARCH_API_METHOD_TOKEN, useValue: 'GET' }]); + }); - expect(component.filters.length).toBe(3); - const filters = fixture.nativeElement.querySelectorAll('.adf-icon'); - expect(filters.length).toBe(3); - expect(filters[0].innerText).toContain('adjust'); - expect(filters[1].innerText).toContain('inbox'); - expect(filters[2].innerText).toContain('done'); - }); + it('should attach specific icon for each filter if hasIcon is true', async () => { + const change = new SimpleChange(undefined, 'my-app-1', true); + component.ngOnChanges({ appName: change }); - it('should not attach icons for each filter if hasIcon is false', async () => { - component.showIcons = false; - const change = new SimpleChange(undefined, 'my-app-1', true); - component.ngOnChanges({ appName: change }); + fixture.detectChanges(); + await fixture.whenStable(); - fixture.detectChanges(); - await fixture.whenStable(); + component.showIcons = true; - const filters: any = fixture.debugElement.queryAll(By.css('.adf-icon')); - expect(filters.length).toBe(0); - }); + fixture.detectChanges(); + await fixture.whenStable(); - it('should display the filters', async () => { - const change = new SimpleChange(undefined, 'my-app-1', true); - component.ngOnChanges({ appName: change }); + expect(component.filters.length).toBe(3); + const filters = fixture.nativeElement.querySelectorAll('.adf-icon'); + expect(filters.length).toBe(3); + expect(filters[0].innerText).toContain('adjust'); + expect(filters[1].innerText).toContain('inbox'); + expect(filters[2].innerText).toContain('done'); + }); - fixture.detectChanges(); - await fixture.whenStable(); + it('should not attach icons for each filter if hasIcon is false', async () => { + component.showIcons = false; + const change = new SimpleChange(undefined, 'my-app-1', true); + component.ngOnChanges({ appName: change }); - component.showIcons = true; + fixture.detectChanges(); + await fixture.whenStable(); - fixture.detectChanges(); - await fixture.whenStable(); + const filters: any = fixture.debugElement.queryAll(By.css('.adf-icon')); + expect(filters.length).toBe(0); + }); - const filters = fixture.debugElement.queryAll(By.css('.adf-process-filters__entry')); - expect(component.filters.length).toBe(3); - expect(filters.length).toBe(3); - expect(filters[0].nativeElement.innerText).toContain('FakeAllProcesses'); - expect(filters[1].nativeElement.innerText).toContain('FakeRunningProcesses'); - expect(filters[2].nativeElement.innerText).toContain('FakeCompletedProcesses'); - expect(Object.keys(component.counters).length).toBe(3); - }); + it('should display the filters', async () => { + const change = new SimpleChange(undefined, 'my-app-1', true); + component.ngOnChanges({ appName: change }); - it('should emit an error with a bad response', () => { - getProcessFiltersSpy.and.returnValue(throwError('wrong request')); + fixture.detectChanges(); + await fixture.whenStable(); - const appName = 'my-app-1'; - const change = new SimpleChange(null, appName, true); + component.showIcons = true; - let lastValue: any; - component.error.subscribe((err) => (lastValue = err)); + fixture.detectChanges(); + await fixture.whenStable(); - component.ngOnChanges({ appName: change }); - fixture.detectChanges(); - expect(lastValue).toBeDefined(); - }); + const filters = fixture.debugElement.queryAll(By.css('.adf-process-filters__entry')); + expect(component.filters.length).toBe(3); + expect(filters.length).toBe(3); + expect(filters[0].nativeElement.innerText).toContain('FakeAllProcesses'); + expect(filters[1].nativeElement.innerText).toContain('FakeRunningProcesses'); + expect(filters[2].nativeElement.innerText).toContain('FakeCompletedProcesses'); + expect(Object.keys(component.counters).length).toBe(3); + }); - it('should emit success with the filters when filters are loaded', async () => { - const successSpy = spyOn(component.success, 'emit'); - const appName = 'my-app-1'; - const change = new SimpleChange(null, appName, true); - - component.ngOnChanges({ appName: change }); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(successSpy).toHaveBeenCalledWith(mockProcessFilters); - expect(component.filters).toBeDefined(); - expect(component.filters[0].name).toEqual('FakeAllProcesses'); - expect(component.filters[1].name).toEqual('FakeRunningProcesses'); - expect(component.filters[2].name).toEqual('FakeCompletedProcesses'); - expect(Object.keys(component.counters).length).toBe(3); - }); + it('should emit success with the filters when filters are loaded', async () => { + const successSpy = spyOn(component.success, 'emit'); + const appName = 'my-app-1'; + const change = new SimpleChange(null, appName, true); - it('should not select any filter as default', async () => { - const appName = 'my-app-1'; - const change = new SimpleChange(null, appName, true); + component.ngOnChanges({ appName: change }); + fixture.detectChanges(); + await fixture.whenStable(); - component.ngOnChanges({ appName: change }); - fixture.detectChanges(); - await fixture.whenStable(); + expect(successSpy).toHaveBeenCalledWith(mockProcessFilters); + expect(component.filters).toBeDefined(); + expect(component.filters[0].name).toEqual('FakeAllProcesses'); + expect(component.filters[1].name).toEqual('FakeRunningProcesses'); + expect(component.filters[2].name).toEqual('FakeCompletedProcesses'); + expect(Object.keys(component.counters).length).toBe(3); + }); - expect(component.currentFilter).toBeUndefined(); - }); + it('should not select any filter as default', async () => { + const appName = 'my-app-1'; + const change = new SimpleChange(null, appName, true); - it('should not select any process filter if filter input does not exist', async () => { - const change = new SimpleChange(null, { name: 'nonexistentFilter' }, true); - fixture.detectChanges(); - await fixture.whenStable(); - component.ngOnChanges({ filterParam: change }); + component.ngOnChanges({ appName: change }); + fixture.detectChanges(); + await fixture.whenStable(); - expect(component.currentFilter).toBeUndefined(); - }); + expect(component.currentFilter).toBeUndefined(); + }); - it('should select the filter based on the input by name param', async () => { - const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); - const change = new SimpleChange(null, { name: 'FakeRunningProcesses' }, true); + it('should filterClicked emit when a filter is clicked from the UI', async () => { + const filterClickedSpy = spyOn(component.filterClicked, 'emit'); + const appName = 'my-app-1'; + const change = new SimpleChange(null, appName, true); + component.ngOnChanges({ appName: change }); - fixture.detectChanges(); - await fixture.whenStable(); - component.ngOnChanges({ filterParam: change }); + fixture.detectChanges(); + await fixture.whenStable(); - expect(component.currentFilter).toEqual(mockProcessFilters[1]); - expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[1]); - }); + const filterButton = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${mockProcessFilters[0].key}_filter"]`); + filterButton.click(); - it('should select the filter based on the input by key param', async () => { - const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); - const change = new SimpleChange(null, { key: 'completed-processes' }, true); + fixture.detectChanges(); + await fixture.whenStable(); - fixture.detectChanges(); - await fixture.whenStable(); - component.ngOnChanges({ filterParam: change }); + expect(component.currentFilter).toEqual(mockProcessFilters[0]); + expect(filterClickedSpy).toHaveBeenCalledWith(mockProcessFilters[0]); + }); - expect(component.currentFilter).toEqual(mockProcessFilters[2]); - expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]); + describe('Highlight Selected Filter', () => { + const allProcessesFilterKey = mockProcessFilters[0].key; + const runningProcessesFilterKey = mockProcessFilters[1].key; + const completedProcessesFilterKey = mockProcessFilters[2].key; + + const getActiveFilterElement = (filterKey: string): Element => { + const activeFilter = fixture.debugElement.query(By.css(`.adf-active`)); + return activeFilter.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`); + }; + + const clickOnFilter = async (filterKey: string) => { + const button = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`); + button.click(); + fixture.detectChanges(); + await fixture.whenStable(); + }; + + it('should apply active CSS class on filter click', async () => { + component.enableNotifications = true; + component.appName = 'mock-app-name'; + const appNameChange = new SimpleChange(null, 'mock-app-name', true); + component.ngOnChanges({ appName: appNameChange }); + fixture.detectChanges(); + await fixture.whenStable(); + + await clickOnFilter(allProcessesFilterKey); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getActiveFilterElement(allProcessesFilterKey)).toBeDefined(); + expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull(); + expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull(); + + await clickOnFilter(runningProcessesFilterKey); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull(); + expect(getActiveFilterElement(runningProcessesFilterKey)).toBeDefined(); + expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull(); + + await clickOnFilter(completedProcessesFilterKey); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull(); + expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull(); + expect(getActiveFilterElement(completedProcessesFilterKey)).toBeDefined(); + }); + }); }); - it('should select the filter based on the input by index param', async () => { - const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); - const change = new SimpleChange(null, { index: 2 }, true); - - fixture.detectChanges(); - await fixture.whenStable(); - component.ngOnChanges({ filterParam: change }); + describe('PROCESS_SEARCH_API_METHOD_TOKEN injected with POST value', () => { + beforeEach(() => { + configureTestingModule([{ provide: PROCESS_SEARCH_API_METHOD_TOKEN, useValue: 'POST' }]); + }); - expect(component.currentFilter).toEqual(mockProcessFilters[2]); - expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]); - }); + it('should attach specific icon for each filter if hasIcon is true', async () => { + const change = new SimpleChange(undefined, 'my-app-1', true); + component.ngOnChanges({ appName: change }); - it('should select the filter based on the input by id param', async () => { - const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); - const change = new SimpleChange(null, { id: '12' }, true); + fixture.detectChanges(); + await fixture.whenStable(); - fixture.detectChanges(); - await fixture.whenStable(); - component.ngOnChanges({ filterParam: change }); + component.showIcons = true; - expect(component.currentFilter).toEqual(mockProcessFilters[2]); - expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]); - }); + fixture.detectChanges(); + await fixture.whenStable(); - it('should filterClicked emit when a filter is clicked from the UI', async () => { - const filterClickedSpy = spyOn(component.filterClicked, 'emit'); - const appName = 'my-app-1'; - const change = new SimpleChange(null, appName, true); - component.ngOnChanges({ appName: change }); + expect(component.filters.length).toBe(3); + const filters = fixture.nativeElement.querySelectorAll('.adf-icon'); + expect(filters.length).toBe(3); + expect(filters[0].innerText).toContain('adjust'); + expect(filters[1].innerText).toContain('inbox'); + expect(filters[2].innerText).toContain('done'); + }); - fixture.detectChanges(); - await fixture.whenStable(); + it('should not attach icons for each filter if hasIcon is false', async () => { + component.showIcons = false; + const change = new SimpleChange(undefined, 'my-app-1', true); + component.ngOnChanges({ appName: change }); - const filterButton = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${mockProcessFilters[0].key}_filter"]`); - filterButton.click(); + fixture.detectChanges(); + await fixture.whenStable(); - fixture.detectChanges(); - await fixture.whenStable(); + const filters: any = fixture.debugElement.queryAll(By.css('.adf-icon')); + expect(filters.length).toBe(0); + }); - expect(component.currentFilter).toEqual(mockProcessFilters[0]); - expect(filterClickedSpy).toHaveBeenCalledWith(mockProcessFilters[0]); - }); + it('should display the filters', async () => { + const change = new SimpleChange(undefined, 'my-app-1', true); + component.ngOnChanges({ appName: change }); - it('should reset the filter when the param is undefined', () => { - const change = new SimpleChange(mockProcessFilters[0], undefined, false); - component.currentFilter = mockProcessFilters[0]; - component.ngOnChanges({ filterParam: change }); + fixture.detectChanges(); + await fixture.whenStable(); - expect(component.currentFilter).toEqual(undefined); - }); + component.showIcons = true; - it('should not emit a filter clicked event when a filter is selected through the filterParam input (filterClicked emits only through a UI click action)', async () => { - const filterClickedSpy = spyOn(component.filterClicked, 'emit'); - const change = new SimpleChange(null, { id: '10' }, true); + fixture.detectChanges(); + await fixture.whenStable(); - fixture.detectChanges(); - await fixture.whenStable(); - component.ngOnChanges({ filterParam: change }); + const filters = fixture.debugElement.queryAll(By.css('.adf-process-filters__entry')); + expect(component.filters.length).toBe(3); + expect(filters.length).toBe(3); + expect(filters[0].nativeElement.innerText).toContain('FakeAllProcesses'); + expect(filters[1].nativeElement.innerText).toContain('FakeRunningProcesses'); + expect(filters[2].nativeElement.innerText).toContain('FakeCompletedProcesses'); + expect(Object.keys(component.counters).length).toBe(3); + }); - expect(component.currentFilter).toBe(mockProcessFilters[0]); - expect(filterClickedSpy).not.toHaveBeenCalled(); - }); + it('should emit success with the filters when filters are loaded', async () => { + const successSpy = spyOn(component.success, 'emit'); + const appName = 'my-app-1'; + const change = new SimpleChange(null, appName, true); - it('should reload filters by appName on binding changes', () => { - spyOn(component, 'getFilters').and.stub(); - const appName = 'my-app-1'; + component.ngOnChanges({ appName: change }); + fixture.detectChanges(); + await fixture.whenStable(); - const change = new SimpleChange(null, appName, true); - component.ngOnChanges({ appName: change }); + expect(successSpy).toHaveBeenCalledWith(mockProcessFilters); + expect(component.filters).toBeDefined(); + expect(component.filters[0].name).toEqual('FakeAllProcesses'); + expect(component.filters[1].name).toEqual('FakeRunningProcesses'); + expect(component.filters[2].name).toEqual('FakeCompletedProcesses'); + expect(Object.keys(component.counters).length).toBe(3); + }); - expect(component.getFilters).toHaveBeenCalledWith(appName); - }); + it('should not select any filter as default', async () => { + const appName = 'my-app-1'; + const change = new SimpleChange(null, appName, true); - it('should not reload filters by appName null on binding changes', () => { - spyOn(component, 'getFilters').and.stub(); - const appName = null; + component.ngOnChanges({ appName: change }); + fixture.detectChanges(); + await fixture.whenStable(); - const change = new SimpleChange(undefined, appName, true); - component.ngOnChanges({ appName: change }); + expect(component.currentFilter).toBeUndefined(); + }); - expect(component.getFilters).not.toHaveBeenCalledWith(appName); - }); + it('should filterClicked emit when a filter is clicked from the UI', async () => { + const filterClickedSpy = spyOn(component.filterClicked, 'emit'); + const appName = 'my-app-1'; + const change = new SimpleChange(null, appName, true); + component.ngOnChanges({ appName: change }); - it('should reload filters by app name on binding changes', () => { - spyOn(component, 'getFilters').and.stub(); - const appName = 'fake-app-name'; + fixture.detectChanges(); + await fixture.whenStable(); - const change = new SimpleChange(null, appName, true); - component.ngOnChanges({ appName: change }); + const filterButton = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${mockProcessFilters[0].key}_filter"]`); + filterButton.click(); - expect(component.getFilters).toHaveBeenCalledWith(appName); - }); + fixture.detectChanges(); + await fixture.whenStable(); - it('should return the current filter after one is selected', () => { - const filter = mockProcessFilters[1]; - component.filters = mockProcessFilters; + expect(component.currentFilter).toEqual(mockProcessFilters[0]); + expect(filterClickedSpy).toHaveBeenCalledWith(mockProcessFilters[0]); + }); - expect(component.currentFilter).toBeUndefined(); - component.selectFilter({ id: filter.id }); - expect(component.getCurrentFilter()).toBe(filter); + describe('Highlight Selected Filter', () => { + const allProcessesFilterKey = mockProcessFilters[0].key; + const runningProcessesFilterKey = mockProcessFilters[1].key; + const completedProcessesFilterKey = mockProcessFilters[2].key; + + const getActiveFilterElement = (filterKey: string): Element => { + const activeFilter = fixture.debugElement.query(By.css(`.adf-active`)); + return activeFilter.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`); + }; + + const clickOnFilter = async (filterKey: string) => { + const button = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`); + button.click(); + fixture.detectChanges(); + await fixture.whenStable(); + }; + + it('should apply active CSS class on filter click', async () => { + component.enableNotifications = true; + component.appName = 'mock-app-name'; + const appNameChange = new SimpleChange(null, 'mock-app-name', true); + component.ngOnChanges({ appName: appNameChange }); + fixture.detectChanges(); + await fixture.whenStable(); + + await clickOnFilter(allProcessesFilterKey); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getActiveFilterElement(allProcessesFilterKey)).toBeDefined(); + expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull(); + expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull(); + + await clickOnFilter(runningProcessesFilterKey); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull(); + expect(getActiveFilterElement(runningProcessesFilterKey)).toBeDefined(); + expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull(); + + await clickOnFilter(completedProcessesFilterKey); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull(); + expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull(); + expect(getActiveFilterElement(completedProcessesFilterKey)).toBeDefined(); + }); + }); }); - it('should remove key from set of updated filters when received refreshed filter key', async () => { - const filterKeyTest = 'filter-key-test'; - component.updatedFiltersSet.add(filterKeyTest); - - expect(component.updatedFiltersSet.size).toBe(1); - processFilterService.filterKeyToBeRefreshed$ = of(filterKeyTest); - fixture.detectChanges(); + describe('API agnostic', () => { + beforeEach(() => { + configureTestingModule([]); + }); - expect(component.updatedFiltersSet.size).toBe(0); - }); + it('should emit an error with a bad response', () => { + getProcessFiltersSpy.and.returnValue(throwError('wrong request')); - describe('Highlight Selected Filter', () => { - const allProcessesFilterKey = mockProcessFilters[0].key; - const runningProcessesFilterKey = mockProcessFilters[1].key; - const completedProcessesFilterKey = mockProcessFilters[2].key; + const appName = 'my-app-1'; + const change = new SimpleChange(null, appName, true); - const getActiveFilterElement = (filterKey: string): Element => { - const activeFilter = fixture.debugElement.query(By.css(`.adf-active`)); - return activeFilter.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`); - }; + let lastValue: any; + component.error.subscribe((err) => (lastValue = err)); - const clickOnFilter = async (filterKey: string) => { - const button = fixture.debugElement.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`); - button.click(); + component.ngOnChanges({ appName: change }); fixture.detectChanges(); - await fixture.whenStable(); - }; + expect(lastValue).toBeDefined(); + }); - it('should apply active CSS class on filter click', async () => { - component.enableNotifications = true; - component.appName = 'mock-app-name'; - const appNameChange = new SimpleChange(null, 'mock-app-name', true); - component.ngOnChanges({ appName: appNameChange }); + it('should not select any process filter if filter input does not exist', async () => { + const change = new SimpleChange(null, { name: 'nonexistentFilter' }, true); fixture.detectChanges(); await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); - await clickOnFilter(allProcessesFilterKey); - fixture.detectChanges(); - await fixture.whenStable(); + expect(component.currentFilter).toBeUndefined(); + }); - expect(getActiveFilterElement(allProcessesFilterKey)).toBeDefined(); - expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull(); - expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull(); + it('should select the filter based on the input by name param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { name: 'FakeRunningProcesses' }, true); - await clickOnFilter(runningProcessesFilterKey); fixture.detectChanges(); await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); - expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull(); - expect(getActiveFilterElement(runningProcessesFilterKey)).toBeDefined(); - expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull(); + expect(component.currentFilter).toEqual(mockProcessFilters[1]); + expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[1]); + }); + + it('should select the filter based on the input by key param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { key: 'completed-processes' }, true); - await clickOnFilter(completedProcessesFilterKey); fixture.detectChanges(); await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); - expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull(); - expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull(); - expect(getActiveFilterElement(completedProcessesFilterKey)).toBeDefined(); + expect(component.currentFilter).toEqual(mockProcessFilters[2]); + expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]); }); - it('Should apply active CSS class when filterParam input changed', async () => { - fixture.detectChanges(); - component.ngOnChanges({ filterParam: new SimpleChange(null, { key: allProcessesFilterKey }, true) }); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(getActiveFilterElement(allProcessesFilterKey)).toBeDefined(); - expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull(); - expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull(); + it('should select the filter based on the input by index param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { index: 2 }, true); - component.ngOnChanges({ filterParam: new SimpleChange(null, { key: runningProcessesFilterKey }, true) }); fixture.detectChanges(); await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(mockProcessFilters[2]); + expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]); + }); - expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull(); - expect(getActiveFilterElement(runningProcessesFilterKey)).toBeDefined(); - expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull(); + it('should select the filter based on the input by id param', async () => { + const filterSelectedSpy = spyOn(component.filterSelected, 'emit'); + const change = new SimpleChange(null, { id: '12' }, true); - component.ngOnChanges({ filterParam: new SimpleChange(null, { key: completedProcessesFilterKey }, true) }); fixture.detectChanges(); await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); - expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull(); - expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull(); - expect(getActiveFilterElement(completedProcessesFilterKey)).toBeDefined(); + expect(component.currentFilter).toEqual(mockProcessFilters[2]); + expect(filterSelectedSpy).toHaveBeenCalledWith(mockProcessFilters[2]); }); - it('should made sbscription', () => { - component.enableNotifications = true; - component.appName = 'mock-app-name'; - const appNameChange = new SimpleChange(null, 'mock-app-name', true); - component.ngOnChanges({ appName: appNameChange }); - fixture.detectChanges(); - expect(getProcessNotificationSubscriptionSpy).toHaveBeenCalled(); + it('should reset the filter when the param is undefined', () => { + const change = new SimpleChange(mockProcessFilters[0], undefined, false); + component.currentFilter = mockProcessFilters[0]; + component.ngOnChanges({ filterParam: change }); + + expect(component.currentFilter).toEqual(undefined); }); - it('should not emit filter key when filter counter is set for first time', () => { - component.currentFiltersValues = {}; - const fakeFilterKey = 'testKey'; - const fakeFilterValue = 10; - const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue); + it('should not emit a filter clicked event when a filter is selected through the filterParam input (filterClicked emits only through a UI click action)', async () => { + const filterClickedSpy = spyOn(component.filterClicked, 'emit'); + const change = new SimpleChange(null, { id: '10' }, true); + fixture.detectChanges(); + await fixture.whenStable(); + component.ngOnChanges({ filterParam: change }); - expect(component.currentFiltersValues).not.toEqual({}); - expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue); - expect(updatedFilterSpy).not.toHaveBeenCalled(); + expect(component.currentFilter).toBe(mockProcessFilters[0]); + expect(filterClickedSpy).not.toHaveBeenCalled(); }); - it('should not emit filter key when filter counter has not changd', () => { - component.currentFiltersValues = {}; - const fakeFilterKey = 'testKey'; - const fakeFilterValue = 10; - const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue); - fixture.detectChanges(); + it('should reload filters by appName on binding changes', () => { + spyOn(component, 'getFilters').and.stub(); + const appName = 'my-app-1'; - expect(component.currentFiltersValues).not.toEqual({}); - expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue); + const change = new SimpleChange(null, appName, true); + component.ngOnChanges({ appName: change }); - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue); - expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue); - expect(updatedFilterSpy).not.toHaveBeenCalled(); + expect(component.getFilters).toHaveBeenCalledWith(appName); }); - it('should emit filter key when filter counter is increased', () => { - component.currentFiltersValues = {}; - const fakeFilterKey = 'testKey'; - const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 10); - fixture.detectChanges(); + it('should not reload filters by appName null on binding changes', () => { + spyOn(component, 'getFilters').and.stub(); + const appName = null; - expect(updatedFilterSpy).not.toHaveBeenCalledWith(fakeFilterKey); - expect(component.currentFiltersValues[fakeFilterKey]).toBe(10); + const change = new SimpleChange(undefined, appName, true); + component.ngOnChanges({ appName: change }); - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 20); - fixture.detectChanges(); + expect(component.getFilters).not.toHaveBeenCalledWith(appName); + }); + + it('should reload filters by app name on binding changes', () => { + spyOn(component, 'getFilters').and.stub(); + const appName = 'fake-app-name'; - expect(updatedFilterSpy).toHaveBeenCalledWith(fakeFilterKey); - expect(component.currentFiltersValues[fakeFilterKey]).toBe(20); + const change = new SimpleChange(null, appName, true); + component.ngOnChanges({ appName: change }); + + expect(component.getFilters).toHaveBeenCalledWith(appName); }); - it('should emit filter key when filter counter is decreased', () => { - component.currentFiltersValues = {}; - const fakeFilterKey = 'testKey'; - const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 10); - fixture.detectChanges(); + it('should return the current filter after one is selected', () => { + const filter = mockProcessFilters[1]; + component.filters = mockProcessFilters; + + expect(component.currentFilter).toBeUndefined(); + component.selectFilter({ id: filter.id }); + expect(component.getCurrentFilter()).toBe(filter); + }); - expect(updatedFilterSpy).not.toHaveBeenCalledWith(fakeFilterKey); - expect(component.currentFiltersValues[fakeFilterKey]).toBe(10); + it('should remove key from set of updated filters when received refreshed filter key', async () => { + const filterKeyTest = 'filter-key-test'; + component.updatedFiltersSet.add(filterKeyTest); - component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 5); + expect(component.updatedFiltersSet.size).toBe(1); + processFilterService.filterKeyToBeRefreshed$ = of(filterKeyTest); fixture.detectChanges(); - expect(updatedFilterSpy).toHaveBeenCalledWith(fakeFilterKey); - expect(component.currentFiltersValues[fakeFilterKey]).toBe(5); + expect(component.updatedFiltersSet.size).toBe(0); + }); + + describe('Highlight Selected Filter', () => { + const allProcessesFilterKey = mockProcessFilters[0].key; + const runningProcessesFilterKey = mockProcessFilters[1].key; + const completedProcessesFilterKey = mockProcessFilters[2].key; + + const getActiveFilterElement = (filterKey: string): Element => { + const activeFilter = fixture.debugElement.query(By.css(`.adf-active`)); + return activeFilter.nativeElement.querySelector(`[data-automation-id="${filterKey}_filter"]`); + }; + + it('Should apply active CSS class when filterParam input changed', async () => { + fixture.detectChanges(); + component.ngOnChanges({ filterParam: new SimpleChange(null, { key: allProcessesFilterKey }, true) }); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getActiveFilterElement(allProcessesFilterKey)).toBeDefined(); + expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull(); + expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull(); + + component.ngOnChanges({ filterParam: new SimpleChange(null, { key: runningProcessesFilterKey }, true) }); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull(); + expect(getActiveFilterElement(runningProcessesFilterKey)).toBeDefined(); + expect(getActiveFilterElement(completedProcessesFilterKey)).toBeNull(); + + component.ngOnChanges({ filterParam: new SimpleChange(null, { key: completedProcessesFilterKey }, true) }); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getActiveFilterElement(allProcessesFilterKey)).toBeNull(); + expect(getActiveFilterElement(runningProcessesFilterKey)).toBeNull(); + expect(getActiveFilterElement(completedProcessesFilterKey)).toBeDefined(); + }); + + it('should made sbscription', () => { + component.enableNotifications = true; + component.appName = 'mock-app-name'; + const appNameChange = new SimpleChange(null, 'mock-app-name', true); + component.ngOnChanges({ appName: appNameChange }); + fixture.detectChanges(); + expect(getProcessNotificationSubscriptionSpy).toHaveBeenCalled(); + }); + + it('should not emit filter key when filter counter is set for first time', () => { + component.currentFiltersValues = {}; + const fakeFilterKey = 'testKey'; + const fakeFilterValue = 10; + const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue); + fixture.detectChanges(); + + expect(component.currentFiltersValues).not.toEqual({}); + expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue); + expect(updatedFilterSpy).not.toHaveBeenCalled(); + }); + + it('should not emit filter key when filter counter has not changd', () => { + component.currentFiltersValues = {}; + const fakeFilterKey = 'testKey'; + const fakeFilterValue = 10; + const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue); + fixture.detectChanges(); + + expect(component.currentFiltersValues).not.toEqual({}); + expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue); + + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, fakeFilterValue); + expect(component.currentFiltersValues[fakeFilterKey]).toBe(fakeFilterValue); + expect(updatedFilterSpy).not.toHaveBeenCalled(); + }); + + it('should emit filter key when filter counter is increased', () => { + component.currentFiltersValues = {}; + const fakeFilterKey = 'testKey'; + const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 10); + fixture.detectChanges(); + + expect(updatedFilterSpy).not.toHaveBeenCalledWith(fakeFilterKey); + expect(component.currentFiltersValues[fakeFilterKey]).toBe(10); + + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 20); + fixture.detectChanges(); + + expect(updatedFilterSpy).toHaveBeenCalledWith(fakeFilterKey); + expect(component.currentFiltersValues[fakeFilterKey]).toBe(20); + }); + + it('should emit filter key when filter counter is decreased', () => { + component.currentFiltersValues = {}; + const fakeFilterKey = 'testKey'; + const updatedFilterSpy = spyOn(component.updatedFilter, 'emit'); + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 10); + fixture.detectChanges(); + + expect(updatedFilterSpy).not.toHaveBeenCalledWith(fakeFilterKey); + expect(component.currentFiltersValues[fakeFilterKey]).toBe(10); + + component.checkIfFilterValuesHasBeenUpdated(fakeFilterKey, 5); + fixture.detectChanges(); + + expect(updatedFilterSpy).toHaveBeenCalledWith(fakeFilterKey); + expect(component.currentFiltersValues[fakeFilterKey]).toBe(5); + }); }); }); }); diff --git a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters-cloud.component.ts b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters-cloud.component.ts index 409b2f77ed..78eb2f5de7 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/components/process-filters-cloud.component.ts @@ -23,6 +23,8 @@ import { AppConfigService, TranslationService } from '@alfresco/adf-core'; import { FilterParamsModel } from '../../../task/task-filters/models/filter-cloud.model'; import { debounceTime, takeUntil, tap } from 'rxjs/operators'; import { ProcessListCloudService } from '../../../process/process-list/services/process-list-cloud.service'; +import { PROCESS_SEARCH_API_METHOD_TOKEN } from '../../../services/cloud-token.service'; +import { ProcessFilterCloudAdapter } from '../../process-list/models/process-cloud-query-request.model'; @Component({ selector: 'adf-cloud-process-filters', @@ -77,6 +79,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges, OnDestro private readonly translationService = inject(TranslationService); private readonly appConfigService = inject(AppConfigService); private readonly processListCloudService = inject(ProcessListCloudService); + private readonly searchMethod = inject<'GET' | 'POST'>(PROCESS_SEARCH_API_METHOD_TOKEN, { optional: true }); ngOnInit() { this.enableNotifications = this.appConfigService.get('notifications', true); @@ -272,8 +275,7 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges, OnDestro * @param filter filter */ updateFilterCounter(filter: ProcessFilterCloudModel): void { - this.processListCloudService - .getProcessCounter(filter.appName, filter.status) + this.fetchProcessFilterCounter(filter) .pipe( tap((filterCounter) => { this.checkIfFilterValuesHasBeenUpdated(filter.key, filterCounter); @@ -312,4 +314,10 @@ export class ProcessFiltersCloudComponent implements OnInit, OnChanges, OnDestro this.updatedFiltersSet.delete(filterKey); }); } + + private fetchProcessFilterCounter(filter: ProcessFilterCloudModel): Observable { + return this.searchMethod === 'POST' + ? this.processListCloudService.getProcessListCounter(new ProcessFilterCloudAdapter(filter)) + : this.processListCloudService.getProcessCounter(filter.appName, filter.status) + } } diff --git a/lib/process-services-cloud/src/lib/process/process-filters/mock/process-filters-cloud.mock.ts b/lib/process-services-cloud/src/lib/process/process-filters/mock/process-filters-cloud.mock.ts index 586faee458..a65181d879 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/mock/process-filters-cloud.mock.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/mock/process-filters-cloud.mock.ts @@ -54,6 +54,7 @@ export const fakeProcessCloudFilters = [ export const mockProcessFilters: any[] = [ { + appName: 'mock-app-name', name: 'FakeAllProcesses', key: 'FakeAllProcesses', icon: 'adjust', @@ -61,6 +62,7 @@ export const mockProcessFilters: any[] = [ status: '' }, { + appName: 'mock-app-name', name: 'FakeRunningProcesses', key: 'FakeRunningProcesses', icon: 'inbox', @@ -68,6 +70,7 @@ export const mockProcessFilters: any[] = [ status: 'RUNNING' }, { + appName: 'mock-app-name', name: 'FakeCompletedProcesses', key: 'completed-processes', icon: 'done', diff --git a/lib/process-services-cloud/src/lib/process/process-filters/models/process-filter-cloud.model.ts b/lib/process-services-cloud/src/lib/process/process-filters/models/process-filter-cloud.model.ts index 9ff7b47a06..53eec0cfe5 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/models/process-filter-cloud.model.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/models/process-filter-cloud.model.ts @@ -48,6 +48,11 @@ export class ProcessFilterCloudModel { completedDate: Date; environmentId?: string; + processDefinitionNames: string[] | null; + initiators: string[] | null; + appVersions: string[] | null; + statuses: string[] | null; + private dateRangeFilterService = new DateRangeFilterService(); private _completedFrom: string; private _completedTo: string; @@ -94,6 +99,11 @@ export class ProcessFilterCloudModel { this.completedDate = obj.completedDate || null; this._suspendedFrom = obj._suspendedFrom || null; this._suspendedTo = obj._suspendedTo || null; + + this.processDefinitionNames = obj.processDefinitionNames || null; + this.initiators = obj.initiators || null; + this.appVersions = obj.appVersions || null; + this.statuses = obj.statuses || null; } } diff --git a/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.spec.ts index f3d9f6d173..df1abc0451 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.spec.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.spec.ts @@ -86,16 +86,19 @@ describe('ProcessFilterCloudService', () => { expect(res[0].id).toBe('1'); expect(res[0].name).toBe('MOCK_PROCESS_NAME_1'); expect(res[0].status).toBe('MOCK_ALL'); + expect(res[0].statuses).toContain('MOCK_ALL'); expect(res[1].appName).toBe('mock-appName'); expect(res[1].id).toBe('2'); expect(res[1].name).toBe('MOCK_PROCESS_NAME_2'); expect(res[1].status).toBe('MOCK-RUNNING'); + expect(res[1].statuses).toContain('MOCK-RUNNING'); expect(res[2].appName).toBe('mock-appName'); expect(res[2].id).toBe('3'); expect(res[2].name).toBe('MOCK_PROCESS_NAME_3'); expect(res[2].status).toBe('MOCK-COMPLETED'); + expect(res[2].statuses).toContain('MOCK-COMPLETED'); expect(createPreferenceSpy).toHaveBeenCalled(); done(); @@ -112,16 +115,19 @@ describe('ProcessFilterCloudService', () => { expect(res[0].id).toBe('1'); expect(res[0].name).toBe('MOCK_PROCESS_NAME_1'); expect(res[0].status).toBe('MOCK_ALL'); + expect(res[0].statuses).toContain('MOCK_ALL'); expect(res[1].appName).toBe('mock-appName'); expect(res[1].id).toBe('2'); expect(res[1].name).toBe('MOCK_PROCESS_NAME_2'); expect(res[1].status).toBe('MOCK-RUNNING'); + expect(res[1].statuses).toContain('MOCK-RUNNING'); expect(res[2].appName).toBe('mock-appName'); expect(res[2].id).toBe('3'); expect(res[2].name).toBe('MOCK_PROCESS_NAME_3'); expect(res[2].status).toBe('MOCK-COMPLETED'); + expect(res[2].statuses).toContain('MOCK-COMPLETED'); expect(getPreferencesSpy).toHaveBeenCalled(); done(); @@ -140,16 +146,19 @@ describe('ProcessFilterCloudService', () => { expect(res[0].id).toBe('1'); expect(res[0].name).toBe('MOCK_PROCESS_NAME_1'); expect(res[0].status).toBe('MOCK_ALL'); + expect(res[0].statuses).toContain('MOCK_ALL'); expect(res[1].appName).toBe('mock-appName'); expect(res[1].id).toBe('2'); expect(res[1].name).toBe('MOCK_PROCESS_NAME_2'); expect(res[1].status).toBe('MOCK-RUNNING'); + expect(res[1].statuses).toContain('MOCK-RUNNING'); expect(res[2].appName).toBe('mock-appName'); expect(res[2].id).toBe('3'); expect(res[2].name).toBe('MOCK_PROCESS_NAME_3'); expect(res[2].status).toBe('MOCK-COMPLETED'); + expect(res[2].statuses).toContain('MOCK-COMPLETED'); expect(getPreferencesSpy).toHaveBeenCalled(); expect(createPreferenceSpy).toHaveBeenCalled(); diff --git a/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.ts b/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.ts index 552ec27c2e..729a9734d8 100644 --- a/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/process/process-filters/services/process-filter-cloud.service.ts @@ -131,7 +131,8 @@ export class ProcessFilterCloudService { } else { return of(this.findFiltersByKeyInPreferences(preferences, key)); } - }) + }), + switchMap((filters) => this.handleCreateFilterBackwardsCompatibility(appName, key, filters)) ) .subscribe((filters) => { this.addFiltersToStream(filters); @@ -414,4 +415,39 @@ export class ProcessFilterCloudService { refreshFilter(filterKey: string): void { this.filterKeyToBeRefreshedSource.next(filterKey); } + + /** + * This method is run after retrieving the filter array from preferences. + * It handles the backwards compatibility with the new API by looking for the new properties and their counterparts in each passed filter. + * If the new property is not found, it is created and assigned the value constructed from the old property. + * The filters are then updated in the preferences and returned. + * Old properties are left untouched for purposes like feature toggling. + * + * @param appName Name of the target app. + * @param key Key of the process filters. + * @param filters Array of process filters to be checked for backward compatibility. + * @returns Observable of process filters with updated properties. + */ + private handleCreateFilterBackwardsCompatibility( + appName: string, + key: string, + filters: ProcessFilterCloudModel[] + ): Observable { + filters.forEach((filter) => { + if (filter.processDefinitionName && !filter.processDefinitionNames) { + filter.processDefinitionNames = [filter.processDefinitionName]; + } + if (filter.initiator && !filter.initiators) { + filter.initiators = [filter.initiator]; + } + if (filter.appVersion && !filter.appVersions) { + filter.appVersions = [filter.appVersion.toString()]; + } + if (filter.status && !filter.statuses) { + filter.statuses = [filter.status]; + } + }); + + return this.updateProcessFilters(appName, key, filters); + } } diff --git a/lib/process-services-cloud/src/lib/process/process-list/components/process-list-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/process/process-list/components/process-list-cloud.component.spec.ts index 6e3ad41709..2e0d0760eb 100644 --- a/lib/process-services-cloud/src/lib/process/process-list/components/process-list-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/process/process-list/components/process-list-cloud.component.spec.ts @@ -35,7 +35,7 @@ import { of } from 'rxjs'; import { shareReplay, skip } from 'rxjs/operators'; import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module'; import { ProcessListCloudSortingModel } from '../models/process-list-sorting.model'; -import { PROCESS_LISTS_PREFERENCES_SERVICE_TOKEN } from '../../../services/cloud-token.service'; +import { PROCESS_LISTS_PREFERENCES_SERVICE_TOKEN, PROCESS_SEARCH_API_METHOD_TOKEN } from '../../../services/cloud-token.service'; import { ProcessListCloudPreferences } from '../models/process-cloud-preferences'; import { PROCESS_LIST_CUSTOM_VARIABLE_COLUMN } from '../../../models/data-column-custom-data'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @@ -76,9 +76,10 @@ describe('ProcessListCloudComponent', () => { const fakeCustomSchemaName = 'fakeCustomSchema'; const schemaWithVariable = 'schemaWithVariableId'; - beforeEach(() => { + const configureTestingModule = (providers: any[]) => { TestBed.configureTestingModule({ - imports: [ProcessServiceCloudTestingModule] + imports: [ProcessServiceCloudTestingModule], + providers: providers }); appConfig = TestBed.inject(AppConfigService); processListCloudService = TestBed.inject(ProcessListCloudService); @@ -119,467 +120,763 @@ describe('ProcessListCloudComponent', () => { component.isColumnSchemaCreated$ = of(true).pipe(shareReplay(1)); loader = TestbedHarnessEnvironment.loader(fixture); - }); + }; afterEach(() => { fixture.destroy(); }); - it('should use the default schemaColumn', () => { - appConfig.config = Object.assign(appConfig.config, { 'adf-cloud-process-list': processListSchemaMock }); - fixture.detectChanges(); + describe('PROCESS_SEARCH_API_METHOD_TOKEN injected with GET value', () => { + beforeEach(() => { + configureTestingModule([{ provide: PROCESS_SEARCH_API_METHOD_TOKEN, useValue: 'GET' }]); + }); - expect(component.columns).toBeDefined(); - expect(component.columns.length).toEqual(10); - }); + it('should load spinner and show the content', async () => { + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); - it('should display empty content when process list is empty', async () => { - const emptyList = { list: { entries: [] } }; - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(emptyList)); + fixture.detectChanges(); + expect(component.isLoading).toBe(true); - fixture.detectChanges(); - expect(component.isLoading).toBe(true); + expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(true); - expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(true); + component.ngOnChanges({ appName }); + fixture.detectChanges(); - const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); - component.ngOnChanges({ appName }); - fixture.detectChanges(); + expect(component.isLoading).toBe(false); + expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false); - expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false); + const emptyContent = fixture.debugElement.query(By.css('.adf-empty-content')); + expect(emptyContent).toBeFalsy(); - const emptyContent = fixture.debugElement.query(By.css('.adf-empty-content')); - expect(emptyContent.nativeElement).toBeDefined(); - }); + expect(component.rows.length).toEqual(3); + }); - it('should load spinner and show the content', async () => { - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + it('should the payload contain the appVersion if it is defined', () => { + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + component.appVersion = 1; + component.ngAfterContentInit(); + component.reload(); - fixture.detectChanges(); - expect(component.isLoading).toBe(true); + expect(component.requestNode.appVersion).toEqual('1'); + }); - expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(true); + it('should the payload contain all the app versions joined by a comma separator', () => { + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + component.appVersion = [1, 2, 3]; + component.ngAfterContentInit(); + component.reload(); - component.ngOnChanges({ appName }); - fixture.detectChanges(); + expect(component.requestNode.appVersion).toEqual('1,2,3'); + }); - expect(component.isLoading).toBe(false); - expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false); + it('should the payload NOT contain any app version when appVersion does not have a value', () => { + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + component.appVersion = undefined; + component.ngAfterContentInit(); + component.reload(); - const emptyContent = fixture.debugElement.query(By.css('.adf-empty-content')); - expect(emptyContent).toBeFalsy(); + expect(component.requestNode.appVersion).toEqual(''); + }); - expect(component.rows.length).toEqual(3); - }); + it('should return the results if an application name is given', (done) => { + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + component.success.subscribe((res) => { + expect(res).toBeDefined(); + expect(component.rows).toBeDefined(); + expect(component.rows.length).toEqual(3); + expect(component.rows[0].entry['appName']).toBe('easy-peasy-japanesey'); + expect(component.rows[0].entry['appVersion']).toBe(1); + expect(component.rows[0].entry['id']).toBe('69eddfa7-d781-11e8-ae24-0a58646001fa'); + expect(component.rows[0].entry['name']).toEqual('starring'); + expect(component.rows[0].entry['processDefinitionId']).toBe('BasicProcess:1:d05062f1-c6fb-11e8-ae24-0a58646001fa'); + expect(component.rows[0].entry['processDefinitionKey']).toBe('BasicProcess'); + expect(component.rows[0].entry['initiator']).toBe('devopsuser'); + expect(component.rows[0].entry['startDate']).toBe(1540381146275); + expect(component.rows[0].entry['businessKey']).toBe('MyBusinessKey'); + expect(component.rows[0].entry['status']).toBe('RUNNING'); + expect(component.rows[0].entry['lastModified']).toBe(1540381146276); + expect(component.rows[0].entry['lastModifiedTo']).toBeNull(); + expect(component.rows[0].entry['lastModifiedFrom']).toBeNull(); + + done(); + }); + component.appName = appName.currentValue; + component.ngAfterContentInit(); + component.ngOnChanges({ appName }); + fixture.detectChanges(); + }); - it('should the payload contain the appVersion if it is defined', () => { - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - component.appVersion = 1; - component.ngAfterContentInit(); - component.reload(); + it('should shown columns selector', () => { + component.showMainDatatableActions = true; + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - expect(component.requestNode.appVersion).toEqual('1'); - }); + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + component.ngAfterContentInit(); + component.ngOnChanges({ appName }); - it('should the payload contain all the app versions joined by a comma separator', () => { - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - component.appVersion = [1, 2, 3]; - component.ngAfterContentInit(); - component.reload(); + fixture.detectChanges(); - expect(component.requestNode.appVersion).toEqual('1,2,3'); - }); + const mainMenuButton = fixture.debugElement.query(By.css('[data-automation-id="adf-datatable-main-menu-button"]')); + expect(mainMenuButton).toBeTruthy(); + }); - it('should the payload NOT contain any app version when appVersion does not have a value', () => { - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - component.appVersion = undefined; - component.ngAfterContentInit(); - component.reload(); + it('should hide columns on applying new columns visibility through columns selector', () => { + component.showMainDatatableActions = true; + fixture.detectChanges(); - expect(component.requestNode.appVersion).toEqual(''); - }); + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - it('should use the custom schemaColumn from app.config.json', () => { - component.presetColumn = fakeCustomSchemaName; - component.ngAfterContentInit(); - fixture.detectChanges(); - expect(component.columns).toEqual(fakeCustomSchema); - }); + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + component.ngOnChanges({ appName }); - it('should fetch custom schemaColumn when the input presetColumn is defined', () => { - component.presetColumn = fakeCustomSchemaName; - fixture.detectChanges(); - expect(component.columns).toBeDefined(); - expect(component.columns.length).toEqual(2); - }); + fixture.detectChanges(); - it('should return the results if an application name is given', (done) => { - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); - component.success.subscribe((res) => { - expect(res).toBeDefined(); - expect(component.rows).toBeDefined(); - expect(component.rows.length).toEqual(3); - expect(component.rows[0].entry['appName']).toBe('easy-peasy-japanesey'); - expect(component.rows[0].entry['appVersion']).toBe(1); - expect(component.rows[0].entry['id']).toBe('69eddfa7-d781-11e8-ae24-0a58646001fa'); - expect(component.rows[0].entry['name']).toEqual('starring'); - expect(component.rows[0].entry['processDefinitionId']).toBe('BasicProcess:1:d05062f1-c6fb-11e8-ae24-0a58646001fa'); - expect(component.rows[0].entry['processDefinitionKey']).toBe('BasicProcess'); - expect(component.rows[0].entry['initiator']).toBe('devopsuser'); - expect(component.rows[0].entry['startDate']).toBe(1540381146275); - expect(component.rows[0].entry['businessKey']).toBe('MyBusinessKey'); - expect(component.rows[0].entry['status']).toBe('RUNNING'); - expect(component.rows[0].entry['lastModified']).toBe(1540381146276); - expect(component.rows[0].entry['lastModifiedTo']).toBeNull(); - expect(component.rows[0].entry['lastModifiedFrom']).toBeNull(); - - done(); - }); - component.appName = appName.currentValue; - component.ngAfterContentInit(); - component.ngOnChanges({ appName }); - fixture.detectChanges(); - }); + const mainMenuButton = fixture.debugElement.query(By.css('[data-automation-id="adf-datatable-main-menu-button"]')); + mainMenuButton.triggerEventHandler('click', {}); + fixture.detectChanges(); - it('should not shown columns selector by default', () => { - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + const columnSelectorMenu = fixture.debugElement.query(By.css('adf-datatable-column-selector')); + expect(columnSelectorMenu).toBeTruthy(); - const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); - component.ngOnChanges({ appName }); + const newColumns = (component.columns as DataColumn[]).map((column, index) => ({ + ...column, + isHidden: index !== 0 // only first one is shown + })); - fixture.detectChanges(); + const columnsSelectorInstance = columnSelectorMenu.componentInstance as ColumnsSelectorComponent; + expect(columnsSelectorInstance.columns).toBe(component.columns, 'should use columns as input'); - const mainMenuButton = fixture.debugElement.query(By.css('[data-automation-id="adf-datatable-main-menu-button"]')); - expect(mainMenuButton).toBeFalsy(); - }); + columnSelectorMenu.triggerEventHandler('submitColumnsVisibility', newColumns); + fixture.detectChanges(); - it('should shown columns selector', () => { - component.showMainDatatableActions = true; - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + const displayedColumns = fixture.debugElement.queryAll(By.css('.adf-datatable-cell-header')); + expect(displayedColumns.length).toBe(2, 'only column with isHidden set to false and action column should be shown'); + }); - const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); - component.ngAfterContentInit(); - component.ngOnChanges({ appName }); + it('should NOT request process variable if columns for process variables are not displayed', () => { + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + spyOn(preferencesService, 'getPreferences').and.returnValue( + of({ + list: { + entries: [] + } + }) + ); - fixture.detectChanges(); + component.ngAfterContentInit(); + component.reload(); - const mainMenuButton = fixture.debugElement.query(By.css('[data-automation-id="adf-datatable-main-menu-button"]')); - expect(mainMenuButton).toBeTruthy(); - }); + expect(component.requestNode.variableKeys).not.toBeDefined(); + }); + + it('should request process variable if column for process variable is displayed', () => { + component.presetColumn = schemaWithVariable; - it('should hide columns on applying new columns visibility through columns selector', () => { - component.showMainDatatableActions = true; - fixture.detectChanges(); + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + spyOn(preferencesService, 'getPreferences').and.returnValue( + of({ + list: { + entries: [ + { + entry: { + key: ProcessListCloudPreferences.columnsVisibility, + value: '{"variableColumnId":"id", "2":true}' + } + } + ] + } + }) + ); - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + component.ngAfterContentInit(); + component.reload(); - const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); - component.ngOnChanges({ appName }); + expect(component.requestNode.variableKeys).toEqual(['processKey/variableName']); + }); + + it('should reload tasks when reload() is called', (done) => { + component.appName = 'fake'; + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + component.success.subscribe((res) => { + expect(res).toBeDefined(); + expect(component.rows).toBeDefined(); + done(); + }); + fixture.detectChanges(); + component.reload(); + }); - fixture.detectChanges(); + it('should call endpoint when a column visibility gets changed', () => { + spyOn(preferencesService, 'updatePreference').and.returnValue(of({})); + spyOn(processListCloudService, 'getProcessByRequest'); + component.ngAfterContentInit(); + spyOn(component, 'createDatatableSchema'); + component.appName = 'fake-app-name'; + component.reload(); + fixture.detectChanges(); - const mainMenuButton = fixture.debugElement.query(By.css('[data-automation-id="adf-datatable-main-menu-button"]')); - mainMenuButton.triggerEventHandler('click', {}); - fixture.detectChanges(); + component.onColumnsVisibilityChange(component.columns); - const columnSelectorMenu = fixture.debugElement.query(By.css('adf-datatable-column-selector')); - expect(columnSelectorMenu).toBeTruthy(); + fixture.detectChanges(); - const newColumns = (component.columns as DataColumn[]).map((column, index) => ({ - ...column, - isHidden: index !== 0 // only first one is shown - })); + expect(processListCloudService.getProcessByRequest).toHaveBeenCalledTimes(1); + }); - const columnsSelectorInstance = columnSelectorMenu.componentInstance as ColumnsSelectorComponent; - expect(columnsSelectorInstance.columns).toBe(component.columns, 'should use columns as input'); + describe('component changes', () => { + beforeEach(() => { + component.rows = fakeProcessCloudList.list.entries; + fixture.detectChanges(); + }); - columnSelectorMenu.triggerEventHandler('submitColumnsVisibility', newColumns); - fixture.detectChanges(); + it('should reload the process list when input parameters changed', () => { + const getProcessByRequestSpy = spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + component.appName = 'mock-app-name'; + component.status = 'mock-status'; + component.initiator = 'mock-initiator'; + const appNameChange = new SimpleChange(undefined, 'mock-app-name', true); + const statusChange = new SimpleChange(undefined, 'mock-status', true); + const initiatorChange = new SimpleChange(undefined, 'mock-initiator', true); + + component.ngOnChanges({ + appName: appNameChange, + assignee: initiatorChange, + status: statusChange + }); + fixture.detectChanges(); + expect(component.isListEmpty()).toBeFalsy(); + expect(getProcessByRequestSpy).toHaveBeenCalled(); + }); - const displayedColumns = fixture.debugElement.queryAll(By.css('.adf-datatable-cell-header')); - expect(displayedColumns.length).toBe(2, 'only column with isHidden set to false and action column should be shown'); - }); + it('should reload process list when sorting on a column changes', () => { + const getProcessByRequestSpy = spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + component.onSortingChanged( + new CustomEvent('sorting-changed', { + detail: { + key: 'fakeName', + direction: 'asc' + }, + bubbles: true + }) + ); + fixture.detectChanges(); + expect(component.sorting).toEqual([ + new ProcessListCloudSortingModel({ + orderBy: 'fakeName', + direction: 'ASC' + }) + ]); + expect(component.formattedSorting).toEqual(['fakeName', 'asc']); + expect(component.isListEmpty()).toBeFalsy(); + expect(getProcessByRequestSpy).toHaveBeenCalled(); + }); - it('should NOT request process variable if columns for process variables are not displayed', () => { - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - spyOn(preferencesService, 'getPreferences').and.returnValue( - of({ - list: { - entries: [] - } - }) - ); + it('should reset pagination when resetPaginationValues is called', (done) => { + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - component.ngAfterContentInit(); - component.reload(); + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + component.ngOnChanges({ appName }); + fixture.detectChanges(); - expect(component.requestNode.variableKeys).not.toBeDefined(); + const size = component.size; + const skipCount = component.skipCount; + component.pagination.pipe(skip(3)).subscribe((updatedPagination) => { + fixture.detectChanges(); + expect(component.size).toBe(size); + expect(component.skipCount).toBe(skipCount); + expect(updatedPagination.maxItems).toEqual(size); + expect(updatedPagination.skipCount).toEqual(skipCount); + done(); + }); + + const pagination = { + maxItems: 250, + skipCount: 200 + }; + component.updatePagination(pagination); + fixture.whenStable().then(() => { + component.resetPagination(); + }); + }); + }); }); - it('should request process variable if column for process variable is displayed', () => { - component.presetColumn = schemaWithVariable; + describe('PROCESS_SEARCH_API_METHOD_TOKEN injected with POST value', () => { + beforeEach(() => { + configureTestingModule([{ provide: PROCESS_SEARCH_API_METHOD_TOKEN, useValue: 'POST' }]); + component.appName = 'fake-app-name'; + }); + + it('should load spinner and show the content', async () => { + spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList)); + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - spyOn(preferencesService, 'getPreferences').and.returnValue( - of({ - list: { - entries: [ - { - entry: { - key: ProcessListCloudPreferences.columnsVisibility, - value: '{"variableColumnId":"id", "2":true}' - } - } - ] - } - }) - ); + fixture.detectChanges(); + expect(component.isLoading).toBe(true); - component.ngAfterContentInit(); - component.reload(); + expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(true); - expect(component.requestNode.variableKeys).toEqual(['processKey/variableName']); - }); + component.ngOnChanges({ appName }); + fixture.detectChanges(); + + expect(component.isLoading).toBe(false); + expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false); + + const emptyContent = fixture.debugElement.query(By.css('.adf-empty-content')); + expect(emptyContent).toBeFalsy(); - it('should reload tasks when reload() is called', (done) => { - component.appName = 'fake'; - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - component.success.subscribe((res) => { - expect(res).toBeDefined(); - expect(component.rows).toBeDefined(); - done(); + expect(component.rows.length).toEqual(3); }); - fixture.detectChanges(); - component.reload(); - }); - it('should emit row click event', (done) => { - const row = new ObjectDataRow({ id: '999' }); - const rowEvent = new DataRowEvent(row, null); - component.rowClick.subscribe((taskId) => { - expect(taskId).toEqual('999'); - expect(component.getCurrentId()).toEqual('999'); - done(); + it('should the payload contain the appVersion if it is defined', () => { + spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList)); + component.appVersions = ['1']; + component.ngAfterContentInit(); + component.reload(); + + expect(component.processListRequestNode.appVersion).toEqual(['1']); }); - component.onRowClick(rowEvent); - }); - it('should re-create columns when a column width gets changed', () => { - component.isResizingEnabled = true; - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + it('should the payload contain all the app versions', () => { + spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList)); + component.appVersions = ['1', '2', '3']; + component.ngAfterContentInit(); + component.reload(); - component.reload(); - fixture.detectChanges(); + expect(component.processListRequestNode.appVersion).toEqual(['1', '2', '3']); + }); - const newColumns = [...component.columns]; - newColumns[0].width = 120; - component.onColumnsWidthChanged(newColumns); + it('should the payload NOT contain any app version when appVersion does not have a value', () => { + spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList)); + component.appVersion = undefined; + component.ngAfterContentInit(); + component.reload(); - expect(component.columns[0].width).toBe(120); - }); + expect(component.processListRequestNode.appVersion.length).toEqual(0); + }); + + it('should return the results if an application name is given', (done) => { + spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList)); + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + component.success.subscribe((res) => { + expect(res).toBeDefined(); + expect(component.rows).toBeDefined(); + expect(component.rows.length).toEqual(3); + expect(component.rows[0].entry['appName']).toBe('easy-peasy-japanesey'); + expect(component.rows[0].entry['appVersion']).toBe(1); + expect(component.rows[0].entry['id']).toBe('69eddfa7-d781-11e8-ae24-0a58646001fa'); + expect(component.rows[0].entry['name']).toEqual('starring'); + expect(component.rows[0].entry['processDefinitionId']).toBe('BasicProcess:1:d05062f1-c6fb-11e8-ae24-0a58646001fa'); + expect(component.rows[0].entry['processDefinitionKey']).toBe('BasicProcess'); + expect(component.rows[0].entry['initiator']).toBe('devopsuser'); + expect(component.rows[0].entry['startDate']).toBe(1540381146275); + expect(component.rows[0].entry['businessKey']).toBe('MyBusinessKey'); + expect(component.rows[0].entry['status']).toBe('RUNNING'); + expect(component.rows[0].entry['lastModified']).toBe(1540381146276); + expect(component.rows[0].entry['lastModifiedTo']).toBeNull(); + expect(component.rows[0].entry['lastModifiedFrom']).toBeNull(); + + done(); + }); + component.appName = appName.currentValue; + component.ngAfterContentInit(); + component.ngOnChanges({ appName }); + fixture.detectChanges(); + }); - it('should update columns widths when a column width gets changed', () => { - spyOn(preferencesService, 'updatePreference').and.returnValue(of({})); - component.appName = 'fake-app-name'; - component.reload(); - fixture.detectChanges(); + it('should shown columns selector', () => { + component.showMainDatatableActions = true; + spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList)); - const newColumns = [...component.columns]; - newColumns[0].width = 120; - component.onColumnsWidthChanged(newColumns); + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + component.ngAfterContentInit(); + component.ngOnChanges({ appName }); - expect(component.columns[0].width).toBe(120); - expect(preferencesService.updatePreference).toHaveBeenCalledWith('fake-app-name', 'processes-cloud-columns-widths', { - id: 120 + fixture.detectChanges(); + + const mainMenuButton = fixture.debugElement.query(By.css('[data-automation-id="adf-datatable-main-menu-button"]')); + expect(mainMenuButton).toBeTruthy(); }); - }); - it('should update columns widths while preserving previously saved widths when a column width gets changed', () => { - spyOn(preferencesService, 'updatePreference').and.returnValue(of({})); - component.appName = 'fake-app-name'; - component.reload(); - fixture.detectChanges(); + it('should hide columns on applying new columns visibility through columns selector', () => { + component.showMainDatatableActions = true; + fixture.detectChanges(); + + spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList)); + + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + component.ngOnChanges({ appName }); + + fixture.detectChanges(); + + const mainMenuButton = fixture.debugElement.query(By.css('[data-automation-id="adf-datatable-main-menu-button"]')); + mainMenuButton.triggerEventHandler('click', {}); + fixture.detectChanges(); + + const columnSelectorMenu = fixture.debugElement.query(By.css('adf-datatable-column-selector')); + expect(columnSelectorMenu).toBeTruthy(); - const newColumns = [...component.columns]; - newColumns[0].width = 120; - component.onColumnsWidthChanged(newColumns); + const newColumns = (component.columns as DataColumn[]).map((column, index) => ({ + ...column, + isHidden: index !== 0 // only first one is shown + })); - expect(component.columns[0].width).toBe(120); - expect(preferencesService.updatePreference).toHaveBeenCalledWith('fake-app-name', 'processes-cloud-columns-widths', { - id: 120 + const columnsSelectorInstance = columnSelectorMenu.componentInstance as ColumnsSelectorComponent; + expect(columnsSelectorInstance.columns).toBe(component.columns, 'should use columns as input'); + + columnSelectorMenu.triggerEventHandler('submitColumnsVisibility', newColumns); + fixture.detectChanges(); + + const displayedColumns = fixture.debugElement.queryAll(By.css('.adf-datatable-cell-header')); + expect(displayedColumns.length).toBe(2, 'only column with isHidden set to false and action column should be shown'); }); - newColumns[1].width = 150; - component.onColumnsWidthChanged(newColumns); + it('should NOT request process variable if columns for process variables are not displayed', () => { + spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList)); + spyOn(preferencesService, 'getPreferences').and.returnValue( + of({ + list: { + entries: [] + } + }) + ); + + component.ngAfterContentInit(); + component.reload(); - expect(component.columns[0].width).toBe(120); - expect(component.columns[1].width).toBe(150); - expect(preferencesService.updatePreference).toHaveBeenCalledWith('fake-app-name', 'processes-cloud-columns-widths', { - id: 120, - startDate: 150 + expect(component.processListRequestNode.variableKeys).not.toBeDefined(); }); - }); - it('should re-create columns when a column order gets changed', () => { - component.reload(); - fixture.detectChanges(); + it('should request process variable if column for process variable is displayed', () => { + component.presetColumn = schemaWithVariable; + + spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList)); + spyOn(preferencesService, 'getPreferences').and.returnValue( + of({ + list: { + entries: [ + { + entry: { + key: ProcessListCloudPreferences.columnsVisibility, + value: '{"variableColumnId":"id", "2":true}' + } + } + ] + } + }) + ); - expect(component.columns[0].title).toBe('ADF_CLOUD_PROCESS_LIST.PROPERTIES.NAME'); - expect(component.columns[1].title).toBe('ADF_CLOUD_PROCESS_LIST.PROPERTIES.START_DATE'); + component.ngAfterContentInit(); + component.reload(); - component.onColumnOrderChanged([component.columns[1], ...component.columns]); - fixture.detectChanges(); + expect(component.processListRequestNode.variableKeys).toEqual(['processKey/variableName']); + }); - expect(component.columns[0].title).toBe('ADF_CLOUD_PROCESS_LIST.PROPERTIES.START_DATE'); - expect(component.columns[1].title).toBe('ADF_CLOUD_PROCESS_LIST.PROPERTIES.NAME'); - }); + it('should reload tasks when reload() is called', (done) => { + component.appName = 'fake'; + spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList)); + component.success.subscribe((res) => { + expect(res).toBeDefined(); + expect(component.rows).toBeDefined(); + done(); + }); + fixture.detectChanges(); + component.reload(); + }); - it('should create datatable schema when a column visibility gets changed', () => { - component.ngAfterContentInit(); - spyOn(component, 'createDatatableSchema'); + it('should call endpoint when a column visibility gets changed', () => { + spyOn(preferencesService, 'updatePreference').and.returnValue(of({})); + spyOn(processListCloudService, 'fetchProcessList'); + component.ngAfterContentInit(); + spyOn(component, 'createDatatableSchema'); + component.appName = 'fake-app-name'; + component.reload(); + fixture.detectChanges(); - component.onColumnsVisibilityChange(component.columns); + component.onColumnsVisibilityChange(component.columns); - fixture.detectChanges(); + fixture.detectChanges(); - expect(component.createDatatableSchema).toHaveBeenCalled(); - }); + expect(processListCloudService.fetchProcessList).toHaveBeenCalledTimes(1); + }); + + describe('component changes', () => { + beforeEach(() => { + component.rows = fakeProcessCloudList.list.entries; + fixture.detectChanges(); + }); + + it('should reload the process list when input parameters changed', () => { + const fetchProcessListSpy = spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList)); + component.appName = 'mock-app-name'; + component.status = 'mock-status'; + component.initiator = 'mock-initiator'; + const appNameChange = new SimpleChange(undefined, 'mock-app-name', true); + const statusChange = new SimpleChange(undefined, 'mock-status', true); + const initiatorChange = new SimpleChange(undefined, 'mock-initiator', true); + + component.ngOnChanges({ + appName: appNameChange, + assignee: initiatorChange, + status: statusChange + }); + fixture.detectChanges(); + expect(component.isListEmpty()).toBeFalsy(); + expect(fetchProcessListSpy).toHaveBeenCalled(); + }); - it('should call endpoint when a column visibility gets changed', () => { - spyOn(preferencesService, 'updatePreference').and.returnValue(of({})); - spyOn(processListCloudService, 'getProcessByRequest'); - component.ngAfterContentInit(); - spyOn(component, 'createDatatableSchema'); - component.appName = 'fake-app-name'; - component.reload(); - fixture.detectChanges(); + it('should reload process list when sorting on a column changes', () => { + const fetchProcessListSpy = spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList)); + component.onSortingChanged( + new CustomEvent('sorting-changed', { + detail: { + key: 'fakeName', + direction: 'asc' + }, + bubbles: true + }) + ); + fixture.detectChanges(); + expect(component.sorting).toEqual([ + new ProcessListCloudSortingModel({ + orderBy: 'fakeName', + direction: 'ASC' + }) + ]); + expect(component.formattedSorting).toEqual(['fakeName', 'asc']); + expect(component.isListEmpty()).toBeFalsy(); + expect(fetchProcessListSpy).toHaveBeenCalled(); + }); - component.onColumnsVisibilityChange(component.columns); + it('should reset pagination when resetPaginationValues is called', (done) => { + spyOn(processListCloudService, 'fetchProcessList').and.returnValue(of(fakeProcessCloudList)); - fixture.detectChanges(); + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + component.ngOnChanges({ appName }); + fixture.detectChanges(); - expect(processListCloudService.getProcessByRequest).toHaveBeenCalledTimes(1); + const size = component.size; + const skipCount = component.skipCount; + component.pagination.pipe(skip(3)).subscribe((updatedPagination) => { + fixture.detectChanges(); + expect(component.size).toBe(size); + expect(component.skipCount).toBe(skipCount); + expect(updatedPagination.maxItems).toEqual(size); + expect(updatedPagination.skipCount).toEqual(skipCount); + done(); + }); + + const pagination = { + maxItems: 250, + skipCount: 200 + }; + component.updatePagination(pagination); + fixture.whenStable().then(() => { + component.resetPagination(); + }); + }); + }); }); - describe('component changes', () => { + describe('API agnostic', () => { beforeEach(() => { - component.rows = fakeProcessCloudList.list.entries; + configureTestingModule([]); + }); + + it('should use the default schemaColumn', () => { + appConfig.config = Object.assign(appConfig.config, { 'adf-cloud-process-list': processListSchemaMock }); fixture.detectChanges(); + + expect(component.columns).toBeDefined(); + expect(component.columns.length).toEqual(10); }); - it('should reload the process list when input parameters changed', () => { - const getProcessByRequestSpy = spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - component.appName = 'mock-app-name'; - component.status = 'mock-status'; - component.initiator = 'mock-initiator'; - const appNameChange = new SimpleChange(undefined, 'mock-app-name', true); - const statusChange = new SimpleChange(undefined, 'mock-status', true); - const initiatorChange = new SimpleChange(undefined, 'mock-initiator', true); + it('should display empty content when process list is empty', async () => { + const emptyList = { list: { entries: [] } }; + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(emptyList)); - component.ngOnChanges({ - appName: appNameChange, - assignee: initiatorChange, - status: statusChange - }); fixture.detectChanges(); - expect(component.isListEmpty()).toBeFalsy(); - expect(getProcessByRequestSpy).toHaveBeenCalled(); - }); + expect(component.isLoading).toBe(true); - it('should set formattedSorting if sorting input changes', () => { - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - spyOn(component, 'formatSorting').and.callThrough(); + expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(true); - component.appName = 'mock-app-name'; - const mockSort = [ - new ProcessListCloudSortingModel({ - orderBy: 'startDate', - direction: 'DESC' - }) - ]; - const sortChange = new SimpleChange(undefined, mockSort, true); - component.ngOnChanges({ - sorting: sortChange - }); + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + component.ngOnChanges({ appName }); fixture.detectChanges(); - expect(component.formatSorting).toHaveBeenCalledWith(mockSort); - expect(component.formattedSorting).toEqual(['startDate', 'desc']); + + expect(await loader.hasHarness(MatProgressSpinnerHarness)).toBe(false); + + const emptyContent = fixture.debugElement.query(By.css('.adf-empty-content')); + expect(emptyContent.nativeElement).toBeDefined(); }); - it('should reload process list when sorting on a column changes', () => { - const getProcessByRequestSpy = spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - component.onSortingChanged( - new CustomEvent('sorting-changed', { - detail: { - key: 'fakeName', - direction: 'asc' - }, - bubbles: true - }) - ); + it('should use the custom schemaColumn from app.config.json', () => { + component.presetColumn = fakeCustomSchemaName; + component.ngAfterContentInit(); fixture.detectChanges(); - expect(component.sorting).toEqual([ - new ProcessListCloudSortingModel({ - orderBy: 'fakeName', - direction: 'ASC' - }) - ]); - expect(component.formattedSorting).toEqual(['fakeName', 'asc']); - expect(component.isListEmpty()).toBeFalsy(); - expect(getProcessByRequestSpy).toHaveBeenCalled(); + expect(component.columns).toEqual(fakeCustomSchema); + }); + + it('should fetch custom schemaColumn when the input presetColumn is defined', () => { + component.presetColumn = fakeCustomSchemaName; + fixture.detectChanges(); + expect(component.columns).toBeDefined(); + expect(component.columns.length).toEqual(2); }); - it('should reset pagination when resetPaginationValues is called', (done) => { + it('should not shown columns selector by default', () => { spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); component.ngOnChanges({ appName }); + fixture.detectChanges(); - const size = component.size; - const skipCount = component.skipCount; - component.pagination.pipe(skip(3)).subscribe((updatedPagination) => { - fixture.detectChanges(); - expect(component.size).toBe(size); - expect(component.skipCount).toBe(skipCount); - expect(updatedPagination.maxItems).toEqual(size); - expect(updatedPagination.skipCount).toEqual(skipCount); + const mainMenuButton = fixture.debugElement.query(By.css('[data-automation-id="adf-datatable-main-menu-button"]')); + expect(mainMenuButton).toBeFalsy(); + }); + + it('should emit row click event', (done) => { + const row = new ObjectDataRow({ id: '999' }); + const rowEvent = new DataRowEvent(row, null); + component.rowClick.subscribe((taskId) => { + expect(taskId).toEqual('999'); + expect(component.getCurrentId()).toEqual('999'); done(); }); + component.onRowClick(rowEvent); + }); + + it('should re-create columns when a column width gets changed', () => { + component.isResizingEnabled = true; + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + + component.reload(); + fixture.detectChanges(); + + const newColumns = [...component.columns]; + newColumns[0].width = 120; + component.onColumnsWidthChanged(newColumns); - const pagination = { - maxItems: 250, - skipCount: 200 - }; - component.updatePagination(pagination); - fixture.whenStable().then(() => { - component.resetPagination(); + expect(component.columns[0].width).toBe(120); + }); + + it('should update columns widths when a column width gets changed', () => { + spyOn(preferencesService, 'updatePreference').and.returnValue(of({})); + component.appName = 'fake-app-name'; + component.reload(); + fixture.detectChanges(); + + const newColumns = [...component.columns]; + newColumns[0].width = 120; + component.onColumnsWidthChanged(newColumns); + + expect(component.columns[0].width).toBe(120); + expect(preferencesService.updatePreference).toHaveBeenCalledWith('fake-app-name', 'processes-cloud-columns-widths', { + id: 120 }); }); - it('should set pagination and reload when updatePagination is called', (done) => { - spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); - spyOn(component, 'reload').and.stub(); - const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); - component.ngOnChanges({ appName }); + it('should update columns widths while preserving previously saved widths when a column width gets changed', () => { + spyOn(preferencesService, 'updatePreference').and.returnValue(of({})); + component.appName = 'fake-app-name'; + component.reload(); + fixture.detectChanges(); + + const newColumns = [...component.columns]; + newColumns[0].width = 120; + component.onColumnsWidthChanged(newColumns); + + expect(component.columns[0].width).toBe(120); + expect(preferencesService.updatePreference).toHaveBeenCalledWith('fake-app-name', 'processes-cloud-columns-widths', { + id: 120 + }); + + newColumns[1].width = 150; + component.onColumnsWidthChanged(newColumns); + + expect(component.columns[0].width).toBe(120); + expect(component.columns[1].width).toBe(150); + expect(preferencesService.updatePreference).toHaveBeenCalledWith('fake-app-name', 'processes-cloud-columns-widths', { + id: 120, + startDate: 150 + }); + }); + + it('should re-create columns when a column order gets changed', () => { + component.reload(); + fixture.detectChanges(); + + expect(component.columns[0].title).toBe('ADF_CLOUD_PROCESS_LIST.PROPERTIES.NAME'); + expect(component.columns[1].title).toBe('ADF_CLOUD_PROCESS_LIST.PROPERTIES.START_DATE'); + + component.onColumnOrderChanged([component.columns[1], ...component.columns]); + fixture.detectChanges(); + + expect(component.columns[0].title).toBe('ADF_CLOUD_PROCESS_LIST.PROPERTIES.START_DATE'); + expect(component.columns[1].title).toBe('ADF_CLOUD_PROCESS_LIST.PROPERTIES.NAME'); + }); + + it('should create datatable schema when a column visibility gets changed', () => { + component.ngAfterContentInit(); + spyOn(component, 'createDatatableSchema'); + + component.onColumnsVisibilityChange(component.columns); + fixture.detectChanges(); - const pagination = { - maxItems: 250, - skipCount: 200 - }; - component.pagination.pipe(skip(1)).subscribe((updatedPagination) => { + expect(component.createDatatableSchema).toHaveBeenCalled(); + }); + + describe('component changes', () => { + beforeEach(() => { + component.rows = fakeProcessCloudList.list.entries; fixture.detectChanges(); - expect(component.size).toBe(pagination.maxItems); - expect(component.skipCount).toBe(pagination.skipCount); - expect(updatedPagination.maxItems).toEqual(pagination.maxItems); - expect(updatedPagination.skipCount).toEqual(pagination.skipCount); - done(); }); - component.updatePagination(pagination); + it('should set formattedSorting if sorting input changes', () => { + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + spyOn(component, 'formatSorting').and.callThrough(); + + component.appName = 'mock-app-name'; + const mockSort = [ + new ProcessListCloudSortingModel({ + orderBy: 'startDate', + direction: 'DESC' + }) + ]; + const sortChange = new SimpleChange(undefined, mockSort, true); + component.ngOnChanges({ + sorting: sortChange + }); + fixture.detectChanges(); + expect(component.formatSorting).toHaveBeenCalledWith(mockSort); + expect(component.formattedSorting).toEqual(['startDate', 'desc']); + }); + + it('should set pagination and reload when updatePagination is called', (done) => { + spyOn(processListCloudService, 'getProcessByRequest').and.returnValue(of(fakeProcessCloudList)); + spyOn(component, 'reload').and.stub(); + const appName = new SimpleChange(null, 'FAKE-APP-NAME', true); + component.ngOnChanges({ appName }); + fixture.detectChanges(); + + const pagination = { + maxItems: 250, + skipCount: 200 + }; + component.pagination.pipe(skip(1)).subscribe((updatedPagination) => { + fixture.detectChanges(); + expect(component.size).toBe(pagination.maxItems); + expect(component.skipCount).toBe(pagination.skipCount); + expect(updatedPagination.maxItems).toEqual(pagination.maxItems); + expect(updatedPagination.skipCount).toEqual(pagination.skipCount); + done(); + }); + + component.updatePagination(pagination); + }); }); }); }); diff --git a/lib/process-services-cloud/src/lib/process/process-list/components/process-list-cloud.component.ts b/lib/process-services-cloud/src/lib/process/process-list/components/process-list-cloud.component.ts index dfd63c9989..6078d098b3 100644 --- a/lib/process-services-cloud/src/lib/process/process-list/components/process-list-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/process/process-list/components/process-list-cloud.component.ts @@ -27,7 +27,8 @@ import { Input, ViewChild, Inject, - OnDestroy + OnDestroy, + Optional } from '@angular/core'; import { DataTableSchema, @@ -45,13 +46,13 @@ import { DataColumn } from '@alfresco/adf-core'; import { ProcessListCloudService } from '../services/process-list-cloud.service'; -import { BehaviorSubject, Subject, of } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import { processCloudPresetsDefaultModel } from '../models/process-cloud-preset.model'; -import { ProcessQueryCloudRequestModel } from '../models/process-cloud-query-request.model'; +import { ProcessListRequestModel, ProcessQueryCloudRequestModel } from '../models/process-cloud-query-request.model'; import { ProcessListCloudSortingModel } from '../models/process-list-sorting.model'; -import { map, switchMap, take, takeUntil, tap } from 'rxjs/operators'; +import { filter, map, switchMap, take, takeUntil } from 'rxjs/operators'; import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface'; -import { PROCESS_LISTS_PREFERENCES_SERVICE_TOKEN } from '../../../services/cloud-token.service'; +import { PROCESS_LISTS_PREFERENCES_SERVICE_TOKEN, PROCESS_SEARCH_API_METHOD_TOKEN } from '../../../services/cloud-token.service'; import { ProcessListCloudPreferences } from '../models/process-cloud-preferences'; import { ProcessListDatatableAdapter } from '../datatable/process-list-datatable-adapter'; import { ProcessListDataColumnCustomData, PROCESS_LIST_CUSTOM_VARIABLE_COLUMN } from '../../../models/data-column-custom-data'; @@ -66,10 +67,8 @@ const PRESET_KEY = 'adf-cloud-process-list.presets'; encapsulation: ViewEncapsulation.None }) export class ProcessListCloudComponent - extends DataTableSchema - implements OnChanges, AfterContentInit, PaginatedComponent, OnDestroy -// eslint-disable-next-line @typescript-eslint/brace-style -{ +extends DataTableSchema +implements OnChanges, AfterContentInit, PaginatedComponent, OnDestroy { @ViewChild(DataTableComponent) dataTable: DataTableComponent; @@ -202,6 +201,34 @@ export class ProcessListCloudComponent @Input() isResizingEnabled: boolean = false; + /** + * Filter the processes. Display only processes with names matching any of the supplied strings. + * This input will be used only if PROCESS_SEARCH_API_METHOD_TOKEN is provided with 'POST' value. + */ + @Input() + names: string[] = []; + + /** + * Filter the processes. Display only processes started by any of the users whose usernames are present in the array. + * This input will be used only if PROCESS_SEARCH_API_METHOD_TOKEN is provided with 'POST' value. + */ + @Input() + initiators: string[] = []; + + /** + * Filter the processes. Display only processes present in any of the specified app versions. + * This input will be used only if PROCESS_SEARCH_API_METHOD_TOKEN is provided with 'POST' value. + */ + @Input() + appVersions: string[] = []; + + /** + * Filter the processes. Display only processes with provided statuses. + * This input will be used only if PROCESS_SEARCH_API_METHOD_TOKEN is provided with 'POST' value. + */ + @Input() + statuses: string[] = []; + /** Emitted when a row in the process list is clicked. */ @Output() rowClick: EventEmitter = new EventEmitter(); @@ -242,11 +269,13 @@ export class ProcessListCloudComponent rows: any[] = []; formattedSorting: any[]; requestNode: ProcessQueryCloudRequestModel; + processListRequestNode: ProcessListRequestModel; dataAdapter: ProcessListDatatableAdapter; private defaultSorting = { key: 'startDate', direction: 'desc' }; constructor( + @Inject(PROCESS_SEARCH_API_METHOD_TOKEN) @Optional() private searchMethod: 'GET' | 'POST', private processListCloudService: ProcessListCloudService, appConfigService: AppConfigService, private userPreferences: UserPreferencesService, @@ -333,13 +362,23 @@ export class ProcessListCloudComponent this.isColumnSchemaCreated$ .pipe( - switchMap(() => of(this.createRequestNode())), - tap((requestNode) => (this.requestNode = requestNode)), - switchMap((requestNode) => this.processListCloudService.getProcessByRequest(requestNode)), + filter((isColumnSchemaCreated) => !!isColumnSchemaCreated), + take(1), + switchMap(() => { + if (this.searchMethod === 'POST') { + const requestNode = this.createProcessListRequestNode(); + this.processListRequestNode = requestNode; + return this.processListCloudService.fetchProcessList(requestNode).pipe(take(1)); + } else { + const requestNode = this.createRequestNode(); + this.requestNode = requestNode; + return this.processListCloudService.getProcessByRequest(requestNode).pipe(take(1)); + } + }), takeUntil(this.onDestroy$) ) - .subscribe( - (processes) => { + .subscribe({ + next: (processes) => { this.rows = this.variableMapperService.mapVariablesByColumnTitle(processes.list.entries, this.columns); this.dataAdapter = new ProcessListDatatableAdapter(this.rows, this.columns); @@ -348,11 +387,11 @@ export class ProcessListCloudComponent this.isLoading = false; this.pagination.next(processes.list.pagination); }, - (error) => { + error: (error) => { this.error.emit(error); this.isLoading = false; } - ); + }); } private isAnyPropertyChanged(changes: SimpleChanges): boolean { @@ -481,6 +520,32 @@ export class ProcessListCloudComponent this.executeRowAction.emit(row); } + private createProcessListRequestNode(): ProcessListRequestModel { + const requestNode = { + appName: this.appName, + pagination: { + maxItems: this.size, + skipCount: this.skipCount + }, + sorting: this.sorting, + name: this.names, + initiator: this.initiators, + appVersion: this.appVersions, + status: this.statuses, + lastModifiedFrom: this.lastModifiedFrom?.toISOString() || '', + lasModifiedTo: this.lastModifiedTo?.toISOString() || '', + startFrom: this.startFrom, + startTo: this.startTo, + completedFrom: this.completedFrom, + completedTo: this.completedTo, + suspendedFrom: this.suspendedFrom, + suspendedTo: this.suspendedTo, + variableKeys: this.getVariableDefinitionsRequestModel() + }; + + return new ProcessListRequestModel(requestNode); + } + private createRequestNode(): ProcessQueryCloudRequestModel { const requestNode = { appName: this.appName, diff --git a/lib/process-services-cloud/src/lib/process/process-list/models/process-cloud-query-request.model.ts b/lib/process-services-cloud/src/lib/process/process-list/models/process-cloud-query-request.model.ts index be9494697f..caa7451d5a 100644 --- a/lib/process-services-cloud/src/lib/process/process-list/models/process-cloud-query-request.model.ts +++ b/lib/process-services-cloud/src/lib/process/process-list/models/process-cloud-query-request.model.ts @@ -15,7 +15,9 @@ * limitations under the License. */ +import { Pagination } from '@alfresco/js-api'; import { ProcessListCloudSortingModel } from './process-list-sorting.model'; +import { ProcessFilterCloudModel } from '../../process-filters/models/process-filter-cloud.model'; export class ProcessQueryCloudRequestModel { appName: string; @@ -76,3 +78,80 @@ export class ProcessQueryCloudRequestModel { } } } + +export interface ProcessListRequestProcessVariableFilter { + processDefinitionKey?: string; + name?: string; + type?: string; + value?: string; + operator?: string; +} + +export class ProcessListRequestModel { + appName: string; + pagination?: Pagination; + sorting?: ProcessListCloudSortingModel[]; + + name?: string[]; + initiator?: string[]; + appVersion?: string[]; + status?: string[]; + lastModifiedFrom?: string; + lasModifiedTo?: string; + startFrom?: string; + startTo?: string; + completedFrom?: string; + completedTo?: string; + suspendedFrom?: string; + suspendedTo?: string; + + processVariableFilters?: ProcessListRequestProcessVariableFilter[]; + variableKeys?: string[]; + + constructor(obj: Partial) { + if (!obj.appName) { + throw new Error('appName not configured'); + } + + this.appName = obj.appName; + this.pagination = obj.pagination; + this.sorting = obj.sorting; + + this.name = obj.name; + this.initiator = obj.initiator; + this.appVersion = obj.appVersion; + this.status = obj.status; + this.lastModifiedFrom = obj.lastModifiedFrom; + this.lasModifiedTo = obj.lasModifiedTo; + this.startFrom = obj.startFrom; + this.startTo = obj.startTo; + this.completedFrom = obj.completedFrom; + this.completedTo = obj.completedTo; + this.suspendedFrom = obj.suspendedFrom; + this.suspendedTo = obj.suspendedTo; + this.variableKeys = obj.variableKeys; + } +} + +export class ProcessFilterCloudAdapter extends ProcessListRequestModel { + constructor(filter: ProcessFilterCloudModel) { + super({ + appName: filter.appName, + pagination: { maxItems: 25, skipCount: 0 }, + sorting: [{ orderBy: filter.sort, direction: filter.order }], + + name: filter.processDefinitionNames, + initiator: filter.initiators, + appVersion: filter.appVersions, + status: filter.statuses, + lastModifiedFrom: filter.lastModifiedFrom?.toISOString(), + lasModifiedTo: filter.lastModifiedTo?.toISOString(), + startFrom: filter.startFrom, + startTo: filter.startTo, + completedFrom: filter.completedFrom, + completedTo: filter.completedTo, + suspendedFrom: filter.suspendedFrom, + suspendedTo: filter.suspendedTo + }); + } +} diff --git a/lib/process-services-cloud/src/lib/process/process-list/public-api.ts b/lib/process-services-cloud/src/lib/process/process-list/public-api.ts index d2887f3118..af4f3ee77f 100644 --- a/lib/process-services-cloud/src/lib/process/process-list/public-api.ts +++ b/lib/process-services-cloud/src/lib/process/process-list/public-api.ts @@ -23,6 +23,5 @@ export * from './models/process-list-sorting.model'; export * from './models/process-cloud-preferences'; export * from './services/process-list-cloud.service'; -export * from './services/process-task-list-cloud.service'; export * from './process-list-cloud.module'; diff --git a/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.spec.ts index c5a28c257c..220121d533 100644 --- a/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.spec.ts +++ b/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.spec.ts @@ -17,10 +17,10 @@ import { fakeAsync, TestBed } from '@angular/core/testing'; import { ProcessListCloudService } from './process-list-cloud.service'; -import { ProcessQueryCloudRequestModel } from '../models/process-cloud-query-request.model'; +import { ProcessListRequestModel, ProcessQueryCloudRequestModel } from '../models/process-cloud-query-request.model'; import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module'; import { AdfHttpClient } from '@alfresco/adf-core/api'; -import { firstValueFrom } from 'rxjs'; +import { catchError, firstValueFrom, of } from 'rxjs'; describe('ProcessListCloudService', () => { let service: ProcessListCloudService; @@ -44,72 +44,132 @@ describe('ProcessListCloudService', () => { requestSpy = spyOn(adfHttpClient, 'request'); })); - it('should append to the call all the parameters', (done) => { - const processRequest = { appName: 'fakeName', skipCount: 0, maxItems: 20, service: 'fake-service' } as ProcessQueryCloudRequestModel; - requestSpy.and.callFake(returnCallQueryParameters); - service.getProcessByRequest(processRequest).subscribe((res) => { + describe('getProcessByRequest', () => { + it('should append to the call all the parameters', (done) => { + const processRequest = { appName: 'fakeName', skipCount: 0, maxItems: 20, service: 'fake-service' } as ProcessQueryCloudRequestModel; + requestSpy.and.callFake(returnCallQueryParameters); + service.getProcessByRequest(processRequest).subscribe((res) => { + expect(res).toBeDefined(); + expect(res).not.toBeNull(); + expect(res.skipCount).toBe(0); + expect(res.maxItems).toBe(20); + expect(res.service).toBe('fake-service'); + done(); + }); + }); + + it('should concat the app name to the request url', (done) => { + const processRequest = { appName: 'fakeName', skipCount: 0, maxItems: 20, service: 'fake-service' } as ProcessQueryCloudRequestModel; + requestSpy.and.callFake(returnCallUrl); + service.getProcessByRequest(processRequest).subscribe((requestUrl) => { + expect(requestUrl).toBeDefined(); + expect(requestUrl).not.toBeNull(); + expect(requestUrl).toContain('/fakeName/query/v1/process-instances'); + done(); + }); + }); + + it('should concat the sorting to append as parameters', (done) => { + const processRequest = { + appName: 'fakeName', + skipCount: 0, + maxItems: 20, + service: 'fake-service', + sorting: [ + { orderBy: 'NAME', direction: 'DESC' }, + { orderBy: 'TITLE', direction: 'ASC' } + ] + } as ProcessQueryCloudRequestModel; + requestSpy.and.callFake(returnCallQueryParameters); + service.getProcessByRequest(processRequest).subscribe((res) => { + expect(res).toBeDefined(); + expect(res).not.toBeNull(); + expect(res.sort).toBe('NAME,DESC&TITLE,ASC'); + done(); + }); + }); + + it('should return an error when app name is not specified', (done) => { + const processRequest = { appName: null } as ProcessQueryCloudRequestModel; + requestSpy.and.callFake(returnCallUrl); + service.getProcessByRequest(processRequest).subscribe( + () => {}, + (error) => { + expect(error).toBe('Appname not configured'); + done(); + } + ); + }); + + it('should return number of total items of processes ', async () => { + const processRequest = { appName: 'fakeName', skipCount: 0, maxItems: 1, service: 'fake-service' } as ProcessQueryCloudRequestModel; + requestSpy.and.callFake(returnCallQueryParameters); + const result = await firstValueFrom(service.getProcessByRequest(processRequest)); + + expect(result).toBeDefined(); + expect(result).not.toBeNull(); + expect(result.skipCount).toBe(0); + expect(result.maxItems).toBe(1); + expect(result.service).toBe('fake-service'); + }); + }); + + describe('fetchProcessList', () => { + it('should append to the call all the parameters', async () => { + const processRequest = { + appName: 'fakeName', + pagination: { skipCount: 0, maxItems: 20 } + } as ProcessListRequestModel; + requestSpy.and.callFake(returnCallQueryParameters); + + const res = await firstValueFrom(service.fetchProcessList(processRequest)); + expect(res).toBeDefined(); expect(res).not.toBeNull(); expect(res.skipCount).toBe(0); expect(res.maxItems).toBe(20); - expect(res.service).toBe('fake-service'); - done(); }); - }); - it('should concat the app name to the request url', (done) => { - const processRequest = { appName: 'fakeName', skipCount: 0, maxItems: 20, service: 'fake-service' } as ProcessQueryCloudRequestModel; - requestSpy.and.callFake(returnCallUrl); - service.getProcessByRequest(processRequest).subscribe((requestUrl) => { - expect(requestUrl).toBeDefined(); - expect(requestUrl).not.toBeNull(); - expect(requestUrl).toContain('/fakeName/query/v1/process-instances'); - done(); + it('should concat the app name to the request url', async () => { + const processRequest = { + appName: 'fakeName', + pagination: { skipCount: 0, maxItems: 20 } + } as ProcessListRequestModel; + requestSpy.and.callFake(returnCallUrl); + + const res = await firstValueFrom(service.fetchProcessList(processRequest)); + + expect(res).toBeDefined(); + expect(res).not.toBeNull(); + expect(res).toContain('/fakeName/query/v1/process-instances/search'); }); - }); - it('should concat the sorting to append as parameters', (done) => { - const processRequest = { - appName: 'fakeName', - skipCount: 0, - maxItems: 20, - service: 'fake-service', - sorting: [ - { orderBy: 'NAME', direction: 'DESC' }, - { orderBy: 'TITLE', direction: 'ASC' } - ] - } as ProcessQueryCloudRequestModel; - requestSpy.and.callFake(returnCallQueryParameters); - service.getProcessByRequest(processRequest).subscribe((res) => { + it('should concat the sorting to append as parameters', async () => { + const processRequest = { + appName: 'fakeName', + pagination: { skipCount: 0, maxItems: 20 }, + sorting: [ + { orderBy: 'NAME', direction: 'DESC' }, + { orderBy: 'TITLE', direction: 'ASC' } + ] + } as ProcessListRequestModel; + requestSpy.and.callFake(returnCallQueryParameters); + + const res = await firstValueFrom(service.fetchProcessList(processRequest)); + expect(res).toBeDefined(); expect(res).not.toBeNull(); expect(res.sort).toBe('NAME,DESC&TITLE,ASC'); - done(); }); - }); - it('should return an error when app name is not specified', (done) => { - const processRequest = { appName: null } as ProcessQueryCloudRequestModel; - requestSpy.and.callFake(returnCallUrl); - service.getProcessByRequest(processRequest).subscribe( - () => {}, - (error) => { - expect(error).toBe('Appname not configured'); - done(); - } - ); - }); + it('should return an error when app name is not specified', async () => { + const taskRequest = { appName: null } as ProcessListRequestModel; + requestSpy.and.callFake(returnCallUrl); - it('should return number of total items of processes ', async () => { - const processRequest = { appName: 'fakeName', skipCount: 0, maxItems: 1, service: 'fake-service' } as ProcessQueryCloudRequestModel; - requestSpy.and.callFake(returnCallQueryParameters); - const result = await firstValueFrom(service.getProcessByRequest(processRequest)); + const res = await firstValueFrom(service.fetchProcessList(taskRequest).pipe(catchError((error) => of(error.message)))); - expect(result).toBeDefined(); - expect(result).not.toBeNull(); - expect(result.skipCount).toBe(0); - expect(result.maxItems).toBe(1); - expect(result.service).toBe('fake-service'); + expect(res).toBe('Appname not configured'); + }); }); describe('getAdminProcessRequest', () => { diff --git a/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.ts b/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.ts index a51647e2e7..5e83f84e87 100644 --- a/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/process/process-list/services/process-list-cloud.service.ts @@ -16,7 +16,7 @@ */ import { Injectable } from '@angular/core'; -import { ProcessQueryCloudRequestModel } from '../models/process-cloud-query-request.model'; +import { ProcessListRequestModel, ProcessQueryCloudRequestModel } from '../models/process-cloud-query-request.model'; import { Observable, throwError } from 'rxjs'; import { ProcessListCloudSortingModel } from '../models/process-list-sorting.model'; import { BaseCloudService } from '../../../services/base-cloud.service'; @@ -56,6 +56,7 @@ export class ProcessListCloudService extends BaseCloudService { /** * Finds a process using an object with optional query properties. * + * @deprecated From Activiti 8.7.0 forward, use ProcessListCloudService.fetchProcessList instead. * @param requestNode Query object * @param queryUrl Query url * @returns Process information @@ -67,6 +68,76 @@ export class ProcessListCloudService extends BaseCloudService { return this.getProcess(callback, defaultQueryUrl, requestNode, queryUrl); } + /** + * Available from Activiti version 8.7.0 onwards. + * Retrieves a list of processes using an object with optional query properties. + * + * @param requestNode Query object + * @param queryUrl Query url + * @returns List of processes + */ + fetchProcessList(requestNode: ProcessListRequestModel, queryUrl?: string): Observable { + if (!requestNode?.appName) { + return throwError(() => new Error('Appname not configured')); + } + + queryUrl = queryUrl || `${this.getBasePath(requestNode.appName)}/query/v1/process-instances/search`; + + const queryParams = { + maxItems: requestNode.pagination?.maxItems || 25, + skipCount: requestNode.pagination?.skipCount || 0, + sort: this.buildSortingParam(requestNode.sorting || []) + }; + + const queryData = this.buildQueryData(requestNode); + return this.post(queryUrl, queryData, queryParams).pipe( + map((response: any) => { + const entries = response.list?.entries; + if (entries) { + response.list.entries = entries.map((entryData) => entryData.entry); + } + return response; + }) + ); + } + + protected buildQueryData(requestNode: ProcessListRequestModel) { + const variableKeys = requestNode.variableKeys?.length > 0 ? requestNode.variableKeys.join(',') : undefined; + + const queryData: any = { + name: requestNode.name, + initiator: requestNode.initiator, + appVersion: requestNode.appVersion, + status: requestNode.status, + lastModifiedFrom: requestNode.lastModifiedFrom, + lasModifiedTo: requestNode.lasModifiedTo, + startFrom: requestNode.startFrom, + startTo: requestNode.startTo, + completedFrom: requestNode.completedFrom, + completedTo: requestNode.completedTo, + suspendedFrom: requestNode.suspendedFrom, + suspendedTo: requestNode.suspendedTo, + variableKeys: variableKeys + }; + + Object.keys(queryData).forEach((key) => { + const value = queryData[key]; + const isValueEmpty = !value; + const isValueArrayWithEmptyValue = Array.isArray(value) && (value.length === 0 || value[0] === null); + if (isValueEmpty || isValueArrayWithEmptyValue) { + delete queryData[key]; + } + }); + return queryData; + } + + getProcessListCounter(requestNode: ProcessListRequestModel): Observable { + if (!requestNode.appName) { + return throwError(() => new Error('Appname not configured')); + } + return this.fetchProcessList(requestNode).pipe(map((processes) => processes.list.pagination.totalItems)); + } + /** * Finds a process using an object with optional query properties. * diff --git a/lib/process-services-cloud/src/lib/process/process-list/services/process-task-list-cloud.service.ts b/lib/process-services-cloud/src/lib/process/process-list/services/process-task-list-cloud.service.ts deleted file mode 100644 index 9ee8554473..0000000000 --- a/lib/process-services-cloud/src/lib/process/process-list/services/process-task-list-cloud.service.ts +++ /dev/null @@ -1,93 +0,0 @@ -/*! - * @license - * Copyright © 2005-2024 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 { Injectable } from '@angular/core'; -import { Observable, throwError } from 'rxjs'; -import { BaseCloudService } from '../../../services/base-cloud.service'; -import { map } from 'rxjs/operators'; -import { TaskQueryCloudRequestModel } from '../../../models/filter-cloud-model'; -import { TaskCloudNodePaging } from '../../../models/task-cloud.model'; -import { TaskListCloudSortingModel } from '../../../models/task-list-sorting.model'; - -@Injectable({ providedIn: 'root' }) -export class ProcessTaskListCloudService extends BaseCloudService { - /** - * Finds a task using an object with optional query properties. - * - * @param requestNode Query object - * @param queryUrl Query url - * @returns Task information - */ - getTaskByRequest(requestNode: TaskQueryCloudRequestModel, queryUrl?: string): Observable { - if (requestNode.appName || requestNode.appName === '') { - queryUrl = queryUrl || `${this.getBasePath(requestNode.appName)}/query/v1/process-instances/${requestNode.processInstanceId}/tasks`; - const queryParams = this.buildQueryParams(requestNode); - const sortingParams = this.buildSortingParam(requestNode.sorting); - if (sortingParams) { - queryParams['sort'] = sortingParams; - } - return this.get(queryUrl, queryParams).pipe( - map((response) => { - const entries = response.list?.entries; - if (entries) { - // TODO: this is a hack of the model and should be revisited - response.list.entries = entries.map((entryData: any) => entryData.entry); - } - return response; - }) - ); - } else { - return throwError('Appname not configured'); - } - } - - protected buildQueryParams(requestNode: TaskQueryCloudRequestModel): any { - const queryParam: any = {}; - for (const property in requestNode) { - if ( - Object.prototype.hasOwnProperty.call(requestNode, property) && - !this.isExcludedField(property) && - this.isPropertyValueValid(requestNode, property) - ) { - queryParam[property] = requestNode[property]; - } - } - return queryParam; - } - - protected isExcludedField(property: string): boolean { - return property === 'appName' || property === 'sorting'; - } - - protected isPropertyValueValid(requestNode: TaskQueryCloudRequestModel, property: string): boolean { - return requestNode[property] !== '' && requestNode[property] !== null && requestNode[property] !== undefined; - } - - protected buildSortingParam(models: TaskListCloudSortingModel[]): string { - let finalSorting: string = ''; - if (models) { - for (const sort of models) { - if (!finalSorting) { - finalSorting = `${sort.orderBy},${sort.direction}`; - } else { - finalSorting = `${finalSorting}&${sort.orderBy},${sort.direction}`; - } - } - } - return encodeURI(finalSorting); - } -} diff --git a/lib/process-services-cloud/src/lib/services/cloud-token.service.ts b/lib/process-services-cloud/src/lib/services/cloud-token.service.ts index d3ad1f7999..1785dc2e4c 100644 --- a/lib/process-services-cloud/src/lib/services/cloud-token.service.ts +++ b/lib/process-services-cloud/src/lib/services/cloud-token.service.ts @@ -34,3 +34,9 @@ export const TASK_LIST_CLOUD_TOKEN = new InjectionToken('task-search-method'); + +/** + * Token used to indicate the API used to search for processes. + * 'POST' value should be provided only if the used Activiti version is 8.7.0 or higher. + */ +export const PROCESS_SEARCH_API_METHOD_TOKEN = new InjectionToken<'GET' | 'POST'>('process-search-method');