Skip to content

Commit

Permalink
#1074 refactor & add state change listeners to FilterBar (#1160)
Browse files Browse the repository at this point in the history
* fix state updates in FilterBar.examples

- follow-up change from `managed named filters`
- Because of the `filter` state inside of DefaultFilterBar, the
  new example of switching between filter sets (added as part
  of managed named filters) was not working as expected.
  So this commit fixes that and introduces a DefaultFilterBarCore,
  which is exactly the same as previous DefaultFilterBar except
  that it doesn't contain the `filter` state.

* #1074 refactor & add state change listeners to FilterBar

- refactors FilterBar to accept optional combined filterState prop
  instead of separate filters and active indices props as both props
  are very closely related and it also simplifies state handling
  in FilterBar.
- adds back useEffect hook to listen to changes in filter
  state and apply those as required, to keep data source
  filtering in sync.
  • Loading branch information
junaidzm13 authored Jan 25, 2024
1 parent f889cd5 commit c408798
Show file tree
Hide file tree
Showing 12 changed files with 502 additions and 440 deletions.
9 changes: 4 additions & 5 deletions vuu-ui/packages/vuu-filter-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,9 @@ export interface FilterWithPartialClause extends MultiClauseFilter {
filters: Array<Partial<Filter>>;
}

export declare type ColumnDescriptorsByName = Record<string, ColumnDescriptor>;

export declare type FilterState = {
filter: Filter | undefined;
filterQuery: string;
filterName?: string;
filters: Filter[];
activeIndices: number[];
};

export declare type ColumnDescriptorsByName = Record<string, ColumnDescriptor>;
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ describe("WHEN it initially renders", () => {
container.get(OVERFLOW_INDICATOR).should("exist");
container.get(OVERFLOW_INDICATOR).should("have.css", "width", "0px");
});
it("AND WHEN filterState passed THEN it calls onApplyFilter with currently active filters", () => {
const onApplyFilter = cy.stub().as("onApplyFilter");
const filter = { column: "currency", op: "!=", value: "CAD" } as const;
cy.mount(
<DefaultFilterBar
onApplyFilter={onApplyFilter}
filterState={{
filters: [filter, { ...filter, value: "USD" }],
activeIndices: [1],
}}
/>
);

cy.get("@onApplyFilter").should("be.calledWithExactly", {
filter: 'currency != "USD"',
filterStruct: { ...filter, value: "USD" },
});
});
});

describe("The mouse user", () => {
Expand Down Expand Up @@ -146,21 +164,24 @@ describe("The mouse user", () => {
};

beforeEach(() => {
const onFiltersChanged = cy.stub().as("filtersChangedHandler");
const onFilterStateChanged = cy.stub().as("filterStateChangeHandler");
const onApplyFilter = cy.stub().as("applyFilterHandler");
cy.mount(
<DefaultFilterBar
onApplyFilter={onApplyFilter}
onFiltersChanged={onFiltersChanged}
onFilterStateChanged={onFilterStateChanged}
/>
);
cy.get(ADD_BUTTON).realClick();
clickListItems(testFilter.column, testFilter.op, testFilter.value);
clickButton("APPLY AND SAVE");
});

it("THEN filtersChangedHandler callback is invoked", () => {
cy.get("@filtersChangedHandler").should("be.calledWith", [testFilter]);
it("THEN filterStateChangeHandler callback is invoked", () => {
cy.get("@filterStateChangeHandler").should("be.calledWith", {
filters: [testFilter],
activeIndices: [0],
});
});

it("THEN filter is applied", () => {
Expand Down Expand Up @@ -190,9 +211,10 @@ describe("The mouse user", () => {
findOverflowItem(".vuuFilterPill")
.find(".vuuEditableLabel")
.should("not.have.class", "vuuEditableLabel-editing");
cy.get("@filtersChangedHandler").should("be.calledWith", [
{ ...testFilter, name: "test" },
]);
cy.get("@filterStateChangeHandler").should("be.calledWith", {
filters: [{ ...testFilter, name: "test" }],
activeIndices: [0],
});
});

it("THEN filter pill has focus", () => {
Expand Down Expand Up @@ -220,9 +242,10 @@ describe("The mouse user", () => {
clickListItems(newFilter.column, newFilter.op, newFilter.value);
clickButton("APPLY AND SAVE");

cy.get("@filtersChangedHandler").should("be.calledWithExactly", [
newFilter,
]);
cy.get("@filterStateChangeHandler").should("be.calledWithExactly", {
filters: [newFilter],
activeIndices: [0],
});
cy.get("@applyFilterHandler").should("be.calledWithExactly", {
filter: 'currency != "CAD"',
filterStruct: newFilter,
Expand All @@ -246,12 +269,12 @@ describe("The mouse user", () => {
};

beforeEach(() => {
const onFiltersChanged = cy.stub().as("filtersChangedHandler");
const onFilterStateChanged = cy.stub().as("filterStateChangeHandler");
const onApplyFilter = cy.stub().as("applyFilterHandler");
cy.mount(
<DefaultFilterBar
onApplyFilter={onApplyFilter}
onFiltersChanged={onFiltersChanged}
onFilterStateChanged={onFilterStateChanged}
/>
);
cy.get(ADD_BUTTON).realClick();
Expand All @@ -267,30 +290,50 @@ describe("The mouse user", () => {
cy.realPress("Enter");
});

it("THEN filtersChangedHandler & applyFilterHandler callbacks are invoked with correct values", () => {
cy.get("@filtersChangedHandler").should("be.calledWith", [
filter1,
filter2,
]);
it("THEN filterStateChangeHandler & applyFilterHandler callbacks are invoked with correct values", () => {
cy.get("@filterStateChangeHandler").should("be.calledWith", {
filters: [filter1, filter2],
activeIndices: [0, 1],
});

cy.get("@applyFilterHandler").should("be.calledWith", {
filter: 'currency != "USD" and currency != "CAD"',
filterStruct: { op: "and", filters: [filter1, filter2] },
});
});

it("AND WHEN one filter is made inactive THEN changes are correctly applied", () => {
findOverflowItem('[data-index="0"]').realClick({ shiftKey: true });
findOverflowItem('[data-index="0"]').find("[aria-selected='false']");
findOverflowItem('[data-index="1"]').find("[aria-selected='true']");

cy.get("@filterStateChangeHandler").should("be.calledWithExactly", {
filters: [filter1, filter2],
activeIndices: [1],
});

cy.get("@applyFilterHandler").should("be.calledWithExactly", {
filter: 'currency != "CAD"',
filterStruct: filter2,
});
});

it("AND WHEN second filter is deleted THEN changes are correctly applied", () => {
findOverflowItem('[data-index="1"]')
.find(".vuuFilterPillMenu")
.realClick();
clickButton("Delete");
clickButton("Remove");

cy.get("@filtersChangedHandler").should("be.calledWithExactly", [
filter1,
]);
findOverflowItem(".vuuFilterPill").should("have.length", 1);
findOverflowItem(".vuuFilterPill").contains(filter1.name);

cy.get("@filterStateChangeHandler").should("be.calledWithExactly", {
filters: [filter1],
activeIndices: [0],
});
cy.get("@applyFilterHandler").should("be.calledWithExactly", {
filter: 'currency != "USD"',
filter: filter1.name,
filterStruct: filter1,
});
});
Expand All @@ -302,11 +345,15 @@ describe("The mouse user", () => {
clickButton("Delete");
clickButton("Remove");

cy.get("@filtersChangedHandler").should("be.calledWithExactly", [
filter2,
]);
findOverflowItem(".vuuFilterPill").should("have.length", 1);
findOverflowItem(".vuuFilterPill").contains(filter2.name);

cy.get("@filterStateChangeHandler").should("be.calledWithExactly", {
filters: [filter2],
activeIndices: [0],
});
cy.get("@applyFilterHandler").should("be.calledWithExactly", {
filter: 'currency != "CAD"',
filter: filter2.name,
filterStruct: filter2,
});
});
Expand Down Expand Up @@ -447,11 +494,11 @@ describe("WHEN a user applies a date filter", () => {

beforeEach(() => {
const onApplyFilter = cy.stub().as("applyFilterHandler");
const onFiltersChanged = cy.stub().as("filtersChangedHandler");
const onFilterStateChanged = cy.stub().as("filterStateChangeHandler");
cy.mount(
<DefaultFilterBar
onApplyFilter={onApplyFilter}
onFiltersChanged={onFiltersChanged}
onFilterStateChanged={onFilterStateChanged}
/>
);
});
Expand Down Expand Up @@ -518,9 +565,10 @@ describe("WHEN a user applies a date filter", () => {
filter: expectedQuery,
filterStruct: expectedFilter,
});
cy.get("@filtersChangedHandler").should("be.calledWithExactly", [
expectedFilter,
]);
cy.get("@filterStateChangeHandler").should("be.calledWithExactly", {
filters: [expectedFilter],
activeIndices: [0],
});
})
);
});
Expand Down
31 changes: 16 additions & 15 deletions vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DataSourceFilter, TableSchema } from "@finos/vuu-data-types";
import { Filter } from "@finos/vuu-filter-types";
import { Filter, FilterState } from "@finos/vuu-filter-types";
import { Prompt } from "@finos/vuu-popups";
import { ActiveItemChangeHandler, Toolbar } from "@finos/vuu-ui-controls";
import { Toolbar } from "@finos/vuu-ui-controls";
import { ColumnDescriptor } from "@finos/vuu-table-types";
import { Button } from "@salt-ds/core";
import cx from "clsx";
Expand All @@ -17,31 +17,34 @@ import "./FilterBar.css";

export interface FilterBarProps extends HTMLAttributes<HTMLDivElement> {
FilterClauseEditorProps?: Partial<FilterClauseEditorProps>;
activeFilterIndex?: number[];
/**
* This is used to apply tailored filters based on column types and other attributes.
* NOTE: Always make sure that these are passed with proper re-render optimization, otherwise,
* might end up with infinite state updates.
*/
columnDescriptors: ColumnDescriptor[];
filters: Filter[];
defaultFilterState?: FilterState;
filterState?: FilterState;
onApplyFilter: (filter: DataSourceFilter) => void;
onChangeActiveFilterIndex: ActiveItemChangeHandler;
onFilterDeleted?: (filter: Filter) => void;
onFilterRenamed?: (filter: Filter, name: string) => void;
onFiltersChanged?: (filters: Filter[]) => void;
onFilterStateChanged?: (state: FilterState) => void;
showMenu?: boolean;
tableSchema: TableSchema;
}

const classBase = "vuuFilterBar";

export const FilterBar = ({
activeFilterIndex: activeFilterIndexProp = [],
FilterClauseEditorProps,
className: classNameProp,
columnDescriptors,
filters: filtersProp,
defaultFilterState,
filterState,
onApplyFilter,
onChangeActiveFilterIndex: onChangeActiveFilterIndexProp,
onFilterDeleted,
onFilterRenamed,
onFiltersChanged,
onFilterStateChanged,
showMenu: showMenuProp = false,
tableSchema,
...htmlAttributes
Expand All @@ -68,17 +71,15 @@ export const FilterBar = ({
promptProps,
showMenu,
} = useFilterBar({
activeFilterIndex: activeFilterIndexProp,
containerRef: rootRef,
columnDescriptors,
filters: filtersProp,
defaultFilterState,
filterState,
onApplyFilter,
onChangeActiveFilterIndex: onChangeActiveFilterIndexProp,
onFiltersChanged,
onFilterStateChanged,
onFilterDeleted,
onFilterRenamed,
showMenu: showMenuProp,
tableSchema,
});

const className = cx(classBase, classNameProp, {
Expand Down
1 change: 0 additions & 1 deletion vuu-ui/packages/vuu-filters/src/filter-bar/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from "./FilterBar";
export * from "./useFilters";
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useCallback, useEffect } from "react";
import {
ColumnDescriptorsByName,
Filter,
FilterState,
} from "@finos/vuu-filter-types";
import { DataSourceFilter } from "@finos/vuu-data-types";
import { filterAsQuery } from "@finos/vuu-utils";

interface ApplyFilterHookProps {
activeFilterIndex: FilterState["activeIndices"];
columnsByName: ColumnDescriptorsByName;
filters: FilterState["filters"];
onApplyFilter: (f: DataSourceFilter) => void;
}

export function useApplyFilterOnChange({
activeFilterIndex,
columnsByName,
filters,
onApplyFilter,
}: ApplyFilterHookProps) {
const applyFilter = useCallback(
(filter?: Filter) => {
const query = filter ? filterAsQuery(filter, { columnsByName }) : "";
onApplyFilter({ filter: query, filterStruct: filter });
},
[columnsByName, onApplyFilter]
);

useEffect(() => {
const activeFilters = activeFilterIndex.map((i) => filters[i]);
if (activeFilters.length === 0) {
applyFilter();
} else if (activeFilters.length === 1) {
const [filter] = activeFilters;
applyFilter(filter);
} else {
applyFilter({ op: "and", filters: activeFilters });
}
}, [activeFilterIndex, applyFilter, filters]);
}
Loading

0 comments on commit c408798

Please sign in to comment.