diff --git a/.vscode/settings.json b/.vscode/settings.json
index dfd9fe7bf0..d1d4ace8ba 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -9,7 +9,6 @@
"**/.happypack": true
},
"editor.renderIndentGuides": true,
- "tslint.configFile": "ng2-components/tslint.json",
"markdownlint.config": {
"MD032": false,
"MD004": false,
diff --git a/demo-shell/src/app.config.json b/demo-shell/src/app.config.json
index 93d0b0aff8..8536010b14 100644
--- a/demo-shell/src/app.config.json
+++ b/demo-shell/src/app.config.json
@@ -51,6 +51,118 @@
"label": "Simplified Chinese"
}
],
+ "search": {
+ "limits": {
+ "permissionEvaluationTime": null,
+ "permissionEvaluationCount": null
+ },
+ "filterQueries": [
+ { "query": "TYPE:'cm:folder' OR TYPE:'cm:content'" },
+ { "query": "NOT cm:creator:System" }
+ ],
+ "facetFields": {
+ "facets": [
+ { "field": "content.mimetype", "mincount": 1, "label": "Type" },
+ { "field": "content.size", "mincount": 1, "label": "Size" },
+ { "field": "creator", "mincount": 1, "label": "Creator" },
+ { "field": "modifier", "mincount": 1, "label": "Modifier" }
+ ]
+ },
+ "facetQueries": [
+ { "query": "created:2018", "label": "Created This Year" },
+ { "query": "content.mimetype", "label": "Type" },
+ { "query": "content.size:[0 TO 10240]", "label": "Size: xtra small"},
+ { "query": "content.size:[10240 TO 102400]", "label": "Size: small"},
+ { "query": "content.size:[102400 TO 1048576]", "label": "Size: medium" },
+ { "query": "content.size:[1048576 TO 16777216]", "label": "Size: large" },
+ { "query": "content.size:[16777216 TO 134217728]", "label": "Size: xtra large" },
+ { "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" }
+ ],
+ "query": {
+ "categories": [
+ {
+ "id": "broken",
+ "name": "Broken Facet",
+ "enabled": false,
+ "expanded": false,
+ "component": {
+ "selector": "adf-search-text",
+ "settings": {
+ "field": "fieldname"
+ }
+ }
+ },
+ {
+ "id": "queryName",
+ "name": "Name",
+ "enabled": true,
+ "expanded": true,
+ "component": {
+ "selector": "adf-search-text",
+ "settings": {
+ "pattern": "cm:name:'(.*?)'",
+ "field": "cm:name",
+ "placeholder": "Enter the name"
+ }
+ }
+ },
+ {
+ "id": "queryFields",
+ "name": "Fields",
+ "enabled": true,
+ "expanded": false,
+ "component": {
+ "selector": "adf-search-fields",
+ "settings": {
+ "field": null,
+ "options": [
+ { "name": "Name", "value": "name", "fields": ["name"], "default": true },
+ { "name": "File Size", "value": "content.sizeInBytes", "fields": ["content"], "default": true },
+ { "name": "Modified On", "value": "modifiedAt", "fields": ["modifiedAt"], "default": true },
+ { "name": "Modified By", "value": "modifiedByUser.displayName", "fields": ["modifiedByUser"], "default": true }
+ ]
+ }
+ }
+ },
+ {
+ "id": "queryType",
+ "name": "Type",
+ "enabled": true,
+ "expanded": false,
+ "component": {
+ "selector": "adf-search-radio",
+ "settings": {
+ "field": null,
+ "options": [
+ { "name": "None", "value": "", "default": true },
+ { "name": "All", "value": "TYPE:'cm:folder' OR TYPE:'cm:content'" },
+ { "name": "Folder", "value": "TYPE:'cm:folder'" },
+ { "name": "Document", "value": "TYPE:'cm:content'" }
+ ]
+ }
+ }
+ },
+ {
+ "id": "queryLocations",
+ "name": "Locations",
+ "enabled": true,
+ "expanded": false,
+ "component": {
+ "selector": "adf-search-scope-locations",
+ "settings": {
+ "field": null,
+ "options": [
+ { "name": "Default", "value": "nodes", "default": true },
+ { "name": "Nodes", "value": "nodes" },
+ { "name": "Deleted Nodes", "value": "deleted-nodes" },
+ { "name": "Versions", "value": "versions" }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ },
"pagination": {
"size": 25,
"supportedPageSizes": [ 5, 10, 15, 20 ]
diff --git a/demo-shell/src/app/app.module.ts b/demo-shell/src/app/app.module.ts
index 7b286246e4..fdbfa8edfc 100644
--- a/demo-shell/src/app/app.module.ts
+++ b/demo-shell/src/app/app.module.ts
@@ -50,20 +50,19 @@ import { ProcessAttachmentsComponent } from './components/process-service/proces
import { SharedLinkViewComponent } from './components/shared-link-view/shared-link-view.component';
import { DemoPermissionComponent } from './components/permissions/demo-permissions.component';
-
@NgModule({
imports: [
+ BrowserModule,
BrowserAnimationsModule,
ReactiveFormsModule,
- BrowserModule,
routing,
FormsModule,
- AdfModule,
MaterialModule,
ThemePickerModule,
FlexLayoutModule,
ChartsModule,
- HttpClientModule
+ HttpClientModule,
+ AdfModule
],
declarations: [
AppComponent,
@@ -98,7 +97,8 @@ import { DemoPermissionComponent } from './components/permissions/demo-permissio
OverlayViewerComponent,
SharedLinkViewComponent,
FormLoadingComponent,
- DemoPermissionComponent
+ DemoPermissionComponent,
+ FormLoadingComponent
],
providers: [
{ provide: AppConfigService, useClass: DebugAppConfigService },
diff --git a/demo-shell/src/app/components/search/search-result.component.html b/demo-shell/src/app/components/search/search-result.component.html
index e8f461e9e1..ae34f6275c 100644
--- a/demo-shell/src/app/components/search/search-result.component.html
+++ b/demo-shell/src/app/components/search/search-result.component.html
@@ -1,3 +1,4 @@
+
-
-
+
+
+
diff --git a/demo-shell/src/app/components/search/search-result.component.scss b/demo-shell/src/app/components/search/search-result.component.scss
index a8a1a2930a..5fa1001ce1 100644
--- a/demo-shell/src/app/components/search/search-result.component.scss
+++ b/demo-shell/src/app/components/search/search-result.component.scss
@@ -1,3 +1,20 @@
+.adf-search-results {
+ display: flex;
+
+ .adf-search-settings {
+ width: 260px;
+ border: 1px solid #eee;
+ }
+
+ &__facets {
+ margin: 5px;
+ }
+
+ &__content {
+ flex: 1;
+ }
+}
+
div.search-results-container {
padding: 0 20px 20px 20px;
}
diff --git a/demo-shell/src/app/components/search/search-result.component.ts b/demo-shell/src/app/components/search/search-result.component.ts
index 7b7288a946..0cf4408601 100644
--- a/demo-shell/src/app/components/search/search-result.component.ts
+++ b/demo-shell/src/app/components/search/search-result.component.ts
@@ -18,7 +18,7 @@
import { Component, OnInit, Optional, ViewChild } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { NodePaging, Pagination } from 'alfresco-js-api';
-import { SearchComponent } from '@alfresco/adf-content-services';
+import { SearchComponent, SearchQueryBuilderService } from '@alfresco/adf-content-services';
import { UserPreferencesService } from '@alfresco/adf-core';
@Component({
@@ -40,6 +40,7 @@ export class SearchResultComponent implements OnInit {
constructor(public router: Router,
private preferences: UserPreferencesService,
+ private queryBuilder: SearchQueryBuilderService,
@Optional() private route: ActivatedRoute) {
this.maxItems = this.preferences.paginationSize;
}
@@ -48,6 +49,8 @@ export class SearchResultComponent implements OnInit {
if (this.route) {
this.route.params.forEach((params: Params) => {
this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null;
+ this.queryBuilder.queryFragments['queryName'] = `cm:name:'${this.searchedWord}'`;
+ this.queryBuilder.update();
});
}
this.maxItems = this.preferences.paginationSize;
@@ -59,8 +62,8 @@ export class SearchResultComponent implements OnInit {
}
onRefreshPagination(pagination: Pagination) {
- this.maxItems = pagination.maxItems;
- this.skipCount = pagination.skipCount;
+ this.maxItems = pagination.maxItems;
+ this.skipCount = pagination.skipCount;
}
onDeleteElementSuccess(element: any) {
diff --git a/docs/content-services/search-chip-list.component.md b/docs/content-services/search-chip-list.component.md
new file mode 100644
index 0000000000..89ae0441c7
--- /dev/null
+++ b/docs/content-services/search-chip-list.component.md
@@ -0,0 +1,13 @@
+---
+Added: v2.3.0
+Status: Active
+---
+
+# Search Chip List Component
+
+```html
+
+
+```
+
+
\ No newline at end of file
diff --git a/docs/content-services/search-filter.component.md b/docs/content-services/search-filter.component.md
new file mode 100644
index 0000000000..d112480990
--- /dev/null
+++ b/docs/content-services/search-filter.component.md
@@ -0,0 +1,154 @@
+---
+Added: v2.3.0
+Status: Active
+---
+
+# Search Settings Component
+
+Represents a main container component for custom search and faceted search settings.
+
+## Usage example
+
+```html
+
+```
+
+The component is based on dynamically created Widgets to modify the resulting query and options,
+and the `Query Builder` to build and execute the search queries.
+
+## Query Builder Service
+
+Stores information from all the custom search and faceted search widgets,
+compiles and runs the final Search query.
+
+The Query Builder is UI agnostic and does not rely on Angular components.
+It is possible to reuse it with multiple component implementations.
+
+Allows custom widgets to populate and edit the following parts of the resulting query:
+
+- categories
+- query fragments that form query expression
+- include fields
+- scope settings
+- filter queries
+- facet fields
+- range queries
+
+```ts
+constructor(queryBuilder: QueryBuilderService) {
+
+ queryBuilder.updated.subscribe(query => {
+ this.queryBuilder.execute();
+ });
+
+ queryBuilder.executed.subscribe(data => {
+ this.onDataLoaded(data);
+ });
+
+}
+```
+
+## Configuration
+
+The configuration should be provided via the `search` entry in the `app.config.json` file.
+
+Below is an example configuration:
+
+```json
+{
+ "search": {
+ "limits": {
+ "permissionEvaluationTime": null,
+ "permissionEvaluationCount": null
+ },
+ "filterQueries": [
+ { "query": "TYPE:'cm:folder' OR TYPE:'cm:content'" },
+ { "query": "NOT cm:creator:System" }
+ ],
+ "facetFields": {
+ "facets": [
+ { "field": "content.mimetype", "mincount": 1, "label": "Type" },
+ { "field": "content.size", "mincount": 1, "label": "Size" },
+ { "field": "creator", "mincount": 1, "label": "Creator" },
+ { "field": "modifier", "mincount": 1, "label": "Modifier" }
+ ]
+ },
+ "facetQueries": [
+ { "query": "created:2018", "label": "Created This Year" },
+ { "query": "content.mimetype", "label": "Type" },
+ { "query": "content.size:[0 TO 10240]", "label": "Size: xtra small"},
+ { "query": "content.size:[10240 TO 102400]", "label": "Size: small"},
+ { "query": "content.size:[102400 TO 1048576]", "label": "Size: medium" },
+ { "query": "content.size:[1048576 TO 16777216]", "label": "Size: large" },
+ { "query": "content.size:[16777216 TO 134217728]", "label": "Size: xtra large" },
+ { "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" }
+ ],
+ "query": {
+ "categories": [
+ {
+ "id": "queryName",
+ "name": "Name",
+ "enabled": true,
+ "expanded": true,
+ "component": {
+ "selector": "adf-search-text",
+ "settings": {
+ "pattern": "cm:name:'(.*?)'",
+ "field": "cm:name",
+ "placeholder": "Enter the name"
+ }
+ }
+ }
+ ]
+ }
+ }
+}
+```
+
+## Categories
+
+The Search Settings component and Query Builder require `categories` section provided within the configuration.
+
+Categories are needed to build Widgets so that users can modify the search query at runtime. Every Category can be represented by a single Angular component, either simple or composite one.
+
+```ts
+export interface SearchCategory {
+ id: string;
+ name: string;
+ enabled: boolean;
+ expanded: boolean;
+ component: {
+ selector: string;
+ settings: SearchWidgetSettings;
+ };
+}
+```
+
+The interface above also describes entries in the `search.query.categories` section for the `app.config.json`.
+
+
+
+### Properties
+
+For the property types please refer to the `SearchCategory` interface.
+
+| Property | Description |
+| --- | --- |
+| id | Unique identifier of the category. Also used to access QueryBuilder customisations for a particular widget. |
+| name | Public display name for the category. |
+| enabled | Toggles category availability. Set to `false` if you want to exclude a category from processing. |
+| expanded | Toggles the expanded state of the category. Use it |
+| component.selector | The id of the Angular component selector to render the Category |
+| component.settings | An object containing component specific settings. Put any properties needed for the target component. |
+
+Every component can expect different set of settings.
+For example Number editors may parse minimum and maximum values, while Text editors can support value formats or length constraints.
+
+You can use `component.settings` to pass any information to your custom Widget using the following interface:
+
+```ts
+export interface SearchWidgetSettings {
+ field: string;
+ [indexer: string]: any;
+}
+```
\ No newline at end of file
diff --git a/docs/docassets/images/search-categories-01.png b/docs/docassets/images/search-categories-01.png
new file mode 100644
index 0000000000..b0b243b823
Binary files /dev/null and b/docs/docassets/images/search-categories-01.png differ
diff --git a/docs/docassets/images/selected-facets.png b/docs/docassets/images/selected-facets.png
new file mode 100644
index 0000000000..952e8651c5
Binary files /dev/null and b/docs/docassets/images/selected-facets.png differ
diff --git a/lib/content-services/content-node-selector/content-node-selector-panel.component.spec.ts b/lib/content-services/content-node-selector/content-node-selector-panel.component.spec.ts
index 5378aa2656..b075762eb2 100644
--- a/lib/content-services/content-node-selector/content-node-selector-panel.component.spec.ts
+++ b/lib/content-services/content-node-selector/content-node-selector-panel.component.spec.ts
@@ -51,10 +51,8 @@ describe('ContentNodeSelectorComponent', () => {
const debounceSearch = 200;
let component: ContentNodeSelectorPanelComponent;
let fixture: ComponentFixture;
- let searchService: SearchService;
let contentNodeSelectorService: ContentNodeSelectorService;
let searchSpy: jasmine.Spy;
- let cnSearchSpy: jasmine.Spy;
let _observer: Observer;
@@ -104,10 +102,8 @@ describe('ContentNodeSelectorComponent', () => {
component = fixture.componentInstance;
component.debounceSearch = 0;
- searchService = TestBed.get(SearchService);
contentNodeSelectorService = TestBed.get(ContentNodeSelectorService);
- cnSearchSpy = spyOn(contentNodeSelectorService, 'search').and.callThrough();
- searchSpy = spyOn(searchService, 'searchByQueryBody').and.callFake(() => {
+ searchSpy = spyOn(contentNodeSelectorService, 'search').and.callFake(() => {
return Observable.create((observer: Observer) => {
_observer = observer;
});
@@ -283,32 +279,6 @@ describe('ContentNodeSelectorComponent', () => {
describe('Search functionality', () => {
let getCorrespondingNodeIdsSpy;
- function defaultSearchOptions(searchTerm, rootNodeId = undefined, skipCount = 0) {
-
- const parentFiltering = rootNodeId ? [{ query: `ANCESTOR:'workspace://SpacesStore/${rootNodeId}'` }] : [];
-
- let defaultSearchNode: any = {
- query: {
- query: searchTerm ? `${searchTerm}* OR name:${searchTerm}*` : searchTerm
- },
- include: ['path', 'allowableOperations'],
- paging: {
- maxItems: 25,
- skipCount: skipCount
- },
- filterQueries: [
- { query: "TYPE:'cm:folder'" },
- { query: 'NOT cm:creator:System' },
- ...parentFiltering
- ],
- scope: {
- locations: ['nodes']
- }
- };
-
- return defaultSearchNode;
- }
-
beforeEach(() => {
const documentListService = TestBed.get(DocumentListService);
const expectedDefaultFolderNode = { path: { elements: [] } };
@@ -331,11 +301,11 @@ describe('ContentNodeSelectorComponent', () => {
fixture.detectChanges();
});
- it('should load the results by calling the search api on search change', (done) => {
+ it('should load the results on search change', (done) => {
typeToSearchBox('kakarot');
setTimeout(() => {
- expect(searchSpy).toHaveBeenCalledWith(defaultSearchOptions('kakarot'));
+ expect(searchSpy).toHaveBeenCalledWith('kakarot', undefined, 0, 25);
done();
}, 300);
});
@@ -350,7 +320,7 @@ describe('ContentNodeSelectorComponent', () => {
}, 300);
});
- it('should call the search api on changing the site selectbox\'s value', (done) => {
+ it('should search on changing the site selectbox value', (done) => {
typeToSearchBox('vegeta');
setTimeout(() => {
@@ -360,50 +330,50 @@ describe('ContentNodeSelectorComponent', () => {
fixture.whenStable().then(() => {
expect(searchSpy.calls.count()).toBe(2, 'Search count should be two after the site change');
- expect(searchSpy.calls.argsFor(1)).toEqual([defaultSearchOptions('vegeta', 'namek')] );
+ expect(searchSpy.calls.argsFor(1)).toEqual([ 'vegeta', 'namek', 0, 25] );
done();
});
}, 300);
});
- it('should call the content node selector\'s search with the right parameters on changing the site selectbox\'s value', (done) => {
+ it('should call the content node selector search with the right parameters on changing the site selectbox value', (done) => {
typeToSearchBox('vegeta');
setTimeout(() => {
- expect(cnSearchSpy.calls.count()).toBe(1);
+ expect(searchSpy.calls.count()).toBe(1);
component.siteChanged( { entry: { guid: '-sites-' } });
fixture.whenStable().then(() => {
- expect(cnSearchSpy).toHaveBeenCalled();
- expect(cnSearchSpy.calls.count()).toBe(2);
- expect(cnSearchSpy).toHaveBeenCalledWith('vegeta', '-sites-', 0, 25);
+ expect(searchSpy).toHaveBeenCalled();
+ expect(searchSpy.calls.count()).toBe(2);
+ expect(searchSpy).toHaveBeenCalledWith('vegeta', '-sites-', 0, 25);
done();
});
}, 300);
});
- it('should call the content node selector\'s search with the right parameters on changing the site selectbox\'s value from a custom dropdown menu', (done) => {
+ it('should call the content node selector search with the right parameters on changing the site selectbox value from a custom dropdown menu', (done) => {
component.dropdownSiteList = {list: {entries: [ { entry: { guid: '-sites-' } }, { entry: { guid: 'namek' } }]}};
fixture.detectChanges();
typeToSearchBox('vegeta');
setTimeout(() => {
- expect(cnSearchSpy.calls.count()).toBe(1);
+ expect(searchSpy.calls.count()).toBe(1);
component.siteChanged( { entry: { guid: '-sites-' } });
fixture.whenStable().then(() => {
- expect(cnSearchSpy).toHaveBeenCalled();
- expect(cnSearchSpy.calls.count()).toBe(2);
- expect(cnSearchSpy).toHaveBeenCalledWith('vegeta', '-sites-', 0, 25, ['123456testId', '09876543testId']);
+ expect(searchSpy).toHaveBeenCalled();
+ expect(searchSpy.calls.count()).toBe(2);
+ expect(searchSpy).toHaveBeenCalledWith('vegeta', '-sites-', 0, 25, ['123456testId', '09876543testId']);
done();
});
}, 300);
});
- it('should get the corresponding node ids before the search call on changing the site selectbox\'s value from a custom dropdown menu', (done) => {
+ it('should get the corresponding node ids before the search call on changing the site selectbox value from a custom dropdown menu', (done) => {
component.dropdownSiteList = {list: {entries: [ { entry: { guid: '-sites-' } }, { entry: { guid: 'namek' } }]}};
fixture.detectChanges();
@@ -531,7 +501,7 @@ describe('ContentNodeSelectorComponent', () => {
component.siteChanged( { entry: { guid: 'namek' } });
expect(searchSpy.calls.count()).toBe(2);
- expect(searchSpy.calls.argsFor(1)).toEqual([defaultSearchOptions('piccolo', 'namek')]);
+ expect(searchSpy.calls.argsFor(1)).toEqual([ 'piccolo', 'namek', 0, 25 ]);
component.clear();
@@ -682,14 +652,14 @@ describe('ContentNodeSelectorComponent', () => {
}, 300);
});
- it('button\'s callback should load the next batch of results by calling the search api', async(() => {
+ it('button callback should load the next batch of results by calling the search api', async(() => {
const skipCount = 8;
component.searchTerm = 'kakarot';
component.getNextPageOfSearch({ skipCount });
fixture.whenStable().then(() => {
- expect(searchSpy).toHaveBeenCalledWith(defaultSearchOptions('kakarot', undefined, skipCount));
+ expect(searchSpy).toHaveBeenCalledWith( 'kakarot', undefined, skipCount, 25);
});
}));
@@ -703,7 +673,7 @@ describe('ContentNodeSelectorComponent', () => {
expect(pagination).not.toBeNull();
});
- it('button\'s callback should load the next batch of folder results when there is no searchTerm', () => {
+ it('button callback should load the next batch of folder results when there is no searchTerm', () => {
const skipCount = 5;
component.searchTerm = '';
diff --git a/lib/content-services/content-node-selector/content-node-selector.service.ts b/lib/content-services/content-node-selector/content-node-selector.service.ts
index 1efd084ccb..9cc7a54caf 100644
--- a/lib/content-services/content-node-selector/content-node-selector.service.ts
+++ b/lib/content-services/content-node-selector/content-node-selector.service.ts
@@ -73,6 +73,8 @@ export class ContentNodeSelectorService {
}
};
- return this.searchService.searchByQueryBody(defaultSearchNode);
+ return Observable.fromPromise(
+ this.searchService.searchByQueryBody(defaultSearchNode)
+ );
}
}
diff --git a/lib/content-services/content.module.ts b/lib/content-services/content.module.ts
index 482e2055ae..0287f954b9 100644
--- a/lib/content-services/content.module.ts
+++ b/lib/content-services/content.module.ts
@@ -43,6 +43,7 @@ import { PropertyDescriptorsService } from './content-metadata/services/property
import { ContentMetadataConfigFactory } from './content-metadata/services/config/content-metadata-config.factory';
import { BasicPropertiesService } from './content-metadata/services/basic-properties.service';
import { PropertyGroupTranslatorService } from './content-metadata/services/property-groups-translator.service';
+import { SearchQueryBuilderService } from './search/search-query-builder.service';
@NgModule({
imports: [
@@ -81,7 +82,8 @@ import { PropertyGroupTranslatorService } from './content-metadata/services/prop
PropertyDescriptorsService,
ContentMetadataConfigFactory,
BasicPropertiesService,
- PropertyGroupTranslatorService
+ PropertyGroupTranslatorService,
+ SearchQueryBuilderService
],
exports: [
CoreModule,
diff --git a/lib/content-services/material.module.ts b/lib/content-services/material.module.ts
index 488f82a6ec..eccbe975f3 100644
--- a/lib/content-services/material.module.ts
+++ b/lib/content-services/material.module.ts
@@ -31,7 +31,8 @@ import {
MatRippleModule,
MatExpansionModule,
MatSelectModule,
- MatSlideToggleModule
+ MatSlideToggleModule,
+ MatCheckboxModule
} from '@angular/material';
export function modules() {
@@ -50,7 +51,8 @@ export function modules() {
MatOptionModule,
MatExpansionModule,
MatSelectModule,
- MatSlideToggleModule
+ MatSlideToggleModule,
+ MatCheckboxModule
];
}
diff --git a/lib/content-services/ng-package.json b/lib/content-services/ng-package.json
index f0cbd098b4..dca9672374 100644
--- a/lib/content-services/ng-package.json
+++ b/lib/content-services/ng-package.json
@@ -4,6 +4,7 @@
"src": "../content-services/",
"dest": "../dist/content-services/",
"lib": {
+ "languageLevel": [ "dom", "es2016" ],
"licensePath": "../config/assets/license_header_add.txt",
"comments" : "none",
"entryFile": "./public-api.ts",
diff --git a/lib/content-services/search/components/search-chip-list/search-chip-list.component.html b/lib/content-services/search/components/search-chip-list/search-chip-list.component.html
new file mode 100644
index 0000000000..030001c329
--- /dev/null
+++ b/lib/content-services/search/components/search-chip-list/search-chip-list.component.html
@@ -0,0 +1,20 @@
+
+
+
+ {{ label }}
+ cancel
+
+
+
+
+ {{ bucket.display || bucket.label }}
+ cancel
+
+
+
diff --git a/lib/content-services/search/components/search-chip-list/search-chip-list.component.ts b/lib/content-services/search/components/search-chip-list/search-chip-list.component.ts
new file mode 100644
index 0000000000..9459dadc74
--- /dev/null
+++ b/lib/content-services/search/components/search-chip-list/search-chip-list.component.ts
@@ -0,0 +1,31 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, ViewEncapsulation, Input } from '@angular/core';
+import { SearchFilterComponent } from '../../components/search-filter/search-filter.component';
+
+@Component({
+ selector: 'adf-search-chip-list',
+ templateUrl: './search-chip-list.component.html',
+ encapsulation: ViewEncapsulation.None,
+ host: { class: 'adf-search-chip-list' }
+})
+export class SearchChipListComponent {
+
+ @Input()
+ searchFilter: SearchFilterComponent;
+}
diff --git a/lib/content-services/search/components/search-control.component.spec.ts b/lib/content-services/search/components/search-control.component.spec.ts
index 509c2d0084..05d395edbc 100644
--- a/lib/content-services/search/components/search-control.component.spec.ts
+++ b/lib/content-services/search/components/search-control.component.spec.ts
@@ -20,7 +20,6 @@ import { async, discardPeriodicTasks, fakeAsync, ComponentFixture, TestBed, tick
import { By } from '@angular/platform-browser';
import { AuthenticationService, SearchService } from '@alfresco/adf-core';
import { ThumbnailService } from '@alfresco/adf-core';
-import { Observable } from 'rxjs/Observable';
import { noResult, results } from '../../mock';
import { SearchControlComponent } from './search-control.component';
import { SearchTriggerDirective } from './search-trigger.directive';
@@ -83,9 +82,9 @@ describe('SearchControlComponent', () => {
}));
it('should emit searchChange when search term input changed', async(() => {
- spyOn(searchService, 'search').and.callFake(() => {
- return Observable.of({ entry: { list: [] } });
- });
+ spyOn(searchService, 'search').and.returnValue(
+ Promise.resolve({ entry: { list: [] } })
+ );
component.searchChange.subscribe(value => {
expect(value).toBe('customSearchTerm');
});
@@ -97,7 +96,7 @@ describe('SearchControlComponent', () => {
it('should update FAYT search when user inputs a valid term', async(() => {
typeWordIntoSearchInput('customSearchTerm');
spyOn(component, 'isSearchBarActive').and.returnValue(true);
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
fixture.detectChanges();
fixture.whenStable().then(() => {
@@ -111,7 +110,7 @@ describe('SearchControlComponent', () => {
it('should NOT update FAYT term when user inputs an empty string as search term ', async(() => {
typeWordIntoSearchInput('');
spyOn(component, 'isSearchBarActive').and.returnValue(true);
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
fixture.detectChanges();
fixture.whenStable().then(() => {
@@ -179,7 +178,7 @@ describe('SearchControlComponent', () => {
});
spyOn(component, 'isSearchBarActive').and.returnValue(true);
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
@@ -199,7 +198,7 @@ describe('SearchControlComponent', () => {
it('should make autocomplete list control visible when search box has focus and there is a search result', (done) => {
spyOn(component, 'isSearchBarActive').and.returnValue(true);
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
fixture.detectChanges();
typeWordIntoSearchInput('TEST');
@@ -214,7 +213,7 @@ describe('SearchControlComponent', () => {
it('should show autocomplete list noe results when search box has focus and there is search result with length 0', async(() => {
spyOn(component, 'isSearchBarActive').and.returnValue(true);
- spyOn(searchService, 'search').and.returnValue(Observable.of(noResult));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(noResult));
fixture.detectChanges();
typeWordIntoSearchInput('NO RES');
@@ -228,7 +227,7 @@ describe('SearchControlComponent', () => {
it('should hide autocomplete list results when the search box loses focus', (done) => {
spyOn(component, 'isSearchBarActive').and.returnValue(true);
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
@@ -249,7 +248,7 @@ describe('SearchControlComponent', () => {
it('should keep autocomplete list control visible when user tabs into results', async(() => {
spyOn(component, 'isSearchBarActive').and.returnValue(true);
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
@@ -269,7 +268,7 @@ describe('SearchControlComponent', () => {
it('should close the autocomplete when user press ESCAPE', (done) => {
spyOn(component, 'isSearchBarActive').and.returnValue(true);
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
@@ -293,7 +292,7 @@ describe('SearchControlComponent', () => {
it('should close the autocomplete when user press ENTER on input', (done) => {
spyOn(component, 'isSearchBarActive').and.returnValue(true);
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
@@ -317,7 +316,7 @@ describe('SearchControlComponent', () => {
it('should focus input element when autocomplete list is cancelled', async(() => {
spyOn(component, 'isSearchBarActive').and.returnValue(true);
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
@@ -333,7 +332,7 @@ describe('SearchControlComponent', () => {
}));
it('should NOT display a autocomplete list control when configured not to', async(() => {
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
component.liveSearchEnabled = false;
fixture.detectChanges();
@@ -345,7 +344,7 @@ describe('SearchControlComponent', () => {
}));
it('should select the first item on autocomplete list when ARROW DOWN is pressed on input', async(() => {
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
fixture.detectChanges();
typeWordIntoSearchInput('TEST');
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
@@ -361,7 +360,7 @@ describe('SearchControlComponent', () => {
}));
it('should select the second item on autocomplete list when ARROW DOWN is pressed on list', async(() => {
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
typeWordIntoSearchInput('TEST');
@@ -382,7 +381,7 @@ describe('SearchControlComponent', () => {
}));
it('should focus the input search when ARROW UP is pressed on the first list item', (done) => {
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
fixture.detectChanges();
let inputDebugElement = debugElement.query(By.css('#adf-control-input'));
typeWordIntoSearchInput('TEST');
@@ -494,7 +493,7 @@ describe('SearchControlComponent', () => {
it('should emit a option clicked event when item is clicked', async(() => {
spyOn(component, 'isSearchBarActive').and.returnValue(true);
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
component.optionClicked.subscribe((item) => {
expect(item.entry.id).toBe('123');
});
@@ -510,7 +509,7 @@ describe('SearchControlComponent', () => {
it('should set deactivate the search after element is clicked', (done) => {
spyOn(component, 'isSearchBarActive').and.returnValue(true);
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
component.optionClicked.subscribe((item) => {
window.setTimeout(() => {
expect(component.subscriptAnimationState).toBe('inactive');
@@ -530,7 +529,7 @@ describe('SearchControlComponent', () => {
it('should NOT reset the search term after element is clicked', async(() => {
spyOn(component, 'isSearchBarActive').and.returnValue(true);
- spyOn(searchService, 'search').and.returnValue(Observable.of(results));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(results));
component.optionClicked.subscribe((item) => {
expect(component.searchTerm).not.toBeFalsy();
expect(component.searchTerm).toBe('TEST');
@@ -585,7 +584,7 @@ describe('SearchControlComponent - No result custom', () => {
it('should display the custom no results when it is configured', async(() => {
const noResultCustomMessage = 'BANDI IS NOTHING';
componentCustom.setCustomMessageForNoResult(noResultCustomMessage);
- spyOn(searchServiceCustom, 'search').and.returnValue(Observable.of(noResult));
+ spyOn(searchServiceCustom, 'search').and.returnValue(Promise.resolve(noResult));
fixtureCustom.detectChanges();
let inputDebugElement = fixtureCustom.debugElement.query(By.css('#adf-control-input'));
diff --git a/lib/content-services/search/components/search-fields/search-fields.component.html b/lib/content-services/search/components/search-fields/search-fields.component.html
new file mode 100644
index 0000000000..8dd4a54b35
--- /dev/null
+++ b/lib/content-services/search/components/search-fields/search-fields.component.html
@@ -0,0 +1,6 @@
+
+ {{ option.name }}
+
diff --git a/lib/content-services/search/components/search-fields/search-fields.component.scss b/lib/content-services/search/components/search-fields/search-fields.component.scss
new file mode 100644
index 0000000000..bb65a956fb
--- /dev/null
+++ b/lib/content-services/search/components/search-fields/search-fields.component.scss
@@ -0,0 +1,8 @@
+.adf-search-fields {
+ display: flex;
+ flex-direction: column;
+
+ .mat-checkbox {
+ margin: 5px;
+ }
+}
diff --git a/lib/content-services/search/components/search-fields/search-fields.component.ts b/lib/content-services/search/components/search-fields/search-fields.component.ts
new file mode 100644
index 0000000000..688c915a96
--- /dev/null
+++ b/lib/content-services/search/components/search-fields/search-fields.component.ts
@@ -0,0 +1,70 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, ViewEncapsulation, OnInit, Input } from '@angular/core';
+import { MatCheckboxChange } from '@angular/material';
+
+import { SearchWidget } from '../../search-widget.interface';
+import { SearchWidgetSettings } from '../../search-widget-settings.interface';
+import { SearchQueryBuilderService } from '../../search-query-builder.service';
+
+@Component({
+ selector: 'adf-search-fields',
+ templateUrl: './search-fields.component.html',
+ styleUrls: ['./search-fields.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+ host: { class: 'adf-search-fields' }
+})
+export class SearchFieldsComponent implements SearchWidget, OnInit {
+
+ @Input()
+ value: string;
+
+ id: string;
+ settings: SearchWidgetSettings;
+ context: SearchQueryBuilderService;
+
+ ngOnInit() {
+ const defaultOptions = (this.settings.options || [])
+ .filter(opt => opt.default)
+ .map(opt => {
+ opt.checked = true;
+ return opt;
+ });
+
+ if (defaultOptions.length > 0) {
+ this.flush(defaultOptions);
+ }
+ }
+
+ changeHandler(event: MatCheckboxChange, option: any) {
+ option.checked = event.checked;
+ this.flush(this.settings.options);
+ }
+
+ flush(opts: any[] = []) {
+ const checkedValues = opts
+ .filter(v => v.checked)
+ .map(v => v.fields)
+ .reduce((prev, curr) => {
+ return prev.concat(curr);
+ }, []);
+
+ this.context.fields[this.id] = checkedValues;
+ this.context.update();
+ }
+}
diff --git a/lib/content-services/search/components/search-filter/search-filter.component.html b/lib/content-services/search/components/search-filter/search-filter.component.html
new file mode 100644
index 0000000000..159329e01c
--- /dev/null
+++ b/lib/content-services/search/components/search-filter/search-filter.component.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+ {{ category.name | translate }}
+
+
+
+
+
+
+
+
+ Facet Queries
+
+
+
+ 0"
+ [checked]="query.$checked"
+ (change)="onFacetQueryToggle($event, query)">
+ {{ query.label }} ({{ query.count }})
+
+
+
+
+
+
+
+ {{ field.label }}
+
+
+
+ {{ bucket.display || bucket.label }} ({{ bucket.count }})
+
+
+
+
+
diff --git a/lib/content-services/search/components/search-filter/search-filter.component.scss b/lib/content-services/search/components/search-filter/search-filter.component.scss
new file mode 100644
index 0000000000..a7928425ad
--- /dev/null
+++ b/lib/content-services/search/components/search-filter/search-filter.component.scss
@@ -0,0 +1,8 @@
+.checklist {
+ display: flex;
+ flex-direction: column;
+
+ .mat-checkbox {
+ margin: 5px;
+ }
+}
diff --git a/lib/content-services/search/components/search-filter/search-filter.component.spec.ts b/lib/content-services/search/components/search-filter/search-filter.component.spec.ts
new file mode 100644
index 0000000000..f1a914dfb4
--- /dev/null
+++ b/lib/content-services/search/components/search-filter/search-filter.component.spec.ts
@@ -0,0 +1,337 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { SearchFilterComponent } from './search-filter.component';
+import { SearchQueryBuilderService } from '../../search-query-builder.service';
+import { SearchConfiguration } from '../../search-configuration.interface';
+import { AppConfigService } from '@alfresco/adf-core';
+import { Subject } from 'rxjs/Subject';
+
+describe('SearchSettingsComponent', () => {
+
+ let component: SearchFilterComponent;
+ let queryBuilder: SearchQueryBuilderService;
+ let appConfig: AppConfigService;
+
+ beforeEach(() => {
+ appConfig = new AppConfigService(null);
+ appConfig.config.search = {};
+
+ queryBuilder = new SearchQueryBuilderService(appConfig, null);
+ const searchMock: any = {
+ dataLoaded: new Subject()
+ };
+ component = new SearchFilterComponent(queryBuilder, searchMock);
+ component.ngOnInit();
+ });
+
+ it('should subscribe to query builder executed event', () => {
+ spyOn(component, 'onDataLoaded').and.stub();
+ const data = {};
+ queryBuilder.executed.next(data);
+
+ expect(component.onDataLoaded).toHaveBeenCalledWith(data);
+ });
+
+ it('should update category model on expand', () => {
+ const category: any = { expanded: false };
+
+ component.onCategoryExpanded(category);
+
+ expect(category.expanded).toBeTruthy();
+ });
+
+ it('should update category model on collapse', () => {
+ const category: any = { expanded: true };
+
+ component.onCategoryCollapsed(category);
+
+ expect(category.expanded).toBeFalsy();
+ });
+
+ it('should update facet field model on expand', () => {
+ const field: any = { $expanded: false };
+
+ component.onFacetFieldExpanded(field);
+
+ expect(field.$expanded).toBeTruthy();
+ });
+
+ it('should update facet field model on collapse', () => {
+ const field: any = { $expanded: true };
+
+ component.onFacetFieldCollapsed(field);
+
+ expect(field.$expanded).toBeFalsy();
+ });
+
+ it('should update bucket model and query builder on facet toggle', () => {
+ spyOn(queryBuilder, 'update').and.stub();
+
+ const event: any = { checked: true };
+ const field: any = {};
+ const bucket: any = { $checked: false, filterQuery: 'q1' };
+
+ component.onFacetToggle(event, field, bucket);
+
+ expect(component.selectedBuckets.length).toBe(1);
+ expect(component.selectedBuckets[0]).toEqual(bucket);
+
+ expect(queryBuilder.filterQueries.length).toBe(1);
+ expect(queryBuilder.filterQueries[0].query).toBe('q1');
+
+ expect(queryBuilder.update).toHaveBeenCalled();
+ });
+
+ it('should update bucket model and query builder on facet un-toggle', () => {
+ spyOn(queryBuilder, 'update').and.stub();
+
+ const event: any = { checked: false };
+ const field: any = { label: 'f1' };
+ const bucket: any = { $checked: true, filterQuery: 'q1', $field: 'f1', label: 'b1' };
+
+ component.selectedBuckets.push(bucket);
+ queryBuilder.addFilterQuery(bucket.filterQuery);
+
+ component.onFacetToggle(event, field, bucket);
+
+ expect(bucket.$checked).toBeFalsy();
+ expect(component.selectedBuckets.length).toBe(0);
+ expect(queryBuilder.filterQueries.length).toBe(0);
+
+ expect(queryBuilder.update).toHaveBeenCalled();
+ });
+
+ it('should unselect facet query and update builder', () => {
+ const config: SearchConfiguration = {
+ facetQueries: [
+ { label: 'q1', query: 'query1' }
+ ]
+ };
+ appConfig.config.search = config;
+ queryBuilder = new SearchQueryBuilderService(appConfig, null);
+ component = new SearchFilterComponent(queryBuilder, null);
+
+ spyOn(queryBuilder, 'update').and.stub();
+ queryBuilder.filterQueries = [{ query: 'query1' }];
+ component.selectedFacetQueries = ['q1'];
+
+ component.unselectFacetQuery('q1');
+
+ expect(component.selectedFacetQueries.length).toBe(0);
+ expect(queryBuilder.filterQueries.length).toBe(0);
+
+ expect(queryBuilder.update).toHaveBeenCalled();
+ });
+
+ it('should unselect facet bucket and update builder', () => {
+ spyOn(queryBuilder, 'update').and.stub();
+
+ const bucket: any = { $checked: true, filterQuery: 'q1', $field: 'f1', label: 'b1' };
+ component.selectedBuckets.push(bucket);
+ queryBuilder.filterQueries.push({ query: 'q1' });
+
+ component.unselectFacetBucket(bucket);
+
+ expect(component.selectedBuckets.length).toBe(0);
+ expect(queryBuilder.filterQueries.length).toBe(0);
+
+ expect(queryBuilder.update).toHaveBeenCalled();
+ });
+
+ it('should fetch facet queries from response payload', () => {
+ component.responseFacetQueries = [];
+ const queries = [
+ { label: 'q1', query: 'query1' },
+ { label: 'q2', query: 'query2' }
+ ];
+ const data = {
+ list: {
+ context: {
+ facetQueries: queries
+ }
+ }
+ };
+
+ component.onDataLoaded(data);
+
+ expect(component.responseFacetQueries.length).toBe(2);
+ expect(component.responseFacetQueries).toEqual(queries);
+ });
+
+ it('should not fetch facet queries from response payload', () => {
+ component.responseFacetQueries = [];
+
+ const data = {
+ list: {
+ context: {
+ facetQueries: null
+ }
+ }
+ };
+
+ component.onDataLoaded(data);
+
+ expect(component.responseFacetQueries.length).toBe(0);
+ });
+
+ it('should restore checked state for new response facet queries', () => {
+ component.selectedFacetQueries = ['q3'];
+ component.responseFacetQueries = [];
+
+ const queries = [
+ { label: 'q1', query: 'query1' },
+ { label: 'q2', query: 'query2' }
+ ];
+ const data = {
+ list: {
+ context: {
+ facetQueries: queries
+ }
+ }
+ };
+
+ component.onDataLoaded(data);
+
+ expect(component.responseFacetQueries.length).toBe(2);
+ expect(( component.responseFacetQueries[0]).$checked).toBeFalsy();
+ expect(( component.responseFacetQueries[1]).$checked).toBeFalsy();
+ });
+
+ it('should not restore checked state for new response facet queries', () => {
+ component.selectedFacetQueries = ['q2'];
+ component.responseFacetQueries = [];
+
+ const queries = [
+ { label: 'q1', query: 'query1' },
+ { label: 'q2', query: 'query2' }
+ ];
+ const data = {
+ list: {
+ context: {
+ facetQueries: queries
+ }
+ }
+ };
+
+ component.onDataLoaded(data);
+
+ expect(component.responseFacetQueries.length).toBe(2);
+ expect(( component.responseFacetQueries[0]).$checked).toBeFalsy();
+ expect(( component.responseFacetQueries[1]).$checked).toBeTruthy();
+ });
+
+ it('should fetch facet fields from response payload', () => {
+ component.responseFacetFields = [];
+
+ const fields = [
+ { label: 'f1', buckets: [] },
+ { label: 'f2', buckets: [] }
+ ];
+ const data = {
+ list: {
+ context: {
+ facetsFields: fields
+ }
+ }
+ };
+
+ component.onDataLoaded(data);
+
+ expect(component.responseFacetFields).toEqual(fields);
+ });
+
+ it('should restore expanded state for new response facet fields', () => {
+ component.responseFacetFields = [
+ { label: 'f1', buckets: [] },
+ { label: 'f2', buckets: [], $expanded: true }
+ ];
+
+ const fields = [
+ { label: 'f1', buckets: [] },
+ { label: 'f2', buckets: [] }
+ ];
+ const data = {
+ list: {
+ context: {
+ facetsFields: fields
+ }
+ }
+ };
+
+ component.onDataLoaded(data);
+
+ expect(component.responseFacetFields.length).toBe(2);
+ expect(component.responseFacetFields[0].$expanded).toBeFalsy();
+ expect(component.responseFacetFields[1].$expanded).toBeTruthy();
+ });
+
+ it('should restore checked buckets for new response facet fields', () => {
+ const bucket1 = { label: 'b1', $field: 'f1', count: 1, filterQuery: 'q1' };
+ const bucket2 = { label: 'b2', $field: 'f2', count: 1, filterQuery: 'q2' };
+
+ component.selectedBuckets = [ bucket2 ];
+ component.responseFacetFields = [
+ { label: 'f2', buckets: [] }
+ ];
+
+ const data = {
+ list: {
+ context: {
+ facetsFields: [
+ { label: 'f1', buckets: [ bucket1 ] },
+ { label: 'f2', buckets: [ bucket2 ] }
+ ]
+ }
+ }
+ };
+
+ component.onDataLoaded(data);
+
+ expect(component.responseFacetFields.length).toBe(2);
+ expect(component.responseFacetFields[0].buckets[0].$checked).toBeFalsy();
+ expect(component.responseFacetFields[1].buckets[0].$checked).toBeTruthy();
+ });
+
+ it('should reset queries and fields on empty response payload', () => {
+ component.responseFacetQueries = [ {}, {}];
+ component.responseFacetFields = [ {}, {}];
+
+ const data = {
+ list: {
+ context: {
+ facetQueries: null,
+ facetsFields: null
+ }
+ }
+ };
+
+ component.onDataLoaded(data);
+
+ expect(component.responseFacetQueries.length).toBe(0);
+ expect(component.responseFacetFields.length).toBe(0);
+ });
+
+ it('should update query builder only when has bucket to unselect', () => {
+ spyOn(queryBuilder, 'update').and.stub();
+
+ component.unselectFacetBucket(null);
+
+ expect(queryBuilder.update).not.toHaveBeenCalled();
+ });
+
+});
diff --git a/lib/content-services/search/components/search-filter/search-filter.component.ts b/lib/content-services/search/components/search-filter/search-filter.component.ts
new file mode 100644
index 0000000000..2da95f2867
--- /dev/null
+++ b/lib/content-services/search/components/search-filter/search-filter.component.ts
@@ -0,0 +1,172 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, ViewEncapsulation, OnInit } from '@angular/core';
+import { MatCheckboxChange } from '@angular/material';
+import { SearchService } from '@alfresco/adf-core';
+import { SearchQueryBuilderService } from '../../search-query-builder.service';
+import { FacetQuery } from '../../facet-query.interface';
+import { ResponseFacetField } from '../../response-facet-field.interface';
+import { FacetFieldBucket } from '../../facet-field-bucket.interface';
+import { SearchCategory } from '../../search-category.interface';
+import { ResponseFacetQuery } from '../../response-facet-query.interface';
+
+@Component({
+ selector: 'adf-search-filter',
+ templateUrl: './search-filter.component.html',
+ styleUrls: ['./search-filter.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+ host: { class: 'adf-search-filter' }
+})
+export class SearchFilterComponent implements OnInit {
+
+ selectedFacetQueries: string[] = [];
+ selectedBuckets: FacetFieldBucket[] = [];
+ responseFacetQueries: FacetQuery[] = [];
+ responseFacetFields: ResponseFacetField[] = [];
+
+ constructor(private queryBuilder: SearchQueryBuilderService, private search: SearchService) {
+ this.queryBuilder.updated.subscribe(query => {
+ this.queryBuilder.execute();
+ });
+ }
+
+ ngOnInit() {
+ if (this.queryBuilder) {
+ this.queryBuilder.executed.subscribe(data => {
+ this.onDataLoaded(data);
+ this.search.dataLoaded.next(data);
+ });
+ }
+ }
+
+ onCategoryExpanded(category: SearchCategory) {
+ category.expanded = true;
+ }
+
+ onCategoryCollapsed(category: SearchCategory) {
+ category.expanded = false;
+ }
+
+ onFacetFieldExpanded(field: ResponseFacetField) {
+ field.$expanded = true;
+ }
+
+ onFacetFieldCollapsed(field: ResponseFacetField) {
+ field.$expanded = false;
+ }
+
+ onFacetQueryToggle(event: MatCheckboxChange, query: ResponseFacetQuery) {
+ const facetQuery = this.queryBuilder.getFacetQuery(query.label);
+
+ if (event.checked) {
+ query.$checked = true;
+ this.selectedFacetQueries.push(facetQuery.label);
+
+ if (facetQuery) {
+ this.queryBuilder.addFilterQuery(facetQuery.query);
+ }
+ } else {
+ query.$checked = false;
+ this.selectedFacetQueries = this.selectedFacetQueries.filter(q => q !== query.label);
+
+ if (facetQuery) {
+ this.queryBuilder.removeFilterQuery(facetQuery.query);
+ }
+ }
+
+ this.queryBuilder.update();
+ }
+
+ onFacetToggle(event: MatCheckboxChange, field: ResponseFacetField, bucket: FacetFieldBucket) {
+ if (event.checked) {
+ bucket.$checked = true;
+ this.selectedBuckets.push({ ...bucket });
+ this.queryBuilder.addFilterQuery(bucket.filterQuery);
+ } else {
+ bucket.$checked = false;
+ const idx = this.selectedBuckets.findIndex(
+ b => b.$field === bucket.$field && b.label === bucket.label
+ );
+
+ if (idx >= 0) {
+ this.selectedBuckets.splice(idx, 1);
+ }
+ this.queryBuilder.removeFilterQuery(bucket.filterQuery);
+ }
+
+ this.queryBuilder.update();
+ }
+
+ unselectFacetQuery(label: string) {
+ const facetQuery = this.queryBuilder.getFacetQuery(label);
+ this.selectedFacetQueries = this.selectedFacetQueries.filter(q => q !== label);
+
+ this.queryBuilder.removeFilterQuery(facetQuery.query);
+ this.queryBuilder.update();
+ }
+
+ unselectFacetBucket(bucket: FacetFieldBucket) {
+ if (bucket) {
+ const idx = this.selectedBuckets.findIndex(
+ b => b.$field === bucket.$field && b.label === bucket.label
+ );
+
+ if (idx >= 0) {
+ this.selectedBuckets.splice(idx, 1);
+ }
+ this.queryBuilder.removeFilterQuery(bucket.filterQuery);
+ this.queryBuilder.update();
+ }
+ }
+
+ onDataLoaded(data: any) {
+ const context = data.list.context;
+
+ if (context) {
+ this.responseFacetQueries = (context.facetQueries || []).map(q => {
+ q.$checked = this.selectedFacetQueries.includes(q.label);
+ return q;
+ });
+
+ const expandedFields = this.responseFacetFields.filter(f => f.$expanded).map(f => f.label);
+
+ this.responseFacetFields = (context.facetsFields || []).map(
+ (field: ResponseFacetField) => {
+ field.$expanded = expandedFields.includes(field.label);
+
+ (field.buckets || []).forEach(bucket => {
+ bucket.$field = field.label;
+ bucket.$checked = false;
+
+ const previousBucket = this.selectedBuckets.find(
+ b => b.$field === bucket.$field && b.label === bucket.label
+ );
+ if (previousBucket) {
+ bucket.$checked = true;
+ }
+ });
+ return field;
+ }
+ );
+ } else {
+ this.responseFacetQueries = [];
+ this.responseFacetFields = [];
+ }
+ }
+
+}
diff --git a/lib/content-services/search/components/search-radio/search-radio.component.html b/lib/content-services/search/components/search-radio/search-radio.component.html
new file mode 100644
index 0000000000..35ef0e3b7b
--- /dev/null
+++ b/lib/content-services/search/components/search-radio/search-radio.component.html
@@ -0,0 +1,6 @@
+
+
+ {{ option.name }}
+
+
diff --git a/lib/content-services/search/components/search-radio/search-radio.component.scss b/lib/content-services/search/components/search-radio/search-radio.component.scss
new file mode 100644
index 0000000000..4229945846
--- /dev/null
+++ b/lib/content-services/search/components/search-radio/search-radio.component.scss
@@ -0,0 +1,10 @@
+.adf-search-radio {
+ .mat-radio-group {
+ display: inline-flex;
+ flex-direction: column;
+ }
+
+ .mat-radio-button {
+ margin: 5px;
+ }
+}
diff --git a/lib/content-services/search/components/search-radio/search-radio.component.ts b/lib/content-services/search/components/search-radio/search-radio.component.ts
new file mode 100644
index 0000000000..bfa64b393d
--- /dev/null
+++ b/lib/content-services/search/components/search-radio/search-radio.component.ts
@@ -0,0 +1,68 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, ViewEncapsulation, OnInit, Input } from '@angular/core';
+import { MatRadioChange } from '@angular/material';
+
+import { SearchWidget } from '../../search-widget.interface';
+import { SearchWidgetSettings } from '../../search-widget-settings.interface';
+import { SearchQueryBuilderService } from '../../search-query-builder.service';
+
+@Component({
+ selector: 'adf-search-radio',
+ templateUrl: './search-radio.component.html',
+ styleUrls: ['./search-radio.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+ host: { class: 'adf-search-radio' }
+})
+export class SearchRadioComponent implements SearchWidget, OnInit {
+
+ @Input()
+ value: string;
+
+ id: string;
+ settings: SearchWidgetSettings;
+ context: SearchQueryBuilderService;
+
+ ngOnInit() {
+ this.setValue(
+ this.getSelectedValue()
+ );
+ }
+
+ private getSelectedValue(): string {
+ const options: any[] = this.settings['options'] || [];
+ if (options && options.length > 0) {
+ let selected = options.find(opt => opt.default);
+ if (!selected) {
+ selected = options[0];
+ }
+ return selected.value;
+ }
+ return null;
+ }
+
+ private setValue(newValue: string) {
+ this.value = newValue;
+ this.context.queryFragments[this.id] = newValue;
+ this.context.update();
+ }
+
+ changeHandler(event: MatRadioChange) {
+ this.setValue(event.value);
+ }
+}
diff --git a/lib/content-services/search/components/search-scope-locations/search-scope-locations.component.html b/lib/content-services/search/components/search-scope-locations/search-scope-locations.component.html
new file mode 100644
index 0000000000..17cfe5077f
--- /dev/null
+++ b/lib/content-services/search/components/search-scope-locations/search-scope-locations.component.html
@@ -0,0 +1,11 @@
+
+
+
+ {{option.name}}
+
+
+
diff --git a/lib/content-services/search/components/search-scope-locations/search-scope-locations.component.ts b/lib/content-services/search/components/search-scope-locations/search-scope-locations.component.ts
new file mode 100644
index 0000000000..06051ce4ec
--- /dev/null
+++ b/lib/content-services/search/components/search-scope-locations/search-scope-locations.component.ts
@@ -0,0 +1,57 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, ViewEncapsulation, OnInit, Input } from '@angular/core';
+import { MatSelectChange } from '@angular/material';
+
+import { SearchWidget } from '../../search-widget.interface';
+import { SearchWidgetSettings } from '../../search-widget-settings.interface';
+import { SearchQueryBuilderService } from '../../search-query-builder.service';
+
+@Component({
+ selector: 'adf-search-scope-locations',
+ templateUrl: './search-scope-locations.component.html',
+ encapsulation: ViewEncapsulation.None,
+ host: { class: 'adf-search-scope-locations' }
+})
+export class SearchScopeLocationsComponent implements SearchWidget, OnInit {
+
+ @Input()
+ value: string;
+
+ id: string;
+ settings: SearchWidgetSettings;
+ context: SearchQueryBuilderService;
+
+ ngOnInit() {
+
+ const defaultSelection = (this.settings.options || []).find(opt => opt.default);
+ if (defaultSelection) {
+ this.flush(defaultSelection.value);
+ }
+ }
+
+ changeHandler(event: MatSelectChange) {
+ this.flush(event.value);
+ }
+
+ flush(value: string) {
+ this.value = value;
+ this.context.scope.locations = value;
+ this.context.update();
+ }
+}
diff --git a/lib/content-services/search/components/search-text/search-text.component.html b/lib/content-services/search/components/search-text/search-text.component.html
new file mode 100644
index 0000000000..4b4c1049c2
--- /dev/null
+++ b/lib/content-services/search/components/search-text/search-text.component.html
@@ -0,0 +1,7 @@
+
+
+
diff --git a/lib/content-services/search/components/search-text/search-text.component.ts b/lib/content-services/search/components/search-text/search-text.component.ts
new file mode 100644
index 0000000000..985928a8fa
--- /dev/null
+++ b/lib/content-services/search/components/search-text/search-text.component.ts
@@ -0,0 +1,57 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, ViewEncapsulation, OnInit, Input } from '@angular/core';
+import { SearchWidget } from '../../search-widget.interface';
+import { SearchWidgetSettings } from '../../search-widget-settings.interface';
+import { SearchQueryBuilderService } from '../../search-query-builder.service';
+
+@Component({
+ selector: 'adf-search-text',
+ templateUrl: './search-text.component.html',
+ encapsulation: ViewEncapsulation.None,
+ host: { class: 'adf-search-text' }
+})
+export class SearchTextComponent implements SearchWidget, OnInit {
+
+ @Input()
+ value = '';
+
+ id: string;
+ settings: SearchWidgetSettings;
+ context: SearchQueryBuilderService;
+
+ ngOnInit() {
+ if (this.context && this.settings) {
+ const pattern = new RegExp(this.settings.pattern, 'g');
+ const match = pattern.exec(this.context.queryFragments[this.id] || '');
+
+ if (match && match.length > 1) {
+ this.value = match[1];
+ }
+ }
+ }
+
+ onChangedHandler(event) {
+ this.value = event.target.value;
+ if (this.value) {
+ this.context.queryFragments[this.id] = `${this.settings.field}:'${this.value}'`;
+ this.context.update();
+ }
+ }
+
+}
diff --git a/lib/content-services/search/components/search-widget-container/search-widget-container.component.ts b/lib/content-services/search/components/search-widget-container/search-widget-container.component.ts
new file mode 100644
index 0000000000..b337fe9783
--- /dev/null
+++ b/lib/content-services/search/components/search-widget-container/search-widget-container.component.ts
@@ -0,0 +1,74 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, Input, ViewChild, ViewContainerRef, OnInit, OnDestroy, Compiler, ModuleWithComponentFactories, ComponentRef } from '@angular/core';
+import { SearchWidgetsModule } from './search-widgets.module';
+import { SearchQueryBuilderService } from '../../search-query-builder.service';
+
+@Component({
+ selector: 'adf-search-widget-container',
+ template: ''
+})
+export class SearchWidgetContainerComponent implements OnInit, OnDestroy {
+
+ @ViewChild('content', { read: ViewContainerRef })
+ content: ViewContainerRef;
+
+ @Input()
+ id: string;
+
+ @Input()
+ selector: string;
+
+ @Input()
+ settings: any;
+
+ @Input()
+ config: any;
+
+ private module: ModuleWithComponentFactories;
+ private componentRef: ComponentRef;
+
+ constructor(compiler: Compiler, private queryBuilder: SearchQueryBuilderService) {
+ this.module = compiler.compileModuleAndAllComponentsSync(SearchWidgetsModule);
+ }
+
+ ngOnInit() {
+ const factory = this.module.componentFactories.find(f => f.selector === this.selector);
+ if (factory) {
+ this.content.clear();
+ this.componentRef = this.content.createComponent(factory, 0);
+ this.setupWidget(this.componentRef);
+ }
+ }
+
+ private setupWidget(ref: ComponentRef) {
+ if (ref && ref.instance) {
+ ref.instance.id = this.id;
+ ref.instance.settings = { ...this.settings };
+ ref.instance.context = this.queryBuilder;
+ }
+ }
+
+ ngOnDestroy() {
+ if (this.componentRef) {
+ this.componentRef.destroy();
+ this.componentRef = null;
+ }
+ }
+
+}
diff --git a/lib/content-services/search/components/search-widget-container/search-widgets.module.ts b/lib/content-services/search/components/search-widget-container/search-widgets.module.ts
new file mode 100644
index 0000000000..c7d322442d
--- /dev/null
+++ b/lib/content-services/search/components/search-widget-container/search-widgets.module.ts
@@ -0,0 +1,58 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { MatButtonModule, MatInputModule, MatRadioModule, MatCheckboxModule, MatSelectModule } from '@angular/material';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { SearchTextComponent } from '../search-text/search-text.component';
+import { SearchRadioComponent } from '../search-radio/search-radio.component';
+import { SearchFieldsComponent } from '../search-fields/search-fields.component';
+import { SearchScopeLocationsComponent } from '../search-scope-locations/search-scope-locations.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ MatButtonModule,
+ MatInputModule,
+ MatRadioModule,
+ MatCheckboxModule,
+ MatSelectModule
+ ],
+ declarations: [
+ SearchTextComponent,
+ SearchRadioComponent,
+ SearchFieldsComponent,
+ SearchScopeLocationsComponent
+ ],
+ exports: [
+ SearchTextComponent,
+ SearchRadioComponent,
+ SearchFieldsComponent,
+ SearchScopeLocationsComponent
+ ],
+ entryComponents: [
+ SearchTextComponent,
+ SearchRadioComponent,
+ SearchFieldsComponent,
+ SearchScopeLocationsComponent
+ ]
+})
+export class SearchWidgetsModule {
+}
diff --git a/lib/content-services/search/components/search.component.spec.ts b/lib/content-services/search/components/search.component.spec.ts
index 1671b2537b..fefabbf62c 100644
--- a/lib/content-services/search/components/search.component.spec.ts
+++ b/lib/content-services/search/components/search.component.spec.ts
@@ -18,19 +18,18 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchService } from '@alfresco/adf-core';
import { QueryBody } from 'alfresco-js-api';
-import { Observable } from 'rxjs/Observable';
import { SearchModule } from '../../index';
import { differentResult, folderResult, result, SimpleSearchTestComponent } from '../../mock';
-function fakeNodeResultSearch(searchNode: QueryBody): Observable {
+function fakeNodeResultSearch(searchNode: QueryBody): Promise {
if (searchNode && searchNode.query.query === 'FAKE_SEARCH_EXMPL') {
- return Observable.of(differentResult);
+ return Promise.resolve(differentResult);
}
if (searchNode && searchNode.filterQueries.length === 1 &&
searchNode.filterQueries[0].query === "TYPE:'cm:folder'") {
- return Observable.of(folderResult);
+ return Promise.resolve(folderResult);
}
- return Observable.of(result);
+ return Promise.resolve(result);
}
describe('SearchComponent', () => {
@@ -60,8 +59,10 @@ describe('SearchComponent', () => {
});
it('should clear results straight away when a new search term is entered', (done) => {
- spyOn(searchService, 'search')
- .and.returnValues(Observable.of(result), Observable.of(differentResult));
+ spyOn(searchService, 'search').and.returnValues(
+ Promise.resolve(result),
+ Promise.resolve(differentResult)
+ );
component.setSearchWordTo('searchTerm');
fixture.detectChanges();
@@ -82,7 +83,7 @@ describe('SearchComponent', () => {
it('should display the returned search results', (done) => {
spyOn(searchService, 'search')
- .and.returnValue(Observable.of(result));
+ .and.returnValue(Promise.resolve(result));
component.setSearchWordTo('searchTerm');
fixture.detectChanges();
@@ -96,7 +97,7 @@ describe('SearchComponent', () => {
it('should emit error event when search call fail', (done) => {
spyOn(searchService, 'search')
- .and.returnValue(Observable.fromPromise(Promise.reject({ status: 402 })));
+ .and.returnValue(Promise.reject({ status: 402 }));
component.setSearchWordTo('searchTerm');
fixture.detectChanges();
fixture.whenStable().then(() => {
@@ -108,8 +109,10 @@ describe('SearchComponent', () => {
});
it('should be able to hide the result panel', (done) => {
- spyOn(searchService, 'search')
- .and.returnValues(Observable.of(result), Observable.of(differentResult));
+ spyOn(searchService, 'search').and.returnValues(
+ Promise.resolve(result),
+ Promise.resolve(differentResult)
+ );
component.setSearchWordTo('searchTerm');
fixture.detectChanges();
@@ -160,8 +163,7 @@ describe('SearchComponent', () => {
});
it('should perform a search with a defaultNode if no searchnode is given', (done) => {
- spyOn(searchService, 'search')
- .and.returnValue(Observable.of(result));
+ spyOn(searchService, 'search').and.returnValue(Promise.resolve(result));
component.setSearchWordTo('searchTerm');
fixture.detectChanges();
fixture.whenStable().then(() => {
diff --git a/lib/content-services/search/components/search.component.ts b/lib/content-services/search/components/search.component.ts
index e7d210d3f6..701a304fbb 100644
--- a/lib/content-services/search/components/search.component.ts
+++ b/lib/content-services/search/components/search.component.ts
@@ -115,6 +115,10 @@ export class SearchComponent implements AfterContentInit, OnChanges {
this.loadSearchResults(searchedWord);
});
+ searchService.dataLoaded.subscribe(
+ data => this.onSearchDataLoaded(data),
+ error => this.onSearchDataError(error)
+ );
}
ngAfterContentInit() {
@@ -153,31 +157,38 @@ export class SearchComponent implements AfterContentInit, OnChanges {
private loadSearchResults(searchTerm?: string) {
this.resetResults();
if (searchTerm) {
- let search$;
if (this.queryBody) {
- search$ = this.searchService.searchByQueryBody(this.queryBody);
+ this.searchService.searchByQueryBody(this.queryBody).then(
+ result => this.onSearchDataLoaded(result),
+ err => this.onSearchDataError(err)
+ );
} else {
- search$ = this.searchService
- .search(searchTerm, this.maxResults, this.skipResults);
+ this.searchService.search(searchTerm, this.maxResults, this.skipResults).then(
+ result => this.onSearchDataLoaded(result),
+ err => this.onSearchDataError(err)
+ );
}
- search$.subscribe(
- results => {
- this.results = results;
- this.resultLoaded.emit(this.results);
- this.isOpen = true;
- this.setVisibility();
- },
- error => {
- if (error.status !== 400) {
- this.results = null;
- this.error.emit(error);
- }
- });
} else {
this.cleanResults();
}
}
+ onSearchDataLoaded(data: NodePaging) {
+ if (data) {
+ this.results = data;
+ this.resultLoaded.emit(this.results);
+ this.isOpen = true;
+ this.setVisibility();
+ }
+ }
+
+ onSearchDataError(error) {
+ if (error && error.status !== 400) {
+ this.results = null;
+ this.error.emit(error);
+ }
+ }
+
hidePanel() {
if (this.isOpen) {
this._classList['adf-search-show'] = false;
diff --git a/lib/content-services/search/facet-field-bucket.interface.ts b/lib/content-services/search/facet-field-bucket.interface.ts
new file mode 100644
index 0000000000..47bcab9fcd
--- /dev/null
+++ b/lib/content-services/search/facet-field-bucket.interface.ts
@@ -0,0 +1,26 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface FacetFieldBucket {
+ count: number;
+ display?: string;
+ label: string;
+ filterQuery: string;
+
+ $checked?: boolean;
+ $field?: string;
+}
diff --git a/lib/content-services/search/facet-field.interface.ts b/lib/content-services/search/facet-field.interface.ts
new file mode 100644
index 0000000000..5edb82408a
--- /dev/null
+++ b/lib/content-services/search/facet-field.interface.ts
@@ -0,0 +1,24 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface FacetField {
+ field: string;
+ label: string;
+ mincount?: number;
+
+ $checked?: boolean;
+}
diff --git a/lib/content-services/search/facet-query.interface.ts b/lib/content-services/search/facet-query.interface.ts
new file mode 100644
index 0000000000..3a83586447
--- /dev/null
+++ b/lib/content-services/search/facet-query.interface.ts
@@ -0,0 +1,21 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface FacetQuery {
+ query: string;
+ label: string;
+}
diff --git a/lib/content-services/search/filter-query.interface.ts b/lib/content-services/search/filter-query.interface.ts
new file mode 100644
index 0000000000..6cd924dca2
--- /dev/null
+++ b/lib/content-services/search/filter-query.interface.ts
@@ -0,0 +1,20 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface FilterQuery {
+ query: string;
+}
diff --git a/lib/content-services/search/public-api.ts b/lib/content-services/search/public-api.ts
index 5c13bfe876..18ad1f7552 100644
--- a/lib/content-services/search/public-api.ts
+++ b/lib/content-services/search/public-api.ts
@@ -15,7 +15,22 @@
* limitations under the License.
*/
+export { FacetFieldBucket } from './facet-field-bucket.interface';
+export { FacetField } from './facet-field.interface';
+export { FacetQuery } from './facet-query.interface';
+export { FilterQuery } from './filter-query.interface';
+export { ResponseFacetField } from './response-facet-field.interface';
+export { ResponseFacetQuery } from './response-facet-query.interface';
+export { SearchCategory } from './search-category.interface';
+export { SearchWidgetSettings } from './search-widget-settings.interface';
+export { SearchWidget } from './search-widget.interface';
+export { SearchConfiguration } from './search-configuration.interface';
+export { SearchQueryBuilderService } from './search-query-builder.service';
+export { SearchRange } from './search-range.interface';
+
export * from './components/search.component';
export * from './components/search-control.component';
export * from './components/search-trigger.directive';
export * from './components/empty-search-result.component';
+export * from './components/search-filter/search-filter.component';
+export * from './components/search-chip-list/search-chip-list.component';
diff --git a/lib/content-services/search/response-facet-field.interface.ts b/lib/content-services/search/response-facet-field.interface.ts
new file mode 100644
index 0000000000..f14bf6d230
--- /dev/null
+++ b/lib/content-services/search/response-facet-field.interface.ts
@@ -0,0 +1,25 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { FacetFieldBucket } from './facet-field-bucket.interface';
+
+export interface ResponseFacetField {
+ label: string;
+ buckets: Array;
+
+ $expanded?: boolean;
+}
diff --git a/lib/content-services/search/response-facet-query.interface.ts b/lib/content-services/search/response-facet-query.interface.ts
new file mode 100644
index 0000000000..2a6df4149f
--- /dev/null
+++ b/lib/content-services/search/response-facet-query.interface.ts
@@ -0,0 +1,23 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface ResponseFacetQuery {
+ label: string;
+ mincount: number;
+
+ $checked?: boolean;
+}
diff --git a/lib/content-services/search/search-category.interface.ts b/lib/content-services/search/search-category.interface.ts
new file mode 100644
index 0000000000..48d050bb95
--- /dev/null
+++ b/lib/content-services/search/search-category.interface.ts
@@ -0,0 +1,29 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { SearchWidgetSettings } from './search-widget-settings.interface';
+
+export interface SearchCategory {
+ id: string;
+ name: string;
+ enabled: boolean;
+ expanded: boolean;
+ component: {
+ selector: string;
+ settings: SearchWidgetSettings;
+ };
+}
diff --git a/lib/content-services/search/search-configuration.interface.ts b/lib/content-services/search/search-configuration.interface.ts
new file mode 100644
index 0000000000..ed6a406bd2
--- /dev/null
+++ b/lib/content-services/search/search-configuration.interface.ts
@@ -0,0 +1,36 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { FilterQuery } from './filter-query.interface';
+import { FacetQuery } from './facet-query.interface';
+import { FacetField } from './facet-field.interface';
+import { SearchCategory } from './search-category.interface';
+
+export interface SearchConfiguration {
+ query?: {
+ categories: Array
+ };
+ limits?: {
+ permissionEvaluationTime?: number;
+ permissionEvaluationCount?: number;
+ };
+ filterQueries?: Array;
+ facetQueries?: Array;
+ facetFields?: {
+ facets: Array
+ };
+}
diff --git a/lib/content-services/search/search-query-builder.service.spec.ts b/lib/content-services/search/search-query-builder.service.spec.ts
new file mode 100644
index 0000000000..bf95d5e1e7
--- /dev/null
+++ b/lib/content-services/search/search-query-builder.service.spec.ts
@@ -0,0 +1,369 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { SearchQueryBuilderService } from './search-query-builder.service';
+import { SearchConfiguration } from './search-configuration.interface';
+import { AppConfigService } from '@alfresco/adf-core';
+
+describe('SearchQueryBuilder', () => {
+
+ const buildConfig = (searchSettings): AppConfigService => {
+ const config = new AppConfigService(null);
+ config.config.search = searchSettings;
+ return config;
+ };
+
+ it('should throw error if configuration not provided', () => {
+ expect(() => {
+ const appConfig = new AppConfigService(null);
+ // tslint:disable-next-line:no-unused-expression
+ new SearchQueryBuilderService(appConfig, null);
+ }).toThrowError('Search configuration not found.');
+ });
+
+ it('should use only enabled categories', () => {
+ const config: SearchConfiguration = {
+ query: {
+ categories: [
+ { id: 'cat1', enabled: true },
+ { id: 'cat2', enabled: false },
+ { id: 'cat3', enabled: true }
+ ]
+ }
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+
+ expect(builder.categories.length).toBe(2);
+ expect(builder.categories[0].id).toBe('cat1');
+ expect(builder.categories[1].id).toBe('cat3');
+ });
+
+ it('should fetch filter queries from config', () => {
+ const config: SearchConfiguration = {
+ query: {
+ categories: []
+ },
+ filterQueries: [
+ { query: 'query1' },
+ { query: 'query2' }
+ ]
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+
+ expect(builder.filterQueries.length).toBe(2);
+ expect(builder.filterQueries[0].query).toBe('query1');
+ expect(builder.filterQueries[1].query).toBe('query2');
+ });
+
+ it('should setup default location scope', () => {
+ const builder = new SearchQueryBuilderService(buildConfig({}), null);
+
+ expect(builder.scope).toBeDefined();
+ expect(builder.scope.locations).toBeNull();
+ });
+
+ it('should add new filter query', () => {
+ const builder = new SearchQueryBuilderService(buildConfig({}), null);
+
+ builder.addFilterQuery('q1');
+
+ expect(builder.filterQueries.length).toBe(1);
+ expect(builder.filterQueries[0].query).toBe('q1');
+ });
+
+ it('should not add empty filter query', () => {
+ const builder = new SearchQueryBuilderService(buildConfig({}), null);
+
+ builder.addFilterQuery(null);
+ builder.addFilterQuery('');
+
+ expect(builder.filterQueries.length).toBe(0);
+ });
+
+ it('should not add duplicate filter query', () => {
+ const builder = new SearchQueryBuilderService(buildConfig({}), null);
+
+ builder.addFilterQuery('q1');
+ builder.addFilterQuery('q1');
+ builder.addFilterQuery('q1');
+
+ expect(builder.filterQueries.length).toBe(1);
+ expect(builder.filterQueries[0].query).toBe('q1');
+ });
+
+ it('should remove filter query', () => {
+ const builder = new SearchQueryBuilderService(buildConfig({}), null);
+
+ builder.addFilterQuery('q1');
+ builder.addFilterQuery('q2');
+ expect(builder.filterQueries.length).toBe(2);
+
+ builder.removeFilterQuery('q1');
+ expect(builder.filterQueries.length).toBe(1);
+ expect(builder.filterQueries[0].query).toBe('q2');
+ });
+
+ it('should not remove empty query', () => {
+ const builder = new SearchQueryBuilderService(buildConfig({}), null);
+ builder.addFilterQuery('q1');
+ builder.addFilterQuery('q2');
+ expect(builder.filterQueries.length).toBe(2);
+
+ builder.removeFilterQuery(null);
+ builder.removeFilterQuery('');
+ expect(builder.filterQueries.length).toBe(2);
+ });
+
+ it('should fetch facet query from config', () => {
+ const config: SearchConfiguration = {
+ facetQueries: [
+ { query: 'q1', label: 'query1' },
+ { query: 'q2', label: 'query2' }
+ ]
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+ const query = builder.getFacetQuery('query2');
+
+ expect(query.query).toBe('q2');
+ expect(query.label).toBe('query2');
+ });
+
+ it('should not fetch empty facet query from the config', () => {
+ const config: SearchConfiguration = {
+ facetQueries: [
+ { query: 'q1', label: 'query1' }
+ ]
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+
+ const query1 = builder.getFacetQuery('');
+ expect(query1).toBeNull();
+
+ const query2 = builder.getFacetQuery(null);
+ expect(query2).toBeNull();
+ });
+
+ it('should build query and raise an event on update', async () => {
+ const builder = new SearchQueryBuilderService(buildConfig({}), null);
+ const query = {};
+ spyOn(builder, 'buildQuery').and.returnValue(query);
+
+ let eventArgs;
+ builder.updated.subscribe(args => eventArgs = args);
+
+ await builder.execute();
+ expect(eventArgs).toBe(query);
+ });
+
+ it('should build query and raise an event on execute', async () => {
+ const data = {};
+ const api = jasmine.createSpyObj('api', ['search']);
+ api.search.and.returnValue(data);
+
+ const builder = new SearchQueryBuilderService(buildConfig({}), api);
+ spyOn(builder, 'buildQuery').and.returnValue({});
+
+ let eventArgs;
+ builder.executed.subscribe(args => eventArgs = args);
+
+ await builder.execute();
+ expect(eventArgs).toBe(data);
+ });
+
+ it('should require a query fragment to build query', () => {
+ const config: SearchConfiguration = {
+ query: {
+ categories: [
+ { id: 'cat1', enabled: true }
+ ]
+ }
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+ builder.queryFragments['cat1'] = null;
+
+ const compiled = builder.buildQuery();
+ expect(compiled).toBeNull();
+ });
+
+ it('should build query with single fragment', () => {
+ const config: SearchConfiguration = {
+ query: {
+ categories: [
+ { id: 'cat1', enabled: true }
+ ]
+ }
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+
+ builder.queryFragments['cat1'] = 'cm:name:test';
+
+ const compiled = builder.buildQuery();
+ expect(compiled.query.query).toBe('(cm:name:test)');
+ });
+
+ it('should build query with multiple fragments', () => {
+ const config: SearchConfiguration = {
+ query: {
+ categories: [
+ { id: 'cat1', enabled: true },
+ { id: 'cat2', enabled: true }
+ ]
+ }
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+
+ builder.queryFragments['cat1'] = 'cm:name:test';
+ builder.queryFragments['cat2'] = 'NOT cm:creator:System';
+
+ const compiled = builder.buildQuery();
+ expect(compiled.query.query).toBe(
+ '(cm:name:test) AND (NOT cm:creator:System)'
+ );
+ });
+
+ it('should build query with custom fields', () => {
+ const config: SearchConfiguration = {
+ query: {
+ categories: [
+ { id: 'cat1', enabled: true },
+ { id: 'cat2', enabled: true }
+ ]
+ }
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+
+ builder.queryFragments['cat1'] = 'cm:name:test';
+ builder.fields['cat1'] = ['field1', 'field3'];
+ builder.fields['cat2'] = ['field2', 'field3'];
+
+ const compiled = builder.buildQuery();
+ expect(compiled.fields).toEqual(['field1', 'field3', 'field2']);
+ });
+
+ it('should build query with empty custom fields', () => {
+ const config: SearchConfiguration = {
+ query: {
+ categories: [
+ { id: 'cat1', enabled: true },
+ { id: 'cat2', enabled: true }
+ ]
+ }
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+
+ builder.queryFragments['cat1'] = 'cm:name:test';
+ builder.fields['cat1'] = [];
+ builder.fields['cat2'] = null;
+
+ const compiled = builder.buildQuery();
+ expect(compiled.fields).toEqual([]);
+ });
+
+ it('should build query with custom filter queries', () => {
+ const config: SearchConfiguration = {
+ query: {
+ categories: [
+ { id: 'cat1', enabled: true }
+ ]
+ }
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+ builder.queryFragments['cat1'] = 'cm:name:test';
+ builder.addFilterQuery('query1');
+
+ const compiled = builder.buildQuery();
+ expect(compiled.filterQueries).toEqual(
+ [{ query: 'query1' }]
+ );
+ });
+
+ it('should build query with custom facet queries', () => {
+ const config: SearchConfiguration = {
+ query: {
+ categories: [
+ { id: 'cat1', enabled: true }
+ ]
+ },
+ facetQueries: [
+ { query: 'q1', label: 'q2' }
+ ]
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+ builder.queryFragments['cat1'] = 'cm:name:test';
+
+ const compiled = builder.buildQuery();
+ expect(compiled.facetQueries).toEqual(config.facetQueries);
+ });
+
+ it('should build query with custom facet fields', () => {
+ const config: SearchConfiguration = {
+ query: {
+ categories: [
+ { id: 'cat1', enabled: true }
+ ]
+ },
+ facetFields: {
+ facets: [
+ { field: 'field1', label: 'field1' },
+ { field: 'field2', label: 'field2' }
+ ]
+ }
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+ builder.queryFragments['cat1'] = 'cm:name:test';
+
+ const compiled = builder.buildQuery();
+ expect(compiled.facetFields).toEqual(config.facetFields);
+ });
+
+ it('should build query with custom limits', () => {
+ const config: SearchConfiguration = {
+ query: {
+ categories: [
+ { id: 'cat1', enabled: true }
+ ]
+ },
+ limits: {
+ permissionEvaluationCount: 100,
+ permissionEvaluationTime: 100
+ }
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+ builder.queryFragments['cat1'] = 'cm:name:test';
+
+ const compiled = builder.buildQuery();
+ expect(compiled.limits).toEqual(config.limits);
+ });
+
+ it('should build query with custom scope', () => {
+ const config: SearchConfiguration = {
+ query: {
+ categories: [
+ { id: 'cat1', enabled: true }
+ ]
+ }
+ };
+ const builder = new SearchQueryBuilderService(buildConfig(config), null);
+ builder.queryFragments['cat1'] = 'cm:name:test';
+ builder.scope.locations = 'custom';
+
+ const compiled = builder.buildQuery();
+ expect(compiled.scope.locations).toEqual('custom');
+
+ });
+
+});
diff --git a/lib/content-services/search/search-query-builder.service.ts b/lib/content-services/search/search-query-builder.service.ts
new file mode 100644
index 0000000000..92a2c3c0ac
--- /dev/null
+++ b/lib/content-services/search/search-query-builder.service.ts
@@ -0,0 +1,143 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Injectable } from '@angular/core';
+import { Subject } from 'rxjs/Subject';
+import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
+import { QueryBody } from 'alfresco-js-api';
+import { SearchCategory } from './search-category.interface';
+import { FilterQuery } from './filter-query.interface';
+import { SearchRange } from './search-range.interface';
+import { SearchConfiguration } from './search-configuration.interface';
+import { FacetQuery } from './facet-query.interface';
+
+@Injectable()
+export class SearchQueryBuilderService {
+
+ updated: Subject = new Subject();
+ executed: Subject = new Subject();
+
+ categories: Array = [];
+ queryFragments: { [id: string]: string } = {};
+ fields: { [id: string]: string[] } = {};
+ scope: { locations?: string };
+ filterQueries: FilterQuery[] = [];
+ ranges: { [id: string]: SearchRange } = {};
+
+ config: SearchConfiguration;
+
+ constructor(appConfig: AppConfigService, private api: AlfrescoApiService) {
+ this.config = appConfig.get('search');
+ if (!this.config) {
+ throw new Error('Search configuration not found.');
+ }
+
+ if (this.config.query && this.config.query.categories) {
+ this.categories = this.config.query.categories.filter(f => f.enabled);
+ }
+
+ this.filterQueries = this.config.filterQueries || [];
+ this.scope = {
+ locations: null
+ };
+ }
+
+ addFilterQuery(query: string): void {
+ if (query) {
+ const existing = this.filterQueries.find(q => q.query === query);
+ if (!existing) {
+ this.filterQueries.push({ query: query });
+ }
+ }
+ }
+
+ removeFilterQuery(query: string): void {
+ if (query) {
+ this.filterQueries = this.filterQueries.filter(f => f.query !== query);
+ }
+ }
+
+ getFacetQuery(label: string): FacetQuery {
+ if (label) {
+ const queries = this.config.facetQueries || [];
+ return queries.find(q => q.label === label);
+ }
+ return null;
+ }
+
+ update(): void {
+ const query = this.buildQuery();
+ this.updated.next(query);
+ }
+
+ async execute() {
+ const query = this.buildQuery();
+ const data = await this.api.searchApi.search(query);
+ this.executed.next(data);
+ }
+
+ buildQuery(): QueryBody {
+ let query = '';
+ const fields: string[] = [];
+
+ this.categories.forEach(facet => {
+ const customQuery = this.queryFragments[facet.id];
+ if (customQuery) {
+ if (query.length > 0) {
+ query += ' AND ';
+ }
+ query += `(${customQuery})`;
+ }
+
+ const customFields = this.fields[facet.id];
+ if (customFields && customFields.length > 0) {
+ for (const field of customFields) {
+ if (!fields.includes(field)) {
+ fields.push(field);
+ }
+ }
+ }
+ });
+
+ if (query) {
+
+ const result: QueryBody = {
+ query: {
+ query: query,
+ language: 'afts'
+ },
+ include: ['path', 'allowableOperations'],
+ fields: fields,
+ /*
+ paging: {
+ maxItems: maxResults,
+ skipCount: skipCount
+ },
+ */
+ filterQueries: this.filterQueries,
+ facetQueries: this.config.facetQueries,
+ facetFields: this.config.facetFields,
+ limits: this.config.limits,
+ scope: this.scope
+ };
+
+ return result;
+ }
+
+ return null;
+ }
+}
diff --git a/lib/content-services/search/search-range.interface.ts b/lib/content-services/search/search-range.interface.ts
new file mode 100644
index 0000000000..9fb03eb7dc
--- /dev/null
+++ b/lib/content-services/search/search-range.interface.ts
@@ -0,0 +1,28 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface SearchRange {
+ field: string;
+ start: string;
+ end: string;
+ gap: string;
+ hardend: boolean;
+ other: Array;
+ include: Array;
+ label: string;
+ excludeFilters: Array;
+}
diff --git a/lib/content-services/search/search-widget-settings.interface.ts b/lib/content-services/search/search-widget-settings.interface.ts
new file mode 100644
index 0000000000..cd3ad0a0a7
--- /dev/null
+++ b/lib/content-services/search/search-widget-settings.interface.ts
@@ -0,0 +1,21 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface SearchWidgetSettings {
+ field: string;
+ [indexer: string]: any;
+}
diff --git a/lib/content-services/search/search-widget.interface.ts b/lib/content-services/search/search-widget.interface.ts
new file mode 100644
index 0000000000..c05bd863de
--- /dev/null
+++ b/lib/content-services/search/search-widget.interface.ts
@@ -0,0 +1,25 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { SearchWidgetSettings } from './search-widget-settings.interface';
+import { SearchQueryBuilderService } from './search-query-builder.service';
+
+export interface SearchWidget {
+ id: string;
+ settings?: SearchWidgetSettings;
+ context?: SearchQueryBuilderService;
+}
diff --git a/lib/content-services/search/search.module.ts b/lib/content-services/search/search.module.ts
index 14095f3d1b..a120a55157 100644
--- a/lib/content-services/search/search.module.ts
+++ b/lib/content-services/search/search.module.ts
@@ -28,12 +28,17 @@ import { SearchTriggerDirective } from './components/search-trigger.directive';
import { SearchControlComponent } from './components/search-control.component';
import { SearchComponent } from './components/search.component';
import { EmptySearchResultComponent } from './components/empty-search-result.component';
+import { SearchWidgetContainerComponent } from './components/search-widget-container/search-widget-container.component';
+import { SearchFilterComponent } from './components/search-filter/search-filter.component';
+import { SearchChipListComponent } from './components/search-chip-list/search-chip-list.component';
export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [
SearchComponent,
SearchControlComponent,
SearchTriggerDirective,
- EmptySearchResultComponent
+ EmptySearchResultComponent,
+ SearchFilterComponent,
+ SearchChipListComponent
];
@NgModule({
@@ -46,10 +51,15 @@ export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [
TranslateModule
],
declarations: [
- ...ALFRESCO_SEARCH_DIRECTIVES
+ ...ALFRESCO_SEARCH_DIRECTIVES,
+ SearchWidgetContainerComponent
],
exports: [
- ...ALFRESCO_SEARCH_DIRECTIVES
+ ...ALFRESCO_SEARCH_DIRECTIVES,
+ SearchWidgetContainerComponent
+ ],
+ entryComponents: [
+ SearchWidgetContainerComponent
]
})
export class SearchModule {}
diff --git a/lib/core/ng-package.json b/lib/core/ng-package.json
index 66164087bf..3222faefdc 100644
--- a/lib/core/ng-package.json
+++ b/lib/core/ng-package.json
@@ -4,6 +4,7 @@
"src": "./core/",
"dest": "../dist/core/",
"lib": {
+ "languageLevel": [ "dom", "es2016" ],
"licensePath": "../config/assets/license_header_add.txt",
"comments" : "none",
"entryFile": "./public-api.ts",
diff --git a/lib/core/services/alfresco-api.service.ts b/lib/core/services/alfresco-api.service.ts
index b6336f7c0f..e18475f30a 100644
--- a/lib/core/services/alfresco-api.service.ts
+++ b/lib/core/services/alfresco-api.service.ts
@@ -19,7 +19,7 @@ import { Injectable } from '@angular/core';
import {
AlfrescoApi, ContentApi, FavoritesApi, NodesApi,
PeopleApi, RenditionsApi, SharedlinksApi, SitesApi,
- VersionsApi, ClassesApi
+ VersionsApi, ClassesApi, SearchApi
} from 'alfresco-js-api';
import * as alfrescoApi from 'alfresco-js-api';
import { AppConfigService } from '../app-config/app-config.service';
@@ -62,7 +62,7 @@ export class AlfrescoApiService {
return this.getInstance().core.peopleApi;
}
- get searchApi() {
+ get searchApi(): SearchApi {
return this.getInstance().search.searchApi;
}
diff --git a/lib/core/services/search.service.spec.ts b/lib/core/services/search.service.spec.ts
index 7065393197..4f15792957 100644
--- a/lib/core/services/search.service.spec.ts
+++ b/lib/core/services/search.service.spec.ts
@@ -57,7 +57,7 @@ describe('SearchService', () => {
it('should call search API with no additional options', (done) => {
let searchTerm = 'searchTerm63688';
spyOn(searchMockApi.core.queriesApi, 'findNodes').and.returnValue(Promise.resolve(fakeSearch));
- service.getNodeQueryResults(searchTerm).subscribe(
+ service.getNodeQueryResults(searchTerm).then(
() => {
expect(searchMockApi.core.queriesApi.findNodes).toHaveBeenCalledWith(searchTerm, undefined);
done();
@@ -72,7 +72,7 @@ describe('SearchService', () => {
nodeType: 'cm:content'
};
spyOn(searchMockApi.core.queriesApi, 'findNodes').and.returnValue(Promise.resolve(fakeSearch));
- service.getNodeQueryResults(searchTerm, options).subscribe(
+ service.getNodeQueryResults(searchTerm, options).then(
() => {
expect(searchMockApi.core.queriesApi.findNodes).toHaveBeenCalledWith(searchTerm, options);
done();
@@ -81,7 +81,7 @@ describe('SearchService', () => {
});
it('should return search results returned from the API', (done) => {
- service.getNodeQueryResults('').subscribe(
+ service.getNodeQueryResults('').then(
(res: any) => {
expect(res).toBeDefined();
expect(res).toEqual(fakeSearch);
@@ -92,7 +92,7 @@ describe('SearchService', () => {
it('should notify errors returned from the API', (done) => {
spyOn(searchMockApi.core.queriesApi, 'findNodes').and.returnValue(Promise.reject(mockError));
- service.getNodeQueryResults('').subscribe(
+ service.getNodeQueryResults('').then(
() => {},
(res: any) => {
expect(res).toBeDefined();
@@ -101,17 +101,4 @@ describe('SearchService', () => {
}
);
});
-
- it('should notify a general error if the API does not return a specific error', (done) => {
- spyOn(searchMockApi.core.queriesApi, 'findNodes').and.returnValue(Promise.reject(null));
- service.getNodeQueryResults('').subscribe(
- () => {},
- (res: any) => {
- expect(res).toBeDefined();
- expect(res).toEqual('Server error');
- done();
- }
- );
- });
-
});
diff --git a/lib/core/services/search.service.ts b/lib/core/services/search.service.ts
index c8d2c91966..7d0722a719 100644
--- a/lib/core/services/search.service.ts
+++ b/lib/core/services/search.service.ts
@@ -17,45 +17,41 @@
import { Injectable } from '@angular/core';
import { NodePaging, QueryBody } from 'alfresco-js-api';
-import { Observable } from 'rxjs/Observable';
-import { AlfrescoApiService } from './alfresco-api.service';
-import { AuthenticationService } from './authentication.service';
import 'rxjs/add/observable/throw';
+import { Subject } from 'rxjs/Subject';
+
+import { AlfrescoApiService } from './alfresco-api.service';
import { SearchConfigurationService } from './search-configuration.service';
@Injectable()
export class SearchService {
- constructor(public authService: AuthenticationService,
- private apiService: AlfrescoApiService,
+ dataLoaded: Subject = new Subject();
+
+ constructor(private apiService: AlfrescoApiService,
private searchConfigurationService: SearchConfigurationService) {
}
- getNodeQueryResults(term: string, options?: SearchOptions): Observable {
- return Observable.fromPromise(this.apiService.getInstance().core.queriesApi.findNodes(term, options))
- .map(res => res)
- .catch(err => this.handleError(err));
+ async getNodeQueryResults(term: string, options?: SearchOptions): Promise {
+ const data = await this.apiService.getInstance().core.queriesApi.findNodes(term, options);
+
+ this.dataLoaded.next(data);
+ return data;
}
- search(searchTerm: string, maxResults: number, skipCount: number): Observable {
- const searchQuery = Object.assign(this.searchConfigurationService.generateQueryBody(searchTerm, maxResults, skipCount));
- const promise = this.apiService.getInstance().search.searchApi.search(searchQuery);
+ async search(searchTerm: string, maxResults: number, skipCount: number): Promise {
+ const searchQuery = this.searchConfigurationService.generateQueryBody(searchTerm, maxResults, skipCount);
+ const data = await this.apiService.searchApi.search(searchQuery);
- return Observable
- .fromPromise(promise)
- .catch(err => this.handleError(err));
+ this.dataLoaded.next(data);
+ return data;
}
- searchByQueryBody(queryBody: QueryBody): Observable {
- const promise = this.apiService.getInstance().search.searchApi.search(queryBody);
+ async searchByQueryBody(queryBody: QueryBody): Promise {
+ const data = await this.apiService.searchApi.search(queryBody);
- return Observable
- .fromPromise(promise)
- .catch(err => this.handleError(err));
- }
-
- private handleError(error: any): Observable {
- return Observable.throw(error || 'Server error');
+ this.dataLoaded.next(data);
+ return data;
}
}
diff --git a/lib/core/services/settings.service.spec.ts b/lib/core/services/settings.service.spec.ts
index bd10592566..168838b6fd 100644
--- a/lib/core/services/settings.service.spec.ts
+++ b/lib/core/services/settings.service.spec.ts
@@ -16,10 +16,8 @@
*/
import { async, TestBed } from '@angular/core/testing';
-import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { SettingsService } from './settings.service';
import { AppConfigModule } from '../app-config/app-config.module';
-import { TranslateLoaderService } from './translate-loader.service';
describe('SettingsService', () => {
@@ -28,13 +26,7 @@ describe('SettingsService', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
- AppConfigModule,
- TranslateModule.forRoot({
- loader: {
- provide: TranslateLoader,
- useClass: TranslateLoaderService
- }
- })
+ AppConfigModule
],
providers: [
SettingsService
diff --git a/lib/insights/ng-package.json b/lib/insights/ng-package.json
index d2efff3269..00d4388b9a 100644
--- a/lib/insights/ng-package.json
+++ b/lib/insights/ng-package.json
@@ -4,6 +4,7 @@
"src": "../insights/",
"dest": "../dist/insights/",
"lib": {
+ "languageLevel": [ "dom", "es2016" ],
"licensePath": "../config/assets/license_header_add.txt",
"comments" : "none",
"entryFile": "./public-api.ts",
diff --git a/lib/package.json b/lib/package.json
index 6e12eee1f1..52202d66f8 100644
--- a/lib/package.json
+++ b/lib/package.json
@@ -168,7 +168,7 @@
"bundlesize": [
{
"path": "./dist/content-services/bundles/adf-content-services.umd.js",
- "maxSize": "50 kb"
+ "maxSize": "60 kb"
},
{
"path": "./dist/process-services/bundles/adf-process-services.umd.js",
diff --git a/lib/process-services/ng-package.json b/lib/process-services/ng-package.json
index 7e40f8b402..720f94e4ce 100644
--- a/lib/process-services/ng-package.json
+++ b/lib/process-services/ng-package.json
@@ -4,6 +4,7 @@
"src": "../process-services/",
"dest": "../dist/process-services/",
"lib": {
+ "languageLevel": [ "dom", "es2016" ],
"licensePath": "../config/assets/license_header_add.txt",
"comments" : "none",
"entryFile": "./public-api.ts",