From e94daab98541f10912ac3f35e1b8734ac3065cbc Mon Sep 17 00:00:00 2001 From: il3ven Date: Mon, 21 Aug 2023 09:40:57 +0000 Subject: [PATCH 01/11] Revert "Merge pull request #1256 from staticGuru/filter_url" This reverts commit 6819bf1f5f40161ae78fe19e4b24e31e5ad72eaa, reversing changes made to 70498bcc0544d2d17e4cb1586645dd6a5f87ae6d. --- .../TableToolbar/Filters/Filters.tsx | 70 ++----------------- src/components/TableToolbar/Filters/utils.tsx | 27 ------- src/types/table.d.ts | 3 +- 3 files changed, 6 insertions(+), 94 deletions(-) delete mode 100644 src/components/TableToolbar/Filters/utils.tsx diff --git a/src/components/TableToolbar/Filters/Filters.tsx b/src/components/TableToolbar/Filters/Filters.tsx index 868d6f86..8ebc32ff 100644 --- a/src/components/TableToolbar/Filters/Filters.tsx +++ b/src/components/TableToolbar/Filters/Filters.tsx @@ -1,10 +1,7 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import { useState, useEffect } from "react"; import { useAtom } from "jotai"; import useMemoValue from "use-memo-value"; import { isEmpty, isDate } from "lodash-es"; -import { useSearchParams } from "react-router-dom"; -import { useSnackbar } from "notistack"; import { Tab, @@ -22,7 +19,6 @@ import TabPanel from "@mui/lab/TabPanel"; import FiltersPopover from "./FiltersPopover"; import FilterInputs from "./FilterInputs"; -import { changePageUrl, separateOperands } from "./utils"; import { projectScope, @@ -66,17 +62,12 @@ export default function Filters() { const [, setTableSorts] = useAtom(tableSortsAtom, tableScope); const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope); const [{ defaultQuery }] = useAtom(tableFiltersPopoverAtom, tableScope); + const tableFilterInputs = useFilterInputs(tableColumnsOrdered); const setTableQuery = tableFilterInputs.setQuery; const userFilterInputs = useFilterInputs(tableColumnsOrdered, defaultQuery); const setUserQuery = userFilterInputs.setQuery; - const { availableFilters, filterColumns } = userFilterInputs; - const [searchParams] = useSearchParams(); - const { enqueueSnackbar } = useSnackbar(); - useEffect(() => { - let isFiltered = searchParams.get("filter"); - if (isFiltered) updateUserFilter(isFiltered); - }, [searchParams]); + const { availableFilters } = userFilterInputs; // Get table filters & user filters from config documents const tableFilters = useMemoValue( @@ -91,44 +82,6 @@ export default function Filters() { const hasTableFilters = Array.isArray(tableFilters) && tableFilters.length > 0; const hasUserFilters = Array.isArray(userFilters) && userFilters.length > 0; - function updateUserFilter(str: string) { - let { operators, operands = [] } = separateOperands(str); - if (!operators.length) return; - if (operators.length) { - let appliedFilter: TableFilter[] = []; - appliedFilter = [ - { - key: operands[0], - operator: operators[0], - value: Number(operands[1]), - }, - ]; - let isValidFilter = checkFilterValidation(appliedFilter[0]); - if (isValidFilter) { - setOverrideTableFilters(true); - setUserFilters(appliedFilter); - } else { - enqueueSnackbar("Oops, Invalid filter!!!", { variant: "error" }); - setUserFilters([]); - setOverrideTableFilters(false); - userFilterInputs.resetQuery(); - } - } - } - function checkFilterValidation(filter: TableFilter): boolean { - let isFilterableColumn = filterColumns?.filter( - (item) => - item.key === filter.key || - item.label === filter.key || - item.type === filter.key - ); - if (!isFilterableColumn?.length) return false; - filter.key = isFilterableColumn?.[0]?.value; - filter.operator = filter.operator === "-is-" ? "id-equal" : filter.operator; - filter.value = - filter.operator === "id-equal" ? filter.value.toString() : filter.value; - return true; - } // Set the local table filter useEffect(() => { @@ -156,7 +109,7 @@ export default function Filters() { } else if (hasUserFilters) { filtersToApply = userFilters; } - updatePageURL(filtersToApply); + setLocalFilters(filtersToApply); // Reset order so we don’t have to make a new index if (filtersToApply.length) { @@ -167,6 +120,7 @@ export default function Filters() { hasUserFilters, setLocalFilters, setTableSorts, + setTableQuery, tableFilters, tableFiltersOverridable, setUserQuery, @@ -219,21 +173,7 @@ export default function Filters() { if (updateUserSettings && filters) updateUserSettings({ tables: { [`${tableId}`]: { filters } } }); }; - function updatePageURL(filters: TableFilter[]) { - if (!filters.length) { - changePageUrl(); - } else { - const [filter] = filters; - const fieldName = filter.key === "_rowy_ref.id" ? "ID" : filter.key; - const operator = - filter.operator === "id-equal" ? "-is-" : filter.operator; - const formattedValue = availableFilters?.valueFormatter - ? availableFilters.valueFormatter(filter.value, filter.operator) - : filter.value.toString(); - const queryParams = `?filter=${fieldName}${operator}${formattedValue}`; - changePageUrl(queryParams); - } - } + return ( `\\${op}`).join("|"), "g") - ); - return { operators, operands }; -} -export function changePageUrl(newURL: string | undefined = URL) { - if (newURL !== URL) { - newURL = URL + newURL; - } - window.history.pushState({ path: newURL }, "", newURL); -} - -function findOperators(str: string) { - const operators = [">=", "<=", ">", "<", "==", "!=", "=", "-is-"]; - const regex = new RegExp(operators.map((op) => `\\${op}`).join("|"), "g"); - return str.match(regex) || []; -} diff --git a/src/types/table.d.ts b/src/types/table.d.ts index 5b33e6d7..3ed4b7c3 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -193,8 +193,7 @@ export type TableFilter = { | "time-minute-equal" | "id-equal" | "color-equal" - | "color-not-equal" - | "-is-"; + | "color-not-equal"; value: any; }; From f7fdf57c36cb52110c2f0480b74b3796cf948524 Mon Sep 17 00:00:00 2001 From: Manmeet Date: Tue, 13 Jun 2023 22:27:08 +0530 Subject: [PATCH 02/11] add multi filters support --- src/atoms/tableScope/table.ts | 2 + .../TableToolbar/Filters/FilterInputs.tsx | 114 ++- .../Filters/FilterInputsCollection.tsx | 123 ++++ .../TableToolbar/Filters/Filters.tsx | 168 +++-- .../TableToolbar/Filters/FiltersPopover.tsx | 103 +-- .../TableToolbar/Filters/useFilterInputs.ts | 89 ++- src/hooks/useFirestoreCollectionWithAtom.ts | 55 +- .../TableSourceFirestore.tsx | 7 + src/types/settings.d.ts | 1 + src/types/table.d.ts | 5 +- yarn.lock | 669 +++++++++--------- 11 files changed, 856 insertions(+), 480 deletions(-) create mode 100644 src/components/TableToolbar/Filters/FilterInputsCollection.tsx diff --git a/src/atoms/tableScope/table.ts b/src/atoms/tableScope/table.ts index f8d519c4..b88e34c4 100644 --- a/src/atoms/tableScope/table.ts +++ b/src/atoms/tableScope/table.ts @@ -59,6 +59,8 @@ export const tableColumnsReducer = ( /** Filters applied to the local view */ export const tableFiltersAtom = atom([]); +/** Join operator applied to mulitple filters */ +export const tableFiltersJoinAtom = atom<"AND" | "OR">("AND"); /** Sorts applied to the local view */ export const tableSortsAtom = atom([]); diff --git a/src/components/TableToolbar/Filters/FilterInputs.tsx b/src/components/TableToolbar/Filters/FilterInputs.tsx index 24fa1b55..d8930dc3 100644 --- a/src/components/TableToolbar/Filters/FilterInputs.tsx +++ b/src/components/TableToolbar/Filters/FilterInputs.tsx @@ -10,6 +10,7 @@ import { Typography, TextField, InputLabel, + IconButton, } from "@mui/material"; import ColumnSelect from "@src/components/Table/ColumnSelect"; @@ -17,11 +18,28 @@ import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton"; import IdFilterInput from "./IdFilterInput"; import { InlineErrorFallback } from "@src/components/ErrorFallback"; -import type { useFilterInputs } from "./useFilterInputs"; import { getFieldType, getFieldProp } from "@src/components/fields"; +import type { IFieldConfig } from "@src/components/fields/types"; -export interface IFilterInputsProps extends ReturnType { +import { TableFilter } from "@src/types/table"; + +import DeleteIcon from "@mui/icons-material/Delete"; +import DragIndicatorOutlinedIcon from "@mui/icons-material/DragIndicatorOutlined"; +import { useFilterInputs } from "./useFilterInputs"; + +export interface IFilterInputsProps { + filterColumns: ReturnType["filterColumns"]; + selectedColumn: ReturnType["filterColumns"][0]; + availableFilters: IFieldConfig["filter"]; disabled?: boolean; + query: TableFilter; + setQuery: (query: TableFilter) => void; + handleDelete: () => void; + index: number; + noOfQueries: number; + handleChangeColumn: (column: string) => void; + joinOperator: "AND" | "OR"; + setJoinOperator: (operator: "AND" | "OR") => void; } export default function FilterInputs({ @@ -32,6 +50,11 @@ export default function FilterInputs({ query, setQuery, disabled, + joinOperator, + setJoinOperator, + handleDelete, + index, + noOfQueries, }: IFilterInputsProps) { const columnType = selectedColumn ? getFieldType(selectedColumn) : null; @@ -76,18 +99,18 @@ export default function FilterInputs({ return ( - + handleChangeColumn(newKey ?? "")} disabled={disabled} /> - + { - setQuery((query) => ({ + const newQuery = { ...query, - operator: e.target.value as string, - })); + operator: e.target.value as TableFilter["operator"], + }; + + setQuery(newQuery); }} SelectProps={{ displayEmpty: true }} - sx={{ "& .MuiSelect-select": { display: "flex" } }} + sx={{ + "& .MuiSelect-select": { display: "flex" }, + }} > Select operator @@ -113,7 +140,7 @@ export default function FilterInputs({ - + {query.key && query.operator && ( { - setQuery((query) => ({ ...query, value })); + const newQuery = { + ...query, + value, + }; + setQuery(newQuery); }, disabled, operator: query.operator, @@ -146,6 +177,67 @@ export default function FilterInputs({ )} + + + + {} + + + + + {noOfQueries > 1 && + index !== noOfQueries - 1 && + (index === 0 ? ( + + + + + setJoinOperator(e.target.value === "AND" ? "AND" : "OR") + } + sx={{ + "& .MuiSelect-select": { + display: "flex", + }, + }} + > + And + Or + + + + + ) : ( + + + {joinOperator === "AND" ? "And" : "Or"} + + + ))} ); } diff --git a/src/components/TableToolbar/Filters/FilterInputsCollection.tsx b/src/components/TableToolbar/Filters/FilterInputsCollection.tsx new file mode 100644 index 00000000..adbc9215 --- /dev/null +++ b/src/components/TableToolbar/Filters/FilterInputsCollection.tsx @@ -0,0 +1,123 @@ +import FilterInputs from "./FilterInputs"; + +import { Button } from "@mui/material"; + +import type { useFilterInputs } from "./useFilterInputs"; + +import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd"; +import AddIcon from "@mui/icons-material/Add"; +import { find } from "lodash-es"; + +export interface IFilterInputsCollectionProps + extends ReturnType { + disabled?: boolean; +} + +export default function FilterInputsCollection({ + filterColumns, + selectedColumns, + handleColumnChange, + availableFiltersForEachSelectedColumn, + queries, + setQueries, + disabled, + joinOperator, + setJoinOperator, +}: IFilterInputsCollectionProps) { + const onDragEnd = (result: any) => { + if (!result.destination) return; + + setQueries((prevQueries) => { + const newQueries = [...prevQueries]; + const [reorderedItem] = newQueries.splice(result.source.index, 1); + newQueries.splice(result.destination.index, 0, reorderedItem); + return newQueries; + }); + }; + + return ( + <> + + + {(provided) => ( +
+ {queries.map((query, index) => { + return ( + + {(provided) => ( +
+ { + handleColumnChange(query.key, key); + }} + availableFilters={ + availableFiltersForEachSelectedColumn[index] + } + query={query} + setQuery={(newQuery: any) => { + setQueries((prevQueries) => { + const newQueries = [...prevQueries]; + newQueries[index] = newQuery; + return newQueries; + }); + }} + disabled={disabled} + joinOperator={joinOperator} + setJoinOperator={setJoinOperator} + handleDelete={() => { + setQueries((prevQueries) => { + const newQueries = [...prevQueries]; + newQueries.splice(index, 1); + return newQueries; + }); + }} + index={index} + noOfQueries={queries.length} + /> +
+ )} +
+ ); + })} + {provided.placeholder} +
+ )} +
+
+ + + + ); +} diff --git a/src/components/TableToolbar/Filters/Filters.tsx b/src/components/TableToolbar/Filters/Filters.tsx index 8ebc32ff..f2fbdf05 100644 --- a/src/components/TableToolbar/Filters/Filters.tsx +++ b/src/components/TableToolbar/Filters/Filters.tsx @@ -18,7 +18,7 @@ import TabList from "@mui/lab/TabList"; import TabPanel from "@mui/lab/TabPanel"; import FiltersPopover from "./FiltersPopover"; -import FilterInputs from "./FilterInputs"; +import FilterInputsCollection from "./FilterInputsCollection"; import { projectScope, @@ -35,16 +35,26 @@ import { tableSortsAtom, updateTableSchemaAtom, tableFiltersPopoverAtom, + tableFiltersJoinAtom, } from "@src/atoms/tableScope"; import { useFilterInputs, INITIAL_QUERY } from "./useFilterInputs"; import { analytics, logEvent } from "@src/analytics"; import type { TableFilter } from "@src/types/table"; -const shouldDisableApplyButton = (value: any) => - isEmpty(value) && - !isDate(value) && - typeof value !== "boolean" && - typeof value !== "number"; +const shouldDisableApplyButton = (queries: any) => { + let disable = queries.length === 0; + + for (let query of queries) { + disable = + disable || + (isEmpty(query.value) && + !isDate(query.value) && + typeof query.value !== "boolean" && + typeof query.value !== "number"); + } + + return disable; +}; enum FilterType { yourFilter = "local_filter", @@ -64,10 +74,17 @@ export default function Filters() { const [{ defaultQuery }] = useAtom(tableFiltersPopoverAtom, tableScope); const tableFilterInputs = useFilterInputs(tableColumnsOrdered); - const setTableQuery = tableFilterInputs.setQuery; + const setTableQueries = tableFilterInputs.setQueries; const userFilterInputs = useFilterInputs(tableColumnsOrdered, defaultQuery); - const setUserQuery = userFilterInputs.setQuery; - const { availableFilters } = userFilterInputs; + const setUserQueries = userFilterInputs.setQueries; + const { availableFiltersForEachSelectedColumn } = userFilterInputs; + const availableFiltersForFirstColumn = + availableFiltersForEachSelectedColumn[0]; + + const [tableFiltersJoin, setTableFiltersJoin] = useAtom( + tableFiltersJoinAtom, + tableScope + ); // Get table filters & user filters from config documents const tableFilters = useMemoValue( @@ -86,14 +103,14 @@ export default function Filters() { // Set the local table filter useEffect(() => { // Set local state for UI - setTableQuery( - Array.isArray(tableFilters) && tableFilters[0] - ? tableFilters[0] + setTableQueries( + Array.isArray(tableFilters) && tableFilters && tableFilters.length > 0 + ? tableFilters : INITIAL_QUERY ); - setUserQuery( - Array.isArray(userFilters) && userFilters[0] - ? userFilters[0] + setUserQueries( + Array.isArray(userFilters) && userFilters && userFilters.length > 0 + ? userFilters : INITIAL_QUERY ); setCanOverrideCheckbox(tableFiltersOverridable); @@ -120,10 +137,10 @@ export default function Filters() { hasUserFilters, setLocalFilters, setTableSorts, - setTableQuery, + setTableQueries, tableFilters, tableFiltersOverridable, - setUserQuery, + setUserQueries, userFilters, userRoles, ]); @@ -151,27 +168,74 @@ export default function Filters() { // When defaultQuery (from atom) is updated, update the UI useEffect(() => { if (defaultQuery) { - setUserQuery(defaultQuery); + setUserQueries([defaultQuery]); setTab("user"); } - }, [setUserQuery, defaultQuery]); + }, [setUserQueries, defaultQuery]); const [overrideTableFilters, setOverrideTableFilters] = useState( tableFiltersOverridden ); + useEffect(() => { + if (userSettings.tables?.[tableId]?.joinOperator) { + userFilterInputs.setJoinOperator( + userSettings.tables?.[tableId]?.joinOperator === "AND" ? "AND" : "OR" + ); + } + + if (tableSchema.joinOperator) { + tableFilterInputs.setJoinOperator( + tableSchema.joinOperator === "AND" ? "AND" : "OR" + ); + } + }, [userSettings.tables?.[tableId]?.joinOperator]); + + useEffect(() => { + if (tableFiltersOverridable && (hasUserFilters || userFilters === null)) { + setTableFiltersJoin( + userSettings.tables?.[tableId]?.joinOperator === "AND" ? "AND" : "OR" + ); + } else if (hasTableFilters) { + setTableFiltersJoin(tableSchema.joinOperator === "AND" ? "AND" : "OR"); + } else if (hasUserFilters) { + setTableFiltersJoin( + userSettings.tables?.[tableId]?.joinOperator === "AND" ? "AND" : "OR" + ); + } + }, [ + tableFiltersOverridable, + hasUserFilters, + hasTableFilters, + userFilters, + tableSchema.joinOperator, + userSettings.tables?.[tableId]?.joinOperator, + ]); + // Save table filters to table schema document - const setTableFilters = (filters: TableFilter[]) => { + const setTableFilters = ( + filters: TableFilter[], + op: "AND" | "OR" = "AND" + ) => { logEvent(analytics, FilterType.tableFilter); if (updateTableSchema) - updateTableSchema({ filters, filtersOverridable: canOverrideCheckbox }); + updateTableSchema({ + filters, + filtersOverridable: canOverrideCheckbox, + joinOperator: op, + }); }; // Save user filters to user document // null overrides table filters - const setUserFilters = (filters: TableFilter[] | null) => { + const setUserFilters = ( + filters: TableFilter[] | null, + op: "AND" | "OR" = "AND" + ) => { logEvent(analytics, FilterType.yourFilter); if (updateUserSettings && filters) - updateUserSettings({ tables: { [`${tableId}`]: { filters } } }); + updateUserSettings({ + tables: { [`${tableId}`]: { filters, joinOperator: op } }, + }); }; return ( @@ -180,7 +244,7 @@ export default function Filters() { hasAppliedFilters={hasAppliedFilters} hasTableFilters={hasTableFilters} tableFiltersOverridden={tableFiltersOverridden} - availableFilters={availableFilters} + availableFilters={availableFiltersForFirstColumn} setUserFilters={setUserFilters} > {({ handleClose }) => { @@ -245,7 +309,7 @@ export default function Filters() { - + {hasTableFilters && ( { setUserFilters(overrideTableFilters ? null : []); userFilterInputs.resetQuery(); }} > - Clear + Clear All {hasTableFilters && (overrideTableFilters ? " (ignore table filter)" @@ -289,12 +353,15 @@ export default function Filters() {