mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[ACS-7338] Simple search input component (#9454)
* feat(content-services): simple search input component for applications * feat(content-services): support i18n for search input * feat(content-services): apply review suggestions * feat(content-services): apply review suggestions * chore: fix husky install
This commit is contained in:
@@ -278,6 +278,8 @@
|
||||
"ARIA-LABEL": "Search button"
|
||||
},
|
||||
"INPUT": {
|
||||
"LABEL": "Search",
|
||||
"PLACEHOLDER": "Search Query",
|
||||
"ARIA-LABEL": "Search input"
|
||||
},
|
||||
"RESULTS": {
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
@@ -0,0 +1,6 @@
|
||||
<div class="adf-search-input-container">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label *ngIf="label">{{ label | translate }}</mat-label>
|
||||
<input matInput [placeholder]="placeholder | translate" [value]="value" (change)="onSearchInputChanged($event)" />
|
||||
</mat-form-field>
|
||||
</div>
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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<SearchInputComponent>;
|
||||
|
||||
/**
|
||||
* 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*")');
|
||||
});
|
||||
});
|
@@ -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<string>();
|
||||
|
||||
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 ') + ')';
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user