From c4087986adc41418f63af1671b44796b393afb78 Mon Sep 17 00:00:00 2001 From: Junaid <62522218+junaidzm13@users.noreply.github.com> Date: Fri, 26 Jan 2024 01:30:15 +0800 Subject: [PATCH] #1074 refactor & add state change listeners to FilterBar (#1160) * 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. --- vuu-ui/packages/vuu-filter-types/index.d.ts | 9 +- .../__component__/filter-bar/Filterbar.cy.tsx | 108 +++++-- .../vuu-filters/src/filter-bar/FilterBar.tsx | 31 +- .../vuu-filters/src/filter-bar/index.ts | 1 - .../src/filter-bar/useApplyFilterOnChange.ts | 42 +++ .../src/filter-bar/useFilterBar.ts | 63 ++--- .../src/filter-bar/useFilterState.ts | 169 ++++++++--- .../vuu-filters/src/filter-bar/useFilters.ts | 127 --------- .../src/useFilterTable.tsx | 40 +-- .../DataTable/FilterTable.examples.tsx | 66 ++--- .../Filters/FilterBar/FilterBar.examples.tsx | 266 +++++++++++------- .../FilterInput/FilterInput.examples.tsx | 20 +- 12 files changed, 502 insertions(+), 440 deletions(-) create mode 100644 vuu-ui/packages/vuu-filters/src/filter-bar/useApplyFilterOnChange.ts delete mode 100644 vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts diff --git a/vuu-ui/packages/vuu-filter-types/index.d.ts b/vuu-ui/packages/vuu-filter-types/index.d.ts index b7da7945c..aaa912055 100644 --- a/vuu-ui/packages/vuu-filter-types/index.d.ts +++ b/vuu-ui/packages/vuu-filter-types/index.d.ts @@ -87,10 +87,9 @@ export interface FilterWithPartialClause extends MultiClauseFilter { filters: Array>; } +export declare type ColumnDescriptorsByName = Record; + export declare type FilterState = { - filter: Filter | undefined; - filterQuery: string; - filterName?: string; + filters: Filter[]; + activeIndices: number[]; }; - -export declare type ColumnDescriptorsByName = Record; diff --git a/vuu-ui/packages/vuu-filters/src/__tests__/__component__/filter-bar/Filterbar.cy.tsx b/vuu-ui/packages/vuu-filters/src/__tests__/__component__/filter-bar/Filterbar.cy.tsx index 55b303add..da2940396 100644 --- a/vuu-ui/packages/vuu-filters/src/__tests__/__component__/filter-bar/Filterbar.cy.tsx +++ b/vuu-ui/packages/vuu-filters/src/__tests__/__component__/filter-bar/Filterbar.cy.tsx @@ -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( + + ); + + cy.get("@onApplyFilter").should("be.calledWithExactly", { + filter: 'currency != "USD"', + filterStruct: { ...filter, value: "USD" }, + }); + }); }); describe("The mouse user", () => { @@ -146,12 +164,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( ); cy.get(ADD_BUTTON).realClick(); @@ -159,8 +177,11 @@ describe("The mouse user", () => { 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", () => { @@ -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", () => { @@ -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, @@ -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( ); cy.get(ADD_BUTTON).realClick(); @@ -267,11 +290,11 @@ 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"', @@ -279,6 +302,22 @@ describe("The mouse user", () => { }); }); + 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") @@ -286,11 +325,15 @@ describe("The mouse user", () => { 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, }); }); @@ -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, }); }); @@ -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( ); }); @@ -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], + }); }) ); }); diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx b/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx index ed24f633d..7e60f2c48 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx @@ -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"; @@ -17,14 +17,18 @@ import "./FilterBar.css"; export interface FilterBarProps extends HTMLAttributes { FilterClauseEditorProps?: Partial; - 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; } @@ -32,16 +36,15 @@ export interface FilterBarProps extends HTMLAttributes { 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 @@ -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, { diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/index.ts b/vuu-ui/packages/vuu-filters/src/filter-bar/index.ts index ae8759ec0..1176cf805 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/index.ts +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/index.ts @@ -1,2 +1 @@ export * from "./FilterBar"; -export * from "./useFilters"; diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/useApplyFilterOnChange.ts b/vuu-ui/packages/vuu-filters/src/filter-bar/useApplyFilterOnChange.ts new file mode 100644 index 000000000..bbc68c6f6 --- /dev/null +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/useApplyFilterOnChange.ts @@ -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]); +} diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts index cb948785e..ca9e1dae2 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts @@ -8,15 +8,10 @@ import { } from "@finos/vuu-filter-types"; import { PromptProps } from "@finos/vuu-popups"; import { - ActiveItemChangeHandler, EditableLabelProps, NavigationOutOfBoundsHandler, } from "@finos/vuu-ui-controls"; -import { - dispatchMouseEvent, - filterAsQuery, - isMultiClauseFilter, -} from "@finos/vuu-utils"; +import { dispatchMouseEvent, isMultiClauseFilter } from "@finos/vuu-utils"; import { FocusEventHandler, KeyboardEvent, @@ -32,21 +27,20 @@ import { FilterPillProps } from "../filter-pill"; import { FilterMenuOptions } from "../filter-pill-menu"; import { addClause, removeLastClause, replaceClause } from "../filter-utils"; import { FilterBarProps } from "./FilterBar"; -import { useFilters } from "./useFilters"; +import { useFilterState } from "./useFilterState"; +import { useApplyFilterOnChange } from "./useApplyFilterOnChange"; export interface FilterBarHookProps extends Pick< FilterBarProps, - | "activeFilterIndex" | "columnDescriptors" - | "filters" + | "defaultFilterState" + | "filterState" | "onApplyFilter" - | "onChangeActiveFilterIndex" | "onFilterDeleted" | "onFilterRenamed" - | "onFiltersChanged" + | "onFilterStateChanged" | "showMenu" - | "tableSchema" > { containerRef: RefObject; } @@ -54,17 +48,15 @@ export interface FilterBarHookProps const EMPTY_FILTER_CLAUSE: Partial = {}; export const useFilterBar = ({ - activeFilterIndex: activeFilterIdexProp = [], + columnDescriptors, containerRef, - filters: filtersProp, + defaultFilterState, + filterState, onApplyFilter, - onChangeActiveFilterIndex: onChangeActiveFilterIndexProp, onFilterDeleted, onFilterRenamed, - onFiltersChanged, + onFilterStateChanged, showMenu: showMenuProp, - tableSchema, - columnDescriptors, }: FilterBarHookProps) => { const addButtonRef = useRef(null); const editingFilter = useRef(); @@ -79,14 +71,6 @@ export const useFilterBar = ({ [columnDescriptors] ); - const applyFilter = useCallback( - (filter?: Filter) => { - const query = filter ? filterAsQuery(filter, { columnsByName }) : ""; - onApplyFilter({ filter: query, filterStruct: filter }); - }, - [columnsByName, onApplyFilter] - ); - const { activeFilterIndex, filters, @@ -95,14 +79,19 @@ export const useFilterBar = ({ onDeleteFilter, onRenameFilter, onChangeActiveFilterIndex, - } = useFilters({ - activeFilterIndex: activeFilterIdexProp, - applyFilter, - filters: filtersProp, + } = useFilterState({ + defaultFilterState, + filterState, onFilterDeleted, onFilterRenamed, - onFiltersChanged, - tableSchema, + onFilterStateChanged, + }); + + useApplyFilterOnChange({ + activeFilterIndex, + columnsByName, + filters, + onApplyFilter, }); const editPillLabel = useCallback( @@ -300,14 +289,6 @@ export const useFilterBar = ({ [editFilter, addIfNewElseUpdate] ); - const handleChangeActiveFilterIndex = useCallback( - (itemIndex) => { - onChangeActiveFilterIndex(itemIndex); - onChangeActiveFilterIndexProp?.(itemIndex); - }, - [onChangeActiveFilterIndexProp, onChangeActiveFilterIndex] - ); - const handleClickAddFilter = useCallback(() => { setEditFilter({}); }, [setEditFilter]); @@ -434,7 +415,7 @@ export const useFilterBar = ({ filters, onBlurFilterClause: handleBlurFilterClause, onCancelFilterClause: handleCancelFilterClause, - onChangeActiveFilterIndex: handleChangeActiveFilterIndex, + onChangeActiveFilterIndex, onClickAddFilter: handleClickAddFilter, onClickRemoveFilter: handleClickRemoveFilter, onChangeFilterClause: handleChangeFilterClause, diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterState.ts b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterState.ts index 6fd8c572e..be367df0e 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterState.ts +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterState.ts @@ -1,67 +1,146 @@ -import { useCallback, useState } from "react"; -import { Filter } from "@finos/vuu-filter-types"; +import { useCallback } from "react"; +import { Filter, FilterState } from "@finos/vuu-filter-types"; import { useControlled } from "@finos/vuu-ui-controls"; -export interface FilterStateHookProps { - activeFilterIndex: number[]; - applyFilter: (f?: Filter) => void; - defaultFilters?: Filter[]; - filters?: Filter[]; +export interface FiltersHookProps { + defaultFilterState?: FilterState; + filterState?: FilterState; + onFilterDeleted?: (filter: Filter) => void; + onFilterRenamed?: (filter: Filter, name: string) => void; + onFilterStateChanged?: (s: FilterState) => void; } -export function useFilterState({ - activeFilterIndex: activeFilterIdexProp, - applyFilter, - defaultFilters, - filters: filtersProp, -}: FilterStateHookProps) { - const [filters, setFilters] = useControlled({ - controlled: filtersProp, - default: defaultFilters ?? [], - name: "useFilters", - state: "Filters", +export const useFilterState = ({ + defaultFilterState, + onFilterDeleted, + onFilterRenamed, + onFilterStateChanged, + filterState: filterStateProp, +}: FiltersHookProps) => { + const [filterState, setFilterState] = useControlled({ + controlled: filterStateProp, + default: defaultFilterState ?? { filters: [], activeIndices: [] }, + name: "useFilterState", + state: "FilterState", }); - const [activeIndices, setActiveIndices] = - useState(activeFilterIdexProp); + const handleFilterStateChange = useCallback( + (s: FilterState) => { + setFilterState(s); + onFilterStateChanged?.(s); + }, + [onFilterStateChanged, setFilterState] + ); + + const handleAddFilter = useCallback( + (filter: Filter) => { + const index = filterState.filters.length; + const newFilters = filterState.filters.concat(filter); + const newIndices = appendIfNotPresent(filterState.activeIndices, index); + handleFilterStateChange({ + filters: newFilters, + activeIndices: newIndices, + }); + return index; + }, + [filterState, handleFilterStateChange] + ); + + const handleDeleteFilter = useCallback( + (filter: Filter) => { + let index = -1; + const newFilters = filterState.filters.filter((f, i) => { + if (f !== filter) { + return true; + } else { + index = i; + return false; + } + }); + + const newIndices = removeIndexAndDecrementLarger( + filterState.activeIndices, + index + ); - const onApplyFilter = useCallback( - ({ activeIndices, filters }: FilterState) => { - if (activeIndices.length > 0) { - const activeFilters = activeIndices.map((i) => filters[i]); - if (activeFilters.length === 1) { - const [filter] = activeFilters; - applyFilter(filter); + handleFilterStateChange({ + filters: newFilters, + activeIndices: newIndices, + }); + onFilterDeleted?.(filter); + return index; + }, + [ + filterState.filters, + filterState.activeIndices, + handleFilterStateChange, + onFilterDeleted, + ] + ); + + const handleRenameFilter = useCallback( + (filter: Filter, name: string) => { + let index = -1; + const newFilters = filterState.filters.map((f, i) => { + if (f === filter) { + index = i; + return { ...filter, name }; } else { - applyFilter({ op: "and", filters: activeFilters }); + return f; } - } else { - applyFilter(); - } + }); + handleFilterStateChange({ ...filterState, filters: newFilters }); + onFilterRenamed?.(filter, name); + + return index; }, - [applyFilter] + [filterState, handleFilterStateChange, onFilterRenamed] ); - const onFilterStateChange = useCallback( - ({ filters, activeIndices }: FilterState) => { - setFilters(filters); - setActiveIndices(activeIndices); - onApplyFilter({ filters, activeIndices }); + const handleChangeFilter = useCallback( + (oldFilter: Filter, newFilter: Filter) => { + let index = -1; + const newFilters = filterState.filters.map((f, i) => { + if (f === oldFilter) { + index = i; + return newFilter; + } else { + return f; + } + }); + handleFilterStateChange({ ...filterState, filters: newFilters }); + + return index; }, - [onApplyFilter] + [filterState, handleFilterStateChange] ); const handleActiveIndicesChange = useCallback( (indices: number[]) => - onFilterStateChange({ filters, activeIndices: indices }), - [filters, onFilterStateChange] + handleFilterStateChange({ ...filterState, activeIndices: indices }), + [filterState, handleFilterStateChange] ); return { - filterState: { activeIndices, filters }, - onActiveIndicesChange: handleActiveIndicesChange, - onFilterStateChange, + activeFilterIndex: filterState.activeIndices, + filters: filterState.filters, + onChangeActiveFilterIndex: handleActiveIndicesChange, + onAddFilter: handleAddFilter, + onChangeFilter: handleChangeFilter, + onDeleteFilter: handleDeleteFilter, + onRenameFilter: handleRenameFilter, }; -} +}; + +const appendIfNotPresent = (ns: number[], n: number) => + ns.includes(n) ? ns : ns.concat(n); -type FilterState = { filters: Filter[]; activeIndices: number[] }; +const removeIndexAndDecrementLarger = ( + indices: number[], + idxToRemove: number +) => { + return indices.reduce((res, i) => { + if (i === idxToRemove) return res; + return res.concat(i > idxToRemove ? i - 1 : i); + }, []); +}; diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts deleted file mode 100644 index 13eed0477..000000000 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { useCallback } from "react"; -import { TableSchema } from "@finos/vuu-data-types"; -import { Filter } from "@finos/vuu-filter-types"; -import { FilterStateHookProps, useFilterState } from "./useFilterState"; - -export interface FiltersHookProps extends FilterStateHookProps { - onFilterDeleted?: (filter: Filter) => void; - onFilterRenamed?: (filter: Filter, name: string) => void; - onFiltersChanged?: (filters: Filter[]) => void; - tableSchema?: TableSchema; -} - -export const useFilters = ({ - onFilterDeleted, - onFilterRenamed, - onFiltersChanged, - tableSchema, - ...filterStateHookProps -}: FiltersHookProps) => { - const { filterState, onFilterStateChange, onActiveIndicesChange } = - useFilterState(filterStateHookProps); - - const handleAddFilter = useCallback( - (filter: Filter) => { - const index = filterState.filters.length; - const newFilters = filterState.filters.concat(filter); - const newIndices = appendIfNotPresent(filterState.activeIndices, index); - onFilterStateChange({ filters: newFilters, activeIndices: newIndices }); - onFiltersChanged?.(newFilters); - return index; - }, - [filterState, onFiltersChanged, onFilterStateChange] - ); - - const handleDeleteFilter = useCallback( - (filter: Filter) => { - let index = -1; - const newFilters = filterState.filters.filter((f, i) => { - if (f !== filter) { - return true; - } else { - index = i; - return false; - } - }); - - const newIndices = removeIndexAndDecrementLarger( - filterState.activeIndices, - index - ); - - onFilterStateChange({ filters: newFilters, activeIndices: newIndices }); - onFiltersChanged?.(newFilters); - onFilterDeleted?.(filter); - return index; - }, - [ - filterState.filters, - filterState.activeIndices, - onFilterStateChange, - onFiltersChanged, - onFilterDeleted, - ] - ); - - const handleRenameFilter = useCallback( - (filter: Filter, name: string) => { - let index = -1; - const newFilters = filterState.filters.map((f, i) => { - if (f === filter) { - index = i; - return { ...filter, name }; - } else { - return f; - } - }); - onFilterStateChange({ ...filterState, filters: newFilters }); - onFiltersChanged?.(newFilters); - onFilterRenamed?.(filter, name); - - return index; - }, - [filterState, onFilterStateChange, onFiltersChanged, onFilterRenamed] - ); - - const handleChangeFilter = useCallback( - (oldFilter: Filter, newFilter: Filter) => { - let index = -1; - const newFilters = filterState.filters.map((f, i) => { - if (f === oldFilter) { - index = i; - return newFilter; - } else { - return f; - } - }); - onFilterStateChange({ ...filterState, filters: newFilters }); - onFiltersChanged?.(newFilters); - - return index; - }, - [filterState, onFiltersChanged, onFilterStateChange] - ); - - return { - ...filterState, - activeFilterIndex: filterState.activeIndices, - onChangeActiveFilterIndex: onActiveIndicesChange, - onAddFilter: handleAddFilter, - onChangeFilter: handleChangeFilter, - onDeleteFilter: handleDeleteFilter, - onRenameFilter: handleRenameFilter, - }; -}; - -const appendIfNotPresent = (ns: number[], n: number) => - ns.includes(n) ? ns : ns.concat(n); - -const removeIndexAndDecrementLarger = ( - indices: number[], - idxToRemove: number -) => { - return indices.reduce((res, i) => { - if (i === idxToRemove) return res; - return res.concat(i > idxToRemove ? i - 1 : i); - }, []); -}; diff --git a/vuu-ui/sample-apps/feature-filter-table/src/useFilterTable.tsx b/vuu-ui/sample-apps/feature-filter-table/src/useFilterTable.tsx index 7fbb7a8a2..e544771e6 100644 --- a/vuu-ui/sample-apps/feature-filter-table/src/useFilterTable.tsx +++ b/vuu-ui/sample-apps/feature-filter-table/src/useFilterTable.tsx @@ -13,19 +13,18 @@ import { TypeaheadSuggestionProvider, VuuFeatureInvocationMessage, } from "@finos/vuu-data-types"; -import { Filter, NamedFilter } from "@finos/vuu-filter-types"; +import { Filter, FilterState, NamedFilter } from "@finos/vuu-filter-types"; import { FilterBarProps } from "@finos/vuu-filters"; import { useViewContext } from "@finos/vuu-layout"; import { TypeaheadParams } from "@finos/vuu-protocol-types"; import { useLayoutManager, useShellContext } from "@finos/vuu-shell"; import { TableConfig, TableConfigChangeHandler } from "@finos/vuu-table-types"; -import { ActiveItemChangeHandler } from "@finos/vuu-ui-controls"; import { applyDefaultColumnConfig, isTypeaheadSuggestionProvider, } from "@finos/vuu-utils"; import { Button } from "@salt-ds/core"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { useSessionDataSource } from "./useSessionDataSource"; import { FilterTableFeatureProps } from "./VuuFilterTableFeature"; @@ -161,32 +160,19 @@ export const useFilterTable = ({ tableSchema }: FilterTableFeatureProps) => { } }, [dataSource, getSuggestions]); - const activeRef = useRef( - filterbarConfigFromState?.activeFilterIndex ?? [] - ); - const [filters, setFilters] = useState( - filterbarConfigFromState?.filters ?? [] - ); + const [filterState, setFilterState] = useState({ + filters: filterbarConfigFromState?.filterState?.filters ?? [], + activeIndices: filterbarConfigFromState?.filterState?.activeIndices ?? [], + }); - const handleFiltersChanged = useCallback( - (filters: Filter[]) => { - save?.( - { activeFilterIndex: activeRef.current, filters }, - "filterbar-config" - ); - setFilters(filters); + const handleFilterStateChanged = useCallback( + (filterState: FilterState) => { + save?.({ filterState }, "filterbar-config"); + setFilterState(filterState); }, [save] ); - const handleChangeActiveFilterIndex = useCallback( - (activeIndex) => { - activeRef.current = activeIndex; - save?.({ activeFilterIndex: activeIndex, filters }, "filterbar-config"); - }, - [filters, save] - ); - const handleApplyFilter = useCallback( (filter: DataSourceFilter) => { dataSource.filter = filter; @@ -269,14 +255,12 @@ export const useFilterTable = ({ tableSchema }: FilterTableFeatureProps) => { suggestionProvider, } : undefined, - activeFilterIndex: filterbarConfigFromState?.activeFilterIndex, columnDescriptors: tableConfig.columns, - filters, + filterState, onApplyFilter: handleApplyFilter, - onChangeActiveFilterIndex: handleChangeActiveFilterIndex, onFilterDeleted: handleFilterDeleted, onFilterRenamed: handleFilterRenamed, - onFiltersChanged: handleFiltersChanged, + onFilterStateChanged: handleFilterStateChanged, tableSchema, }; diff --git a/vuu-ui/showcase/src/examples/DataTable/FilterTable.examples.tsx b/vuu-ui/showcase/src/examples/DataTable/FilterTable.examples.tsx index 42eeef05f..ec2d405ad 100644 --- a/vuu-ui/showcase/src/examples/DataTable/FilterTable.examples.tsx +++ b/vuu-ui/showcase/src/examples/DataTable/FilterTable.examples.tsx @@ -5,11 +5,10 @@ import { vuuModule, } from "@finos/vuu-data-test"; import type { DataSourceFilter } from "@finos/vuu-data-types"; -import { FilterTable } from "@finos/vuu-datatable"; -import type { Filter } from "@finos/vuu-filter-types"; +import { FilterTable, FilterTableProps } from "@finos/vuu-datatable"; +import type { FilterState } from "@finos/vuu-filter-types"; import type { TableProps } from "@finos/vuu-table"; import type { TableConfig } from "@finos/vuu-table-types"; -import type { ActiveItemChangeHandler } from "@finos/vuu-ui-controls"; import { useCallback, useMemo, useState } from "react"; import { useTestDataSource } from "../utils"; @@ -24,36 +23,26 @@ export const DefaultFilterTable = () => { const [tableConfig] = useState(config); + const [filterState, setFilterState] = useState({ + filters: [], + activeIndices: [], + }); + const handleApplyFilter = useCallback((filter: DataSourceFilter) => { - console.log("apply filter", { - filter, - }); + console.log("apply filter", { filter }); dataSource.filter = filter; }, []); - const handleChangeFilter = useCallback( - (filter: Filter, newFilter: Filter) => { - console.log("change filter", { - filter, - newFilter, - }); - }, - [] - ); - - const handleChangeActiveFilterIndex = useCallback( - (index) => { - console.log(`active filters ${index.join(",")}`); - }, - [] - ); + const handleFilterStateChange = useCallback((fs: FilterState) => { + console.log("filter state changed:", fs); + setFilterState(fs); + }, []); - const filterBarProps = { + const filterBarProps: FilterTableProps["FilterBarProps"] = { columnDescriptors: config.columns, - filters: [], + filterState, onApplyFilter: handleApplyFilter, - onChangeFilter: handleChangeFilter, - onChangeActiveFilterIndex: handleChangeActiveFilterIndex, + onFilterStateChanged: handleFilterStateChange, tableSchema, }; @@ -96,28 +85,29 @@ export const FilterTableArrayDataInstruments = () => { [schema] ); + const [filterState, setFilterState] = useState({ + filters: [], + activeIndices: [], + }); + const handleApplyFilter = useCallback( (filter: DataSourceFilter) => { - console.log("apply filter", { - filter, - }); + console.log("apply filter", { filter }); dataSource.filter = filter; }, [dataSource] ); - const handleChangeActiveFilterIndex = useCallback( - (index) => { - console.log(`active filters ${index.join(",")}`); - }, - [] - ); + const handleFilterStateChange = useCallback((fs: FilterState) => { + console.log("filter state changed:", fs); + setFilterState(fs); + }, []); - const filterBarProps = { + const filterBarProps: FilterTableProps["FilterBarProps"] = { columnDescriptors: config.columns, - filters: [], + filterState, onApplyFilter: handleApplyFilter, - onChangeActiveFilterIndex: handleChangeActiveFilterIndex, + onFilterStateChanged: handleFilterStateChange, tableSchema: getSchema("instruments"), }; diff --git a/vuu-ui/showcase/src/examples/Filters/FilterBar/FilterBar.examples.tsx b/vuu-ui/showcase/src/examples/Filters/FilterBar/FilterBar.examples.tsx index 48b35cf06..efd00188f 100644 --- a/vuu-ui/showcase/src/examples/Filters/FilterBar/FilterBar.examples.tsx +++ b/vuu-ui/showcase/src/examples/Filters/FilterBar/FilterBar.examples.tsx @@ -1,5 +1,5 @@ import { FilterBar, FilterBarProps } from "@finos/vuu-filters"; -import type { Filter } from "@finos/vuu-filter-types"; +import type { Filter, FilterState } from "@finos/vuu-filter-types"; import { SyntheticEvent, useCallback, @@ -11,9 +11,6 @@ import { import type { DataSourceFilter } from "@finos/vuu-data-types"; import { Input, ToggleButton, ToggleButtonGroup } from "@salt-ds/core"; import { getSchema, vuuModule } from "@finos/vuu-data-test"; -import type { ActiveItemChangeHandler } from "@finos/vuu-ui-controls"; - -let displaySequence = 1; const lastUpdatedColumn = { name: "lastUpdated", @@ -21,19 +18,21 @@ const lastUpdatedColumn = { type: "date/time", } as const; -export const DefaultFilterBar = ({ - filters: filtersProp = [], +const DefaultFilterBarCore = ({ + filterState, onApplyFilter, onFilterDeleted, onFilterRenamed, - onFiltersChanged, + onFilterStateChanged, style, }: Partial) => { - const [filters, setFilters] = useState(filtersProp); const [filterStruct, setFilterStruct] = useState(null); const inputRef = useRef(null); - const tableSchema = getSchema("instruments"); - const columns = [...tableSchema.columns, lastUpdatedColumn]; + const tableSchema = useMemo(() => getSchema("instruments"), []); + const columns = useMemo( + () => [...tableSchema.columns, lastUpdatedColumn], + [tableSchema] + ); const { typeaheadHook } = vuuModule("SIMUL"); const handleApplyFilter = useCallback( @@ -45,20 +44,14 @@ export const DefaultFilterBar = ({ [onApplyFilter] ); - const handleFiltersChanged = useCallback( - (filters: Filter[]) => { - onFiltersChanged?.(filters); - console.log(`filters changed ${JSON.stringify(filters, null, 2)}`); - setFilters(filters); - }, - [onFiltersChanged] - ); - - const handleChangeActiveFilterIndex = useCallback( - (index) => { - console.log(`filters changed ${index.join(",")}`); + const handleFilterStateChange = useCallback( + (filterState: FilterState) => { + onFilterStateChanged?.(filterState); + console.log( + `filter state changed ${JSON.stringify(filterState, null, 2)}` + ); }, - [] + [onFilterStateChanged] ); useEffect(() => { @@ -77,12 +70,11 @@ export const DefaultFilterBar = ({ suggestionProvider: typeaheadHook, }} data-testid="filterbar" - filters={filters} + filterState={filterState} onApplyFilter={handleApplyFilter} - onChangeActiveFilterIndex={handleChangeActiveFilterIndex} onFilterDeleted={onFilterDeleted} onFilterRenamed={onFilterRenamed} - onFiltersChanged={handleFiltersChanged} + onFilterStateChanged={handleFilterStateChange} tableSchema={{ ...tableSchema, columns }} columnDescriptors={columns} /> @@ -91,14 +83,43 @@ export const DefaultFilterBar = ({ ); }; + +let displaySequence = 1; + +export const DefaultFilterBar = ({ + filterState: filterStateProp = { filters: [], activeIndices: [] }, + onFilterStateChanged, + ...rest +}: Partial) => { + const [filterState, setFilterState] = useState(filterStateProp); + + const handleFilterStateChange = useCallback( + (fs: FilterState) => { + onFilterStateChanged?.(fs); + setFilterState(fs); + }, + [onFilterStateChanged] + ); + + return ( + + ); +}; DefaultFilterBar.displaySequence = displaySequence++; export const FilterBarOneSimpleFilter = () => { return ( ); }; @@ -107,14 +128,17 @@ FilterBarOneSimpleFilter.displaySequence = displaySequence++; export const FilterBarOneMultiValueFilter = () => { return ( ); }; @@ -126,31 +150,34 @@ export const FilterBarMultipleFilters = ({ }: Partial) => { return ( ", value: 1000 }, - ], - }, - ]} + filterState={{ + filters: [ + { column: "currency", name: "Filter One", op: "=", value: "EUR" }, + { column: "exchange", name: "Filter Two", op: "=", value: "XLON" }, + { + column: "ric", + name: "Filter Three", + op: "in", + values: ["AAPL", "BP.L", "VOD.L"], + }, + { + column: "ric", + name: "Filter Four", + op: "in", + values: ["AAPL", "BP.L", "VOD.L", "TSLA"], + }, + { + op: "and", + name: "Filter Five", + filters: [ + { column: "ric", op: "in", values: ["AAPL", "VOD.L"] }, + { column: "exchange", op: "=", value: "NASDAQ" }, + { column: "price", op: ">", value: 1000 }, + ], + }, + ], + activeIndices: [], + }} onFilterDeleted={onFilterDeleted} onFilterRenamed={onFilterRenamed} /> @@ -159,8 +186,10 @@ export const FilterBarMultipleFilters = ({ FilterBarMultipleFilters.displaySequence = displaySequence++; export const FilterBarMultipleFilterSets = () => { + const [filterSets, setFilterSets] = useState(initialFilterSets); const [selectedIndex, setSelectedIndex] = useState(0); - const handleChangeFilterSet = useCallback( + + const handleChangeSelectedIndex = useCallback( (evt: SyntheticEvent) => { const { value } = evt.target as HTMLButtonElement; const index = parseInt(value); @@ -168,50 +197,81 @@ export const FilterBarMultipleFilterSets = () => { }, [] ); - const filters = useMemo(() => { - if (selectedIndex === 0) { - return [ - { column: "currency", name: "Filter One", op: "=", value: "EUR" }, - { column: "exchange", name: "Filter Two", op: "=", value: "XLON" }, - { - column: "ric", - name: "Filter Three", - op: "in", - values: ["AAPL", "BP.L", "VOD.L"], - }, - ]; - } else if (selectedIndex === 1) { - return [ - { - column: "ric", - name: "Filter Four", - op: "in", - values: ["AAPL", "BP.L", "VOD.L", "TSLA"], - }, - { - op: "and", - name: "Filter Five", - filters: [ - { column: "ric", op: "in", values: ["AAPL", "VOD.L"] }, - { column: "exchange", op: "=", value: "NASDAQ" }, - { column: "price", op: ">", value: 1000 }, - ], - }, - ]; - } else { - throw Error(`selectedIndex ${selectedIndex} out of range`); - } - }, [selectedIndex]); - - console.log({ filters }); + + const handleChangeFilterState = useCallback( + (fs: FilterState) => { + setFilterSets((s) => [ + ...s.slice(0, selectedIndex), + fs, + ...s.slice(selectedIndex + 1), + ]); + }, + [selectedIndex] + ); + return (
- - Filter Set 1 (three filters) - Filter Set 2 (two filters) + + {filterSets.map((fs, i) => ( + + {`Filter Set ${i + 1} (${fs.filters.length} filters)`} + + ))} - +
); }; + +const initialFilterSets: FilterState[] = [ + { + filters: [ + { + column: "currency", + name: "Filter One", + op: "=", + value: "EUR", + }, + { + column: "exchange", + name: "Filter Two", + op: "=", + value: "XLON", + }, + { + column: "ric", + name: "Filter Three", + op: "in", + values: ["AAPL", "BP.L", "VOD.L"], + }, + ], + activeIndices: [], + }, + { + filters: [ + { + column: "ric", + name: "Filter Four", + op: "in", + values: ["AAPL", "BP.L", "VOD.L", "TSLA"], + }, + { + op: "and", + name: "Filter Five", + filters: [ + { column: "ric", op: "in", values: ["AAPL", "VOD.L"] }, + { column: "exchange", op: "=", value: "NASDAQ" }, + { column: "price", op: ">", value: 1000 }, + ], + }, + ], + activeIndices: [], + }, +]; FilterBarMultipleFilterSets.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/Filters/FilterInput/FilterInput.examples.tsx b/vuu-ui/showcase/src/examples/Filters/FilterInput/FilterInput.examples.tsx index 4f1274a47..ef99d44a1 100644 --- a/vuu-ui/showcase/src/examples/Filters/FilterInput/FilterInput.examples.tsx +++ b/vuu-ui/showcase/src/examples/Filters/FilterInput/FilterInput.examples.tsx @@ -1,6 +1,6 @@ import { NamedDataSourceFilter } from "@finos/vuu-data-types"; import { JsonTable } from "@finos/vuu-datatable"; -import { Filter, FilterState } from "@finos/vuu-filter-types"; +import { Filter } from "@finos/vuu-filter-types"; import { addFilter, FilterInput, @@ -21,9 +21,15 @@ let displaySequence = 1; const { columns, table } = getSchema("instruments"); +type State = { + filter: Filter | undefined; + filterQuery: string; + filterName?: string; +}; + export const DefaultFilterInput = () => { const namedFilters = useMemo(() => new Map(), []); - const [filterState, setFilterState] = useState({ + const [filterState, setFilterState] = useState({ filter: undefined, filterQuery: "", }); @@ -42,7 +48,7 @@ export const DefaultFilterInput = () => { mode: FilterSubmissionMode = "replace", filterName?: string ) => { - let newFilterState: FilterState; + let newFilterState: State; if (newFilter && mode === "and") { const fullFilter = addFilter(filterState.filter, newFilter) as Filter; newFilterState = { @@ -93,7 +99,7 @@ DefaultFilterInput.displaySequence = displaySequence++; export const DefaultFilterInputWithPersistence = () => { const user = { username: "test-user", token: "test-token" }; const namedFilters = useMemo(() => new Map(), []); - const [filterState, setFilterState] = useState({ + const [filterState, setFilterState] = useState({ filter: undefined, filterQuery: "", }); @@ -116,7 +122,7 @@ export const DefaultFilterInputWithPersistence = () => { mode: FilterSubmissionMode = "replace", filterName ) => { - let newFilterState: FilterState; + let newFilterState: State; if (newFilter && mode === "and") { const fullFilter = addFilter(filterState.filter, newFilter) as Filter; newFilterState = { @@ -195,7 +201,7 @@ export const FilterInputTabs = () => { ); const namedFilters = useMemo(() => new Map(), []); - const [filterState, setFilterState] = useState({ + const [filterState, setFilterState] = useState({ filter: undefined, filterQuery: "", }); @@ -218,7 +224,7 @@ export const FilterInputTabs = () => { if (mode === "tab") { alert("create a new tab"); } else { - let newFilterState: FilterState; + let newFilterState: State; if (newFilter && mode === "and") { const fullFilter = addFilter(filterState.filter, newFilter) as Filter; newFilterState = {