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 328b02117..55b303add 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 @@ -1,6 +1,9 @@ // TODO try and get TS path alias working to avoid relative paths like this import { defaultPatternsByType, formatDate } from "@finos/vuu-utils"; -import { DefaultFilterBar } from "../../../../../../showcase/src/examples/Filters/FilterBar/FilterBar.examples"; +import { + DefaultFilterBar, + FilterBarMultipleFilters, +} from "../../../../../../showcase/src/examples/Filters/FilterBar/FilterBar.examples"; // Common selectors const OVERFLOW_CONTAINER = ".vuuOverflowContainer-wrapContainer"; @@ -521,3 +524,45 @@ describe("WHEN a user applies a date filter", () => { }) ); }); + +describe("Deleting and renaming filters", () => { + describe("WHEN user deletes a filter", () => { + it("THEN onFilterDeleted callback is called", () => { + const onFilterDeleted = cy.stub().as("onFilterDeleted"); + cy.mount(); + + findOverflowItem('[data-index="0"]').findByRole("button").realClick(); + clickButton("Delete"); + clickButton("Remove"); + + cy.get("@onFilterDeleted").should("be.calledWithExactly", { + column: "currency", + name: "Filter One", + op: "=", + value: "EUR", + }); + }); + }); + + describe("WHEN user renames a filter", () => { + it("THEN onFilterRenamed callback is called", () => { + const onFilterRenamed = cy.stub().as("onFilterRenamed"); + cy.mount(); + + findOverflowItem('[data-index="0"]').findByText("Filter One").dblclick(); + cy.realType("Test"); + cy.realPress("Enter"); + + cy.get("@onFilterRenamed").should( + "be.calledWithExactly", + { + column: "currency", + name: "Filter One", + op: "=", + value: "EUR", + }, + "Test" + ); + }); + }); +}); 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 3fb7b58d6..ed24f633d 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx @@ -22,6 +22,8 @@ export interface FilterBarProps extends HTMLAttributes { filters: Filter[]; onApplyFilter: (filter: DataSourceFilter) => void; onChangeActiveFilterIndex: ActiveItemChangeHandler; + onFilterDeleted?: (filter: Filter) => void; + onFilterRenamed?: (filter: Filter, name: string) => void; onFiltersChanged?: (filters: Filter[]) => void; showMenu?: boolean; tableSchema: TableSchema; @@ -37,6 +39,8 @@ export const FilterBar = ({ filters: filtersProp, onApplyFilter, onChangeActiveFilterIndex: onChangeActiveFilterIndexProp, + onFilterDeleted, + onFilterRenamed, onFiltersChanged, showMenu: showMenuProp = false, tableSchema, @@ -71,6 +75,8 @@ export const FilterBar = ({ onApplyFilter, onChangeActiveFilterIndex: onChangeActiveFilterIndexProp, onFiltersChanged, + onFilterDeleted, + onFilterRenamed, showMenu: showMenuProp, tableSchema, }); diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBarMenu.tsx b/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBarMenu.tsx index 84a4e2470..99195bbb8 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBarMenu.tsx +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBarMenu.tsx @@ -1,19 +1,10 @@ import { PopupMenu } from "@finos/vuu-popups"; -import { useFilterBarMenu } from "./useFilterBarMenu"; export const FilterBarMenu = () => { const classBase = "vuuFilterBarMenu"; - - const { menuBuilder, menuActionHandler } = useFilterBarMenu(); - return (
- +
); }; 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 b9d63dd3a..cb948785e 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBar.ts @@ -42,6 +42,8 @@ export interface FilterBarHookProps | "filters" | "onApplyFilter" | "onChangeActiveFilterIndex" + | "onFilterDeleted" + | "onFilterRenamed" | "onFiltersChanged" | "showMenu" | "tableSchema" @@ -57,6 +59,8 @@ export const useFilterBar = ({ filters: filtersProp, onApplyFilter, onChangeActiveFilterIndex: onChangeActiveFilterIndexProp, + onFilterDeleted, + onFilterRenamed, onFiltersChanged, showMenu: showMenuProp, tableSchema, @@ -95,6 +99,8 @@ export const useFilterBar = ({ activeFilterIndex: activeFilterIdexProp, applyFilter, filters: filtersProp, + onFilterDeleted, + onFilterRenamed, onFiltersChanged, tableSchema, }); @@ -162,7 +168,7 @@ export const useFilterBar = ({ } }); }, - [focusFilterPill, onDeleteFilter] + [filters.length, focusFilterPill, onDeleteFilter] ); const getDeletePrompt = useMemo( @@ -372,7 +378,6 @@ export const useFilterBar = ({ const handleKeyDownMenu = useCallback( (evt) => { - console.log(`keydown from List ${evt.key}`); const { current: container } = containerRef; if (evt.key === "Backspace" && container) { evt.preventDefault(); diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBarMenu.ts b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBarMenu.ts deleted file mode 100644 index 19d3ef33a..000000000 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilterBarMenu.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useCallback, useMemo } from "react"; -import { - ContextMenuItemDescriptor, - MenuActionHandler, - MenuBuilder, -} from "@finos/vuu-data-types"; -import { MenuActionClosePopup } from "@finos/vuu-popups"; - -export const useFilterBarMenu = () => { - const menuBuilder = useCallback(() => { - return [ - { - label: `You have no saved filters for this table`, - action: `no-action`, - } as ContextMenuItemDescriptor, - ]; - }, []); - - const menuActionHandler = useMemo( - () => (action: MenuActionClosePopup) => { - console.log(`invoke menuId `, { - action, - }); - return false; - }, - [] - ); - - return { - menuBuilder, - menuActionHandler, - }; -}; diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts index 2aa637fca..13eed0477 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/useFilters.ts @@ -1,99 +1,25 @@ import { useCallback } from "react"; import { TableSchema } from "@finos/vuu-data-types"; -import { Filter, NamedFilter } from "@finos/vuu-filter-types"; -import { useLayoutManager } from "@finos/vuu-shell"; +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 { getApplicationSettings, saveApplicationSettings } = - useLayoutManager(); const { filterState, onFilterStateChange, onActiveIndicesChange } = useFilterState(filterStateHookProps); - const saveFilterToSettings = useCallback( - (filter: Filter, name?: string) => { - if (tableSchema && name) { - const savedFilters = getApplicationSettings( - "filters" - ) as SavedFilterMap; - let newFilters = savedFilters; - const { module, table } = tableSchema.table; - const key = `${module}:${table}`; - if (savedFilters) { - if (savedFilters[key]) { - if (hasFilterWithName(savedFilters[key], name)) { - newFilters = { - ...savedFilters, - [key]: savedFilters[key].map((f) => - f.name === name ? { ...filter, name } : f - ), - }; - } else if ( - filter?.name && - filter?.name !== name && - hasFilterWithName(savedFilters[key], filter.name) - ) { - newFilters = { - ...savedFilters, - [key]: savedFilters[key].map((f) => - f.name === filter.name ? { ...filter, name } : f - ), - }; - } else { - newFilters = { - ...savedFilters, - [key]: savedFilters[key].concat({ ...filter, name }), - }; - } - } else { - newFilters = { - ...savedFilters, - [key]: [{ ...filter, name }], - }; - } - } else { - newFilters = { - [key]: [{ ...filter, name }], - }; - } - if (newFilters !== savedFilters) { - saveApplicationSettings(newFilters, "filters"); - } - } - }, - [getApplicationSettings, saveApplicationSettings, tableSchema] - ); - - const removeFilterFromSettings = useCallback( - (filter: Filter | NamedFilter) => { - if (!tableSchema || !filter.name) return; - - const savedFilters = getApplicationSettings("filters") as SavedFilterMap; - if (!savedFilters) return; - - const { module, table } = tableSchema.table; - const key = `${module}:${table}`; - - if (hasFilterWithName(savedFilters[key], filter.name)) { - const newSavedFilters = { - ...savedFilters, - [key]: savedFilters[key].filter((f) => f.name !== filter.name), - }; - saveApplicationSettings(newSavedFilters, "filters"); - } - }, - [getApplicationSettings, saveApplicationSettings, tableSchema] - ); - const handleAddFilter = useCallback( (filter: Filter) => { const index = filterState.filters.length; @@ -125,14 +51,15 @@ export const useFilters = ({ onFilterStateChange({ filters: newFilters, activeIndices: newIndices }); onFiltersChanged?.(newFilters); - removeFilterFromSettings(filter); + onFilterDeleted?.(filter); return index; }, [ - filterState, - onFiltersChanged, + filterState.filters, + filterState.activeIndices, onFilterStateChange, - removeFilterFromSettings, + onFiltersChanged, + onFilterDeleted, ] ); @@ -149,11 +76,11 @@ export const useFilters = ({ }); onFilterStateChange({ ...filterState, filters: newFilters }); onFiltersChanged?.(newFilters); - saveFilterToSettings(filter, name); + onFilterRenamed?.(filter, name); return index; }, - [filterState, onFiltersChanged, onFilterStateChange, saveFilterToSettings] + [filterState, onFilterStateChange, onFiltersChanged, onFilterRenamed] ); const handleChangeFilter = useCallback( @@ -186,13 +113,6 @@ export const useFilters = ({ }; }; -type SavedFilterMap = { - [key: string]: NamedFilter[]; -}; - -const hasFilterWithName = (filters: NamedFilter[], name: string) => - filters.findIndex((f) => f.name === name) !== -1; - const appendIfNotPresent = (ns: number[], n: number) => ns.includes(n) ? ns : ns.concat(n); diff --git a/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.tsx b/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.tsx index d7364af1f..49faf66a6 100644 --- a/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.tsx +++ b/vuu-ui/sample-apps/feature-filter-table/src/VuuFilterTableFeature.tsx @@ -16,16 +16,16 @@ export interface FilterTableFeatureProps { const VuuFilterTableFeature = ({ tableSchema }: FilterTableFeatureProps) => { const { - buildViewserverMenuOptions, + buildFilterTableMenuOptions, filterBarProps, - handleMenuAction, + handleFilterTableMenuAction, tableProps, } = useFilterTable({ tableSchema }); return ( & { name: string }[]; +}; + +const hasFilterWithName = (filters: NamedFilter[], name: string) => + filters.findIndex((f) => f.name === name) !== -1; + export const useFilterTable = ({ tableSchema }: FilterTableFeatureProps) => { const { dispatch, load, save } = useViewContext(); + const { getApplicationSettings, saveApplicationSettings } = + useLayoutManager(); + + const savedFilters = useMemo(() => { + const { + table: { module, table }, + } = tableSchema; + const savedFilters = getApplicationSettings("filters") as SavedFilterMap; + const key = `${module}:${table}`; + return savedFilters?.[key] ?? []; + }, [getApplicationSettings, tableSchema]); const { "available-columns": availableColumnsFromState, @@ -59,6 +80,81 @@ export const useFilterTable = ({ tableSchema }: FilterTableFeatureProps) => { [dataSource] ); + const removeFilterFromSettings = useCallback( + (filter: Filter | NamedFilter) => { + if (!tableSchema || !filter.name) return; + + const savedFilters = getApplicationSettings("filters") as SavedFilterMap; + if (!savedFilters) return; + + const { module, table } = tableSchema.table; + const key = `${module}:${table}`; + + if (hasFilterWithName(savedFilters[key], filter.name)) { + const newSavedFilters = { + ...savedFilters, + [key]: savedFilters[key].filter((f) => f.name !== filter.name), + }; + saveApplicationSettings(newSavedFilters, "filters"); + } + }, + [getApplicationSettings, saveApplicationSettings, tableSchema] + ); + + const saveFilterToSettings = useCallback( + (filter: Filter, name?: string) => { + if (tableSchema && name) { + const savedFilters = getApplicationSettings( + "filters" + ) as SavedFilterMap; + let newFilters = savedFilters; + const { module, table } = tableSchema.table; + const key = `${module}:${table}`; + if (savedFilters) { + if (savedFilters[key]) { + if (hasFilterWithName(savedFilters[key], name)) { + newFilters = { + ...savedFilters, + [key]: savedFilters[key].map((f) => + f.name === name ? { ...filter, name } : f + ), + }; + } else if ( + filter?.name && + filter?.name !== name && + hasFilterWithName(savedFilters[key], filter.name) + ) { + newFilters = { + ...savedFilters, + [key]: savedFilters[key].map((f) => + f.name === filter.name ? { ...filter, name } : f + ), + }; + } else { + newFilters = { + ...savedFilters, + [key]: savedFilters[key].concat({ ...filter, name }), + }; + } + } else { + newFilters = { + ...savedFilters, + [key]: [{ ...filter, name }], + }; + } + } else { + newFilters = { + [key]: [{ ...filter, name }], + }; + } + if (newFilters !== savedFilters) { + saveApplicationSettings(newFilters, "filters"); + } + } + }, + [getApplicationSettings, saveApplicationSettings, tableSchema] + ); + const suggestionProvider = useMemo(() => { if (isTypeaheadSuggestionProvider(dataSource)) { return () => getSuggestions; @@ -153,6 +249,20 @@ export const useFilterTable = ({ tableSchema }: FilterTableFeatureProps) => { [getDefaultColumnConfig, tableConfigFromState, tableSchema] ); + const handleFilterDeleted = useCallback( + (filter: Filter) => { + removeFilterFromSettings(filter); + }, + [removeFilterFromSettings] + ); + + const handleFilterRenamed = useCallback( + (filter: Filter, name: string) => { + saveFilterToSettings(filter, name); + }, + [saveFilterToSettings] + ); + const filterBarProps: FilterBarProps = { FilterClauseEditorProps: suggestionProvider ? { @@ -164,6 +274,8 @@ export const useFilterTable = ({ tableSchema }: FilterTableFeatureProps) => { filters, onApplyFilter: handleApplyFilter, onChangeActiveFilterIndex: handleChangeActiveFilterIndex, + onFilterDeleted: handleFilterDeleted, + onFilterRenamed: handleFilterRenamed, onFiltersChanged: handleFiltersChanged, tableSchema, }; @@ -197,10 +309,50 @@ export const useFilterTable = ({ tableSchema }: FilterTableFeatureProps) => { onRpcResponse: handleRpcResponse, }); + const buildFilterTableMenuOptions = useCallback( + (location, options) => { + if (location === "filter-bar-menu") { + if (savedFilters.length > 0) { + return savedFilters.map((filter) => ({ + action: "add-filter", + label: filter.name, + options: { filter }, + })); + } else { + return [ + { + label: `You have no saved filters for this table`, + action: `no-action`, + } as ContextMenuItemDescriptor, + ]; + } + } else { + return buildViewserverMenuOptions(location, options); + } + }, + [buildViewserverMenuOptions, savedFilters] + ); + + const handleFilterTableMenuAction = useCallback( + (menuAction) => { + const { menuId, options } = menuAction; + if (menuId === "add-filter") { + console.log(`add filter `, { + options, + }); + } else { + return handleMenuAction(menuAction); + } + console.log(menuId, options); + // return false; + }, + [handleMenuAction] + ); + return { - buildViewserverMenuOptions, + buildFilterTableMenuOptions, filterBarProps, - handleMenuAction, + handleFilterTableMenuAction, tableProps, }; }; 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 25441b047..48b35cf06 100644 --- a/vuu-ui/showcase/src/examples/Filters/FilterBar/FilterBar.examples.tsx +++ b/vuu-ui/showcase/src/examples/Filters/FilterBar/FilterBar.examples.tsx @@ -1,8 +1,15 @@ import { FilterBar, FilterBarProps } from "@finos/vuu-filters"; import type { Filter } from "@finos/vuu-filter-types"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { + SyntheticEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import type { DataSourceFilter } from "@finos/vuu-data-types"; -import { Input } from "@salt-ds/core"; +import { Input, ToggleButton, ToggleButtonGroup } from "@salt-ds/core"; import { getSchema, vuuModule } from "@finos/vuu-data-test"; import type { ActiveItemChangeHandler } from "@finos/vuu-ui-controls"; @@ -17,6 +24,8 @@ const lastUpdatedColumn = { export const DefaultFilterBar = ({ filters: filtersProp = [], onApplyFilter, + onFilterDeleted, + onFilterRenamed, onFiltersChanged, style, }: Partial) => { @@ -71,6 +80,8 @@ export const DefaultFilterBar = ({ filters={filters} onApplyFilter={handleApplyFilter} onChangeActiveFilterIndex={handleChangeActiveFilterIndex} + onFilterDeleted={onFilterDeleted} + onFilterRenamed={onFilterRenamed} onFiltersChanged={handleFiltersChanged} tableSchema={{ ...tableSchema, columns }} columnDescriptors={columns} @@ -109,7 +120,10 @@ export const FilterBarOneMultiValueFilter = () => { }; FilterBarOneMultiValueFilter.displaySequence = displaySequence++; -export const FilterBarMultipleFilters = () => { +export const FilterBarMultipleFilters = ({ + onFilterDeleted, + onFilterRenamed, +}: Partial) => { return ( { ], }, ]} + onFilterDeleted={onFilterDeleted} + onFilterRenamed={onFilterRenamed} /> ); }; FilterBarMultipleFilters.displaySequence = displaySequence++; + +export const FilterBarMultipleFilterSets = () => { + const [selectedIndex, setSelectedIndex] = useState(0); + const handleChangeFilterSet = useCallback( + (evt: SyntheticEvent) => { + const { value } = evt.target as HTMLButtonElement; + const index = parseInt(value); + setSelectedIndex(index); + }, + [] + ); + 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 }); + return ( +
+ + Filter Set 1 (three filters) + Filter Set 2 (two filters) + + +
+ ); +}; +FilterBarMultipleFilterSets.displaySequence = displaySequence++;