diff --git a/projects/step-core/src/lib/modules/basics/components/form-field/form-field.component.html b/projects/step-core/src/lib/modules/basics/components/form-field/form-field.component.html index f05942b08..6ad7d9d12 100644 --- a/projects/step-core/src/lib/modules/basics/components/form-field/form-field.component.html +++ b/projects/step-core/src/lib/modules/basics/components/form-field/form-field.component.html @@ -18,6 +18,11 @@
+
+
+ +
+
diff --git a/projects/step-core/src/lib/modules/basics/components/form-field/form-field.component.ts b/projects/step-core/src/lib/modules/basics/components/form-field/form-field.component.ts index 87f44bbcb..cf8a05add 100644 --- a/projects/step-core/src/lib/modules/basics/components/form-field/form-field.component.ts +++ b/projects/step-core/src/lib/modules/basics/components/form-field/form-field.component.ts @@ -1,6 +1,7 @@ import { Component, computed, contentChild, input, Input, ViewEncapsulation } from '@angular/core'; import { AbstractControl, NgControl } from '@angular/forms'; import { getControlWarningsContainer } from '../../types/form-control-warnings-extension'; +import { FormFieldFocusAddonDirective } from '../../directives/form-field-focus-addon.directive'; @Component({ selector: 'step-form-field', templateUrl: './form-field.component.html', @@ -25,6 +26,11 @@ export class FormFieldComponent { /* @ContentChild(NgControl) */ protected contentControl = contentChild(NgControl); + /* @ContentChild(FormFieldFocusAddonDirective) */ + private formFieldFocusAddon = contentChild(FormFieldFocusAddonDirective); + + protected readonly hasFocusAddon = computed(() => this.formFieldFocusAddon()); + protected readonly control = computed(() => { const contentControl = this.contentControl()?.control; return this.explicitControl() ?? contentControl; diff --git a/projects/step-core/src/lib/modules/basics/components/input-filter-regex-switcher/input-filter-regex-switcher.component.html b/projects/step-core/src/lib/modules/basics/components/input-filter-regex-switcher/input-filter-regex-switcher.component.html new file mode 100644 index 000000000..af3daac52 --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/input-filter-regex-switcher/input-filter-regex-switcher.component.html @@ -0,0 +1,2 @@ + +Regex diff --git a/projects/step-core/src/lib/modules/basics/components/input-filter-regex-switcher/input-filter-regex-switcher.component.scss b/projects/step-core/src/lib/modules/basics/components/input-filter-regex-switcher/input-filter-regex-switcher.component.scss new file mode 100644 index 000000000..d127a3e86 --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/input-filter-regex-switcher/input-filter-regex-switcher.component.scss @@ -0,0 +1,22 @@ +:host { + display: flex; + align-items: center; + justify-content: space-between; +} + +span { + font-size: 11px; +} + +mat-slide-toggle { + transform: scale(0.8); +} + +.selected { + font-weight: 600; + text-decoration: underline; +} + +::ng-deep step-form-field.ng-invalid.ng-touched div.mdc-switch__ripple { + display: none; +} diff --git a/projects/step-core/src/lib/modules/basics/components/input-filter-regex-switcher/input-filter-regex-switcher.component.ts b/projects/step-core/src/lib/modules/basics/components/input-filter-regex-switcher/input-filter-regex-switcher.component.ts new file mode 100644 index 000000000..dba5e509f --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/input-filter-regex-switcher/input-filter-regex-switcher.component.ts @@ -0,0 +1,19 @@ +import { Component, inject } from '@angular/core'; +import { FilterRegexSwitcherService } from '../../injectables/filter-regex-switcher.service'; + +@Component({ + selector: 'step-input-filter-regex-switcher', + templateUrl: './input-filter-regex-switcher.component.html', + styleUrl: './input-filter-regex-switcher.component.scss', +}) +export class InputFilterRegexSwitcherComponent { + protected readonly _regexSwitcher = inject(FilterRegexSwitcherService, { optional: true }); + + toggleSwitch(active: any) { + if (active) { + this._regexSwitcher?.switchToRegex?.(); + } else { + this._regexSwitcher?.switchToText?.(); + } + } +} diff --git a/projects/step-core/src/lib/modules/basics/components/input-filter/input-filter.component.html b/projects/step-core/src/lib/modules/basics/components/input-filter/input-filter.component.html index d93d75565..0fbdb89b8 100644 --- a/projects/step-core/src/lib/modules/basics/components/input-filter/input-filter.component.html +++ b/projects/step-core/src/lib/modules/basics/components/input-filter/input-filter.component.html @@ -1,3 +1,8 @@ + @if (useRegexSwitcher()) { + + + + } diff --git a/projects/step-core/src/lib/modules/basics/components/input-filter/input-filter.component.ts b/projects/step-core/src/lib/modules/basics/components/input-filter/input-filter.component.ts index 9232a46c2..96873936b 100644 --- a/projects/step-core/src/lib/modules/basics/components/input-filter/input-filter.component.ts +++ b/projects/step-core/src/lib/modules/basics/components/input-filter/input-filter.component.ts @@ -1,4 +1,14 @@ -import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { + Component, + ElementRef, + forwardRef, + inject, + Input, + OnChanges, + OnInit, + signal, + SimpleChanges, +} from '@angular/core'; import { FormBuilder, FormControl } from '@angular/forms'; import { debounceTime, filter, merge, Observable, of } from 'rxjs'; import { BaseFilterComponent } from '../base-filter/base-filter.component'; @@ -14,9 +24,21 @@ import { BaseFilterComponent } from '../base-filter/base-filter.component'; }, ], }) -export class InputFilterComponent extends BaseFilterComponent implements OnChanges { +export class InputFilterComponent extends BaseFilterComponent implements OnChanges, OnInit { + private _elRef = inject>(ElementRef); + + protected useRegexSwitcher = signal(false); + @Input() externalSearchValue?: string; + override ngOnInit(): void { + super.ngOnInit(); + // The attribute check has been used instead of injection, because + // attempt to inject FilterConnect inside InputFilter will cause DI cycle, as + // InputFilter has been already injected in FilterConnect directive + this.useRegexSwitcher.set(this._elRef.nativeElement.hasAttribute('stepfilterconnect')); + } + ngOnChanges(changes: SimpleChanges): void { const cExternalSearchValue = changes['externalSearchValue']; if ( diff --git a/projects/step-core/src/lib/modules/basics/directives/form-field-focus-addon.directive.ts b/projects/step-core/src/lib/modules/basics/directives/form-field-focus-addon.directive.ts new file mode 100644 index 000000000..6e8b6f274 --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/directives/form-field-focus-addon.directive.ts @@ -0,0 +1,7 @@ +import { Directive } from '@angular/core'; + +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector + selector: 'step-form-field-focus-addon', +}) +export class FormFieldFocusAddonDirective {} diff --git a/projects/step-core/src/lib/modules/basics/injectables/filter-regex-switcher.service.ts b/projects/step-core/src/lib/modules/basics/injectables/filter-regex-switcher.service.ts new file mode 100644 index 000000000..8cafe9527 --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/injectables/filter-regex-switcher.service.ts @@ -0,0 +1,7 @@ +import { Signal } from '@angular/core'; + +export abstract class FilterRegexSwitcherService { + abstract readonly isRegex: Signal; + abstract switchToText(): void; + abstract switchToRegex(): void; +} diff --git a/projects/step-core/src/lib/modules/basics/step-basics.module.ts b/projects/step-core/src/lib/modules/basics/step-basics.module.ts index 382cc4617..27854cf93 100644 --- a/projects/step-core/src/lib/modules/basics/step-basics.module.ts +++ b/projects/step-core/src/lib/modules/basics/step-basics.module.ts @@ -64,6 +64,8 @@ import { ControlWarningsPipe } from './pipes/control-warnings.pipe'; import { BigNumberPipe } from './pipes/big-number.pipe'; import { BooleanFilterComponent } from './components/boolean-filter/boolean-filter.component'; import { StatusIconPipe } from './pipes/status-icon.pipe'; +import { FormFieldFocusAddonDirective } from './directives/form-field-focus-addon.directive'; +import { InputFilterRegexSwitcherComponent } from './components/input-filter-regex-switcher/input-filter-regex-switcher.component'; @NgModule({ imports: [CommonModule, FormsModule, ReactiveFormsModule, StepMaterialModule, RouterModule, NgxMatSelectSearchModule], @@ -119,6 +121,7 @@ import { StatusIconPipe } from './pipes/status-icon.pipe'; MarkerComponent, AlertsContainerComponent, InputModelFormatterDirective, + FormFieldFocusAddonDirective, ProjectNamePipe, GetObjectFieldPipe, StatusCommonComponent, @@ -128,6 +131,7 @@ import { StatusIconPipe } from './pipes/status-icon.pipe'; BigNumberPipe, BooleanFilterComponent, StatusIconPipe, + InputFilterRegexSwitcherComponent, ], exports: [ CommonModule, @@ -146,6 +150,7 @@ import { StatusIconPipe } from './pipes/status-icon.pipe'; WidthExpandersDirective, ModalWindowComponent, FormFieldComponent, + FormFieldFocusAddonDirective, LabelDirective, PrefixDirective, SuffixDirective, @@ -249,6 +254,7 @@ export * from './directives/error.directive'; export * from './directives/warning.directive'; export * from './directives/alert.directive'; export * from './directives/prevent-chars.directive'; +export * from './directives/form-field-focus-addon.directive'; export * from './directives/allow-chars.directive'; export * from './directives/label-addon.directive'; export * from './injectables/array-item-label-value-extractor'; @@ -314,3 +320,4 @@ export * from './injectables/time-converters-factory.service'; export * from './components/simple-object-input/simple-object-input.component'; export * from './injectables/statuses-colors.token'; export * from './pipes/status-icon.pipe'; +export * from './injectables/filter-regex-switcher.service'; diff --git a/projects/step-core/src/lib/modules/table/components/table/table.component.html b/projects/step-core/src/lib/modules/table/components/table/table.component.html index 7441065b3..e8b9e6330 100644 --- a/projects/step-core/src/lib/modules/table/components/table/table.component.html +++ b/projects/step-core/src/lib/modules/table/components/table/table.component.html @@ -21,7 +21,7 @@ @for (searchCol of searchColumns; track searchCol.colName) { -
+ @if (searchCol.searchName) { @if (searchCol.template) { diff --git a/projects/step-core/src/lib/modules/table/components/table/table.component.scss b/projects/step-core/src/lib/modules/table/components/table/table.component.scss index 5294120ee..d5cd3029b 100644 --- a/projects/step-core/src/lib/modules/table/components/table/table.component.scss +++ b/projects/step-core/src/lib/modules/table/components/table/table.component.scss @@ -12,6 +12,10 @@ step-table { width: 100%; border-collapse: separate; + th.step-search-cell { + overflow: auto !important; + } + thead tr { &:nth-child(1) { top: 0; diff --git a/projects/step-core/src/lib/modules/table/directives/filter-connect.directive.ts b/projects/step-core/src/lib/modules/table/directives/filter-connect.directive.ts index fab7537e1..acbc6e289 100644 --- a/projects/step-core/src/lib/modules/table/directives/filter-connect.directive.ts +++ b/projects/step-core/src/lib/modules/table/directives/filter-connect.directive.ts @@ -1,5 +1,17 @@ -import { AfterViewInit, DestroyRef, Directive, inject, Input, OnDestroy } from '@angular/core'; -import { BaseFilterComponent, isRegexValidator } from '../../basics/step-basics.module'; +import { + AfterViewInit, + computed, + DestroyRef, + Directive, + effect, + forwardRef, + inject, + input, + model, + OnDestroy, + signal, +} from '@angular/core'; +import { BaseFilterComponent, FilterRegexSwitcherService, isRegexValidator } from '../../basics/step-basics.module'; import { SearchValue } from '../shared/search-value'; import { FilterCondition } from '../shared/filter-condition'; import { map } from 'rxjs'; @@ -7,33 +19,83 @@ import { SearchColumnAccessor } from '../shared/search-column-accessor'; import { FilterConditionFactoryService } from '../services/filter-condition-factory.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +type CreateConditionFn = (value: T, additionalParams?: any) => FilterCondition; + @Directive({ selector: '[stepFilterConnect]', + providers: [ + { + provide: FilterRegexSwitcherService, + useExisting: forwardRef(() => FilterConnectDirective), + }, + ], }) -export class FilterConnectDirective implements AfterViewInit, OnDestroy { +export class FilterConnectDirective implements AfterViewInit, OnDestroy, FilterRegexSwitcherService { private _destroyRef = inject(DestroyRef); private _filterConditionFactory = inject(FilterConditionFactoryService); private _searchCol = inject(SearchColumnAccessor, { optional: true }); private _filter = inject>(BaseFilterComponent, { optional: true }); + private isInitialized = signal(false); + private get isConnected(): boolean { return !!this._searchCol && !!this._filter; } - @Input() createConditionFn?: (value: T, additionalParams?: any) => FilterCondition; - @Input() conditionFnAdditionalParams?: any; - @Input() useRegex?: boolean; + /** @Input() **/ + readonly createConditionFn = input | undefined>(undefined); + + /** @Input() **/ + readonly conditionFnAdditionalParams = input(undefined); + + /** @Input() **/ + readonly useRegex = model(true); + + readonly hasRegex = computed(() => { + const useRegex = this.useRegex(); + const hasFilterCondition = !!this.createConditionFn(); + if (hasFilterCondition) { + return false; + } + return useRegex; + }); + + readonly isRegex = this.useRegex.asReadonly(); + + switchToRegex(): void { + this.useRegex.set(true); + queueMicrotask(() => this._filter?.filterControl?.updateValueAndValidity()); + } + + switchToText() { + this.useRegex.set(false); + queueMicrotask(() => this._filter?.filterControl?.updateValueAndValidity()); + } ngAfterViewInit(): void { this.setupValueChanges(); this.setupFilterChanges(); - this.setupFilterValidation(); + this.isInitialized.set(true); } ngOnDestroy(): void { this.destroyFilterValidation(); } + private effectSetupFilterValidation = effect(() => { + const isInitialized = this.isInitialized(); + const hasRegex = this.hasRegex(); + if (!isInitialized) { + return; + } + if (!this.isConnected) { + console.warn('[stepFilterConnect] not connected'); + return; + } + + this.toggleValidator(hasRegex); + }); + private setupValueChanges(): void { if (!this.isConnected) { console.warn('[stepFilterConnect] not connected'); @@ -45,7 +107,12 @@ export class FilterConnectDirective implements AfterViewInit, O map((searchValue) => this.convertToFilterValue(searchValue)), takeUntilDestroyed(this._destroyRef), ) - .subscribe((filterValue) => this._filter!.assignValue(filterValue)); + .subscribe(({ value, regex }) => { + if (regex !== undefined) { + this.useRegex.set(regex); + } + this._filter!.assignValue(value); + }); } private setupFilterChanges(): void { @@ -59,46 +126,45 @@ export class FilterConnectDirective implements AfterViewInit, O ).subscribe((searchValue) => this._searchCol!.search(searchValue)); } - private setupFilterValidation(): void { - if (!this.isConnected) { - console.warn('[stepFilterConnect] not connected'); - return; - } - if (this.useRegex || !this.createConditionFn) { - this._filter?.filterControl.addValidators(isRegexValidator); - } - } - private destroyFilterValidation(): void { if (!this.isConnected) { return; } - this._filter?.filterControl.removeValidators(isRegexValidator); + this.toggleValidator(false); } private convertToSearchValue(value: T): SearchValue { - if (this.createConditionFn) { - return this.createConditionFn.call(this._filterConditionFactory, value, this.conditionFnAdditionalParams); - } - - if (this.useRegex !== undefined) { - return { value: value as string, regex: this.useRegex }; + const createConditionFn = this.createConditionFn(); + if (createConditionFn) { + return createConditionFn.call(this._filterConditionFactory, value, this.conditionFnAdditionalParams()); } - return value as string; + return { value: value as string, regex: this.hasRegex() }; } - private convertToFilterValue(value?: SearchValue): T | undefined { + private convertToFilterValue(value?: SearchValue): { value: T | undefined; regex?: boolean } { if (!value) { - return undefined; + return { value: undefined }; } if (typeof value === 'string') { - return value as T; + return { value: value as T }; } if (value instanceof FilterCondition) { - return value.getSearchValue() as T; + return { value: value.getSearchValue() as T }; } - return value?.value as T; + return { value: value?.value as T, regex: value?.regex }; + } + + private toggleValidator(isActive: boolean): void { + const filterControl = this._filter?.filterControl; + if (!filterControl) { + return; + } + if (isActive && !filterControl.hasValidator(isRegexValidator)) { + filterControl.addValidators(isRegexValidator); + } else if (!isActive && filterControl.hasValidator(isRegexValidator)) { + filterControl.removeValidators(isRegexValidator); + } } } diff --git a/projects/step-core/styles/components/_step-inputs.scss b/projects/step-core/styles/components/_step-inputs.scss index 91c9ac870..7a1b470b1 100644 --- a/projects/step-core/styles/components/_step-inputs.scss +++ b/projects/step-core/styles/components/_step-inputs.scss @@ -3,6 +3,7 @@ @function step-define-inputs-config( $primary-color: #0082cb, + $primary-contrast-color: #ffffff, $primary-text-color: #293e50, $border-color: #d0d5dd, $error-color: #ff0000, @@ -10,6 +11,7 @@ ) { $config: ( primary-color: $primary-color, + primary-contrast-color: $primary-contrast-color, primary-text-color: $primary-text-color, border-color: $border-color, error-color: $error-color, @@ -90,10 +92,17 @@ } } } + + & > .form-field-focus-addon { + div { + background: $highlight-color; + } + } } @mixin step-form-field($config) { $primary-color: map.get($config, primary-color); + $primary-contrast-color: map.get($config, primary-contrast-color); $primary-text-color: map.get($config, primary-text-color); $border-color: map.get($config, border-color); $error-color: map.get($config, error-color); @@ -115,6 +124,25 @@ line-height: 1; gap: 0.4rem; + & > .form-field-focus-addon { + display: none; + &.has-focus-addon { + opacity: 0; + display: flex; + justify-content: flex-end; + margin-top: -1.5rem; + margin-bottom: 1.5rem; + } + & > div { + position: absolute; + background: $primary-color; + color: $primary-contrast-color; + border-bottom-left-radius: var(--form-field-border-radius); + border-bottom-right-radius: var(--form-field-border-radius); + padding: 1.5rem 0.7rem 0.5rem 0.7rem; + } + } + & > .label-container { display: flex; justify-content: flex-start; @@ -210,6 +238,10 @@ } } } + + & > .form-field-focus-addon { + opacity: 1; + } } &.ng-invalid.ng-touched { @@ -223,6 +255,7 @@ .form-field-content { display: inline-flex; width: 100%; + z-index: 1; & > mat-select, & > step-multi-level-select,