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,
|