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 = {