diff --git a/docs/content-services/components/search-input.component.md b/docs/content-services/components/search-input.component.md new file mode 100644 index 0000000000..34858993ff --- /dev/null +++ b/docs/content-services/components/search-input.component.md @@ -0,0 +1,70 @@ +# Search Input + +`Component`, `Standalone` + +A minimalistic search input component that formats user query according to the provided fields. + +```html + + +``` + +> Notes: this component does not perform search operations. +> It handles the user input, formats and produces the search query to use with `Search Query Builder` or other services. + +## Properties + +- `fields` **string[]** - optional, a list of fields to use in the formatted search query, defaults to `[cm:name]` +- `value` **string** - optional, initial input value +- `label` **string** - optional, display label +- `placeholder` **string** - optional, display placeholder + +## Events + +- `changed` **EventEmitter\**: emits when user presses `Enter` or moves the focus out of the input area + +## Examples + +```html + + +``` + +In the example above, the search is performed against the following fields: +`cm:name`, `cm:title`, `cm:description`, `TEXT` and `TAG`. + +The Search Input is going to produce the following results for user inputs: + +user types `test` + +```text +(cm:name:"test*" OR cm:title:"test*" OR cm:description:"test*" OR TEXT:"test*" OR TAG:"test*") +``` + +user types `*` + +```text +(cm:name:"**" OR cm:title:"**" OR cm:description:"**" OR TEXT:"**" OR TAG:"**") +``` + +user types `one two` + +```text +(cm:name:"one*" OR cm:title:"one*" OR cm:description:"one*" OR TEXT:"one*" OR TAG:"one*") AND (cm:name:"two*" OR cm:title:"two*" OR cm:description:"two*" OR TEXT:"two*" OR TAG:"two*") +``` + +user types `one AND two` + +```text +(cm:name:"one*" OR cm:title:"one*" OR cm:description:"one*" OR TEXT:"one*" OR TAG:"one*") AND (cm:name:"two*" OR cm:title:"two*" OR cm:description:"two*" OR TEXT:"two*" OR TAG:"two*") +``` + +user types `one OR two` + +```text +(cm:name:"one*" OR cm:title:"one*" OR cm:description:"one*" OR TEXT:"one*" OR TAG:"one*") OR (cm:name:"two*" OR cm:title:"two*" OR cm:description:"two*" OR TEXT:"two*" OR TAG:"two*") +``` diff --git a/lib/content-services/.eslintignore b/lib/content-services/.eslintignore new file mode 100644 index 0000000000..16523ff94f --- /dev/null +++ b/lib/content-services/.eslintignore @@ -0,0 +1,2 @@ +.storybook +coverage diff --git a/lib/content-services/src/lib/i18n/en.json b/lib/content-services/src/lib/i18n/en.json index d0272e1e39..bc7b075e26 100644 --- a/lib/content-services/src/lib/i18n/en.json +++ b/lib/content-services/src/lib/i18n/en.json @@ -278,6 +278,8 @@ "ARIA-LABEL": "Search button" }, "INPUT": { + "LABEL": "Search", + "PLACEHOLDER": "Search Query", "ARIA-LABEL": "Search input" }, "RESULTS": { diff --git a/lib/content-services/src/lib/search/components/index.ts b/lib/content-services/src/lib/search/components/index.ts index b38c8ab48c..434f20bca1 100644 --- a/lib/content-services/src/lib/search/components/index.ts +++ b/lib/content-services/src/lib/search/components/index.ts @@ -27,6 +27,7 @@ export * from './search-filter-chips'; export * from './search-filter-container'; export * from './search-filter-tabbed'; export * from './search-form'; +export * from './search-input'; export * from './search-logical-filter'; export * from './search-number-range'; export * from './search-panel'; diff --git a/lib/content-services/src/lib/search/components/search-input/index.ts b/lib/content-services/src/lib/search/components/search-input/index.ts new file mode 100644 index 0000000000..2a3afd0bb7 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-input/index.ts @@ -0,0 +1,18 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './search-input.component'; diff --git a/lib/content-services/src/lib/search/components/search-input/search-input.component.html b/lib/content-services/src/lib/search/components/search-input/search-input.component.html new file mode 100644 index 0000000000..2adba70603 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-input/search-input.component.html @@ -0,0 +1,6 @@ +
+ + {{ label | translate }} + + +
diff --git a/lib/content-services/src/lib/search/components/search-input/search-input.component.scss b/lib/content-services/src/lib/search/components/search-input/search-input.component.scss new file mode 100644 index 0000000000..20a12e11be --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-input/search-input.component.scss @@ -0,0 +1,12 @@ +/* stylelint-disable selector-class-pattern */ +.adf-search-input-container { + margin: 10px; + + mat-form-field { + width: 100%; + + .mat-form-field-wrapper { + padding: 0; + } + } +} diff --git a/lib/content-services/src/lib/search/components/search-input/search-input.component.spec.ts b/lib/content-services/src/lib/search/components/search-input/search-input.component.spec.ts new file mode 100644 index 0000000000..28ad56f71a --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-input/search-input.component.spec.ts @@ -0,0 +1,113 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatInputHarness } from '@angular/material/input/testing'; +import { SearchInputComponent } from '@alfresco/adf-content-services'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TranslateModule } from '@ngx-translate/core'; +import { ContentTestingModule } from '../../../testing/content.testing.module'; + +describe('SearchInputComponent', () => { + let loader: HarnessLoader; + let component: SearchInputComponent; + let fixture: ComponentFixture; + + /** + * Sets the search input value + * + * @param value the value to set + */ + async function setInputValue(value: string) { + const input = await loader.getHarness(MatInputHarness); + await input.setValue(value); + await (await input.host()).dispatchEvent('change'); + + fixture.detectChanges(); + await fixture.whenStable(); + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), ContentTestingModule, SearchInputComponent] + }); + + fixture = TestBed.createComponent(SearchInputComponent); + component = fixture.componentInstance; + + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should show custom placeholder', async () => { + component.placeholder = 'custom placeholder'; + + const input = await loader.getHarness(MatInputHarness); + const placeholder = await input.getPlaceholder(); + expect(placeholder).toBe('custom placeholder'); + }); + + it('should use multiple fields', async () => { + component.fields = ['cm:description', 'TAG']; + + fixture.detectChanges(); + await fixture.whenStable(); + + let formatted = ''; + component.changed.subscribe((val) => (formatted = val)); + + await setInputValue('test'); + + expect(formatted).toBe('(cm:description:"test*" OR TAG:"test*")'); + }); + + it('should emit changed event with [cm:name]', async () => { + let formatted = ''; + component.changed.subscribe((val) => (formatted = val)); + + await setInputValue('test'); + + expect(formatted).toBe('(cm:name:"test*")'); + }); + + it('should format with AND by default', async () => { + let formatted = ''; + component.changed.subscribe((val) => (formatted = val)); + + await setInputValue('one two'); + + expect(formatted).toBe('(cm:name:"one*") AND (cm:name:"two*")'); + }); + + it('should format with OR if specified directly', async () => { + let formatted = ''; + component.changed.subscribe((val) => (formatted = val)); + + await setInputValue('one OR two'); + + expect(formatted).toBe('(cm:name:"one*") OR (cm:name:"two*")'); + }); + + it('should format with AND if specified directly', async () => { + let formatted = ''; + component.changed.subscribe((val) => (formatted = val)); + + await setInputValue('one AND two'); + + expect(formatted).toBe('(cm:name:"one*") AND (cm:name:"two*")'); + }); +}); diff --git a/lib/content-services/src/lib/search/components/search-input/search-input.component.ts b/lib/content-services/src/lib/search/components/search-input/search-input.component.ts new file mode 100644 index 0000000000..7b14865e09 --- /dev/null +++ b/lib/content-services/src/lib/search/components/search-input/search-input.component.ts @@ -0,0 +1,105 @@ +/*! + * @license + * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { TranslateModule } from '@ngx-translate/core'; + +@Component({ + selector: 'app-search-input', + standalone: true, + imports: [CommonModule, MatFormFieldModule, MatInputModule, TranslateModule], + templateUrl: `./search-input.component.html`, + styleUrls: ['./search-input.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class SearchInputComponent { + @Input() + value = ''; + + @Input() + label = 'SEARCH.INPUT.LABEL'; + + @Input() + placeholder = 'SEARCH.INPUT.PLACEHOLDER'; + + @Input() + fields = ['cm:name']; + + @Output() + changed = new EventEmitter(); + + onSearchInputChanged(event: Event) { + const input = event.target as HTMLInputElement; + const searchTerm = input.value; + + const query = this.formatSearchQuery(searchTerm, this.fields); + if (query) { + this.changed.emit(decodeURIComponent(query)); + } + } + + private formatSearchQuery(userInput: string, fields = ['cm:name']): string { + if (!userInput) { + return null; + } + + if (/^https?:\/\//.test(userInput)) { + return this.formatFields(fields, userInput); + } + + userInput = userInput.trim(); + + if (userInput.includes(':') || userInput.includes('"')) { + return userInput; + } + + const words = userInput.split(' '); + + if (words.length > 1) { + const separator = words.some(this.isOperator) ? ' ' : ' AND '; + return words.map((term) => (this.isOperator(term) ? term : this.formatFields(fields, term))).join(separator); + } + + return this.formatFields(fields, userInput); + } + + private isOperator(input: string): boolean { + if (input) { + input = input.trim().toUpperCase(); + + const operators = ['AND', 'OR']; + return operators.includes(input); + } + return false; + } + + private formatFields(fields: string[], term: string): string { + let prefix = ''; + let suffix = '*'; + + if (term.startsWith('=')) { + prefix = '='; + suffix = ''; + term = term.substring(1); + } + + return '(' + fields.map((field) => `${prefix}${field}:"${term}${suffix}"`).join(' OR ') + ')'; + } +} diff --git a/lib/content-services/src/lib/search/search.module.ts b/lib/content-services/src/lib/search/search.module.ts index c8ca2c22a0..de295666ba 100644 --- a/lib/content-services/src/lib/search/search.module.ts +++ b/lib/content-services/src/lib/search/search.module.ts @@ -58,6 +58,7 @@ import { SearchDateRangeTabbedComponent } from './components/search-date-range-t import { SearchFilterTabDirective } from './components/search-filter-tabbed/search-filter-tab.directive'; import { SearchFacetChipTabbedComponent } from './components/search-filter-chips/search-facet-chip-tabbed/search-facet-chip-tabbed.component'; import { SearchFacetTabbedContentComponent } from './components/search-filter-chips/search-facet-chip-tabbed/search-facet-tabbed-content.component'; +import { SearchInputComponent } from './components/search-input'; @NgModule({ imports: [ @@ -67,7 +68,8 @@ import { SearchFacetTabbedContentComponent } from './components/search-filter-ch ReactiveFormsModule, MaterialModule, CoreModule, - SearchTextModule + SearchTextModule, + SearchInputComponent ], declarations: [ SearchComponent, diff --git a/lib/core/.eslintignore b/lib/core/.eslintignore new file mode 100644 index 0000000000..abb75b0d35 --- /dev/null +++ b/lib/core/.eslintignore @@ -0,0 +1 @@ +.storybook diff --git a/lib/process-services-cloud/.eslintignore b/lib/process-services-cloud/.eslintignore new file mode 100644 index 0000000000..abb75b0d35 --- /dev/null +++ b/lib/process-services-cloud/.eslintignore @@ -0,0 +1 @@ +.storybook diff --git a/package-lock.json b/package-lock.json index a7f9cbbcb2..19a7591842 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "alfresco-ng2-components", "version": "6.7.1", - "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@angular/animations": "14.1.3", @@ -34311,8 +34310,9 @@ }, "node_modules/husky": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz", + "integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==", "dev": true, - "license": "MIT", "bin": { "husky": "lib/bin.js" }, diff --git a/package.json b/package.json index 61fee99b16..91d0c9d038 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "version": "6.7.1", "author": "Hyland Software, Inc. and its affiliates", "scripts": { - "postinstall": "node ./decorate-angular-cli.js && ngcc --properties es2015 browser module main", + "prepare": "husky install", "ng": "nx", "00": "echo -------------------------------------------- DOC -----------------------------------------------", "build-doc-tools": "tsc -p ./tools/doc/tsconfig.json", @@ -99,17 +99,13 @@ "@angular-eslint/template-parser": "16.2.0", "@angular/cli": "~14.2.12", "@angular/compiler-cli": "14.1.3", - "@editorjs/editorjs": "^2.29.0", "@editorjs/code": "2.9.0", + "@editorjs/editorjs": "^2.29.0", "@editorjs/header": "2.8.1", "@editorjs/inline-code": "1.5.0", "@editorjs/list": "1.9.0", "@editorjs/marker": "1.4.0", "@editorjs/underline": "1.1.0", - "editorjs-text-color-plugin": "2.0.4", - "editorjs-html": "3.4.3", - "editorjs-paragraph-with-alignment": "3.0.0", - "@quanzo/change-font-size": "1.0.0", "@nrwl/angular": "14.8.9", "@nrwl/cli": "14.8.9", "@nrwl/eslint-plugin-nx": "14.5.4", @@ -118,6 +114,7 @@ "@nrwl/workspace": "14.8.9", "@paperist/types-remark": "0.1.3", "@playwright/test": "^1.35.1", + "@quanzo/change-font-size": "1.0.0", "@storybook/addon-essentials": "6.5.10", "@storybook/angular": "6.5.16", "@storybook/builder-webpack5": "6.5.10", @@ -141,6 +138,9 @@ "commander": "6.2.1", "css-loader": "^6.10.0", "dotenv": "16.1.3", + "editorjs-html": "3.4.3", + "editorjs-paragraph-with-alignment": "3.0.0", + "editorjs-text-color-plugin": "2.0.4", "ejs": "^3.1.9", "eslint": "^8.47.0", "eslint-config-prettier": "^8.10.0",