();
+ protected readonly _afterClosed = new Subject();
/** @hidden */
protected readonly _afterLoaded = new Subject();
@@ -23,7 +23,7 @@ export class DialogRefBase {
* Closes the dialog and passes the argument to the afterClosed observable.
* @param result Value passed back to the observable as a result.
*/
- close(result?: any): void {
+ close(result?: P): void {
this._afterClosed.next(result);
this._afterClosed.complete();
}
diff --git a/libs/core/src/lib/dialog/dialog-service/dialog.service.ts b/libs/core/src/lib/dialog/dialog-service/dialog.service.ts
index b5c7b7244af..8954140d182 100644
--- a/libs/core/src/lib/dialog/dialog-service/dialog.service.ts
+++ b/libs/core/src/lib/dialog/dialog-service/dialog.service.ts
@@ -21,7 +21,7 @@ export class DialogService extends DialogBaseService {
}
/**
- * Opens a dialog component with with provided content.
+ * Opens a dialog component with provided content.
* @param content Content of the dialog component.
* @param dialogConfig Configuration of the dialog component.
*/
diff --git a/libs/core/src/lib/popover/popover.component.ts b/libs/core/src/lib/popover/popover.component.ts
index 165d8658e76..e95082ce885 100644
--- a/libs/core/src/lib/popover/popover.component.ts
+++ b/libs/core/src/lib/popover/popover.component.ts
@@ -281,8 +281,6 @@ export class PopoverComponent
injector
);
- console.log(111111, this._mobileModeComponentRef);
-
this._listenOnTriggerRefClicks();
}
diff --git a/libs/core/src/lib/select/select.component.ts b/libs/core/src/lib/select/select.component.ts
index 9920a4bef1e..7e4e3ccf575 100644
--- a/libs/core/src/lib/select/select.component.ts
+++ b/libs/core/src/lib/select/select.component.ts
@@ -70,7 +70,7 @@ export class FdSelectChange {
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
- '[class.fd-select-custom-class]': 'true',
+ '[class.fd-select-custom-class]': 'inline',
'[class.fd-select-custom-class--mobile]': 'mobile'
},
providers: [
@@ -192,6 +192,10 @@ export class SelectComponent
@Input()
mobileConfig: MobileModeConfig = { hasCloseButton: true };
+ /** Should select be inlined. */
+ @Input()
+ inline = true;
+
/** Event emitted when the popover open state changes. */
@Output()
readonly isOpenChange: EventEmitter = new EventEmitter();
diff --git a/libs/core/src/lib/token/tokenizer.component.ts b/libs/core/src/lib/token/tokenizer.component.ts
index d71c255d5ec..3ec77e6facb 100644
--- a/libs/core/src/lib/token/tokenizer.component.ts
+++ b/libs/core/src/lib/token/tokenizer.component.ts
@@ -178,6 +178,8 @@ export class TokenizerComponent
}
this.tokenListChangesSubscription = this.tokenList.changes.subscribe(() => {
this._cdRef.detectChanges();
+ this.moreTokensLeft = [];
+ this.moreTokensRight = [];
this.previousTokenCount > this.tokenList.length ? this._expandTokens() : this._collapseTokens();
this.previousTokenCount = this.tokenList.length;
this.handleTokenClickSubscriptions();
@@ -249,7 +251,7 @@ export class TokenizerComponent
return [this.class];
}
- elementRef(): ElementRef {
+ elementRef(): ElementRef {
return this._elementRef;
}
@@ -456,7 +458,7 @@ export class TokenizerComponent
let combinedTokenWidth = this.getCombinedTokenWidth(); // the combined width of all tokens, the "____ more" text, and the input
let i = 0;
/*
- When resizing, we want to collapse the tokens on the left first. However when the user is navigating through a
+ When resizing, we want to collapse the tokens on the left first. However when the user is navigating through
a group of overflowing tokens using the arrow left key, we may need to hide tokens on the right. So if this
function has been called with the param 'right' it will collapse tokens from the right side of the list rather
than the (default) left side.
@@ -468,6 +470,7 @@ export class TokenizerComponent
// loop through the tokens and hide them until the combinedTokenWidth fits in the elementWidth
const token = this.tokenList.find((item, index) => index === i);
const moreTokens = side === 'right' ? this.moreTokensRight : this.moreTokensLeft;
+
if (moreTokens.indexOf(token) === -1) {
moreTokens.push(token);
}
@@ -625,11 +628,8 @@ export class TokenizerComponent
}
this.tokenList.forEach((token, indexOfToken) => {
- if (indexOfToken >= this._firstElementInSelection && indexOfToken <= this._lastElementInSelection) {
- token.selected = true;
- } else {
- token.selected = false;
- }
+ token.selected =
+ indexOfToken >= this._firstElementInSelection && indexOfToken <= this._lastElementInSelection;
});
this._ctrlPrevious = false;
}
diff --git a/libs/platform/src/lib/form/date-picker/date-picker.component.html b/libs/platform/src/lib/form/date-picker/date-picker.component.html
index 4785291ca76..99cf50d70ba 100644
--- a/libs/platform/src/lib/form/date-picker/date-picker.component.html
+++ b/libs/platform/src/lib/form/date-picker/date-picker.component.html
@@ -31,6 +31,7 @@
[disableFunction]="disableFunction"
[disableRangeStartFunction]="disableRangeStartFunction"
[disableRangeEndFunction]="disableRangeEndFunction"
+ [inline]="inline"
[(ngModel)]="value"
(ngModelChange)="handleDateChange($event)"
(isOpenChange)="handleOpenChange($event)"
diff --git a/libs/platform/src/lib/form/date-picker/date-picker.component.ts b/libs/platform/src/lib/form/date-picker/date-picker.component.ts
index 262675c65e2..08e69c966c0 100644
--- a/libs/platform/src/lib/form/date-picker/date-picker.component.ts
+++ b/libs/platform/src/lib/form/date-picker/date-picker.component.ts
@@ -204,6 +204,10 @@ export class PlatformDatePickerComponent extends BaseInput {
@Input()
isOpen = false;
+ /** Should date picker be inlined. */
+ @Input()
+ inline = true;
+
/** Event emitted when the state of the isOpen property changes. */
@Output()
readonly isOpenChange = new EventEmitter();
diff --git a/libs/platform/src/lib/form/form-generator/base-dynamic-form-generator-control.ts b/libs/platform/src/lib/form/form-generator/base-dynamic-form-generator-control.ts
index d51c049c82b..bcaf225294d 100644
--- a/libs/platform/src/lib/form/form-generator/base-dynamic-form-generator-control.ts
+++ b/libs/platform/src/lib/form/form-generator/base-dynamic-form-generator-control.ts
@@ -39,5 +39,6 @@ export abstract class BaseDynamicFormGeneratorControl implements BaseDynamicForm
*/
@Input() formField: FormField;
+ /** @description Inner form group name */
@Input() formGroupName: string;
}
diff --git a/libs/platform/src/lib/form/form-generator/config/default-components-list.ts b/libs/platform/src/lib/form/form-generator/config/default-components-list.ts
index 7727aa36c35..992ab2ba08d 100644
--- a/libs/platform/src/lib/form/form-generator/config/default-components-list.ts
+++ b/libs/platform/src/lib/form/form-generator/config/default-components-list.ts
@@ -6,6 +6,7 @@ import { DynamicFormGeneratorSelectComponent } from '../dynamic-form-generator-s
import { FormComponentDefinition } from '../interfaces/form-component-definition';
import { DynamicFormGeneratorDatepickerComponent } from '../dynamic-form-generator-datepicker/dynamic-form-generator-datepicker.component';
import { DynamicFormGeneratorSwitchComponent } from '../dynamic-form-generator-switch/dynamic-form-generator-switch.component';
+import { DynamicFormGeneratorMultiInputComponent } from '../dynamic-form-generator-multi-input/dynamic-form-generator-multi-input.component';
export const DEFAULT_COMPONENTS_LIST: FormComponentDefinition[] = [
{
@@ -35,5 +36,9 @@ export const DEFAULT_COMPONENTS_LIST: FormComponentDefinition[] = [
{
types: ['switch'],
component: DynamicFormGeneratorSwitchComponent
+ },
+ {
+ types: ['multi-input'],
+ component: DynamicFormGeneratorMultiInputComponent
}
];
diff --git a/libs/platform/src/lib/form/form-generator/dynamic-form-generator-datepicker/dynamic-form-generator-datepicker.component.html b/libs/platform/src/lib/form/form-generator/dynamic-form-generator-datepicker/dynamic-form-generator-datepicker.component.html
index 06e56872927..f9840f97d53 100644
--- a/libs/platform/src/lib/form/form-generator/dynamic-form-generator-datepicker/dynamic-form-generator-datepicker.component.html
+++ b/libs/platform/src/lib/form/form-generator/dynamic-form-generator-datepicker/dynamic-form-generator-datepicker.component.html
@@ -5,6 +5,7 @@
[placeholder]="formItem.placeholder || formItem.message"
[name]="name"
[formControlName]="name"
+ [inline]="formItem.guiOptions?.inline !== false"
[allowNull]="false"
>
diff --git a/libs/platform/src/lib/form/form-generator/dynamic-form-generator-multi-input/dynamic-form-generator-multi-input.component.html b/libs/platform/src/lib/form/form-generator/dynamic-form-generator-multi-input/dynamic-form-generator-multi-input.component.html
new file mode 100644
index 00000000000..aae3b42d259
--- /dev/null
+++ b/libs/platform/src/lib/form/form-generator/dynamic-form-generator-multi-input/dynamic-form-generator-multi-input.component.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/libs/platform/src/lib/form/form-generator/dynamic-form-generator-multi-input/dynamic-form-generator-multi-input.component.ts b/libs/platform/src/lib/form/form-generator/dynamic-form-generator-multi-input/dynamic-form-generator-multi-input.component.ts
new file mode 100644
index 00000000000..0635c85e4f0
--- /dev/null
+++ b/libs/platform/src/lib/form/form-generator/dynamic-form-generator-multi-input/dynamic-form-generator-multi-input.component.ts
@@ -0,0 +1,16 @@
+import { Component, ViewEncapsulation } from '@angular/core';
+
+import { BaseDynamicFormGeneratorControl } from '../base-dynamic-form-generator-control';
+import { dynamicFormFieldProvider, dynamicFormGroupChildProvider } from '../providers/providers';
+
+@Component({
+ selector: 'fdp-dynamic-form-generator-multi-input',
+ templateUrl: './dynamic-form-generator-multi-input.component.html',
+ encapsulation: ViewEncapsulation.None,
+ viewProviders: [dynamicFormFieldProvider, dynamicFormGroupChildProvider]
+})
+export class DynamicFormGeneratorMultiInputComponent extends BaseDynamicFormGeneratorControl {
+ constructor() {
+ super();
+ }
+}
diff --git a/libs/platform/src/lib/form/form-generator/dynamic-form-generator-select/dynamic-form-generator-select.component.html b/libs/platform/src/lib/form/form-generator/dynamic-form-generator-select/dynamic-form-generator-select.component.html
index f65ebbb420d..53b85d8fcb2 100644
--- a/libs/platform/src/lib/form/form-generator/dynamic-form-generator-select/dynamic-form-generator-select.component.html
+++ b/libs/platform/src/lib/form/form-generator/dynamic-form-generator-select/dynamic-form-generator-select.component.html
@@ -3,6 +3,7 @@
{
- const obj = formItem.onchange(value, this.forms);
+ const obj = formItem.onchange(value, this.forms, formControl);
await this._getFunctionValue(obj);
});
@@ -217,6 +217,7 @@ export class FormGeneratorService implements OnDestroy {
if (formItem.transformer) {
const obj = formItem.transformer(formValue[i], formValue, formItem);
+
formValue[i] = await this._getFunctionValue(obj);
}
@@ -253,13 +254,10 @@ export class FormGeneratorService implements OnDestroy {
c.types.filter((t) => types.includes(t))
);
- for (const existingComponent of existingComponents) {
+ existingComponents.forEach((existingComponent, index) => {
existingComponent.types = existingComponent.types.filter((t) => !types.includes(t));
-
- const index = this._formComponentDefinitions.findIndex((c) => c.component === existingComponent.component);
-
this._formComponentDefinitions[index] = existingComponent;
- }
+ });
this._formComponentDefinitions.push({
types,
diff --git a/libs/platform/src/lib/form/form-generator/form-generator/form-generator.component.html b/libs/platform/src/lib/form/form-generator/form-generator/form-generator.component.html
index 6570570ca76..466314dc4d6 100644
--- a/libs/platform/src/lib/form/form-generator/form-generator/form-generator.component.html
+++ b/libs/platform/src/lib/form/form-generator/form-generator/form-generator.component.html
@@ -17,6 +17,7 @@
[fieldColumnLayout]="fieldColumnLayout"
[gapColumnLayout]="gapColumnLayout"
[unifiedLayout]="unifiedLayout"
+ [inlineColumnLayout]="inlineColumnLayout"
>
@@ -75,7 +76,7 @@
-
+
diff --git a/libs/platform/src/lib/form/form-generator/form-generator/form-generator.component.ts b/libs/platform/src/lib/form/form-generator/form-generator/form-generator.component.ts
index 9500a6387ac..02b57b22061 100644
--- a/libs/platform/src/lib/form/form-generator/form-generator/form-generator.component.ts
+++ b/libs/platform/src/lib/form/form-generator/form-generator/form-generator.component.ts
@@ -140,10 +140,21 @@ export class FormGeneratorComponent implements OnDestroy {
@Input()
gapColumnLayout: ColumnLayout = DefaultGapLayout;
- /** Whether or not all form items should have identical layout provided for form group. */
+ /** Whether all form items should have identical layout provided for form group. */
@Input()
unifiedLayout = true;
+ @Input()
+ inlineColumnLayout: ColumnLayout = DefaultVerticalFieldLayout;
+
+ /**
+ * @hidden
+ * Removes extra empty row.
+ * TODO: remove after #7533 has been fixed.
+ */
+ @Input()
+ noAdditionalContent = false;
+
/**
* @description Event which notifies parent component that the form has been successfuly created
* and all controls are in place.
@@ -218,6 +229,14 @@ export class FormGeneratorComponent implements OnDestroy {
this._onDestroy$.complete();
}
+ /**
+ * Refreshes form items visibility with 'when' condition.
+ */
+ async refreshStepsVisibility(): Promise {
+ this.shouldShowFields = await this._fgService.checkVisibleFormItems(this.form);
+ this._cd.markForCheck();
+ }
+
/**
* @hidden
*/
diff --git a/libs/platform/src/lib/form/form-generator/interfaces/dynamic-form-item.ts b/libs/platform/src/lib/form/form-generator/interfaces/dynamic-form-item.ts
index af7cdb44c30..acf6a62d65b 100644
--- a/libs/platform/src/lib/form/form-generator/interfaces/dynamic-form-item.ts
+++ b/libs/platform/src/lib/form/form-generator/interfaces/dynamic-form-item.ts
@@ -5,6 +5,7 @@ import { ContentDensity } from '@fundamental-ngx/core/utils';
import { InlineLayout, ColumnLayout, HintPlacement, LabelLayout, SelectItem } from '@fundamental-ngx/platform/shared';
import { InputType } from '../../input/input.component';
import { DynamicFormGroup } from './dynamic-form-group';
+import { DynamicFormControl } from '../dynamic-form-control';
export type DynamicFormItemChoices = number | string | SelectItem;
export type DynamicFormItemValidationResult = null | boolean | string;
@@ -169,14 +170,18 @@ export interface DynamicFormFieldItem {
* @param formValue the form value hash.
* @returns Boolean
*/
- when?: (formValue?: DynamicFormValue) => boolean | Promise | Observable;
+ when?: (formValue: DynamicFormValue) => boolean | Promise | Observable;
/**
* @description Callback function that is triggered after field value has been changed.
* @param fieldValue Field value.
* @param formGeneratorService Form generator service instance.
*/
- onchange?: (fieldValue?: any, forms?: Map) => void | Promise | Observable;
+ onchange?: (
+ fieldValue: any,
+ forms: Map,
+ control: DynamicFormControl
+ ) => void | Promise | Observable;
/**
* @hidden
diff --git a/libs/platform/src/lib/form/form-group/form-field/form-field.component.html b/libs/platform/src/lib/form/form-group/form-field/form-field.component.html
index 53762a0544e..42e380bfdd7 100644
--- a/libs/platform/src/lib/form/form-group/form-field/form-field.component.html
+++ b/libs/platform/src/lib/form/form-group/form-field/form-field.component.html
@@ -1,6 +1,6 @@
-
+
{
- overall.push(`fd-col-${ColumnLayoutGridClass[value[0]]}--${value[1]}`);
- return overall;
- }, [])
- .join(' ');
- }
-
- /**
- * @hidden
- */
- private _normalizeColumnLayout(layout: ColumnLayout, defaultColumn = 12): ColumnLayout {
- layout['S'] = layout['S'] !== undefined ? layout['S'] : defaultColumn;
- layout['M'] = layout['M'] || layout['S'];
- layout['L'] = layout['L'] || layout['M'];
- layout['XL'] = layout['XL'] || layout['L'];
-
- return layout;
- }
-
/** @hidden */
private _setLayout(): void {
try {
- this.columnLayout = this._normalizeColumnLayout(this.columnLayout, 1);
+ this.columnLayout = normalizeColumnLayout(this.columnLayout, 1);
this._sColumnNumber = this.columnLayout['S'];
this._mdColumnNumber = this.columnLayout['M'];
this._lgColumnNumber = this.columnLayout['L'];
diff --git a/libs/platform/src/lib/form/form-group/form-group.component.html b/libs/platform/src/lib/form/form-group/form-group.component.html
index fd31c90e9ff..f99e26c41ed 100644
--- a/libs/platform/src/lib/form/form-group/form-group.component.html
+++ b/libs/platform/src/lib/form/form-group/form-group.component.html
@@ -28,6 +28,7 @@
diff --git a/libs/platform/src/lib/form/form-group/form-group.component.ts b/libs/platform/src/lib/form/form-group/form-group.component.ts
index ba36adf7c92..026ef1806ef 100644
--- a/libs/platform/src/lib/form/form-group/form-group.component.ts
+++ b/libs/platform/src/lib/form/form-group/form-group.component.ts
@@ -72,6 +72,7 @@ import {
DefaultVerticalLabelLayout,
FORM_GROUP_CHILD_FIELD_TOKEN
} from './constants';
+import { normalizeColumnLayout, generateColumnClass } from './helpers';
export const formGroupProvider: Provider = {
provide: FormGroupContainer,
@@ -211,6 +212,25 @@ export class FormGroupComponent
this._labelLayout === 'horizontal' ? DefaultHorizontalFieldLayout : DefaultVerticalFieldLayout;
}
+ /**
+ * Defines column layout for inline items.
+ */
+ @Input()
+ set inlineColumnLayout(value: ColumnLayout) {
+ this._inlineColumnLayout = normalizeColumnLayout(value);
+ this._updateInlineColumnLayout();
+ }
+
+ get inlineColumnLayout(): ColumnLayout {
+ return this._inlineColumnLayout;
+ }
+
+ /** @hidden */
+ _inlineColumnLayoutClass: string;
+
+ /** @hidden */
+ private _inlineColumnLayout = DefaultVerticalFieldLayout;
+
/**
* Defines label's column layout.
*/
@@ -394,6 +414,7 @@ export class FormGroupComponent
);
}
this.buildComponentCssClass();
+ this._updateInlineColumnLayout();
}
/** @hidden */
@@ -666,4 +687,9 @@ export class FormGroupComponent
return children.filter((child) => this._formGroupDirectChildren.includes(child));
}
+
+ /** @hidden */
+ private _updateInlineColumnLayout(): void {
+ this._inlineColumnLayoutClass = generateColumnClass(this.inlineColumnLayout);
+ }
}
diff --git a/libs/platform/src/lib/form/form-group/helpers.ts b/libs/platform/src/lib/form/form-group/helpers.ts
new file mode 100644
index 00000000000..1c0c6891693
--- /dev/null
+++ b/libs/platform/src/lib/form/form-group/helpers.ts
@@ -0,0 +1,19 @@
+import { ColumnLayout, ColumnLayoutGridClass } from '@fundamental-ngx/platform/shared';
+
+export function normalizeColumnLayout(layout: ColumnLayout, defaultColumn = 12): ColumnLayout {
+ layout['S'] = layout['S'] !== undefined ? layout['S'] : defaultColumn;
+ layout['M'] = layout['M'] || layout['S'];
+ layout['L'] = layout['L'] || layout['M'];
+ layout['XL'] = layout['XL'] || layout['L'];
+
+ return layout;
+}
+
+export function generateColumnClass(layout: ColumnLayout): string {
+ return Object.entries(layout)
+ .reduce((overall, value) => {
+ overall.push(`fd-col-${ColumnLayoutGridClass[value[0]]}--${value[1]}`);
+ return overall;
+ }, [])
+ .join(' ');
+}
diff --git a/libs/platform/src/lib/form/multi-input/multi-input.component.ts b/libs/platform/src/lib/form/multi-input/multi-input.component.ts
index 4a003ce9861..6d591e3d08b 100644
--- a/libs/platform/src/lib/form/multi-input/multi-input.component.ts
+++ b/libs/platform/src/lib/form/multi-input/multi-input.component.ts
@@ -30,7 +30,8 @@ import {
FormField,
FormFieldControl,
MultiInputDataSource,
- MultiInputOption
+ MultiInputOption,
+ isFunction
} from '@fundamental-ngx/platform/shared';
import { ListComponent, SelectionType } from '@fundamental-ngx/platform/list';
@@ -135,6 +136,10 @@ export class PlatformMultiInputComponent extends BaseMultiInput implements OnIni
@Input()
closeOnOutsideClick = true;
+ /** Callback function when add-on button clicked. */
+ @Input()
+ addOnButtonClickFn: () => void;
+
/** @hidden */
@ViewChild(TokenizerComponent)
tokenizer: TokenizerComponent;
@@ -231,6 +236,11 @@ export class PlatformMultiInputComponent extends BaseMultiInput implements OnIni
/** @hidden */
addOnButtonClick(): void {
+ if (isFunction(this.addOnButtonClickFn)) {
+ this.addOnButtonClickFn();
+ return;
+ }
+
this.showList(!this.isOpen);
}
diff --git a/libs/platform/src/lib/form/public_api.ts b/libs/platform/src/lib/form/public_api.ts
index d493a2f2844..b1656611f0c 100644
--- a/libs/platform/src/lib/form/public_api.ts
+++ b/libs/platform/src/lib/form/public_api.ts
@@ -39,6 +39,7 @@ export * from './form-generator/dynamic-form-generator-input/dynamic-form-genera
export * from './form-generator/dynamic-form-generator-radio/dynamic-form-generator-radio.component';
export * from './form-generator/dynamic-form-generator-select/dynamic-form-generator-select.component';
export * from './form-generator/dynamic-form-generator-switch/dynamic-form-generator-switch.component';
+export * from './form-generator/dynamic-form-generator-multi-input/dynamic-form-generator-multi-input.component';
export * from './form-generator/form-generator/form-generator.component';
export * from './form-generator/interfaces/dynamic-abstract-control';
export * from './form-generator/interfaces/dynamic-form-group';
diff --git a/libs/platform/src/lib/form/select/select/select.component.html b/libs/platform/src/lib/form/select/select/select.component.html
index 8c4991de178..9e3c36f9e0e 100644
--- a/libs/platform/src/lib/form/select/select/select.component.html
+++ b/libs/platform/src/lib/form/select/select/select.component.html
@@ -19,6 +19,7 @@
[stateMessage]="stateMessage"
[mobile]="mobile"
[mobileConfig]="mobileConfig"
+ [inline]="inline"
[(ngModel)]="value"
(valueChange)="_onSelection($event)"
>
@@ -29,7 +30,7 @@
diff --git a/libs/platform/src/lib/form/select/select/select.component.ts b/libs/platform/src/lib/form/select/select/select.component.ts
index 39f5855095a..06830a68e1b 100644
--- a/libs/platform/src/lib/form/select/select/select.component.ts
+++ b/libs/platform/src/lib/form/select/select/select.component.ts
@@ -62,6 +62,10 @@ export class SelectComponent extends BaseSelect implements AfterViewInit, AfterV
this.setValue(newValue);
}
+ /** Should select be inlined. */
+ @Input()
+ inline = true;
+
@ViewChild(CoreSelect, { static: true })
select: CoreSelect;
diff --git a/libs/platform/src/lib/form/time-picker/time-picker.component.spec.ts b/libs/platform/src/lib/form/time-picker/time-picker.component.spec.ts
index 16e3af5a030..817ed40b88b 100644
--- a/libs/platform/src/lib/form/time-picker/time-picker.component.spec.ts
+++ b/libs/platform/src/lib/form/time-picker/time-picker.component.spec.ts
@@ -111,8 +111,6 @@ describe('PlatformTimePickerComponent', () => {
await wait(fixture);
- console.log(component.result);
-
expect(component.result).toEqual({ timePicker: time });
});
diff --git a/libs/platform/src/lib/fundamental-ngx.module.ts b/libs/platform/src/lib/fundamental-ngx.module.ts
index 2dc10b19966..85b2359c0da 100644
--- a/libs/platform/src/lib/fundamental-ngx.module.ts
+++ b/libs/platform/src/lib/fundamental-ngx.module.ts
@@ -48,6 +48,7 @@ import { PlatformUploadCollectionModule } from '@fundamental-ngx/platform/upload
import { PlatformValueHelpDialogModule } from '@fundamental-ngx/platform/value-help-dialog';
import { PlatformWizardGeneratorModule } from '@fundamental-ngx/platform/wizard-generator';
import { PlatformIconTabBarModule } from '@fundamental-ngx/platform/icon-tab-bar';
+import { PlatformSmartFilterBarModule } from '@fundamental-ngx/platform/smart-filter-bar';
@NgModule({
imports: [CommonModule, FormsModule],
@@ -96,7 +97,8 @@ import { PlatformIconTabBarModule } from '@fundamental-ngx/platform/icon-tab-bar
PlatformTimePickerModule,
PlatformDatePickerModule,
PlatformFormGeneratorModule,
- PlatformIconTabBarModule
+ PlatformIconTabBarModule,
+ PlatformSmartFilterBarModule
],
providers: []
})
diff --git a/libs/platform/src/lib/public_api.ts b/libs/platform/src/lib/public_api.ts
index 72d32f8439e..07f66f609cc 100644
--- a/libs/platform/src/lib/public_api.ts
+++ b/libs/platform/src/lib/public_api.ts
@@ -27,3 +27,4 @@ export * from '@fundamental-ngx/platform/upload-collection';
export * from '@fundamental-ngx/platform/value-help-dialog';
export * from '@fundamental-ngx/platform/wizard-generator';
export * from '@fundamental-ngx/platform/icon-tab-bar';
+export * from '@fundamental-ngx/platform/smart-filter-bar';
diff --git a/libs/platform/src/lib/search-field/search-field.component.html b/libs/platform/src/lib/search-field/search-field.component.html
index 93cbd166402..ff1fb53e7a0 100644
--- a/libs/platform/src/lib/search-field/search-field.component.html
+++ b/libs/platform/src/lib/search-field/search-field.component.html
@@ -85,7 +85,7 @@
{
diff --git a/libs/platform/src/lib/shared/async-strategy/function-strategy.class.ts b/libs/platform/src/lib/shared/async-strategy/function-strategy.class.ts
index b912810ac55..5d744423f7b 100644
--- a/libs/platform/src/lib/shared/async-strategy/function-strategy.class.ts
+++ b/libs/platform/src/lib/shared/async-strategy/function-strategy.class.ts
@@ -4,8 +4,8 @@ import { isFunction } from '../utils/lang';
/**
* @description Executes function and passes returned value into callback function.
*/
-export class FunctionStrategy implements SubscriptionStrategy {
- createSubscription(fn: () => void, updateLatestValue: (v: any) => any): Promise {
+export class FunctionStrategy implements SubscriptionStrategy {
+ createSubscription(fn: () => T, updateLatestValue: (v: T) => any): Promise {
const result = isFunction(fn) ? fn() : fn;
return Promise.resolve(result).then(updateLatestValue);
}
diff --git a/libs/platform/src/lib/shared/async-strategy/observable-strategy.class.ts b/libs/platform/src/lib/shared/async-strategy/observable-strategy.class.ts
index 37733afc821..84fa1e654d6 100644
--- a/libs/platform/src/lib/shared/async-strategy/observable-strategy.class.ts
+++ b/libs/platform/src/lib/shared/async-strategy/observable-strategy.class.ts
@@ -4,8 +4,8 @@ import { SubscriptionStrategy } from './subscription-strategy.interface';
/**
* @description Converts observable into Promise and passes returned value into callback function.
*/
-export class ObservableStrategy implements SubscriptionStrategy {
- createSubscription(async: Observable, updateLatestValue: any): Promise {
+export class ObservableStrategy implements SubscriptionStrategy {
+ createSubscription(async: Observable, updateLatestValue: any): Promise {
return async.toPromise().then(updateLatestValue, (e) => {
console.error(e);
});
diff --git a/libs/platform/src/lib/shared/async-strategy/promise-strategy.class.ts b/libs/platform/src/lib/shared/async-strategy/promise-strategy.class.ts
index 17bd827b20f..8c15dd7bd0d 100644
--- a/libs/platform/src/lib/shared/async-strategy/promise-strategy.class.ts
+++ b/libs/platform/src/lib/shared/async-strategy/promise-strategy.class.ts
@@ -3,8 +3,8 @@ import { SubscriptionStrategy } from './subscription-strategy.interface';
/**
* @description awaits for promise to resolve it's value and passes returned value into callback function.
*/
-export class PromiseStrategy implements SubscriptionStrategy {
- createSubscription(async: Promise, updateLatestValue: (v: any) => any): Promise {
+export class PromiseStrategy implements SubscriptionStrategy {
+ createSubscription(async: Promise, updateLatestValue: (v: T) => any): Promise {
return async.then(updateLatestValue, (e) => {
console.error(e);
});
diff --git a/libs/platform/src/lib/shared/async-strategy/select-strategy.class.ts b/libs/platform/src/lib/shared/async-strategy/select-strategy.class.ts
index 97c9a43a5dd..4c6dbb1b902 100644
--- a/libs/platform/src/lib/shared/async-strategy/select-strategy.class.ts
+++ b/libs/platform/src/lib/shared/async-strategy/select-strategy.class.ts
@@ -12,18 +12,18 @@ import { ValueStrategy } from './value-strategy.class';
* @param obj object to get value from
* @returns appropriate strategy to retrieve value from the object
*/
-export function selectStrategy(obj: Observable | Promise | (() => void)): SubscriptionStrategy {
+export function selectStrategy(obj: Observable | Promise | (() => void) | T): SubscriptionStrategy {
if (isPromise(obj)) {
- return new PromiseStrategy();
+ return new PromiseStrategy();
}
if (isSubscribable(obj)) {
- return new ObservableStrategy();
+ return new ObservableStrategy();
}
if (isFunction(obj)) {
- return new FunctionStrategy();
+ return new FunctionStrategy();
}
- return new ValueStrategy();
+ return new ValueStrategy();
}
diff --git a/libs/platform/src/lib/shared/async-strategy/subscription-strategy.interface.ts b/libs/platform/src/lib/shared/async-strategy/subscription-strategy.interface.ts
index 187874ccf7b..30fe3515ea1 100644
--- a/libs/platform/src/lib/shared/async-strategy/subscription-strategy.interface.ts
+++ b/libs/platform/src/lib/shared/async-strategy/subscription-strategy.interface.ts
@@ -1,11 +1,14 @@
import { Observable } from 'rxjs';
-export interface SubscriptionStrategy {
+export interface SubscriptionStrategy {
/**
* @description Awaits object to resolve it's value and passes it to the callback function.
* @param obj Object to be awaited.
* @param updateLatestValue Callback function where awaited value will be passed.
* as an argument
*/
- createSubscription(obj: Observable | Promise | (() => void), updateLatestValue: any): Promise;
+ createSubscription(
+ obj: Observable | Promise | (() => void) | T,
+ updateLatestValue: (value: T) => void
+ ): Promise;
}
diff --git a/libs/platform/src/lib/shared/async-strategy/value-strategy.class.ts b/libs/platform/src/lib/shared/async-strategy/value-strategy.class.ts
index 10a37a23fb0..4bc991a2116 100644
--- a/libs/platform/src/lib/shared/async-strategy/value-strategy.class.ts
+++ b/libs/platform/src/lib/shared/async-strategy/value-strategy.class.ts
@@ -3,8 +3,8 @@ import { SubscriptionStrategy } from './subscription-strategy.interface';
/**
* @description Passes value object into callback function.
*/
-export class ValueStrategy implements SubscriptionStrategy {
- createSubscription(value: any, updateLatestValue: (v: any) => any): Promise {
+export class ValueStrategy implements SubscriptionStrategy {
+ createSubscription(value: T, updateLatestValue: (v: T) => any): Promise {
return Promise.resolve(value).then(updateLatestValue);
}
}
diff --git a/libs/platform/src/lib/shared/domain/data-model.ts b/libs/platform/src/lib/shared/domain/data-model.ts
index b948c0cc8e9..5e3d5054b09 100644
--- a/libs/platform/src/lib/shared/domain/data-model.ts
+++ b/libs/platform/src/lib/shared/domain/data-model.ts
@@ -7,7 +7,7 @@ import { isBlank } from './../utils/lang';
*
* Used in various controls: Select, RadioGroup, CheckboxGroup, Combobox
*/
-export interface SelectItem {
+export interface SelectItem {
/**
* Item text shown in the popup
*/
@@ -16,7 +16,7 @@ export interface SelectItem {
/**
* References to the object instance
*/
- value: any;
+ value: T;
disabled?: boolean;
icon?: string;
diff --git a/libs/platform/src/lib/shared/domain/data-source.ts b/libs/platform/src/lib/shared/domain/data-source.ts
index 752030f85d2..955d17125c8 100644
--- a/libs/platform/src/lib/shared/domain/data-source.ts
+++ b/libs/platform/src/lib/shared/domain/data-source.ts
@@ -94,7 +94,7 @@ export function getMatchingStrategyStartsWithPerTermReqexp(value: string): RegEx
return new RegExp(`(\\s|^)(${value})`, 'gi');
}
-export function isDataSource(value: any): value is DataSource {
+export function isDataSource(value: any): value is DataSource {
return value && typeof value.open === 'function';
}
diff --git a/libs/platform/src/lib/smart-filter-bar/.eslintrc.json b/libs/platform/src/lib/smart-filter-bar/.eslintrc.json
new file mode 100644
index 00000000000..815e5424626
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/.eslintrc.json
@@ -0,0 +1,35 @@
+{
+ "extends": ["../../../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": ["plugin:@nrwl/nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
+ "rules": {
+ "@angular-eslint/no-host-metadata-property": "off",
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "fdp",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "fdp",
+ "style": "kebab-case"
+ }
+ ]
+ },
+ "plugins": ["@angular-eslint/eslint-plugin", "@typescript-eslint"]
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nrwl/nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/README.md b/libs/platform/src/lib/smart-filter-bar/README.md
new file mode 100644
index 00000000000..fc25b5e262e
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/README.md
@@ -0,0 +1,29 @@
+# ActionBar
+
+This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.0.5.
+
+## Code scaffolding
+
+Run `ng generate component component-name --project platform-smart-filter-bar` to generate a new component. You can also
+use `ng generate directive|pipe|service|class|guard|interface|enum|module --project platform-smart-filter-bar`.
+
+> Note: Don't forget to add `--project platform-smart-filter-bar` or else it will be added to the default project in your `angular.json` file.
+
+## Build
+
+Run `ng build platform-smart-filter-bar` to build the project. The build artifacts will be stored in the `dist/`
+directory.
+
+## Publishing
+
+After building your library with `ng build platform-smart-filter-bar`, go to the dist
+folder `cd dist/platform-smart-filter-bar` and run `npm publish`.
+
+## Running unit tests
+
+Run `ng test platform-smart-filter-bar` to execute the unit tests via [Karma](https://karma-runner.github.io).
+
+## Further help
+
+To get more help on the Angular CLI use `ng help` or go check out
+the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
diff --git a/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-condition-field/base-smart-filter-bar-condition-field.ts b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-condition-field/base-smart-filter-bar-condition-field.ts
new file mode 100644
index 00000000000..021a451311b
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-condition-field/base-smart-filter-bar-condition-field.ts
@@ -0,0 +1,96 @@
+import { Directive } from '@angular/core';
+import { DialogService } from '@fundamental-ngx/core/dialog';
+import {
+ BaseDynamicFormGeneratorControl,
+ DynamicFormControl,
+ dynamicFormFieldProvider,
+ dynamicFormGroupChildProvider
+} from '@fundamental-ngx/platform/form';
+import { take } from 'rxjs/operators';
+import { SmartFilterBarCondition, SmartFilterBarConditionBuilder } from '../../interfaces/smart-filter-bar-condition';
+import { SmartFilterBarConditionsDialogComponent } from '../smart-filter-bar-conditions-dialog/smart-filter-bar-conditions-dialog.component';
+import { SmartFilterBar } from '../../smart-filter-bar.class';
+import { smartFilterBarProvider } from '../../providers/smart-filter-bar.provider';
+import { isSelectItem, SelectItem } from '@fundamental-ngx/platform/shared';
+
+@Directive({
+ providers: [dynamicFormFieldProvider, dynamicFormGroupChildProvider, smartFilterBarProvider]
+})
+export abstract class BaseSmartFilterBarConditionField extends BaseDynamicFormGeneratorControl {
+ protected constructor(protected _dialogService: DialogService, protected _smartFilterBar: SmartFilterBar) {
+ super();
+ }
+
+ /**
+ * Method for opening conditions configuration dialog.
+ */
+ openConditionsDialog: () => void = () => {
+ const currentValue = this.getField().value;
+
+ const dialogData: SmartFilterBarConditionBuilder = {
+ header: this.formItem.message as string,
+ dataType: this.formItem.guiOptions?.additionalData.dataType,
+ filterType: this.formItem.guiOptions?.additionalData.type,
+ conditions: currentValue || [],
+ choices: this.formItem.guiOptions?.additionalData.choices,
+ controlType: this.formItem.guiOptions?.additionalData.controlType,
+ defineStrategyLabels: this._smartFilterBar.defineStrategyLabels
+ };
+
+ const dialogRef = this._dialogService.open(SmartFilterBarConditionsDialogComponent, {
+ data: dialogData,
+ width: '67.5rem',
+ minHeight: '30%'
+ });
+
+ dialogRef.afterClosed.pipe(take(1)).subscribe(
+ async (conditions: SmartFilterBarCondition[]) => {
+ const field = this.getField();
+ const normalizedConditions = (await this.normalizeConditions(conditions)).map((c: any) => {
+ if (isSelectItem(c)) {
+ return c;
+ }
+
+ const condition: SelectItem = {
+ label: c.displayValue,
+ value: c
+ };
+
+ return condition;
+ });
+ field.setValue(normalizedConditions);
+ },
+ (_) => {}
+ );
+ };
+
+ /**
+ * Method for retrieving current form control of the generated form.
+ * @returns Found form control.
+ */
+ getField(): DynamicFormControl {
+ return this.form.get(`${this.formGroupName}.${this.name}`) as DynamicFormControl;
+ }
+
+ /**
+ * Method for normalizing generated conditions to display in human-readable format.
+ * @param conditions Array of generated conditions.
+ * @returns Formatted array of conditions.
+ */
+ async normalizeConditions(conditions: SmartFilterBarCondition[]): Promise {
+ const normalizedConditions: SmartFilterBarCondition[] = [];
+
+ for (const condition of conditions) {
+ const newCondition = Object.assign({}, condition);
+
+ newCondition.displayValue = await this._smartFilterBar.getDisplayValue(
+ newCondition,
+ this.formItem.guiOptions?.additionalData.type
+ );
+
+ normalizedConditions.push(newCondition);
+ }
+
+ return normalizedConditions;
+ }
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-condition-field/smart-filter-bar-condition-field.component.html b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-condition-field/smart-filter-bar-condition-field.component.html
new file mode 100644
index 00000000000..ff50f3229ad
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-condition-field/smart-filter-bar-condition-field.component.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-condition-field/smart-filter-bar-condition-field.component.ts b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-condition-field/smart-filter-bar-condition-field.component.ts
new file mode 100644
index 00000000000..cb25884e0a1
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-condition-field/smart-filter-bar-condition-field.component.ts
@@ -0,0 +1,16 @@
+import { Component, ViewEncapsulation } from '@angular/core';
+import { DialogService } from '@fundamental-ngx/core/dialog';
+import { BaseSmartFilterBarConditionField } from './base-smart-filter-bar-condition-field';
+import { SmartFilterBar } from '../../smart-filter-bar.class';
+
+@Component({
+ selector: 'fdp-smart-filter-bar-condition-field',
+ templateUrl: './smart-filter-bar-condition-field.component.html',
+ encapsulation: ViewEncapsulation.None
+})
+export class SmartFilterBarConditionFieldComponent extends BaseSmartFilterBarConditionField {
+ /** @hidden */
+ constructor(dialogService: DialogService, smartFilterBar: SmartFilterBar) {
+ super(dialogService, smartFilterBar);
+ }
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-conditions-dialog/smart-filter-bar-conditions-dialog.component.html b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-conditions-dialog/smart-filter-bar-conditions-dialog.component.html
new file mode 100644
index 00000000000..f05ffc3e827
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-conditions-dialog/smart-filter-bar-conditions-dialog.component.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+ {{ config.header }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-conditions-dialog/smart-filter-bar-conditions-dialog.component.spec.ts b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-conditions-dialog/smart-filter-bar-conditions-dialog.component.spec.ts
new file mode 100644
index 00000000000..f171570ff95
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-conditions-dialog/smart-filter-bar-conditions-dialog.component.spec.ts
@@ -0,0 +1,132 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SmartFilterBarConditionsDialogComponent } from './smart-filter-bar-conditions-dialog.component';
+import {
+ DynamicFormFieldItem,
+ FilterableColumnDataType,
+ PlatformSmartFilterBarModule,
+ SmartFilterBarConditionBuilder
+} from '@fundamental-ngx/platform';
+import { DialogConfig, DialogRef } from '@fundamental-ngx/core';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { whenStable } from '@fundamental-ngx/core/tests';
+
+const mockData: SmartFilterBarConditionBuilder = {
+ header: 'Test',
+ dataType: FilterableColumnDataType.STRING,
+ filterType: 'input',
+ conditions: [
+ {
+ value: 'first condition value one',
+ value2: 'first condition value two',
+ operator: 'between'
+ },
+ {
+ value: 'second condition value one',
+ value2: undefined,
+ operator: 'equalTo'
+ }
+ ],
+ choices: [],
+ controlType: 'text',
+ defineStrategyLabels: {
+ contains: 'contains',
+ equalTo: 'equal to',
+ between: 'between',
+ beginsWith: 'starts with',
+ endsWith: 'ends with',
+ lessThan: 'less than',
+ lessThanOrEqualTo: 'less than or equal to',
+ greaterThan: 'greater than',
+ greaterThanOrEqualTo: 'greater than or equal to',
+ after: 'after',
+ onOrAfter: 'on or after',
+ before: 'before',
+ beforeOrOn: 'before or on'
+ }
+};
+
+describe('SmartFilterBarConditionsDialogComponent', () => {
+ let component: SmartFilterBarConditionsDialogComponent;
+ let fixture: ComponentFixture;
+
+ const dialogConfig = new DialogConfig();
+
+ const dialogRefMock: DialogRef = new DialogRef();
+ dialogRefMock.data = mockData;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [NoopAnimationsModule, PlatformSmartFilterBarModule],
+ declarations: [SmartFilterBarConditionsDialogComponent],
+ providers: [
+ {
+ provide: DialogRef,
+ useValue: dialogRefMock
+ },
+ {
+ provide: DialogConfig,
+ useValue: dialogConfig
+ }
+ ]
+ }).compileComponents();
+ });
+
+ beforeEach(async () => {
+ fixture = TestBed.createComponent(SmartFilterBarConditionsDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ await whenStable(fixture);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should add predefined conditions', () => {
+ expect(component._formItems.length).toEqual(2);
+ });
+
+ it('should properly parse predefined conditions', () => {
+ const formItems = component._formItems;
+
+ formItems.forEach((form: DynamicFormFieldItem[], index: number) => {
+ const operator = form.find((field) => field.name === 'operator').default;
+ const firstValue = form.find((field) => field.name === 'value').default;
+ const secondValue = form.find((field) => field.name === 'value2').default;
+
+ expect(operator).toEqual(mockData.conditions[index].operator);
+ expect(firstValue).toEqual(mockData.conditions[index].value);
+ expect(secondValue).toEqual(mockData.conditions[index].value2);
+ });
+ });
+
+ it('should add empty condition', () => {
+ component.addCondition();
+ expect(component._formItems.length).toEqual(3);
+ });
+
+ it('should remove condition', () => {
+ component.removeCondition(0);
+
+ expect(component._formItems.length).toEqual(1);
+ expect((component._formItems[0][1] as DynamicFormFieldItem).default).toEqual(mockData.conditions[1].value);
+ });
+
+ it('should close dialog when all forms are submitted', async () => {
+ const spy = spyOn(component, '_onFormSubmitted').and.callThrough();
+ const dialogSpy = spyOn((component as any)._dialogRef, 'close');
+
+ const formValue = mockData.conditions.map((condition: any) => {
+ condition.value1 = condition.value;
+ return condition;
+ });
+
+ component.applyConditions();
+
+ await new Promise((resolve) => setTimeout(() => resolve(null), 200));
+
+ expect(spy).toHaveBeenCalled();
+ expect(dialogSpy).toHaveBeenCalledOnceWith(formValue);
+ });
+});
diff --git a/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-conditions-dialog/smart-filter-bar-conditions-dialog.component.ts b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-conditions-dialog/smart-filter-bar-conditions-dialog.component.ts
new file mode 100644
index 00000000000..e924ce9c002
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-conditions-dialog/smart-filter-bar-conditions-dialog.component.ts
@@ -0,0 +1,217 @@
+import { ChangeDetectionStrategy, Component, QueryList, ViewChildren, ViewEncapsulation } from '@angular/core';
+
+import { FilterAllStrategy, FILTER_STRATEGY } from '@fundamental-ngx/platform/table';
+import { SelectItem } from '@fundamental-ngx/platform/shared';
+import { DialogRef } from '@fundamental-ngx/core/dialog';
+import { DynamicFormControl, DynamicFormItem, FormGeneratorComponent } from '@fundamental-ngx/platform/form';
+import { SmartFilterBarCondition, SmartFilterBarConditionBuilder } from '../../interfaces/smart-filter-bar-condition';
+import { SmartFilterBarService } from '../../smart-filter-bar.service';
+import { getSelectItemValue } from '../../helpers';
+
+@Component({
+ selector: 'fdp-smart-filter-bar-conditions-dialog',
+ templateUrl: './smart-filter-bar-conditions-dialog.component.html',
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class SmartFilterBarConditionsDialogComponent {
+ /** Condition builder config. */
+ config: SmartFilterBarConditionBuilder;
+
+ /** Available condition operator options. */
+ conditionOperatorOptions: SelectItem[] = [];
+
+ /** @hidden */
+ _formItems: DynamicFormItem[][] = [];
+
+ /**
+ * Generated condition forms.
+ */
+ @ViewChildren(FormGeneratorComponent)
+ formGenerators!: QueryList;
+
+ /** @hidden */
+ private _submittedForms: any[] = [];
+
+ /** @hidden */
+ constructor(
+ private _dialogRef: DialogRef,
+ private _smartFilterBarService: SmartFilterBarService
+ ) {
+ this.config = this._dialogRef.data;
+
+ this.conditionOperatorOptions = this._getApplicableConditionOperators();
+
+ this._addExistingConditions(getSelectItemValue(this.config.conditions));
+
+ if (this.config.conditions.length === 0) {
+ // Add first empty condition
+ this.addCondition();
+ }
+ }
+
+ /**
+ * Adds empty condition.
+ */
+ addCondition(): void {
+ this._formItems.push(this._generateFormGeneratorItems());
+ }
+
+ /**
+ * Removes condition at defined index
+ * @param index Index of the condition in the array.
+ */
+ removeCondition(index: number): void {
+ this._formItems.splice(index, 1);
+
+ if (this._formItems.length === 0) {
+ this.addCondition();
+ }
+ }
+
+ /**
+ * Submits all condition forms.
+ */
+ applyConditions(): void {
+ this._submittedForms = [];
+ this.formGenerators.toArray().forEach((formGenerator) => formGenerator.submit());
+ }
+
+ /**
+ * @hidden
+ * Callback function when form generator form has been submitted.
+ * Will close dialog in case if all forms are valid.
+ * @param form Form generator form value.
+ */
+ _onFormSubmitted(form: SmartFilterBarCondition): void {
+ this._submittedForms.push(form);
+
+ if (this._submittedForms.length === this.formGenerators.length) {
+ const formsResult = this._submittedForms.map((f) => {
+ f.value = f.value1 !== undefined ? f.value1 : f.value;
+ return f;
+ });
+
+ this._dialogRef.close(
+ formsResult.filter((c: SmartFilterBarCondition) => c.value !== undefined && c.value !== null)
+ );
+ }
+ }
+
+ /** @hidden */
+ _cancel(): void {
+ this._dialogRef.dismiss();
+ }
+
+ /**
+ * @hidden
+ * Returns applicable condition options of the filter.
+ * @returns Array of applicable condition options.
+ */
+ private _getApplicableConditionOperators(): SelectItem[] {
+ const strategy = this._smartFilterBarService.getApplicableFilterConditions(
+ this.config.filterType,
+ this.config.dataType
+ );
+
+ const selectItems: SelectItem[] = [];
+
+ strategy.forEach((s: FilterAllStrategy) => {
+ selectItems.push({
+ label: this.config.defineStrategyLabels[s],
+ value: s
+ });
+ });
+
+ return selectItems;
+ }
+
+ /**
+ * Transforms filter conditions into form generator form items array.
+ * @param conditions existing smart filter bar filter conditions.
+ */
+ private _addExistingConditions(conditions: SmartFilterBarCondition[]): void {
+ conditions.forEach((condition) => {
+ const conditionGroup = this._generateFormGeneratorItems(condition);
+
+ this._formItems.push(conditionGroup);
+ });
+ }
+
+ /** @hidden */
+ private _generateFormGeneratorItems(condition?: SmartFilterBarCondition): DynamicFormItem[] {
+ return [
+ {
+ name: 'operator',
+ message: 'operator',
+ default: condition
+ ? this.conditionOperatorOptions.find((o) => o.value === condition.operator)?.value
+ : this.conditionOperatorOptions[0].value,
+ type: 'select',
+ choices: this.conditionOperatorOptions,
+ required: true,
+ guiOptions: {
+ inline: false,
+ column: 1,
+ noLabelLayout: true,
+ contentDensity: 'compact'
+ }
+ },
+ {
+ name: 'value',
+ message: 'value',
+ default: condition?.value,
+ type: this.config.filterType,
+ choices: this.config.choices,
+ placeholder: 'value',
+ controlType: this.config.controlType,
+ when: (value) => value.operator !== FILTER_STRATEGY.BETWEEN,
+ onchange: (value, _, control: DynamicFormControl) => {
+ control.parent?.get('value1')?.setValue(value, { emitEvent: false });
+ },
+ guiOptions: {
+ inline: false,
+ column: 2,
+ noLabelLayout: true,
+ contentDensity: 'compact'
+ }
+ },
+ {
+ name: 'value1',
+ message: 'value1',
+ default: condition?.value,
+ type: this.config.filterType,
+ choices: this.config.choices,
+ placeholder: 'from',
+ controlType: this.config.controlType,
+ when: (value) => value.operator === FILTER_STRATEGY.BETWEEN,
+ onchange: (value, _, control: DynamicFormControl) => {
+ control.parent?.get('value')?.setValue(value, { emitEvent: false });
+ },
+ guiOptions: {
+ inline: false,
+ column: 2,
+ noLabelLayout: true,
+ contentDensity: 'compact'
+ }
+ },
+ {
+ name: 'value2',
+ message: 'value2',
+ default: condition?.value2,
+ type: this.config.filterType,
+ choices: this.config.choices,
+ placeholder: 'to',
+ required: true,
+ controlType: this.config.controlType,
+ when: (value) => value?.operator === FILTER_STRATEGY.BETWEEN,
+ guiOptions: {
+ inline: false,
+ column: 3,
+ noLabelLayout: true,
+ contentDensity: 'compact'
+ }
+ }
+ ];
+ }
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-settings-dialog/data-provider.ts b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-settings-dialog/data-provider.ts
new file mode 100644
index 00000000000..40bba444c8b
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-settings-dialog/data-provider.ts
@@ -0,0 +1,63 @@
+import { ArrayTableDataProvider } from '@fundamental-ngx/platform/table';
+import { FieldFilterItem } from '../../interfaces/smart-filter-bar-field-filter-item';
+import { SmartFilterBarVisibilityCategory } from '../../interfaces/smart-filter-bar-visibility-category';
+
+export class SmartFilterBarOptionsDataProvider extends ArrayTableDataProvider {
+ constructor(items: FieldFilterItem[]) {
+ super(items);
+ }
+
+ /**
+ * Filters rows depending on applied filter.
+ * @param option filter visibility category.
+ */
+ filter(option: SmartFilterBarVisibilityCategory): void {
+ let items: FieldFilterItem[];
+
+ switch (option) {
+ case 'visible':
+ items = this._getVisibleItems();
+ break;
+ case 'active':
+ items = this._getActiveItems();
+ break;
+ case 'visibleAndActive':
+ items = this._getVisibleAndActiveItems();
+ break;
+ case 'mandatory':
+ items = this._getMandatoryItems();
+ break;
+ case 'all':
+ default:
+ items = this._getAllItems();
+ break;
+ }
+
+ this.items$.next(items);
+ }
+
+ /** @hidden */
+ private _getAllItems(): FieldFilterItem[] {
+ return this.items;
+ }
+
+ /** @hidden */
+ private _getVisibleItems(): FieldFilterItem[] {
+ return this.items.filter((i) => i.visible);
+ }
+
+ /** @hidden */
+ private _getActiveItems(): FieldFilterItem[] {
+ return this.items.filter((i) => i.active);
+ }
+
+ /** @hidden */
+ private _getVisibleAndActiveItems(): FieldFilterItem[] {
+ return this.items.filter((i) => i.visible || i.active);
+ }
+
+ /** @hidden */
+ private _getMandatoryItems(): FieldFilterItem[] {
+ return this.items.filter((i) => i.mandatory);
+ }
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-settings-dialog/smart-filter-bar-settings-dialog.component.html b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-settings-dialog/smart-filter-bar-settings-dialog.component.html
new file mode 100644
index 00000000000..4253fa3b365
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-settings-dialog/smart-filter-bar-settings-dialog.component.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+ Filters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-settings-dialog/smart-filter-bar-settings-dialog.component.spec.ts b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-settings-dialog/smart-filter-bar-settings-dialog.component.spec.ts
new file mode 100644
index 00000000000..2368d601ffa
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-settings-dialog/smart-filter-bar-settings-dialog.component.spec.ts
@@ -0,0 +1,115 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SmartFilterBarSettingsDialogComponent } from './smart-filter-bar-settings-dialog.component';
+import {
+ FdpSelectionChangeEvent,
+ FilterableColumnDataType,
+ FilterType,
+ PlatformSmartFilterBarModule,
+ SmartFilterSettingsDialogConfig
+} from '@fundamental-ngx/platform';
+import { DialogConfig, DialogRef } from '@fundamental-ngx/core';
+import { whenStable } from '@fundamental-ngx/core/tests';
+import { SmartFilterBarVisibilityCategory } from '../../interfaces/smart-filter-bar-visibility-category';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+
+const mockData: SmartFilterSettingsDialogConfig = {
+ fields: [
+ {
+ name: 'test',
+ dataType: FilterableColumnDataType.STRING,
+ filterType: FilterType.INPUT,
+ key: 'test',
+ label: 'test',
+ filterable: true,
+ required: true,
+ defaultSelected: false,
+ hasOptions: false,
+ conditionStrategy: 'or'
+ }
+ ],
+ visibilityCategories: {
+ all: 'Custom "All" label',
+ visible: 'Custom "Visible" label',
+ active: 'Custom "Active" label',
+ visibleAndActive: 'Custom "Visible and active" label',
+ mandatory: 'Custom "Mandatory" label'
+ },
+ filterBy: [],
+ selectedFilters: []
+};
+
+describe('SmartFilterBarSettingsDialogComponent', () => {
+ let component: SmartFilterBarSettingsDialogComponent;
+ let fixture: ComponentFixture;
+
+ const dialogConfig = new DialogConfig();
+
+ const dialogRefMock: DialogRef = new DialogRef();
+ dialogRefMock.data = mockData;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [NoopAnimationsModule, PlatformSmartFilterBarModule],
+ declarations: [SmartFilterBarSettingsDialogComponent],
+ providers: [
+ {
+ provide: DialogRef,
+ useValue: dialogRefMock
+ },
+ {
+ provide: DialogConfig,
+ useValue: dialogConfig
+ }
+ ]
+ }).compileComponents();
+ });
+
+ beforeEach(async () => {
+ fixture = TestBed.createComponent(SmartFilterBarSettingsDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ await whenStable(fixture);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('transform custom visibility categories', () => {
+ const mockDataLabels = Object.values(mockData.visibilityCategories);
+ expect(component._filterVisibilityOptions.map((o) => o.label)).toEqual(mockDataLabels);
+ });
+
+ it('should apply filtering', () => {
+ const filterTypes: SmartFilterBarVisibilityCategory[] = [
+ 'visibleAndActive',
+ 'visible',
+ 'active',
+ 'mandatory',
+ 'all'
+ ];
+
+ const source = component.source.dataProvider as any;
+
+ const allFiltersSpy = spyOn(source, '_getAllItems').and.callThrough();
+ const mandatoryFiltersSpy = spyOn(source, '_getMandatoryItems').and.callThrough();
+ const visibleFiltersSpy = spyOn(source, '_getVisibleItems').and.callThrough();
+ const activeFiltersSpy = spyOn(source, '_getActiveItems').and.callThrough();
+ const visibleActiveFiltersSpy = spyOn(source, '_getVisibleAndActiveItems').and.callThrough();
+
+ filterTypes.forEach((f) => {
+ const evt: FdpSelectionChangeEvent = {
+ payload: f
+ };
+
+ component._onFilterVisibilityChange(evt);
+ });
+
+ expect(allFiltersSpy).toHaveBeenCalled();
+ expect(mandatoryFiltersSpy).toHaveBeenCalled();
+ expect(visibleFiltersSpy).toHaveBeenCalled();
+ expect(activeFiltersSpy).toHaveBeenCalled();
+ expect(visibleActiveFiltersSpy).toHaveBeenCalled();
+ });
+});
diff --git a/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-settings-dialog/smart-filter-bar-settings-dialog.component.ts b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-settings-dialog/smart-filter-bar-settings-dialog.component.ts
new file mode 100644
index 00000000000..ea257d25ea2
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/components/smart-filter-bar-settings-dialog/smart-filter-bar-settings-dialog.component.ts
@@ -0,0 +1,166 @@
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ Component,
+ OnDestroy,
+ ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
+import { asyncScheduler, BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
+import { observeOn, takeUntil } from 'rxjs/operators';
+
+import { DialogRef } from '@fundamental-ngx/core/dialog';
+import { FdpSelectionChangeEvent } from '@fundamental-ngx/platform/form';
+import { SelectItem } from '@fundamental-ngx/platform/shared';
+import {
+ Resettable,
+ RESETTABLE_TOKEN,
+ Table,
+ TableDataSource,
+ TableRowSelectionChangeEvent
+} from '@fundamental-ngx/platform/table';
+
+import { SmartFilterBarFieldDefinition } from '../../interfaces/smart-filter-bar-field-definition';
+import { SmartFilterSettingsDialogConfig } from '../../interfaces/smart-filter-bar-settings-dialog-config';
+import { FieldFilterItem } from '../../interfaces/smart-filter-bar-field-filter-item';
+import { SmartFilterBarOptionsDataProvider } from './data-provider';
+
+@Component({
+ selector: 'fdp-smart-filter-bar-settings-dialog',
+ templateUrl: './smart-filter-bar-settings-dialog.component.html',
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [{ provide: RESETTABLE_TOKEN, useExisting: SmartFilterBarSettingsDialogComponent }]
+})
+export class SmartFilterBarSettingsDialogComponent implements Resettable, AfterViewInit, OnDestroy {
+ /**
+ * Table instance
+ */
+ @ViewChild('table') table!: Table;
+
+ /**
+ * Table data source.
+ */
+ source!: TableDataSource;
+
+ /** @hidden */
+ _availableFields!: FieldFilterItem[];
+
+ /** @hidden */
+ _filterVisibilityOptions: SelectItem[] = [];
+
+ /** @hidden */
+ private _sourceSubscription!: Subscription;
+
+ /** @hidden */
+ private _selectedFilters: string[] = [];
+
+ /**
+ * @hidden
+ * An RxJS Subject that will kill the data stream upon component’s destruction (for unsubscribing)
+ */
+ private readonly _onDestroy$: Subject = new Subject();
+
+ /** @hidden */
+ private _isResetAvailableSubject$: BehaviorSubject = new BehaviorSubject(false);
+
+ /** Indicates when reset command is available */
+ readonly isResetAvailable$: Observable = this._isResetAvailableSubject$.asObservable();
+
+ /** @hidden */
+ constructor(private _dialogRef: DialogRef) {
+ this.transformVisibilityLabels();
+ this.setInitialTableState();
+ }
+
+ /** @hidden */
+ ngAfterViewInit(): void {
+ this.setSelectedFilters();
+ }
+
+ /**
+ * Transforms visibility options into appropriate select item object.
+ */
+ transformVisibilityLabels(): void {
+ for (const [selectValue, selectLabel] of Object.entries(this._dialogRef.data.visibilityCategories)) {
+ this._filterVisibilityOptions.push({
+ label: selectLabel,
+ value: selectValue
+ });
+ }
+ }
+
+ /**
+ * Checks the checkbox in the table row if the field is selected.
+ */
+ setSelectedFilters(): void {
+ this._sourceSubscription?.unsubscribe();
+ this._sourceSubscription = this.source
+ .open()
+ .pipe(observeOn(asyncScheduler), takeUntil(this._onDestroy$))
+ .subscribe((items) => {
+ items.forEach((field, index) => {
+ if (field.visible) {
+ this.table.toggleSelectableRow(index);
+ }
+ });
+ });
+ }
+
+ /** @hidden */
+ ngOnDestroy(): void {
+ this._onDestroy$.next();
+ this._onDestroy$.complete();
+ }
+
+ /**
+ * Resets filters selection.
+ */
+ reset(): void {
+ this.setInitialTableState();
+ this.setSelectedFilters();
+ }
+
+ /**
+ * Sets initial state of the table.
+ */
+ setInitialTableState(): void {
+ this._availableFields = this._dialogRef.data.fields
+ .filter((c: SmartFilterBarFieldDefinition) => c.filterable)
+ .map((c: SmartFilterBarFieldDefinition) => ({
+ label: c.label,
+ active: !!this._dialogRef.data.filterBy.find((f) => f.field === c.name),
+ mandatory: c.required,
+ visible: this._dialogRef.data.selectedFilters.includes(c.name),
+ key: c.key,
+ name: c.name
+ }));
+
+ this.source = new TableDataSource(new SmartFilterBarOptionsDataProvider(this._availableFields));
+ }
+
+ /** @hidden */
+ _cancel(): void {
+ this._dialogRef.dismiss();
+ }
+
+ /** @hidden */
+ _confirm(): void {
+ this._dialogRef.close(this._selectedFilters);
+ }
+
+ /** @hidden */
+ _onRowSelectionChange(event: TableRowSelectionChangeEvent): void {
+ this._selectedFilters = event.selection.map((c) => c.name);
+ this._isResetAvailableSubject$.next(true);
+ }
+
+ /** @hidden */
+ _onFilterVisibilityChange(event: FdpSelectionChangeEvent): void {
+ if (!this.table) {
+ return;
+ }
+ (this.source.dataProvider as SmartFilterBarOptionsDataProvider).filter(event.payload);
+ this.source.fetch(this.table.getTableState());
+ }
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/constants.ts b/libs/platform/src/lib/smart-filter-bar/constants.ts
new file mode 100644
index 00000000000..41c6785a5f0
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/constants.ts
@@ -0,0 +1 @@
+export const SMART_FILTER_BAR_RENDERER_COMPONENT = 'smart-filter-condition-renderer';
diff --git a/libs/platform/src/lib/smart-filter-bar/directives/smart-filter-bar-field-definition.directive.spec.ts b/libs/platform/src/lib/smart-filter-bar/directives/smart-filter-bar-field-definition.directive.spec.ts
new file mode 100644
index 00000000000..21571598a42
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/directives/smart-filter-bar-field-definition.directive.spec.ts
@@ -0,0 +1,41 @@
+import { whenStable } from '@fundamental-ngx/core/tests';
+import { Component, ViewChild } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { PlatformSmartFilterBarModule } from '../smart-filter-bar.module';
+import { SmartFilterBarFieldDefinitionDirective } from './smart-filter-bar-field-definition.directive';
+
+@Component({
+ template: `
`
+})
+class TestComponent {
+ @ViewChild(SmartFilterBarFieldDefinitionDirective)
+ directive: SmartFilterBarFieldDefinitionDirective;
+}
+
+describe('SmartFilterBarFieldDefinitionDirective', () => {
+ let component: TestComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [PlatformSmartFilterBarModule],
+ declarations: [TestComponent]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should convert string to boolean', async () => {
+ await whenStable(fixture);
+ expect(component.directive.filterable).toBeFalse();
+ expect(component.directive.required).toBeTrue();
+ });
+});
diff --git a/libs/platform/src/lib/smart-filter-bar/directives/smart-filter-bar-field-definition.directive.ts b/libs/platform/src/lib/smart-filter-bar/directives/smart-filter-bar-field-definition.directive.ts
new file mode 100644
index 00000000000..b088be7d61f
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/directives/smart-filter-bar-field-definition.directive.ts
@@ -0,0 +1,83 @@
+import { coerceBooleanProperty } from '@angular/cdk/coercion';
+import { Directive, Input } from '@angular/core';
+import { CollectionFilterGroupStrategy, FilterableColumnDataType, FilterType } from '@fundamental-ngx/platform/table';
+
+@Directive({
+ selector: '[fdpSmartFilterBarFieldDefinition], [fdp-smart-filter-bar-field-definition]'
+})
+export class SmartFilterBarFieldDefinitionDirective {
+ /** Field data accessor key. */
+ @Input()
+ key!: string;
+
+ /** Unique field identifier. */
+ @Input()
+ name!: string;
+
+ /** Field label. */
+ @Input()
+ label!: string;
+ /** Field filter type */
+ @Input()
+ filterType!: FilterType;
+ /** Field data type */
+ @Input()
+ dataType!: FilterableColumnDataType;
+ /** Field custom filter type */
+ @Input()
+ customFilterType!: string;
+
+ @Input()
+ conditionStrategy: CollectionFilterGroupStrategy = 'or';
+
+ /** @hidden */
+ private _filterable = false;
+
+ get filterable(): boolean {
+ return this._filterable;
+ }
+
+ /** Whether this field can be filtered. */
+ @Input()
+ set filterable(value: boolean) {
+ this._filterable = coerceBooleanProperty(value);
+ }
+
+ /** @hidden */
+ private _required = false;
+
+ get required(): boolean {
+ return this._required;
+ }
+
+ /** Whether this field filter is mandatory. */
+ @Input()
+ set required(value: boolean) {
+ this._required = coerceBooleanProperty(value);
+ }
+
+ /** @hidden */
+ private _defaultSelected = false;
+
+ get defaultSelected(): boolean {
+ return this._defaultSelected;
+ }
+
+ /** Whether this field filter is selected by default. */
+ @Input()
+ set defaultSelected(value: boolean) {
+ this._defaultSelected = coerceBooleanProperty(value);
+ }
+ /** Whether this field has autocomplete options */
+ @Input()
+ set hasOptions(value: boolean) {
+ this._hasOptions = coerceBooleanProperty(value);
+ }
+
+ get hasOptions(): boolean {
+ return this._hasOptions;
+ }
+
+ /** @hidden */
+ private _hasOptions = false;
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/directives/smart-filter-bar-subject.directive.spec.ts b/libs/platform/src/lib/smart-filter-bar/directives/smart-filter-bar-subject.directive.spec.ts
new file mode 100644
index 00000000000..6727940e0b7
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/directives/smart-filter-bar-subject.directive.spec.ts
@@ -0,0 +1,168 @@
+import { Component, ViewChild } from '@angular/core';
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { whenStable } from '@fundamental-ngx/core/tests';
+import { PlatformSmartFilterBarModule } from '../smart-filter-bar.module';
+import { ArrayTableDataProvider, PlatformTableModule, TableDataSource } from '@fundamental-ngx/platform/table';
+import { SmartFilterBarSubjectDirective } from './smart-filter-bar-subject.directive';
+
+interface SourceItem {
+ id: string;
+ name: string;
+ description: string;
+ status: string;
+ isVerified: boolean;
+ price: {
+ value: number;
+ currency: string;
+ };
+}
+
+const generateItems = (length = 50): SourceItem[] =>
+ Array.from(Array(length)).map(
+ (_, index): SourceItem => ({
+ id: `${index}`,
+ name: `Product ${index}`,
+ description: `Description ${index}`,
+ price: {
+ value: index,
+ currency: 'USD'
+ },
+ status: index < length / 2 ? 'valid' : 'invalid',
+ isVerified: index < length / 2
+ })
+ );
+
+class TableDataProviderMock extends ArrayTableDataProvider {
+ constructor() {
+ super(generateItems(50));
+ }
+}
+
+@Component({
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+ `
+})
+class TestComponent {
+ @ViewChild(SmartFilterBarSubjectDirective)
+ directive: SmartFilterBarSubjectDirective;
+
+ source: TableDataSource;
+
+ constructor() {
+ this.source = new TableDataSource(new TableDataProviderMock());
+ }
+}
+
+describe('SmartFilterBarSubjectDirective', () => {
+ let component: TestComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(
+ waitForAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [PlatformTableModule, PlatformSmartFilterBarModule],
+ declarations: [TestComponent]
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(
+ waitForAsync(async () => {
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ await whenStable(fixture);
+ })
+ );
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should return subject', () => {
+ const source = component.directive.getDataSource();
+ expect(source).toBeInstanceOf(TableDataSource);
+ });
+
+ it('should return subject fields', () => {
+ const spy = spyOn(component.directive, '_transformSubjectField').and.callThrough();
+
+ const subjectFieldNames = component.directive.getSubjectFields().map((f) => f.name);
+
+ expect(spy).toHaveBeenCalled();
+ expect(subjectFieldNames).toEqual(['name', 'description', 'price', 'status']);
+ });
+
+ it('should return field variants', async () => {
+ const options = (await component.directive.getFieldVariants('status').toPromise()).map((o) => o.value);
+ expect(options.filter((o: string, i: number) => options.indexOf(o) === i)).toEqual(['valid', 'invalid']);
+ });
+
+ it('should return default fields', () => {
+ const defaultFields = component.directive.getDefaultFields();
+
+ expect(defaultFields).toEqual(['name', 'price', 'status']);
+ });
+
+ it('should return subject state', () => {
+ const state = component.directive.getState();
+ expect(state.columns).toEqual(['name', 'description', 'price', 'status']);
+ });
+
+ it('should return subject', () => {
+ const subject = component.directive.getSubject();
+ expect(subject).toBeTruthy();
+ });
+});
diff --git a/libs/platform/src/lib/smart-filter-bar/directives/smart-filter-bar-subject.directive.ts b/libs/platform/src/lib/smart-filter-bar/directives/smart-filter-bar-subject.directive.ts
new file mode 100644
index 00000000000..7c6de6dedd3
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/directives/smart-filter-bar-subject.directive.ts
@@ -0,0 +1,91 @@
+import { AfterViewInit, ContentChildren, Directive, QueryList } from '@angular/core';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { SelectItem } from '@fundamental-ngx/platform/shared';
+import { Table, TableDataSource, TableState } from '@fundamental-ngx/platform/table';
+import { SmartFilterBarFieldDefinition } from '../interfaces/smart-filter-bar-field-definition';
+import { SmartFilterBarFieldDefinitionDirective } from './smart-filter-bar-field-definition.directive';
+
+@Directive({
+ selector: '[fdpSmartFilterBarSubject], [fdp-smart-filter-bar-subject]',
+ exportAs: 'fdp-smart-filter-bar-subject'
+})
+export class SmartFilterBarSubjectDirective implements AfterViewInit {
+ /** @hidden */
+ @ContentChildren(SmartFilterBarFieldDefinitionDirective)
+ _fieldDefinitions!: QueryList;
+
+ /** @hidden */
+ _fieldDefinitionsSubject = new BehaviorSubject([]);
+
+ /**
+ * Observable of available fields.
+ */
+ readonly fieldsStream = this._fieldDefinitionsSubject.asObservable();
+
+ /** @hidden */
+ constructor(private _subject: Table) {}
+
+ /** @hidden */
+ ngAfterViewInit(): void {
+ this._fieldDefinitionsSubject.next(this._fieldDefinitions.toArray().map((d) => this._transformSubjectField(d)));
+ this._fieldDefinitions.changes.subscribe((definitions: SmartFilterBarFieldDefinitionDirective[]) => {
+ this._fieldDefinitionsSubject.next(definitions.map((d) => this._transformSubjectField(d)));
+ });
+ }
+
+ /**
+ * Retrieves data source of the subject.
+ * @returns Subject's data source.
+ */
+ getDataSource(): TableDataSource {
+ return this._subject.getDataSource();
+ }
+
+ /**
+ * Retrieves available fields of the subject.
+ * @returns Available fields of the subject.
+ */
+ getSubjectFields(): SmartFilterBarFieldDefinition[] {
+ return this._fieldDefinitions.toArray().map((f) => this._transformSubjectField(f));
+ }
+
+ /** Get available field variants */
+ getFieldVariants(field: string): Observable {
+ return this.getDataSource().dataProvider.getFieldOptions(field);
+ }
+
+ getDefaultFields(): string[] {
+ return this.getSubjectFields()
+ ?.filter((f) => f.defaultSelected)
+ .map((f) => f.name);
+ }
+
+ /** Get subject state */
+ getState(): TableState {
+ return this._subject.getTableState();
+ }
+
+ /**
+ * @returns Subject component.
+ */
+ getSubject(): Table {
+ return this._subject;
+ }
+
+ /** @hidden */
+ private _transformSubjectField(column: SmartFilterBarFieldDefinitionDirective): SmartFilterBarFieldDefinition {
+ return {
+ key: column.key,
+ name: column.name,
+ label: column.label,
+ filterType: column.filterType,
+ filterable: column.filterable,
+ dataType: column.dataType,
+ required: column.required,
+ customFilterType: column.customFilterType,
+ defaultSelected: column.defaultSelected,
+ hasOptions: column.hasOptions,
+ conditionStrategy: column.conditionStrategy
+ };
+ }
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/helpers.ts b/libs/platform/src/lib/smart-filter-bar/helpers.ts
new file mode 100644
index 00000000000..4f12a0daa20
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/helpers.ts
@@ -0,0 +1,9 @@
+import { isSelectItem } from '@fundamental-ngx/platform/shared';
+
+export function getSelectItemValue(item: any): any {
+ if (Array.isArray(item)) {
+ item = item.map((i) => getSelectItemValue(i));
+ }
+
+ return isSelectItem(item) ? item.value : item;
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/index.ts b/libs/platform/src/lib/smart-filter-bar/index.ts
new file mode 100644
index 00000000000..4aaf8f92eda
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/index.ts
@@ -0,0 +1 @@
+export * from './public_api';
diff --git a/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-condition.ts b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-condition.ts
new file mode 100644
index 00000000000..d3816007d06
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-condition.ts
@@ -0,0 +1,55 @@
+import { FilterableColumnDataType, FilterStrategy } from '@fundamental-ngx/platform/table';
+import { SelectItem } from '@fundamental-ngx/platform/shared';
+import { InputType } from '@fundamental-ngx/platform/form';
+import { SmartFilterBarStrategyLabels } from './strategy-labels.type';
+
+export interface SmartFilterBarConditionBuilder {
+ /**
+ * Header of the dialog window.
+ */
+ header: string;
+ /**
+ * Filter data type.
+ */
+ dataType: FilterableColumnDataType;
+ /**
+ * Applied conditions for the filter.
+ */
+ conditions: SmartFilterBarCondition[];
+ /**
+ * Filter type.
+ */
+ filterType: string;
+ /**
+ * Available options for filtering.
+ */
+ choices: SelectItem[];
+ /**
+ * Input type.
+ */
+ controlType: InputType;
+
+ /**
+ * Condition strategy labels.
+ */
+ defineStrategyLabels: SmartFilterBarStrategyLabels;
+}
+
+export interface SmartFilterBarCondition {
+ /**
+ * Filter value.
+ */
+ value: T;
+ /**
+ * Range filter value.
+ */
+ value2?: T;
+ /**
+ * Condition type.
+ */
+ operator: FilterStrategy;
+ /**
+ * Human-readable value to be displayed in smart filter bar.
+ */
+ displayValue?: string;
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-custom-filter-config.ts b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-custom-filter-config.ts
new file mode 100644
index 00000000000..5739adcb397
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-custom-filter-config.ts
@@ -0,0 +1,47 @@
+import { Type } from '@angular/core';
+import { BaseDynamicFormGeneratorControl } from '@fundamental-ngx/platform/form';
+import { FilterAllStrategy } from '@fundamental-ngx/platform/table';
+import { Observable } from 'rxjs';
+import { SmartFilterBarCondition } from './smart-filter-bar-condition';
+import { SelectItem } from '@fundamental-ngx/platform/shared';
+
+export interface SmartFilterBarCustomFilterConfig {
+ /**
+ * Component to be used to render the control in conditions dialog.
+ */
+ conditionComponent?: Type;
+
+ /**
+ * Component to be used to render the condition in smart filter bar.
+ */
+ rendererComponent?: Type;
+
+ /**
+ * Available filtering strategies.
+ */
+ filterStrategies?: FilterAllStrategy[];
+ /**
+ * Transforms raw filter item value.
+ * @param value raw filter item value.
+ * @returns updated filter item value to be used in the filters value hash.
+ */
+ valueTransformer?: (value: any) => SelectItem[] | any[] | undefined;
+
+ /**
+ * Transforms filter raw value so it could be rendered in renderer component.
+ * @param condition Generated condition rule.
+ * @returns string which will be used in renderer component.
+ */
+ valueRenderer?: (condition: SmartFilterBarCondition) => string | Promise | Observable;
+
+ /**
+ * Filter type.
+ */
+ types: string[];
+ /**
+ * Additional data that can be used later in the component.
+ */
+ additionalData?: {
+ [key: string]: any;
+ };
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-field-definition.ts b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-field-definition.ts
new file mode 100644
index 00000000000..6cb7b2bc254
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-field-definition.ts
@@ -0,0 +1,15 @@
+import { FilterType, FilterableColumnDataType, CollectionFilterGroupStrategy } from '@fundamental-ngx/platform/table';
+
+export interface SmartFilterBarFieldDefinition {
+ key: string;
+ name: string;
+ label: string;
+ filterType: FilterType;
+ dataType: FilterableColumnDataType;
+ filterable: boolean;
+ required: boolean;
+ customFilterType?: string;
+ defaultSelected: boolean;
+ hasOptions: boolean;
+ conditionStrategy: CollectionFilterGroupStrategy;
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-field-filter-item.ts b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-field-filter-item.ts
new file mode 100644
index 00000000000..95e09614adb
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-field-filter-item.ts
@@ -0,0 +1,8 @@
+export interface FieldFilterItem {
+ label: string;
+ active: boolean;
+ mandatory: boolean;
+ visible: boolean;
+ key: string;
+ name: string;
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-settings-dialog-config.ts b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-settings-dialog-config.ts
new file mode 100644
index 00000000000..b91115a0cdd
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-settings-dialog-config.ts
@@ -0,0 +1,23 @@
+import { CollectionFilterAndGroup } from '@fundamental-ngx/platform/table';
+import { SmartFilterBarFieldDefinition } from './smart-filter-bar-field-definition';
+import { SmartFilterBarVisibilityCategoryLabels } from './smart-filter-bar-visibility-category';
+
+export interface SmartFilterSettingsDialogConfig {
+ /**
+ * Available fields.
+ */
+ fields: SmartFilterBarFieldDefinition[];
+ /**
+ * Applied filters.
+ */
+ filterBy: CollectionFilterAndGroup[];
+ /**
+ * Selected filters.
+ */
+ selectedFilters: string[];
+
+ /**
+ * Filters visibility category labels.
+ */
+ visibilityCategories: SmartFilterBarVisibilityCategoryLabels;
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-visibility-category.ts b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-visibility-category.ts
new file mode 100644
index 00000000000..0ce3c92de0d
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-bar-visibility-category.ts
@@ -0,0 +1,2 @@
+export type SmartFilterBarVisibilityCategory = 'all' | 'visible' | 'active' | 'visibleAndActive' | 'mandatory';
+export type SmartFilterBarVisibilityCategoryLabels = { [key in SmartFilterBarVisibilityCategory]: string };
diff --git a/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-dynamic-form-item.ts b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-dynamic-form-item.ts
new file mode 100644
index 00000000000..311ea3d2bcb
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/interfaces/smart-filter-dynamic-form-item.ts
@@ -0,0 +1,20 @@
+import { DynamicFormFieldItem, DynamicFormItemGuiOptions, InputType } from '@fundamental-ngx/platform/form';
+import { SelectItem } from '@fundamental-ngx/platform/shared';
+import { FilterableColumnDataType, FilterType } from '@fundamental-ngx/platform/table';
+import { Observable } from 'rxjs';
+
+export interface SmartFilterBarDynamicFormFieldItem extends DynamicFormFieldItem {
+ guiOptions: SmartFilterBarDynamicFormFieldGuiOptions;
+}
+
+export interface SmartFilterBarDynamicFormFieldGuiOptions extends DynamicFormItemGuiOptions {
+ additionalData: {
+ type: string;
+ dataType: FilterableColumnDataType;
+ filterType: FilterType;
+ controlType: InputType;
+ choices?: () => Observable | SelectItem[];
+ /** Additional config properties. */
+ [key: string]: any;
+ };
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/interfaces/strategy-labels.type.ts b/libs/platform/src/lib/smart-filter-bar/interfaces/strategy-labels.type.ts
new file mode 100644
index 00000000000..6bb0c44a4dd
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/interfaces/strategy-labels.type.ts
@@ -0,0 +1,5 @@
+import { FILTER_STRATEGY } from '@fundamental-ngx/platform/table';
+
+export type SmartFilterBarStrategyLabels = {
+ [key in typeof FILTER_STRATEGY[keyof typeof FILTER_STRATEGY]]: string;
+};
diff --git a/libs/platform/src/lib/smart-filter-bar/karma.conf.js b/libs/platform/src/lib/smart-filter-bar/karma.conf.js
new file mode 100644
index 00000000000..76fed7b1cf8
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/karma.conf.js
@@ -0,0 +1,17 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+const { join } = require('path');
+const getBaseKarmaConfig = require('../../../../../karma.conf');
+
+module.exports = function (config) {
+ const baseConfig = getBaseKarmaConfig();
+ config.set({
+ ...baseConfig,
+ coverageIstanbulReporter: {
+ ...baseConfig.coverageIstanbulReporter,
+ dir: join(__dirname, '../../../../../coverage/libs/platform/smart-filter-bar')
+ },
+ browsers: ['ChromeHeadless']
+ });
+};
diff --git a/libs/platform/src/lib/smart-filter-bar/ng-package.json b/libs/platform/src/lib/smart-filter-bar/ng-package.json
new file mode 100644
index 00000000000..534e885fb71
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../../../../dist/libs/platform/smart-filter-bar",
+ "lib": {
+ "entryFile": "./public_api.ts"
+ }
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/package.json b/libs/platform/src/lib/smart-filter-bar/package.json
new file mode 100644
index 00000000000..1565e89ce6e
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@fundamental-ngx/platform/smart-filter-bar",
+ "version": "VERSION_PLACEHOLDER",
+ "peerDependencies": {
+ "@angular/common": "ANGULAR_VER_PLACEHOLDER",
+ "@angular/core": "ANGULAR_VER_PLACEHOLDER"
+ },
+ "dependencies": {
+ "tslib": "^2.0.0"
+ }
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/providers/smart-filter-bar.provider.ts b/libs/platform/src/lib/smart-filter-bar/providers/smart-filter-bar.provider.ts
new file mode 100644
index 00000000000..c005e56ebc2
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/providers/smart-filter-bar.provider.ts
@@ -0,0 +1,8 @@
+import { forwardRef, Provider } from '@angular/core';
+import { SmartFilterBar } from '../smart-filter-bar.class';
+import { SmartFilterBarComponent } from '../smart-filter-bar.component';
+
+export const smartFilterBarProvider: Provider = {
+ provide: SmartFilterBar,
+ useExisting: forwardRef(() => SmartFilterBarComponent)
+};
diff --git a/libs/platform/src/lib/smart-filter-bar/public_api.ts b/libs/platform/src/lib/smart-filter-bar/public_api.ts
new file mode 100644
index 00000000000..cd57bdc6f38
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/public_api.ts
@@ -0,0 +1,19 @@
+export * from './smart-filter-bar.module';
+export * from './smart-filter-bar.component';
+export * from './smart-filter-bar.service';
+export * from './components/smart-filter-bar-condition-field/base-smart-filter-bar-condition-field';
+export * from './components/smart-filter-bar-condition-field/smart-filter-bar-condition-field.component';
+export * from './components/smart-filter-bar-conditions-dialog/smart-filter-bar-conditions-dialog.component';
+export * from './components/smart-filter-bar-settings-dialog/smart-filter-bar-settings-dialog.component';
+export * from './directives/smart-filter-bar-field-definition.directive';
+export * from './directives/smart-filter-bar-subject.directive';
+export * from './interfaces/smart-filter-bar-condition';
+export * from './interfaces/smart-filter-bar-custom-filter-config';
+export * from './interfaces/smart-filter-bar-field-definition';
+export * from './interfaces/smart-filter-bar-settings-dialog-config';
+export * from './interfaces/smart-filter-dynamic-form-item';
+export * from './interfaces/smart-filter-bar-visibility-category';
+export * from './interfaces/strategy-labels.type';
+export * from './constants';
+export * from './providers/smart-filter-bar.provider';
+export * from './smart-filter-bar.class';
diff --git a/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.class.ts b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.class.ts
new file mode 100644
index 00000000000..dab6bb7a2e1
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.class.ts
@@ -0,0 +1,7 @@
+import { SmartFilterBarCondition } from './interfaces/smart-filter-bar-condition';
+import { SmartFilterBarStrategyLabels } from './interfaces/strategy-labels.type';
+
+export abstract class SmartFilterBar {
+ defineStrategyLabels!: SmartFilterBarStrategyLabels;
+ getDisplayValue!: (condition: SmartFilterBarCondition, filterType: string) => Promise;
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.component.html b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.component.html
new file mode 100644
index 00000000000..a7b2f0d2898
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.component.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.component.scss b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.component.scss
new file mode 100644
index 00000000000..546a23c86a2
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.component.scss
@@ -0,0 +1,71 @@
+$fd-block: fdp-smart-filter-bar;
+
+@mixin fd-responsive-paddings() {
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+ @media (min-width: 600px) {
+ padding-left: 2rem;
+ padding-right: 2rem;
+ }
+}
+
+.#{$fd-block} {
+ display: block;
+ box-shadow: var(--sapContent_HeaderShadow);
+ background-color: var(--sapObjectHeader_Background);
+
+ &--transparent {
+ background-color: var(--sapToolbar_Background);
+ box-shadow: none;
+ }
+
+ .fd-toolbar {
+ &--title {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ height: 3rem;
+ min-height: 3rem;
+ border-bottom: none;
+ @include fd-responsive-paddings();
+ }
+
+ fdp-search-field {
+ min-width: 15.5rem;
+ }
+ }
+
+ .#{$fd-block}__filters {
+ @include fd-responsive-paddings();
+ }
+
+ .fd-container.fd-form-layout-grid-container {
+ padding: 0;
+
+ .fd-row {
+ .fd-col__form-group {
+ padding-left: 0.25rem;
+ padding-right: 0.25rem;
+ }
+ }
+ }
+
+ &__conditions-dialog {
+ .#{$fd-block}__actions-column {
+ min-width: auto;
+ flex: 0 0 auto;
+ }
+
+ .fd-popover-custom {
+ display: block;
+ }
+
+ .fd-dialog__body {
+ background-color: var(--sapBackgroundColor, #f7f7f7);
+ }
+
+ .fd-container.fd-form-layout-grid-container,
+ .fd-container.fd-form-layout-grid-container .fd-row .fd-col__form-group {
+ padding: 0 1rem;
+ }
+ }
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.component.spec.ts b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.component.spec.ts
new file mode 100644
index 00000000000..51c23ea1937
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.component.spec.ts
@@ -0,0 +1,380 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { Component, ViewChild } from '@angular/core';
+
+import { SmartFilterBarComponent } from './smart-filter-bar.component';
+import { whenStable } from '@fundamental-ngx/core/tests';
+import { FdDate } from '@fundamental-ngx/core';
+import { PlatformTableModule } from '@fundamental-ngx/platform/table';
+import { PlatformSmartFilterBarModule } from './smart-filter-bar.module';
+
+@Component({
+ selector: 'fdp-smart-filter-bar-test',
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+})
+class TestComponent {
+ source: ExampleItem[] = ITEMS;
+
+ @ViewChild(SmartFilterBarComponent) smartFilterBar: SmartFilterBarComponent;
+
+ trackBy(_: number, item: ExampleItem): number {
+ return item.id;
+ }
+}
+
+export interface ExampleItem {
+ id: number;
+ name: string;
+ description: string;
+ price: {
+ value: number;
+ currency: string;
+ };
+ status: string;
+ statusColor?: string;
+ date: FdDate;
+ verified: boolean;
+}
+
+// Example items
+const ITEMS: ExampleItem[] = [
+ {
+ id: 1,
+ name: '10 Portable DVD player',
+ description: 'diam neque vestibulum eget vulputate',
+ price: {
+ value: 66.04,
+ currency: 'CNY'
+ },
+ status: 'Stocked on demand',
+ statusColor: 'informative',
+ date: new FdDate(2020, 1, 7),
+ verified: true
+ },
+ {
+ id: 2,
+ name: 'Astro Laptop 1516',
+ description: 'pede malesuada',
+ price: {
+ value: 489.01,
+ currency: 'EUR'
+ },
+ status: 'Out of stock',
+ statusColor: 'negative',
+ date: new FdDate(2020, 2, 5),
+ verified: true
+ },
+ {
+ id: 3,
+ name: 'Astro Phone 6',
+ description: 'penatibus et magnis',
+ price: {
+ value: 154.1,
+ currency: 'IDR'
+ },
+ status: 'Stocked on demand',
+ statusColor: 'informative',
+ date: new FdDate(2020, 1, 12),
+ verified: true
+ },
+ {
+ id: 4,
+ name: 'Beam Breaker B-1',
+ description: 'fermentum donec ut',
+ price: {
+ value: 36.56,
+ currency: 'NZD'
+ },
+ status: 'Stocked on demand',
+ statusColor: 'informative',
+ date: new FdDate(2020, 11, 24),
+ verified: false
+ },
+ {
+ id: 5,
+ name: 'Beam Breaker B-2',
+ description: 'sapien in sapien iaculis congue',
+ price: {
+ value: 332.57,
+ currency: 'NZD'
+ },
+ status: 'No info',
+ date: new FdDate(2020, 10, 23),
+ verified: true
+ },
+ {
+ id: 6,
+ name: 'Benda Laptop 1408',
+ description: 'suspendisse potenti cras in',
+ price: {
+ value: 243.49,
+ currency: 'CNY'
+ },
+ status: 'Stocked on demand',
+ statusColor: 'informative',
+ date: new FdDate(2020, 9, 22),
+ verified: true
+ },
+ {
+ id: 7,
+ name: 'Bending Screen 21HD',
+ description: 'nunc nisl duis bibendum',
+ price: {
+ value: 66.46,
+ currency: 'EUR'
+ },
+ status: 'Available',
+ statusColor: 'positive',
+ date: new FdDate(2020, 8, 14),
+ verified: false
+ },
+ {
+ id: 8,
+ name: 'Blaster Extreme',
+ description: 'quisque ut',
+ price: {
+ value: 436.88,
+ currency: 'USD'
+ },
+ status: 'Available',
+ statusColor: 'positive',
+ date: new FdDate(2020, 8, 15),
+ verified: true
+ },
+ {
+ id: 9,
+ name: 'Broad Screen 22HD',
+ description: 'ultrices posuere',
+ price: {
+ value: 458.18,
+ currency: 'CNY'
+ },
+ status: 'Available',
+ statusColor: 'positive',
+ date: new FdDate(2020, 5, 4),
+ verified: true
+ },
+ {
+ id: 10,
+ name: 'Camcorder View',
+ description: 'integer ac leo pellentesque',
+ price: {
+ value: 300.52,
+ currency: 'USD'
+ },
+ status: 'Available',
+ statusColor: 'positive',
+ date: new FdDate(2020, 5, 5),
+ verified: true
+ },
+ {
+ id: 11,
+ name: 'Cepat Tablet 10.5',
+ description: 'rutrum rutrum neque aenean auctor',
+ price: {
+ value: 365.12,
+ currency: 'NZD'
+ },
+ status: 'No info',
+ date: new FdDate(2020, 5, 6),
+ verified: true
+ },
+ {
+ id: 12,
+ name: 'Ergo Mousepad',
+ description: 'tortor duis mattis egestas',
+ price: {
+ value: 354.46,
+ currency: 'EUR'
+ },
+ status: 'Stocked on demand',
+ statusColor: 'informative',
+ date: new FdDate(2020, 5, 7),
+ verified: true
+ },
+ {
+ id: 13,
+ name: 'Ergo Screen E-I',
+ description: 'massa quis augue luctus tincidunt',
+ price: {
+ value: 387.23,
+ currency: 'NZD'
+ },
+ status: 'Stocked on demand',
+ statusColor: 'informative',
+ date: new FdDate(2020, 3, 23),
+ verified: true
+ },
+ {
+ id: 14,
+ name: 'Ergo Screen E-II',
+ description: 'orci eget',
+ price: {
+ value: 75.86,
+ currency: 'EUR'
+ },
+ status: 'No info',
+ date: new FdDate(2020, 3, 20),
+ verified: false
+ },
+ {
+ id: 15,
+ name: 'Gaming Monster',
+ description: 'cubilia curae',
+ price: {
+ value: 152.95,
+ currency: 'EGP'
+ },
+ status: 'No info',
+ date: new FdDate(2020, 9, 20),
+ verified: false
+ },
+ {
+ id: 16,
+ name: 'Gaming Monster Pro',
+ description: 'pharetra magna vestibulum aliquet',
+ price: {
+ value: 213.47,
+ currency: 'MZN'
+ },
+ status: 'Out of stock',
+ statusColor: 'negative',
+ date: new FdDate(2020, 4, 17),
+ verified: false
+ }
+];
+
+describe('SmartFilterBarComponent', () => {
+ let component: TestComponent;
+ let smartFilterBar: SmartFilterBarComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [PlatformTableModule, PlatformSmartFilterBarModule],
+ declarations: [TestComponent]
+ }).compileComponents();
+ });
+
+ beforeEach(
+ waitForAsync(async () => {
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ await whenStable(fixture);
+ smartFilterBar = component.smartFilterBar;
+ })
+ );
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should open filters dialog', async () => {
+ await whenStable(fixture);
+
+ const dialogSpy = spyOn((smartFilterBar as any)._dialogService, 'open').and.callThrough();
+
+ smartFilterBar.showFilteringSettings();
+
+ expect(dialogSpy).toHaveBeenCalled();
+ });
+
+ it('should submit form', async () => {
+ await whenStable(fixture);
+
+ const fgSpy = spyOn(component.smartFilterBar, '_onFormSubmitted').and.callThrough();
+
+ smartFilterBar.submitForm();
+
+ await new Promise((resolve) => setTimeout(() => resolve(null), 200));
+
+ expect(fgSpy).toHaveBeenCalled();
+ });
+
+ it('should toggle filter bar', async () => {
+ smartFilterBar._toggleFilterBar();
+ fixture.detectChanges();
+ await whenStable(fixture);
+
+ expect(smartFilterBar._showFilterBar).toBeFalse();
+ expect(fixture.nativeElement.querySelector('fdp-form-generator')).toBeNull();
+ });
+});
diff --git a/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.component.ts b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.component.ts
new file mode 100644
index 00000000000..e0142f686cf
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.component.ts
@@ -0,0 +1,532 @@
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ forwardRef,
+ Input,
+ OnDestroy,
+ Provider,
+ ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
+import { Validators } from '@angular/forms';
+
+import { Observable, Subscription } from 'rxjs';
+import { map, take } from 'rxjs/operators';
+
+import { DialogConfig, DialogService } from '@fundamental-ngx/core/dialog';
+import {
+ CollectionFilter,
+ CollectionFilterGroup,
+ FilterableColumnDataType,
+ FilterType,
+ SearchInput
+} from '@fundamental-ngx/platform/table';
+import {
+ DynamicFormFieldItem,
+ DynamicFormItem,
+ FormGeneratorComponent,
+ FormGeneratorService
+} from '@fundamental-ngx/platform/form';
+import { ColumnLayout, SelectItem } from '@fundamental-ngx/platform/shared';
+
+import { SmartFilterBarSettingsDialogComponent } from './components/smart-filter-bar-settings-dialog/smart-filter-bar-settings-dialog.component';
+import { SmartFilterBarSubjectDirective } from './directives/smart-filter-bar-subject.directive';
+import { SmartFilterBarFieldDefinition } from './interfaces/smart-filter-bar-field-definition';
+import { SmartFilterBarDynamicFormFieldItem } from './interfaces/smart-filter-dynamic-form-item';
+import { SmartFilterBarCondition } from './interfaces/smart-filter-bar-condition';
+import { SmartFilterSettingsDialogConfig } from './interfaces/smart-filter-bar-settings-dialog-config';
+import { SmartFilterBarService } from './smart-filter-bar.service';
+import { SMART_FILTER_BAR_RENDERER_COMPONENT } from './constants';
+import { SmartFilterBarVisibilityCategoryLabels } from './interfaces/smart-filter-bar-visibility-category';
+import { SmartFilterBar } from './smart-filter-bar.class';
+import { SmartFilterBarConditionFieldComponent } from './components/smart-filter-bar-condition-field/smart-filter-bar-condition-field.component';
+import { getSelectItemValue } from './helpers';
+import { SmartFilterBarStrategyLabels } from './interfaces/strategy-labels.type';
+import { coerceBooleanProperty } from '@angular/cdk/coercion';
+
+const defaultColumnsLayout = { S: 12, M: 6, L: 4, XL: 3 };
+
+/**
+ * Default dialog configuration.
+ */
+const dialogConfig: DialogConfig = {
+ responsivePadding: true,
+ verticalPadding: true,
+ minWidth: '30rem',
+ /** 88px it's the header + footer height */
+ bodyMinHeight: 'calc(50vh - 88px)'
+};
+
+const smartFilterBarProvider: Provider = {
+ provide: SmartFilterBar,
+ useExisting: forwardRef(() => SmartFilterBarComponent)
+};
+
+@Component({
+ selector: 'fdp-smart-filter-bar',
+ templateUrl: './smart-filter-bar.component.html',
+ styleUrls: ['./smart-filter-bar.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [smartFilterBarProvider],
+ host: {
+ class: 'fdp-smart-filter-bar',
+ '[class.fdp-smart-filter-bar--transparent]': 'transparent'
+ }
+})
+export class SmartFilterBarComponent implements OnDestroy, SmartFilterBar {
+ /**
+ * Subject which will provide configuration data: data source, columns definitions, etc.
+ */
+ @Input()
+ set subject(value: SmartFilterBarSubjectDirective) {
+ this._setSubject(value);
+ }
+
+ get subject(): SmartFilterBarSubjectDirective {
+ return this._subject;
+ }
+ /**
+ * 'Show filters' button label.
+ */
+ @Input()
+ showFiltersLabel = 'Show filters';
+
+ /**
+ * 'Hide filters' button label.
+ */
+ @Input()
+ hideFiltersLabel = 'Hide filters';
+
+ /**
+ * 'Filters' button label.
+ */
+ @Input()
+ filtersLabel = 'Filters';
+
+ /**
+ * Whether smart filter bar background should be transparent.
+ */
+ @Input()
+ set transparent(value: boolean) {
+ this._transparent = coerceBooleanProperty(value);
+ }
+
+ get transparent(): boolean {
+ return this._transparent;
+ }
+
+ /**
+ * Condition strategy labels.
+ */
+ @Input()
+ defineStrategyLabels: SmartFilterBarStrategyLabels = {
+ contains: 'contains',
+ equalTo: 'equal to',
+ between: 'between',
+ beginsWith: 'starts with',
+ endsWith: 'ends with',
+ lessThan: 'less than',
+ lessThanOrEqualTo: 'less than or equal to',
+ greaterThan: 'greater than',
+ greaterThanOrEqualTo: 'greater than or equal to',
+ after: 'after',
+ onOrAfter: 'on or after',
+ before: 'before',
+ beforeOrOn: 'before or on'
+ };
+
+ /**
+ * Filters visibility category labels.
+ */
+ @Input()
+ filtersVisibilityCategoryLabels: SmartFilterBarVisibilityCategoryLabels = {
+ all: 'All',
+ visible: 'Visible',
+ active: 'Active',
+ visibleAndActive: 'Visible and active',
+ mandatory: 'Mandatory'
+ };
+
+ /**
+ * Columns layout.
+ */
+ @Input()
+ filtersColumnLayout: ColumnLayout = defaultColumnsLayout;
+ /**
+ * Calculated array of filters to apply for the subject's data source.
+ */
+ filterBy: CollectionFilter[] | CollectionFilterGroup[] = [];
+ /**
+ * Search field value to apply for the subject's data source.
+ */
+ search: SearchInput | undefined;
+ /**
+ * @hidden
+ * Form generator component instance.
+ */
+ @ViewChild(FormGeneratorComponent) _formGenerator!: FormGeneratorComponent;
+ /** @hidden */
+ _formItems: DynamicFormItem[] = [];
+ /** @hidden */
+ _selectedFilters: string[] = [];
+ /** @hidden */
+ _showFilterBar = true;
+ /** @hidden */
+ private _subscriptions = new Subscription();
+ /** @hidden */
+ private _subjectSubscriptions = new Subscription();
+
+ /** @hidden */
+ private _transparent = false;
+
+ /** @hidden */
+ constructor(
+ private _dialogService: DialogService,
+ private _cdr: ChangeDetectorRef,
+ private _smartFilterBarService: SmartFilterBarService,
+ private _fgService: FormGeneratorService
+ ) {
+ this._fgService.addComponent(SmartFilterBarConditionFieldComponent, [SMART_FILTER_BAR_RENDERER_COMPONENT]);
+ }
+
+ /** @hidden */
+ private _subject!: SmartFilterBarSubjectDirective;
+
+ /** @hidden */
+ ngOnDestroy(): void {
+ this._subscriptions.unsubscribe();
+ this._unsubscribeFromSubject();
+ }
+
+ /**
+ * Transforms condition value into human-readable text.
+ * @param condition Smart filter bar condition.
+ * @param filterType Condition filter type.
+ */
+ async getDisplayValue(condition: SmartFilterBarCondition, filterType: string): Promise {
+ return this._smartFilterBarService.getDisplayValue(condition, filterType);
+ }
+
+ /** Open Filtering Settings Dialog */
+ showFilteringSettings(): void {
+ const columns = this._getSubjectDefinitions();
+ const dialogData: SmartFilterSettingsDialogConfig = {
+ fields: columns,
+ filterBy: this.filterBy,
+ selectedFilters: this._selectedFilters,
+ visibilityCategories: this.filtersVisibilityCategoryLabels
+ };
+
+ const dialogRef = this._dialogService.open(SmartFilterBarSettingsDialogComponent, {
+ ...dialogConfig,
+ responsivePadding: false,
+ verticalPadding: false,
+ width: '50rem',
+ data: dialogData
+ });
+
+ dialogRef.afterClosed.pipe(take(1)).subscribe(
+ (selectedFilters: string[]) => {
+ this._setSelectedFilters(selectedFilters);
+ },
+ (_) => {}
+ );
+ }
+
+ /** @hidden */
+ _onSearchInputChange(event: SearchInput): void {
+ this.search = event;
+ }
+
+ /**
+ * Submits filters and search form.
+ */
+ submitForm(): void {
+ this._formGenerator.submit();
+ }
+
+ /**
+ * @hidden
+ * Callback function when form generator form has been successfully validated and submitted.
+ */
+ _onFormSubmitted(event: any): void {
+ const conditions = this._generateCollectionFilterGroups(event);
+ this._applyFiltering(conditions);
+ }
+
+ /**
+ * @hidden
+ * Callback method when form has been created.
+ * Populates selected filters array with user-defined default filters.
+ */
+ _onFormCreated(): void {
+ this._setSelectedFilters([...this.subject?.getDefaultFields(), ...this._selectedFilters]);
+ }
+
+ /**
+ * @hidden
+ */
+ _toggleFilterBar(): void {
+ this._showFilterBar = !this._showFilterBar;
+ this._cdr.markForCheck();
+ }
+
+ /** @hidden */
+ private _generateCollectionFilterGroups(value: {
+ [key: string]: SmartFilterBarCondition[];
+ }): CollectionFilterGroup[] {
+ const collectionFilterGroups: CollectionFilterGroup[] = [];
+
+ const columns = this._getSubjectDefinitions();
+
+ Object.entries(value)
+ .filter(([_, fieldConditions]) => !!fieldConditions)
+ .forEach(([fieldName, fieldConditions]) => {
+ const column = columns.find((c) => c.name === fieldName) as SmartFilterBarFieldDefinition;
+ const filterGroup: CollectionFilterGroup = {
+ filters: [],
+ strategy: column.conditionStrategy,
+ field: column.key
+ };
+ fieldConditions.forEach((condition) => {
+ filterGroup.filters.push({
+ strategy: condition.operator,
+ field: column.key,
+ value: condition.value,
+ value2: condition.value2,
+ type: column.dataType
+ });
+ });
+
+ collectionFilterGroups.push(filterGroup);
+ });
+
+ return collectionFilterGroups;
+ }
+
+ /** @hidden */
+ private _setSubject(subject: SmartFilterBarSubjectDirective): void {
+ if (!subject) {
+ return;
+ }
+
+ this._subject = subject;
+ this.unsubscribeFromSubject();
+ this._listenToSubjectColumns();
+ }
+
+ /** @hidden */
+ private unsubscribeFromSubject(): void {
+ if (!this._subject) {
+ return;
+ }
+ }
+
+ /** @hidden */
+ private _listenToSubjectColumns(): void {
+ this._subjectSubscriptions.add(
+ this._subject.fieldsStream.subscribe(async (columns: SmartFilterBarFieldDefinition[]) => {
+ setTimeout(async () => {
+ await this._generateForm(columns.filter((c) => c.filterable));
+ });
+ })
+ );
+ }
+
+ /**
+ * @hidden
+ * Generates form items to be consumed by form generator component.
+ * @param columns columns definition
+ */
+ private async _generateForm(columns: SmartFilterBarFieldDefinition[]): Promise {
+ const items: DynamicFormFieldItem[] = [];
+
+ if (columns.length === 0) {
+ return;
+ }
+
+ columns.forEach((column) => {
+ let item: SmartFilterBarDynamicFormFieldItem = this._generateBaseFieldItem(column);
+
+ if (column.required) {
+ this._selectedFilters.push(column.name);
+ }
+
+ switch (column.filterType) {
+ case FilterType.SINGLE:
+ case FilterType.CATEGORY:
+ item.guiOptions.additionalData.type = 'select';
+ item.guiOptions.inline = false;
+ break;
+ case FilterType.MULTI:
+ item.guiOptions.additionalData.type = 'multi-input';
+ break;
+ case FilterType.CUSTOM:
+ item = this._generateCustomFilterFieldItem(column, item);
+ break;
+ case FilterType.INPUT:
+ default:
+ item = this._generateInputFieldItem(column, item);
+ break;
+ }
+
+ items.push(item);
+ });
+
+ this._formItems = items;
+
+ this._cdr.detectChanges();
+ }
+
+ /** @hidden */
+ private _generateCustomFilterFieldItem(
+ column: SmartFilterBarFieldDefinition,
+ item: SmartFilterBarDynamicFormFieldItem
+ ): SmartFilterBarDynamicFormFieldItem {
+ if (!column.customFilterType) {
+ return item;
+ }
+ item.guiOptions.additionalData.type = column.customFilterType;
+ item.guiOptions.additionalData = Object.assign(
+ item.guiOptions.additionalData,
+ this._smartFilterBarService.getCustomFilterConfiguration(column.customFilterType)
+ );
+
+ item.type = item.guiOptions.additionalData.rendererComponent
+ ? `${item.guiOptions.additionalData.type}-renderer`
+ : item.type;
+ item.transformer = item.guiOptions.additionalData.valueTransformer;
+
+ return item;
+ }
+
+ /**
+ * @hidden
+ * Generates form item based on data type.
+ * @param column
+ * @param item
+ * @returns
+ */
+ private _generateInputFieldItem(
+ column: SmartFilterBarFieldDefinition,
+ item: SmartFilterBarDynamicFormFieldItem
+ ): SmartFilterBarDynamicFormFieldItem {
+ switch (column.dataType) {
+ case FilterableColumnDataType.DATE:
+ item.guiOptions.additionalData.type = 'datepicker';
+ break;
+ case FilterableColumnDataType.NUMBER:
+ item.guiOptions.additionalData.controlType = 'number';
+ break;
+ case FilterableColumnDataType.BOOLEAN:
+ case FilterableColumnDataType.STRING:
+ default:
+ item.guiOptions.additionalData.controlType = 'text';
+ break;
+ }
+
+ return item;
+ }
+
+ /**
+ * @hidden
+ * Generates base form item object to be consumed by form generator component.
+ * @param column column definition
+ * @returns base form item object.
+ */
+ private _generateBaseFieldItem(column: SmartFilterBarFieldDefinition): SmartFilterBarDynamicFormFieldItem {
+ return {
+ name: column.name,
+ message: column.label,
+ required: column.required,
+ type: SMART_FILTER_BAR_RENDERER_COMPONENT,
+ placeholder: ' ',
+ validators: column.required ? [Validators.required] : [],
+ choices: column.hasOptions ? this._getFilterDefaultOptions(column.key, column.filterType) : undefined,
+ transformer: (itemValue) => getSelectItemValue(itemValue),
+ guiOptions: {
+ contentDensity: 'compact',
+ additionalData: {
+ type: 'input',
+ dataType: column.dataType,
+ filterType: column.filterType,
+ controlType: 'text',
+ choices: column.hasOptions ? this._getFilterAvailableOptions(column.key) : undefined
+ }
+ },
+ when: () => this._selectedFilters.includes(column.name)
+ };
+ }
+
+ /**
+ * @hidden
+ * Retrieves available options for particular column by using data source method.
+ * @param column property name of the data source items.
+ * @returns {Observable} Observable with the array of available options.
+ */
+ private _getFilterAvailableOptions(column: string): () => Observable {
+ return () => this._getFieldVariants(column);
+ }
+
+ /** @hidden */
+ private _getFilterDefaultOptions(column: string, filterType: FilterType): () => Promise {
+ return async () => {
+ const variants = await this._getFieldVariants(column).pipe(take(1)).toPromise();
+
+ const availableDefaultConditions: SmartFilterBarCondition[] = [];
+
+ for (const option of variants) {
+ const condition: SmartFilterBarCondition = {
+ value: option.value,
+ operator: 'equalTo'
+ };
+
+ condition.displayValue = await this.getDisplayValue(condition, filterType);
+
+ availableDefaultConditions.push(condition);
+ }
+
+ return availableDefaultConditions;
+ };
+ }
+
+ /** @hidden */
+ private _getFieldVariants(column: string): Observable {
+ return this.subject.getFieldVariants(column).pipe(
+ take(1),
+ map((data) => data.filter((item, index) => data.findIndex((_item) => _item.value === item.value) === index))
+ );
+ }
+
+ /** @hidden */
+ private _getSubjectDefinitions(): SmartFilterBarFieldDefinition[] {
+ return this.subject?.getSubjectFields() || [];
+ }
+
+ /**
+ * @hidden
+ * Sets provided filters directly to the data source.
+ * @param filters array of filters.
+ */
+ private _applyFiltering(filters: CollectionFilterGroup[]): void {
+ // Apply outside filtering and force subject to fetch new data.
+ const source = this.subject.getDataSource();
+ this.filterBy = filters;
+ source.dataProvider.setFilters(filters, this.search);
+ this.subject.getSubject().fetch();
+ }
+
+ /** @hidden */
+ private _unsubscribeFromSubject(): void {
+ this._subjectSubscriptions.unsubscribe();
+ this._subjectSubscriptions = new Subscription();
+ }
+
+ /** @hidden */
+ private _setSelectedFilters(filters: string[]): void {
+ this._selectedFilters = filters.filter((f: string, i: number) => filters.indexOf(f) === i);
+ this._formGenerator.refreshStepsVisibility();
+ }
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.module.ts b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.module.ts
new file mode 100644
index 00000000000..2cade655897
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.module.ts
@@ -0,0 +1,74 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+
+import { PopoverModule } from '@fundamental-ngx/core/popover';
+import { ButtonModule } from '@fundamental-ngx/core/button';
+import {
+ PlatformFormGeneratorModule,
+ PlatformInputGroupModule,
+ PlatformMultiInputModule,
+ FdpFormGroupModule,
+ FormGeneratorService
+} from '@fundamental-ngx/platform/form';
+import { ToolbarModule } from '@fundamental-ngx/core/toolbar';
+import { PlatformButtonModule } from '@fundamental-ngx/platform/button';
+import { PlatformSelectModule } from '@fundamental-ngx/platform/form';
+import { IconModule } from '@fundamental-ngx/core/icon';
+import { PlatformSearchFieldModule } from '@fundamental-ngx/platform/search-field';
+import { DialogModule } from '@fundamental-ngx/core/dialog';
+import { ListModule } from '@fundamental-ngx/core/list';
+import { CheckboxModule } from '@fundamental-ngx/core/checkbox';
+import { PlatformTableModule } from '@fundamental-ngx/platform/table';
+import { LayoutGridModule } from '@fundamental-ngx/core/layout-grid';
+
+import { SmartFilterBarComponent } from './smart-filter-bar.component';
+
+import { SmartFilterBarSettingsDialogComponent } from './components/smart-filter-bar-settings-dialog/smart-filter-bar-settings-dialog.component';
+import { SmartFilterBarFieldDefinitionDirective } from './directives/smart-filter-bar-field-definition.directive';
+import { SmartFilterBarSubjectDirective } from './directives/smart-filter-bar-subject.directive';
+import { SmartFilterBarConditionsDialogComponent } from './components/smart-filter-bar-conditions-dialog/smart-filter-bar-conditions-dialog.component';
+import { SmartFilterBarConditionFieldComponent } from './components/smart-filter-bar-condition-field/smart-filter-bar-condition-field.component';
+import { SmartFilterBarService } from './smart-filter-bar.service';
+
+@NgModule({
+ declarations: [
+ SmartFilterBarComponent,
+ SmartFilterBarSettingsDialogComponent,
+ SmartFilterBarFieldDefinitionDirective,
+ SmartFilterBarSubjectDirective,
+ SmartFilterBarConditionsDialogComponent,
+ SmartFilterBarConditionFieldComponent
+ ],
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ PlatformFormGeneratorModule,
+ ButtonModule,
+ PopoverModule,
+ ToolbarModule,
+ PlatformButtonModule,
+ PlatformInputGroupModule,
+ IconModule,
+ PlatformSearchFieldModule,
+ DialogModule,
+ ListModule,
+ CheckboxModule,
+ PlatformTableModule,
+ PlatformSelectModule,
+ PlatformMultiInputModule,
+ FdpFormGroupModule,
+ LayoutGridModule
+ ],
+ exports: [
+ SmartFilterBarComponent,
+ SmartFilterBarSettingsDialogComponent,
+ SmartFilterBarFieldDefinitionDirective,
+ SmartFilterBarSubjectDirective,
+ SmartFilterBarConditionsDialogComponent,
+ SmartFilterBarConditionFieldComponent
+ ],
+ providers: [SmartFilterBarService, FormGeneratorService]
+})
+export class PlatformSmartFilterBarModule {}
diff --git a/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.service.spec.ts b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.service.spec.ts
new file mode 100644
index 00000000000..e732a6a5c48
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.service.spec.ts
@@ -0,0 +1,74 @@
+import { TestBed } from '@angular/core/testing';
+import { Component } from '@angular/core';
+
+import { SmartFilterBarService } from './smart-filter-bar.service';
+import { PlatformSmartFilterBarModule } from './smart-filter-bar.module';
+import { SmartFilterBarCondition } from './interfaces/smart-filter-bar-condition';
+import { SmartFilterBarCustomFilterConfig } from './interfaces/smart-filter-bar-custom-filter-config';
+import { BaseDynamicFormGeneratorControl } from '@fundamental-ngx/platform/form';
+
+@Component({
+ selector: 'fdp-smart-filter-bar-slider-test',
+ template: ``
+})
+class TestFilterComponent extends BaseDynamicFormGeneratorControl {
+ constructor() {
+ super();
+ }
+}
+
+const filterConfig: SmartFilterBarCustomFilterConfig = {
+ conditionComponent: TestFilterComponent,
+ filterStrategies: ['equalTo', 'greaterThan', 'greaterThanOrEqualTo', 'lessThan', 'lessThanOrEqualTo'],
+ valueRenderer: (condition: SmartFilterBarCondition) => {
+ const value1 = condition.value;
+ const value2 = condition.value2;
+
+ return `${value1}-${value2}`;
+ },
+ types: ['test']
+};
+
+describe('SmartFilterBarService', () => {
+ let service: SmartFilterBarService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [PlatformSmartFilterBarModule],
+ declarations: [TestFilterComponent]
+ });
+ service = TestBed.inject(SmartFilterBarService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should add custom filter', () => {
+ const result = service.addCustomFilter(filterConfig);
+ expect(result).toBeTrue();
+ });
+
+ it('should return custom filter configuration', () => {
+ service.addCustomFilter(filterConfig);
+ const configuration = service.getCustomFilterConfiguration('test');
+ expect(configuration).toEqual(filterConfig);
+ });
+
+ it('should return applicable filter condition operators', () => {
+ service.addCustomFilter(filterConfig);
+ const operators = service.getApplicableFilterConditions('test');
+ expect(operators).toEqual(filterConfig.filterStrategies);
+ });
+
+ it('should use custom value renderer', async () => {
+ service.addCustomFilter(filterConfig);
+ const condition: SmartFilterBarCondition = {
+ value: 'value one',
+ value2: 'value two',
+ operator: 'equalTo'
+ };
+ const displayValue = await service.getDisplayValue(condition, 'test');
+ expect(displayValue).toEqual(`${condition.value}-${condition.value2}`);
+ });
+});
diff --git a/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.service.ts b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.service.ts
new file mode 100644
index 00000000000..8fe3d2a1a10
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/smart-filter-bar.service.ts
@@ -0,0 +1,166 @@
+import { Injectable, Type } from '@angular/core';
+import { FdDate } from '@fundamental-ngx/core/datetime';
+import { BaseDynamicFormGeneratorControl, FormGeneratorService } from '@fundamental-ngx/platform/form';
+import {
+ FilterableColumnDataType,
+ FilterAllStrategy,
+ getFilterStrategiesBasedOnDataType
+} from '@fundamental-ngx/platform/table';
+import { isSelectItem, selectStrategy } from '@fundamental-ngx/platform/shared';
+
+import { SmartFilterBarCustomFilterConfig } from './interfaces/smart-filter-bar-custom-filter-config';
+import { SmartFilterBarCondition } from './interfaces/smart-filter-bar-condition';
+
+@Injectable()
+export class SmartFilterBarService {
+ /** @hidden */
+ private _customFilterConditions: Map, SmartFilterBarCustomFilterConfig> =
+ new Map, SmartFilterBarCustomFilterConfig>();
+
+ /** @hidden */
+ constructor(private _fgService: FormGeneratorService) {}
+
+ /**
+ * Adds custom filter type to the smart filter bar.
+ * @param options Configuration options.
+ * @returns true if component was added successfully.
+ */
+ addCustomFilter(options: SmartFilterBarCustomFilterConfig): boolean {
+ if (!options.conditionComponent && options.rendererComponent) {
+ options.conditionComponent = options.rendererComponent;
+ }
+
+ if (!options.conditionComponent) {
+ return false;
+ }
+
+ const result = this._fgService.addComponent(options.conditionComponent, options.types);
+
+ if (result) {
+ this._customFilterConditions.set(options.conditionComponent, options);
+ }
+
+ if (result && options.rendererComponent) {
+ this._fgService.addComponent(
+ options.rendererComponent,
+ options.types.map((t) => `${t}-renderer`)
+ );
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns provided configuration object of the custom filter.
+ * @param type Filter type.
+ * @returns Configuration object.
+ */
+ getCustomFilterConfiguration(type: string): SmartFilterBarCustomFilterConfig | undefined {
+ const componentDefinition = this._fgService.getComponentDefinitionByType(type);
+
+ if (!componentDefinition) {
+ return undefined;
+ }
+
+ return this._customFilterConditions.get(componentDefinition.component);
+ }
+
+ /**
+ * Returns applicable condition options of the filter.
+ * @param type Filter type.
+ * @param dataType Filter data type.
+ * @returns Array of applicable condition options.
+ */
+ getApplicableFilterConditions(
+ type: string,
+ dataType: FilterableColumnDataType = FilterableColumnDataType.STRING
+ ): FilterAllStrategy[] {
+ const componentDefinition = this._fgService.getComponentDefinitionByType(type);
+
+ const defaultConditions: FilterAllStrategy[] = getFilterStrategiesBasedOnDataType(
+ dataType
+ ) as FilterAllStrategy[];
+
+ if (!componentDefinition) {
+ return defaultConditions;
+ }
+
+ const filterDefinition = this._customFilterConditions.get(componentDefinition.component);
+
+ return filterDefinition?.filterStrategies || defaultConditions;
+ }
+
+ /** @hidden */
+ async getDisplayValue(condition: SmartFilterBarCondition, filterType: string): Promise {
+ const configuration = this.getCustomFilterConfiguration(filterType);
+
+ if (configuration?.valueRenderer) {
+ const obj = configuration?.valueRenderer(condition);
+ const strategy = selectStrategy(obj);
+
+ let returnValue!: string;
+
+ await strategy.createSubscription(obj, (value) => {
+ returnValue = value;
+ });
+
+ return returnValue;
+ }
+
+ const value1 = this._normalizeValue(condition.value);
+ const value2 = this._normalizeValue(condition.value2);
+
+ switch (condition.operator) {
+ case 'equalTo':
+ return `=${value1}`;
+ case 'contains':
+ return `*${value1}*`;
+ case 'between':
+ return `${value1}...${value2}`;
+ case 'beginsWith':
+ return `${value1}*`;
+ case 'endsWith':
+ return `*${value1}`;
+ case 'greaterThan':
+ return `>${value1}`;
+ case 'greaterThanOrEqualTo':
+ return `>=${value1}`;
+ case 'lessThan':
+ return `<${value1}`;
+ case 'lessThanOrEqualTo':
+ return `<=${value1}`;
+ case 'after':
+ return `>${value1}`;
+ case 'onOrAfter':
+ return `>=${value1}`;
+ case 'before':
+ return `<${value1}`;
+ case 'beforeOrOn':
+ return `<=${value1}`;
+ default:
+ return `${value1}`;
+ }
+ }
+
+ /**
+ * @hidden
+ * Normalizes condition value.
+ * @param value
+ * @returns
+ */
+ private _normalizeValue(value: any): any {
+ if (Array.isArray(value)) {
+ return value.map((v) => this._normalizeValue(v)).join(', ');
+ }
+
+ if (isSelectItem(value)) {
+ return value.label;
+ }
+
+ if (value instanceof FdDate) {
+ return value.toDateString();
+ }
+
+ return value;
+ }
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/test.ts b/libs/platform/src/lib/smart-filter-bar/test.ts
new file mode 100644
index 00000000000..b80d611fb02
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/test.ts
@@ -0,0 +1,18 @@
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'core-js/es/reflect';
+import 'zone.js';
+import '@angular/localize/init';
+
+import 'zone.js/testing';
+import { getTestBed } from '@angular/core/testing';
+import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
+
+declare const require: any;
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().map(context);
diff --git a/libs/platform/src/lib/smart-filter-bar/tsconfig.json b/libs/platform/src/lib/smart-filter-bar/tsconfig.json
new file mode 100644
index 00000000000..e54914a1c26
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "extends": "../../../../../tsconfig.base.json",
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.lib.prod.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "compilerOptions": {
+ "forceConsistentCasingInFileNames": true,
+ "strict": false,
+ "noImplicitReturns": false,
+ "noFallthroughCasesInSwitch": true
+ },
+ "angularCompilerOptions": {
+ "strictInjectionParameters": false,
+ "strictTemplates": false
+ }
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/tsconfig.lib.json b/libs/platform/src/lib/smart-filter-bar/tsconfig.lib.json
new file mode 100644
index 00000000000..ba746e06265
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/tsconfig.lib.json
@@ -0,0 +1,19 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/lib",
+ "target": "es2015",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": [],
+ "lib": ["dom", "es2018"]
+ },
+ "angularCompilerOptions": {
+ "skipTemplateCodegen": true,
+ "strictMetadataEmit": true,
+ "enableResourceInlining": true
+ },
+ "exclude": ["src/test.ts", "**/*.spec.ts"]
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/tsconfig.lib.prod.json b/libs/platform/src/lib/smart-filter-bar/tsconfig.lib.prod.json
new file mode 100644
index 00000000000..f8b01637a83
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/tsconfig.lib.prod.json
@@ -0,0 +1,10 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.lib.json",
+ "compilerOptions": {
+ "declarationMap": false
+ },
+ "angularCompilerOptions": {
+ "enableIvy": false
+ }
+}
diff --git a/libs/platform/src/lib/smart-filter-bar/tsconfig.spec.json b/libs/platform/src/lib/smart-filter-bar/tsconfig.spec.json
new file mode 100644
index 00000000000..5663ceded34
--- /dev/null
+++ b/libs/platform/src/lib/smart-filter-bar/tsconfig.spec.json
@@ -0,0 +1,10 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/spec",
+ "types": ["jasmine"]
+ },
+ "files": ["./test.ts"],
+ "include": ["**/*.spec.ts", "**/*.d.ts"]
+}
diff --git a/libs/platform/src/lib/table/directives/table-cell-resizable.directive.ts b/libs/platform/src/lib/table/directives/table-cell-resizable.directive.ts
index 814c8c92d15..54f2bab4c83 100644
--- a/libs/platform/src/lib/table/directives/table-cell-resizable.directive.ts
+++ b/libs/platform/src/lib/table/directives/table-cell-resizable.directive.ts
@@ -1,4 +1,4 @@
-import { AfterViewInit, Directive, ElementRef, HostListener, Input, Optional } from '@angular/core';
+import { AfterViewInit, Directive, ElementRef, HostListener, Input, OnDestroy, Optional } from '@angular/core';
import { RtlService } from '@fundamental-ngx/core/utils';
@@ -12,7 +12,7 @@ export const TABLE_CELL_RESIZABLE_THRESHOLD_PX = 4;
* Tracks mouse movement over the cell if the mouse pointer near the side of the cell, informs resize service.
*/
@Directive({ selector: '[fdpTableCellResizable]' })
-export class PlatformTableCellResizableDirective implements AfterViewInit {
+export class PlatformTableCellResizableDirective implements AfterViewInit, OnDestroy {
/** First column can be resized only by its end */
@Input('fdpTableCellResizable')
set resizableSide(value: TableColumnResizableSide) {
@@ -51,6 +51,10 @@ export class PlatformTableCellResizableDirective implements AfterViewInit {
this._tableColumnResizeService?.registerColumnCell(this.columnName, this._elRef);
}
+ ngOnDestroy(): void {
+ this._tableColumnResizeService?.unregisterColumnCell(this.columnName, this._elRef);
+ }
+
/** @hidden */
@HostListener('mousemove', ['$event'])
_onMouseMove(event: MouseEvent): void {
diff --git a/libs/platform/src/lib/table/domain/array-data-source.ts b/libs/platform/src/lib/table/domain/array-data-source.ts
index 87202971bd8..fc170a8a689 100644
--- a/libs/platform/src/lib/table/domain/array-data-source.ts
+++ b/libs/platform/src/lib/table/domain/array-data-source.ts
@@ -1,36 +1,56 @@
-import { of, Observable } from 'rxjs';
+import { BehaviorSubject, Observable } from 'rxjs';
import { TableDataSource } from './table-data-source';
import { TableDataProvider } from './table-data-provider';
import { TableState } from '../interfaces/table-state.interface';
+import { map } from 'rxjs/operators';
+import { DatetimeAdapter } from '@fundamental-ngx/core/datetime';
/**
* Table Data Provider based on an array.
*
* Used to convert array source to the TableDataProvider interface.
*
- * For now it does not handle table state and used just for back
- * compatibility with the previous table interface.
- *
*/
-
export class ArrayTableDataProvider extends TableDataProvider {
- items = [];
- totalItems = 0;
+ /** @hidden */
+ protected items$ = new BehaviorSubject([]);
- constructor(items: T[]) {
+ /** @hidden */
+ constructor(items: T[], dateTimeAdapter?: DatetimeAdapter) {
super();
this.items = items;
this.totalItems = this.items.length;
+ this.items$.next(this.items);
+ this.dateTimeAdapter = dateTimeAdapter;
}
- fetch(state: TableState): Observable