Skip to content

Commit

Permalink
ADW Saved Search (#10306)
Browse files Browse the repository at this point in the history
Co-authored-by: MichalKinas <michal.kinas@hyland.com>
  • Loading branch information
dominikiwanekhyland and MichalKinas authored Oct 17, 2024
1 parent 537b4f6 commit d146225
Show file tree
Hide file tree
Showing 45 changed files with 1,591 additions and 812 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@ Represents an input with autocomplete options.

### Properties

| Name | Type | Default value | Description |
|---------------------------|--------------------------|----|-----------------------------------------------------------------------------------------------------------------------------------------------|
| autocompleteOptions | `AutocompleteOption[]` | [] | Options for autocomplete |
| onReset$ | [`Observable`](https://rxjs.dev/guide/observable)`<void>` | | Observable that will listen to any reset event causing component to clear the chips and input |
| allowOnlyPredefinedValues | boolean | true | A flag that indicates whether it is possible to add a value not from the predefined ones |
| placeholder | string | 'SEARCH.FILTER.ACTIONS.ADD_OPTION' | Placeholder which should be displayed in input. |
| compareOption | (option1: AutocompleteOption, option2: AutocompleteOption) => boolean | | Function which is used to selected options with all options so it allows to detect which options are already selected. |
| formatChipValue | (option: string) => string | | Function which is used to format custom typed options. |
| filter | (options: AutocompleteOption[], value: string) => AutocompleteOption[] | | Function which is used to filter out possible options from hint. By default it checks if option includes typed value and is case insensitive. |
| Name | Type | Default value | Description |
|----------------------------|------------------------------------------------------------------------|----|-----------------------------------------------------------------------------------------------------------------------------------------------|
| autocompleteOptions | `AutocompleteOption[]` | [] | Options for autocomplete |
| preselectedOptions | `AutocompleteOption[]` | [] | Options which are selected from start |
| onReset$ | [`Observable`](https://rxjs.dev/guide/observable)`<void>` | | Observable that will listen to any reset event causing component to clear the chips and input |
| allowOnlyPredefinedValues | boolean | true | A flag that indicates whether it is possible to add a value not from the predefined ones |
| placeholder | string | 'SEARCH.FILTER.ACTIONS.ADD_OPTION' | Placeholder which should be displayed in input. |
| compareOption | (option1: AutocompleteOption, option2: AutocompleteOption) => boolean | | Function which is used to selected options with all options so it allows to detect which options are already selected. |
| formatChipValue | (option: string) => string | | Function which is used to format custom typed options. |
| filter | (options: AutocompleteOption[], value: string) => AutocompleteOption[] | | Function which is used to filter out possible options from hint. By default it checks if option includes typed value and is case insensitive. |

### Events

Expand Down
11 changes: 10 additions & 1 deletion docs/content-services/services/search-query-builder.service.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Stores information from all the custom search and faceted search widgets, compil
- **buildQuery**(): `SearchRequest`<br/>
Builds the current query.
- **Returns** `SearchRequest` - The finished query
- **encodeQuery**()<br/>
Encodes query shards stored in `filterRawParams` property.
- **execute**(queryBody?: `SearchRequest`)<br/>
Builds and executes the current query.
- _queryBody:_ `SearchRequest` - (Optional)
Expand Down Expand Up @@ -70,7 +72,12 @@ Stores information from all the custom search and faceted search widgets, compil

- **loadConfiguration**(): [`SearchConfiguration`](../../../lib/content-services/src/lib/search/models/search-configuration.interface.ts)<br/>

- **Returns** [`SearchConfiguration`](../../../lib/content-services/src/lib/search/models/search-configuration.interface.ts) -
- **Returns** [`SearchConfiguration`](../../../lib/content-services/src/lib/search/models/search-configuration.interface.ts) -

- **navigateToSearch**(query: `string`, searchUrl: `string`) <br/>
Updates user query, executes existing search configuration, encodes the query and navigates to searchUrl.
- _query:_ `string` - The query to use as user query
- _searchUrl:_ `string` - Search url to navigate to

- **removeFilterQuery**(query: `string`)<br/>
Removes an existing filter query.
Expand All @@ -93,6 +100,8 @@ Stores information from all the custom search and faceted search widgets, compil
- **update**(queryBody?: `SearchRequest`)<br/>
Builds the current query and triggers the `updated` event.
- _queryBody:_ `SearchRequest` - (Optional)
- **updateSearchQueryParams**() <br/>
Encodes the query and navigates to existing search route adding encoded query as a search param.
- **updateSelectedConfiguration**(index: `number`)<br/>

- _index:_ `number` -
Expand Down
64 changes: 64 additions & 0 deletions docs/core/services/saved-searches.service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

# Saved Searches Service

Manages operations related to saving and retrieving user-defined searches in the Alfresco Process Services (APS) environment.

## Class members

### Properties

- **savedSearches$**: [`ReplaySubject`](https://rxjs.dev/api/index/class/ReplaySubject)`<SavedSearch[]>`<br/>
Stores the list of saved searches and emits new value whenever there is a change.

### Methods

#### getSavedSearches(): [`Observable`](https://rxjs.dev/api/index/class/Observable)`<SavedSearch[]>`

Fetches the file with list of saved searches either from a locally cached node ID or by querying the APS server. Then it reads the file and maps JSON objects into SavedSearches

- **Returns**:
- [`Observable`](https://rxjs.dev/api/index/class/Observable)`<SavedSearch[]>` - An observable that emits the list of saved searches.

#### saveSearch(newSaveSearch: Pick<SavedSearch, 'name' | 'description' | 'encodedUrl'>): [`Observable`](https://rxjs.dev/api/index/class/Observable)`<NodeEntry>`

Saves a new search and updates the existing list of saved searches stored in file and in service property savedSearches$.

- **Parameters**:
- `newSaveSearch`: An object containing the `name`, `description`, and `encodedUrl` of the new search.

- **Returns**:
- [`Observable`](https://rxjs.dev/api/index/class/Observable)`<NodeEntry>` - An observable that emits the response of the node entry after saving.

### Usage Examples

#### Fetching Saved Searches

The following example shows how to fetch saved searches:

```typescript
this.savedSearchService.getSavedSearches().subscribe((searches: SavedSearch[]) => {
console.log('Saved searches:', searches);
});
```

#### Saving a New Search

To save a new search:

```typescript
const newSearch = { name: 'New Search', description: 'A sample search', encodedUrl: 'url3' };
this.savedSearchService.saveSearch(newSearch).subscribe((response) => {
console.log('Saved new search:', response);
});
```

#### Creating Saved Searches Node

When the saved searches file does not exist, it will be created:

```typescript
this.savedSearchService.createSavedSearchesNode('parent-node-id').subscribe((node) => {
console.log('Created saved-searches.json node:', node);
});
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*!
* @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.
*/

export interface SavedSearch {
name: string;
description?: string;
encodedUrl: string;
order: number;
}
2 changes: 2 additions & 0 deletions lib/content-services/src/lib/common/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export * from './services/nodes-api.service';
export * from './services/discovery-api.service';
export * from './services/people-content.service';
export * from './services/content.service';
export * from './services/saved-searches.service';

export * from './events/file.event';

Expand All @@ -36,4 +37,5 @@ export * from './models/permissions.enum';
export * from './models/allowable-operations.enum';

export * from './interfaces/search-configuration.interface';
export * from './interfaces/saved-search.interface';
export * from './mocks/ecm-user.service.mock';
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*!
* @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 { TestBed } from '@angular/core/testing';
import { AlfrescoApiService } from '../../services/alfresco-api.service';
import { NodeEntry } from '@alfresco/js-api';
import { SavedSearchesService } from './saved-searches.service';
import { AlfrescoApiServiceMock } from '@alfresco/adf-content-services';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { AuthenticationService } from '@alfresco/adf-core';
import { Subject } from 'rxjs';

describe('SavedSearchesService', () => {
let service: SavedSearchesService;
let authService: AuthenticationService;
let testUserName: string;

const testNodeId = 'test-node-id';
const SAVED_SEARCHES_NODE_ID = 'saved-searches-node-id__';
const SAVED_SEARCHES_CONTENT = JSON.stringify([
{ name: 'Search 1', description: 'Description 1', encodedUrl: 'url1', order: 0 },
{ name: 'Search 2', description: 'Description 2', encodedUrl: 'url2', order: 1 }
]);

/**
* Creates a stub with Promise returning a Blob
*
* @returns Promise with Blob
*/
function createBlob() {
return Promise.resolve(new Blob([SAVED_SEARCHES_CONTENT]));
}

beforeEach(() => {
testUserName = 'test-user';
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock },
{ provide: AuthenticationService, useValue: { getUsername: () => {}, onLogin: new Subject() } },
SavedSearchesService
]
});
service = TestBed.inject(SavedSearchesService);
authService = TestBed.inject(AuthenticationService);
spyOn(service.nodesApi, 'getNode').and.callFake(() => Promise.resolve({ entry: { id: testNodeId } } as NodeEntry));
spyOn(service.searchApi, 'search').and.callFake(() => Promise.resolve({ list: { entries: [] } }));
});

afterEach(() => {
localStorage.removeItem(SAVED_SEARCHES_NODE_ID + testUserName);
});

it('should retrieve saved searches from the saved-searches.json file', (done) => {
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
spyOn(localStorage, 'getItem').and.callFake(() => testNodeId);
spyOn(service.nodesApi, 'getNodeContent').and.callFake(() => createBlob());
service.innit();

service.getSavedSearches().subscribe((searches) => {
expect(localStorage.getItem).toHaveBeenCalledWith(SAVED_SEARCHES_NODE_ID + testUserName);
expect(service.nodesApi.getNodeContent).toHaveBeenCalledWith(testNodeId);
expect(searches.length).toBe(2);
expect(searches[0].name).toBe('Search 1');
expect(searches[1].name).toBe('Search 2');
done();
});
});

it('should create saved-searches.json file if it does not exist', (done) => {
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
spyOn(service.nodesApi, 'createNode').and.callFake(() => Promise.resolve({ entry: { id: 'new-node-id' } }));
spyOn(service.nodesApi, 'getNodeContent').and.callFake(() => Promise.resolve(new Blob([''])));
service.innit();

service.getSavedSearches().subscribe((searches) => {
expect(service.nodesApi.getNode).toHaveBeenCalledWith('-my-');
expect(service.searchApi.search).toHaveBeenCalled();
expect(service.nodesApi.createNode).toHaveBeenCalledWith(testNodeId, jasmine.objectContaining({ name: 'saved-searches.json' }));
expect(searches.length).toBe(0);
done();
});
});

it('should save a new search', (done) => {
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
const nodeId = 'saved-searches-node-id';
spyOn(localStorage, 'getItem').and.callFake(() => nodeId);
spyOn(service.nodesApi, 'getNodeContent').and.callFake(() => createBlob());
const newSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3' };
spyOn(service.nodesApi, 'updateNodeContent').and.callFake(() => Promise.resolve({ entry: {} } as NodeEntry));
service.innit();

service.saveSearch(newSearch).subscribe(() => {
expect(service.nodesApi.updateNodeContent).toHaveBeenCalledWith(nodeId, jasmine.any(String));
expect(service.savedSearches$).toBeDefined();
service.savedSearches$.subscribe((searches) => {
expect(searches.length).toBe(3);
expect(searches[2].name).toBe('Search 3');
expect(searches[2].order).toBe(2);
done();
});
});
});

it('should emit initial saved searches on subscription', (done) => {
const nodeId = 'saved-searches-node-id';
spyOn(localStorage, 'getItem').and.returnValue(nodeId);
spyOn(service.nodesApi, 'getNodeContent').and.returnValue(createBlob());
service.innit();

service.savedSearches$.pipe().subscribe((searches) => {
expect(searches.length).toBe(2);
expect(searches[0].name).toBe('Search 1');
done();
});

service.getSavedSearches().subscribe();
});

it('should emit updated saved searches after saving a new search', (done) => {
spyOn(authService, 'getUsername').and.callFake(() => testUserName);
spyOn(localStorage, 'getItem').and.callFake(() => testNodeId);
spyOn(service.nodesApi, 'getNodeContent').and.callFake(() => createBlob());
const newSearch = { name: 'Search 3', description: 'Description 3', encodedUrl: 'url3' };
spyOn(service.nodesApi, 'updateNodeContent').and.callFake(() => Promise.resolve({ entry: {} } as NodeEntry));
service.innit();

let emissionCount = 0;

service.savedSearches$.subscribe((searches) => {
emissionCount++;
if (emissionCount === 1) {
expect(searches.length).toBe(2);
}
if (emissionCount === 2) {
expect(searches.length).toBe(3);
expect(searches[2].name).toBe('Search 3');
done();
}
});

service.saveSearch(newSearch).subscribe();
});
});
Loading

0 comments on commit d146225

Please sign in to comment.