From e3be71c837b3229ae7d7e8f450eb6a3e75475415 Mon Sep 17 00:00:00 2001 From: Cody O'Donnell Date: Mon, 24 Jun 2024 13:54:37 -0700 Subject: [PATCH 01/13] Add test commit comment --- strudel-taskflows/src/pages/explore-data/_layout.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/strudel-taskflows/src/pages/explore-data/_layout.tsx b/strudel-taskflows/src/pages/explore-data/_layout.tsx index 702a1652..657edb2a 100644 --- a/strudel-taskflows/src/pages/explore-data/_layout.tsx +++ b/strudel-taskflows/src/pages/explore-data/_layout.tsx @@ -11,6 +11,7 @@ import { taskflow } from './_config/taskflow.config'; * Inner pages are rendered inside the `` component */ const ExploreDataLayout: React.FC = () => { + // Test commit const entities = useDataFromSource(taskflow.data.items.source); /** From c6368b0ef2109aa9c1e5b55343c2fe0553609435 Mon Sep 17 00:00:00 2001 From: Cody O'Donnell Date: Mon, 24 Jun 2024 13:59:04 -0700 Subject: [PATCH 02/13] Add test comment from branch --- strudel-taskflows/src/pages/explore-data/_layout.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/strudel-taskflows/src/pages/explore-data/_layout.tsx b/strudel-taskflows/src/pages/explore-data/_layout.tsx index 657edb2a..4804972e 100644 --- a/strudel-taskflows/src/pages/explore-data/_layout.tsx +++ b/strudel-taskflows/src/pages/explore-data/_layout.tsx @@ -12,6 +12,7 @@ import { taskflow } from './_config/taskflow.config'; */ const ExploreDataLayout: React.FC = () => { // Test commit + // Commit from feature/test const entities = useDataFromSource(taskflow.data.items.source); /** From 7043226ec21b0216d330a9ca47b8236d04351fff Mon Sep 17 00:00:00 2001 From: Cody O'Donnell Date: Mon, 24 Jun 2024 15:30:50 -0700 Subject: [PATCH 03/13] Delete test comments --- strudel-taskflows/src/pages/explore-data/_layout.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/strudel-taskflows/src/pages/explore-data/_layout.tsx b/strudel-taskflows/src/pages/explore-data/_layout.tsx index 4804972e..702a1652 100644 --- a/strudel-taskflows/src/pages/explore-data/_layout.tsx +++ b/strudel-taskflows/src/pages/explore-data/_layout.tsx @@ -11,8 +11,6 @@ import { taskflow } from './_config/taskflow.config'; * Inner pages are rendered inside the `` component */ const ExploreDataLayout: React.FC = () => { - // Test commit - // Commit from feature/test const entities = useDataFromSource(taskflow.data.items.source); /** From 6b1150e372ba6457c29df49db0a942b9a694ee2e Mon Sep 17 00:00:00 2001 From: Cody O'Donnell Date: Fri, 19 Jul 2024 15:49:56 -0700 Subject: [PATCH 04/13] Update filter components to use Accordion and new props --- .../lib/components/CheckboxList.tsx | 2 +- .../lib/components/FilterField.tsx | 42 ++-- .../lib/components/FilterGroup.tsx | 90 +++++---- strudel-components/lib/components/Filters.tsx | 61 ++++-- .../lib/components/FiltersPanel.tsx | 46 ----- .../lib/components/StrudelSlider.tsx | 25 ++- strudel-components/package-lock.json | 138 ++++++++++--- strudel-components/package.json | 3 + strudel-components/src/App.tsx | 182 ++++++++++++++++-- .../src/examples/Filters/FiltersExGroups.tsx | 88 +++++++++ .../examples/Filters/FiltersExNoGroups.tsx | 83 ++++++++ .../explore-data/_components/FiltersPanel.tsx | 3 +- 12 files changed, 588 insertions(+), 175 deletions(-) delete mode 100644 strudel-components/lib/components/FiltersPanel.tsx create mode 100644 strudel-components/src/examples/Filters/FiltersExGroups.tsx create mode 100644 strudel-components/src/examples/Filters/FiltersExNoGroups.tsx diff --git a/strudel-components/lib/components/CheckboxList.tsx b/strudel-components/lib/components/CheckboxList.tsx index 8f7cbff6..5e585f5c 100644 --- a/strudel-components/lib/components/CheckboxList.tsx +++ b/strudel-components/lib/components/CheckboxList.tsx @@ -51,7 +51,7 @@ export const CheckboxList: React.FC = ({ onChange={(e, checked) => handleChange(checked, option.value)} sx={{ pr: 1, - pl: 0, + pl: 1, pb: 0, pt: 0 }} diff --git a/strudel-components/lib/components/FilterField.tsx b/strudel-components/lib/components/FilterField.tsx index f738d546..a4cd2a33 100644 --- a/strudel-components/lib/components/FilterField.tsx +++ b/strudel-components/lib/components/FilterField.tsx @@ -1,46 +1,42 @@ import React, { ReactNode } from 'react'; import { Box, Stack, StackProps, Typography } from '@mui/material'; -import { Collapsible } from './Collapsible'; interface FilterFieldProps extends StackProps { label: ReactNode; filter: ReactNode; - isCollapsible?: boolean; } export const FilterField: React.FC = ({ label, filter, - isCollapsible, color, ...rest }) => { const defaultLabel = ( - + {label} ); const labelComponent = typeof label === 'string' ? defaultLabel : label; return ( - <> - {isCollapsible ? ( - - - {filter} - - - ): ( - - {labelComponent} - - {filter} - - - )} - + + {labelComponent} + + {filter} + + ) } \ No newline at end of file diff --git a/strudel-components/lib/components/FilterGroup.tsx b/strudel-components/lib/components/FilterGroup.tsx index 1b1ba596..4c5a1b1e 100644 --- a/strudel-components/lib/components/FilterGroup.tsx +++ b/strudel-components/lib/components/FilterGroup.tsx @@ -1,50 +1,66 @@ -import { Stack, Typography } from '@mui/material'; -import React from 'react'; -import { Collapsible, CollapsibleProps } from './Collapsible'; +import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'; +import { Accordion, AccordionDetails, AccordionSummary, Stack, Typography } from '@mui/material'; +import React, { useState } from 'react'; -interface FilterGroupProps extends CollapsibleProps { - isCollapsible?: boolean; +interface FilterGroupProps { + label?: React.ReactNode; + children: React.ReactNode; } export const FilterGroup: React.FC = ({ label, - isCollapsible, - children, - ...rest + children }) => { - const defaultLabel = ( - ('panel1'); + + const handleChange = (panel: string) => (event: React.SyntheticEvent, newExpanded: boolean) => { + setExpanded(newExpanded ? panel : false); + }; + + return ( + - {label} - - ); - const labelComponent = typeof label === 'string' ? defaultLabel : label; - - return ( - <> - {isCollapsible ? ( - - - {children} - - - ): ( - - {labelComponent} - - {children} - + } + aria-controls="panel1d-content" + id="panel1d-header" + sx={{ + flexDirection: 'row-reverse', + '& .MuiAccordionSummary-expandIconWrapper .MuiSvgIcon-root': { + fontSize: '1rem', + }, + '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { + transform: 'rotate(90deg)', + }, + '& .MuiAccordionSummary-content': { + marginLeft: 1, + marginTop: 2, + marginBottom: 2, + }, + }} + > + {label} + + + + {children} - )} - + + ) } \ No newline at end of file diff --git a/strudel-components/lib/components/Filters.tsx b/strudel-components/lib/components/Filters.tsx index 932bc19a..09e5703d 100644 --- a/strudel-components/lib/components/Filters.tsx +++ b/strudel-components/lib/components/Filters.tsx @@ -1,27 +1,66 @@ -import React from 'react'; -import { IconButton, Paper, PaperProps, Stack, Typography } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; +import { Box, Button, IconButton, Paper, PaperProps, Stack } from '@mui/material'; +import React from 'react'; interface FilterPanelProps extends PaperProps { onClose?: () => any; + config?: any; + header?: React.ReactNode; + grouped?: boolean; } -export const Filters: React.FC = ({ +export const Filters: React.FC = ({ + header = 'Filters', + grouped = true, onClose, + config, children, ...rest }) => { return ( - + - - FILTERS - - - - {children} - + {header && ( + + {header} + + + + )} + {grouped && ( + + {children} + + )} + {!grouped && ( + + {children} + + )} ) diff --git a/strudel-components/lib/components/FiltersPanel.tsx b/strudel-components/lib/components/FiltersPanel.tsx deleted file mode 100644 index e901c30a..00000000 --- a/strudel-components/lib/components/FiltersPanel.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import CloseIcon from '@mui/icons-material/Close'; -import { IconButton, Stack, StackProps, Typography } from '@mui/material'; -import React from 'react'; - -enum FilterType { - CHECKBOX_LIST = 'CHECKBOX_LIST', - RANGE_SLIDER = 'RANGE_SLIDER', -} - -interface Filter { - label: string; - field: string; - type: FilterType; - defaultValue: any; -} - -interface FiltersProps extends StackProps { - filters?: Filter[]; - onChange?: () => any; - onClose?: () => any; -} - -// const initFilterValues = (filters: Filter[]) => { -// const filterValues: any = {}; -// filters.forEach((filter) => { -// filterValues[filter.field] = filter.defaultValue; -// }) -// }; - -export const Filters: React.FC = ({ - onClose, - children, - ...rest -}) => { - return ( - - - FILTERS - - - - {children} - - - ) -} \ No newline at end of file diff --git a/strudel-components/lib/components/StrudelSlider.tsx b/strudel-components/lib/components/StrudelSlider.tsx index 79561188..e7587584 100644 --- a/strudel-components/lib/components/StrudelSlider.tsx +++ b/strudel-components/lib/components/StrudelSlider.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Slider, SliderProps } from '@mui/material'; +import { Box, Slider, SliderProps } from '@mui/material'; interface StrudelSliderProps extends SliderProps { @@ -32,13 +32,20 @@ export const StrudelSlider: React.FC = ({ ]; return ( - + + + ) } \ No newline at end of file diff --git a/strudel-components/package-lock.json b/strudel-components/package-lock.json index 843d6042..7585ad71 100644 --- a/strudel-components/package-lock.json +++ b/strudel-components/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "strudel-components", "version": "0.0.0", + "dependencies": { + "dayjs": "^1.11.12" + }, "devDependencies": { "@react-docgen/cli": "^2.0.3", "@types/node": "^20.12.7", @@ -28,6 +31,7 @@ "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.15", "@mui/material": "^5.15.15", + "@mui/x-date-pickers": "^7.11.0", "react": "^18.0.0", "react-dom": "^18.0.0" } @@ -347,9 +351,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", - "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", "peer": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1429,13 +1433,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz", - "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.4.tgz", + "integrity": "sha512-ZsAm8cq31SJ37SVWLRlu02v9SRthxnfQofaiv14L5Bht51B0dz6yQEoVU/V8UduZDCCIrWkBHuReVfKhE/UuXA==", "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.15.14", + "@mui/utils": "^5.16.4", "prop-types": "^15.8.1" }, "engines": { @@ -1456,9 +1460,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz", - "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.4.tgz", + "integrity": "sha512-0+mnkf+UiAmTVB8PZFqOhqf729Yh0Cxq29/5cA3VAyDVTRIUUQ8FXQhiAhUIbijFmM72rY80ahFPXIm4WDbzcA==", "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", @@ -1488,16 +1492,16 @@ } }, "node_modules/@mui/system": { - "version": "5.15.15", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.15.tgz", - "integrity": "sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.4.tgz", + "integrity": "sha512-ET1Ujl2/8hbsD611/mqUuNArMCGv/fIWO/f8B3ZqF5iyPHM2aS74vhTNyjytncc4i6dYwGxNk+tLa7GwjNS0/w==", "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.15.14", - "@mui/styled-engine": "^5.15.14", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", + "@mui/private-theming": "^5.16.4", + "@mui/styled-engine": "^5.16.4", + "@mui/types": "^7.2.15", + "@mui/utils": "^5.16.4", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1528,9 +1532,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.14", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", - "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", + "version": "7.2.15", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.15.tgz", + "integrity": "sha512-nbo7yPhtKJkdf9kcVOF8JZHPZTmqXjJ/tI0bdWgHg5tp9AnIN4Y7f7wm9T+0SyGYJk76+GYZ8Q5XaTYAsUHN0Q==", "peer": true, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0" @@ -1542,15 +1546,16 @@ } }, "node_modules/@mui/utils": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", - "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.4.tgz", + "integrity": "sha512-nlppYwq10TBIFqp7qxY0SvbACOXeOjeVL3pOcDsK0FT8XjrEXh9/+lkg8AEIzD16z7YfiJDQjaJG2OLkE7BxNg==", "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", - "@types/prop-types": "^15.7.11", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.2.0" + "react-is": "^18.3.1" }, "engines": { "node": ">=12.0.0" @@ -1569,6 +1574,72 @@ } } }, + "node_modules/@mui/x-date-pickers": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.11.0.tgz", + "integrity": "sha512-+zPWs1dwe7J1nZ2iFhTgCae31BLMYMQ2VtQfHxx21Dh6gbBRy/U7YJZg1LdhfQyE093S3e4A5uMZ6PUWdne7iA==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.24.8", + "@mui/base": "^5.0.0-beta.40", + "@mui/system": "^5.16.2", + "@mui/utils": "^5.16.2", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14", + "date-fns": "^2.25.0 || ^3.2.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2621,9 +2692,9 @@ } }, "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "peer": true, "engines": { "node": ">=6" @@ -2705,6 +2776,11 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/dayjs": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz", + "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==" + }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -4129,9 +4205,9 @@ } }, "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "peer": true }, "node_modules/react-refresh": { diff --git a/strudel-components/package.json b/strudel-components/package.json index 7abf5968..f47ea859 100644 --- a/strudel-components/package.json +++ b/strudel-components/package.json @@ -4,6 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { + "start": "vite", "dev": "vite", "build": "tsc --p ./tsconfig-build.json && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", @@ -36,6 +37,8 @@ "@emotion/styled": "^11.11.5", "@mui/icons-material": "^5.15.15", "@mui/material": "^5.15.15", + "@mui/x-date-pickers": "^7.11.0", + "dayjs": "^1.11.12", "react": "^18.0.0", "react-dom": "^18.0.0" } diff --git a/strudel-components/src/App.tsx b/strudel-components/src/App.tsx index edd2c912..2fa1f8b6 100644 --- a/strudel-components/src/App.tsx +++ b/strudel-components/src/App.tsx @@ -1,22 +1,172 @@ -import { LabelValueTable } from '../lib/components/LabelValueTable' +import { LocalizationProvider } from '@mui/x-date-pickers' +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' +import { Filters } from '../lib/components/Filters' +import { Box, CssBaseline, Stack, ThemeProvider, Typography, createTheme } from '@mui/material' +import { CheckboxList } from '../lib/components/CheckboxList' +import { StrudelSlider } from '../lib/components/StrudelSlider' +import { FilterGroup } from '../lib/components/FilterGroup' +import { FilterField } from '../lib/components/FilterField' function App() { return ( - <> - Test - - + + + + + My Filters} + sx={{ + maxWidth: '400px' + }} + > + + null} + /> + } + /> + null} + /> + } + /> + null} + /> + } + /> + + + null} + /> + } + /> + null} + /> + } + /> + + + + null} + /> + } + /> + null} + /> + } + /> + null} + /> + } + /> + null} + /> + } + /> + null} + /> + } + /> + + + + ) } diff --git a/strudel-components/src/examples/Filters/FiltersExGroups.tsx b/strudel-components/src/examples/Filters/FiltersExGroups.tsx new file mode 100644 index 00000000..7025a9ea --- /dev/null +++ b/strudel-components/src/examples/Filters/FiltersExGroups.tsx @@ -0,0 +1,88 @@ +import { Typography } from "@mui/material" +import { FilterField } from "../../../lib/components/FilterField" +import { FilterGroup } from "../../../lib/components/FilterGroup" +import { Filters } from "../../../lib/components/Filters" +import { StrudelSlider } from "../../../lib/components/StrudelSlider" +import { CheckboxList } from "../../../lib/components/CheckboxList" + +export const FiltersExGroups: React.FC = () => { + return ( + My Filters} + sx={{ + maxWidth: '400px' + }} + > + + null} + /> + } + /> + null} + /> + } + /> + null} + /> + } + /> + + + null} + /> + } + /> + null} + /> + } + /> + + + ) +} \ No newline at end of file diff --git a/strudel-components/src/examples/Filters/FiltersExNoGroups.tsx b/strudel-components/src/examples/Filters/FiltersExNoGroups.tsx new file mode 100644 index 00000000..b1a0145e --- /dev/null +++ b/strudel-components/src/examples/Filters/FiltersExNoGroups.tsx @@ -0,0 +1,83 @@ +import { CheckboxList } from "../../../lib/components/CheckboxList" +import { FilterField } from "../../../lib/components/FilterField" +import { Filters } from "../../../lib/components/Filters" +import { StrudelSlider } from "../../../lib/components/StrudelSlider" + +export const FiltersExNoGroups: React.FC = () => { + return ( + + null} + /> + } + /> + null} + /> + } + /> + null} + /> + } + /> + null} + /> + } + /> + null} + /> + } + /> + + ) +} \ No newline at end of file diff --git a/strudel-taskflows/src/pages/explore-data/_components/FiltersPanel.tsx b/strudel-taskflows/src/pages/explore-data/_components/FiltersPanel.tsx index cca35abd..c7209baf 100644 --- a/strudel-taskflows/src/pages/explore-data/_components/FiltersPanel.tsx +++ b/strudel-taskflows/src/pages/explore-data/_components/FiltersPanel.tsx @@ -8,6 +8,7 @@ import { StrudelSlider } from '../../../components/StrudelSlider'; import { FilterConfig, FilterOperator } from '../../../types/filters.types'; import { useExploreData } from '../_context/ContextProvider'; import { setFilter } from '../_context/actions'; +import { taskflow } from '../_config/taskflow.config'; interface FiltersPanelProps { onClose: () => any @@ -90,7 +91,7 @@ export const FiltersPanel: React.FC = (props) => { paddingRight: 2 }} > - {state.filters.map((f, i) => ( + {taskflow.pages.index.tableFilters.map((f, i) => ( Date: Fri, 19 Jul 2024 17:17:12 -0700 Subject: [PATCH 05/13] Add filter context to control expanded groups --- .../lib/components/FilterContext.tsx | 58 +++++++ .../lib/components/FilterGroup.tsx | 13 +- strudel-components/src/App.tsx | 157 +++++++++--------- 3 files changed, 146 insertions(+), 82 deletions(-) create mode 100644 strudel-components/lib/components/FilterContext.tsx diff --git a/strudel-components/lib/components/FilterContext.tsx b/strudel-components/lib/components/FilterContext.tsx new file mode 100644 index 00000000..8f03c3fa --- /dev/null +++ b/strudel-components/lib/components/FilterContext.tsx @@ -0,0 +1,58 @@ +import React, { PropsWithChildren, useContext, useEffect } from 'react'; + +export interface FilterState { + expandedGroup: string | number | boolean; +} + +const FilterContextAPI = React.createContext<{state: FilterState; dispatch: React.Dispatch} | undefined>(undefined); + +const initialState: FilterState = { + expandedGroup: false, +} + +export enum FilterActionType { + SET_EXPANDED_GROUP = 'SET_EXPANDED_GROUP', +} + +export interface FilterAction { + type: FilterActionType; + payload?: any; +} + +export const setExpandedGroup = (expandedGroup: FilterState['expandedGroup']): FilterAction => ({ + type: FilterActionType.SET_EXPANDED_GROUP, + payload: expandedGroup, +}); + +function filterReducer(state: FilterState, action: FilterAction): FilterState { + switch (action.type) { + case FilterActionType.SET_EXPANDED_GROUP: { + return { + ...state, + expandedGroup: action.payload + } + } + default: { + throw new Error(`Unhandled action type: ${action.type}`) + } + } +} + +export const FilterContext: React.FC = ({ children }) => { + const [state, dispatch] = React.useReducer(filterReducer, initialState); + const value = { state, dispatch }; + + return ( + + {children} + + ) +} + +export const useFilters = () => { + const context = useContext(FilterContextAPI) + if (context === undefined) { + throw new Error('useFilters must be used within a FilterContext') + } + return context +} \ No newline at end of file diff --git a/strudel-components/lib/components/FilterGroup.tsx b/strudel-components/lib/components/FilterGroup.tsx index 4c5a1b1e..c5853227 100644 --- a/strudel-components/lib/components/FilterGroup.tsx +++ b/strudel-components/lib/components/FilterGroup.tsx @@ -1,28 +1,31 @@ import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'; import { Accordion, AccordionDetails, AccordionSummary, Stack, Typography } from '@mui/material'; import React, { useState } from 'react'; +import { setExpandedGroup, useFilters } from './FilterContext'; interface FilterGroupProps { label?: React.ReactNode; + groupId: string | number; children: React.ReactNode; } export const FilterGroup: React.FC = ({ label, + groupId, children }) => { - const [expanded, setExpanded] = useState('panel1'); + const { state, dispatch } = useFilters(); - const handleChange = (panel: string) => (event: React.SyntheticEvent, newExpanded: boolean) => { - setExpanded(newExpanded ? panel : false); + const handleChange = (panel: string | number) => (event: React.SyntheticEvent, newExpanded: boolean) => { + dispatch(setExpandedGroup(newExpanded ? panel : false)); }; return ( - My Filters} - sx={{ - maxWidth: '400px' - }} - > - - null} - /> - } - /> - null} - /> - } - /> - null} - /> - } - /> - - - null} - /> - } - /> - null} - /> - } - /> - - + + My Filters} + sx={{ + maxWidth: '400px' + }} + > + + null} + /> + } + /> + null} + /> + } + /> + null} + /> + } + /> + + + null} + /> + } + /> + null} + /> + } + /> + + + Date: Mon, 22 Jul 2024 11:04:03 -0700 Subject: [PATCH 06/13] Monitor active filters status from context. Implement filter renderer in FilterField. --- .../lib/components/CheckboxList.tsx | 39 ++-- .../lib/components/FilterContext.tsx | 28 ++- .../lib/components/FilterField.tsx | 177 ++++++++++++++++-- .../lib/components/FilterGroup.tsx | 8 +- .../{StrudelSlider.tsx => RangeSlider.tsx} | 8 +- strudel-components/src/App.tsx | 51 ++++- .../src/examples/Filters/FiltersExGroups.tsx | 2 +- .../examples/Filters/FiltersExNoGroups.tsx | 2 +- 8 files changed, 259 insertions(+), 56 deletions(-) rename strudel-components/lib/components/{StrudelSlider.tsx => RangeSlider.tsx} (77%) diff --git a/strudel-components/lib/components/CheckboxList.tsx b/strudel-components/lib/components/CheckboxList.tsx index 5e585f5c..6eabe9ac 100644 --- a/strudel-components/lib/components/CheckboxList.tsx +++ b/strudel-components/lib/components/CheckboxList.tsx @@ -9,6 +9,7 @@ export interface CheckboxOption { } interface CheckboxListProps extends Omit { + values: string[] | number[] | null; options: CheckboxOption[]; onChange?: (values: CheckboxOptionValue[] | null) => any; } @@ -16,37 +17,51 @@ interface CheckboxListProps extends Omit { export const CheckboxList: React.FC = ({ options = [], onChange, + values, + sx, ...rest }) => { - const [values, setValues] = useState(null); + const [checkValues, setCheckValues] = useState(values); const handleChange = (checked: boolean, value: CheckboxOption['value']) => { - if (values === null && checked) { - setValues([value]); - } else if (values !== null && checked) { - setValues([...values, value]); - } else if (values !== null && !checked) { - const newValues = values.filter((v) => v !== value); + if (checkValues === null && checked) { + setCheckValues([value]); + } else if (checkValues !== null && checked) { + setCheckValues([...checkValues, value]); + } else if (checkValues !== null && !checked) { + const newValues = checkValues.filter((v) => v !== value); if (newValues.length > 0) { - setValues(newValues); + setCheckValues(newValues); } else { - setValues(null); + setCheckValues(null); } } }; useEffect(() => { - if (onChange) onChange(values); + if (onChange) onChange(checkValues); + }, [checkValues]); + + useEffect(() => { + console.log(values) + setCheckValues(values); }, [values]); return ( - + {options.map((option, i) => ( -1} value={option.value} onChange={(e, checked) => handleChange(checked, option.value)} sx={{ diff --git a/strudel-components/lib/components/FilterContext.tsx b/strudel-components/lib/components/FilterContext.tsx index 8f03c3fa..a69cce08 100644 --- a/strudel-components/lib/components/FilterContext.tsx +++ b/strudel-components/lib/components/FilterContext.tsx @@ -1,39 +1,37 @@ import React, { PropsWithChildren, useContext, useEffect } from 'react'; export interface FilterState { + activeFilters: { [key: string]: any } expandedGroup: string | number | boolean; } const FilterContextAPI = React.createContext<{state: FilterState; dispatch: React.Dispatch} | undefined>(undefined); const initialState: FilterState = { + activeFilters: {}, expandedGroup: false, } -export enum FilterActionType { - SET_EXPANDED_GROUP = 'SET_EXPANDED_GROUP', -} - -export interface FilterAction { - type: FilterActionType; - payload?: any; -} - -export const setExpandedGroup = (expandedGroup: FilterState['expandedGroup']): FilterAction => ({ - type: FilterActionType.SET_EXPANDED_GROUP, - payload: expandedGroup, -}); +export type FilterAction = + | { type: 'SET_FILTER', payload: { field: string, value: any } } + | { type: 'SET_EXPANDED_GROUP', payload: FilterState['expandedGroup']; } function filterReducer(state: FilterState, action: FilterAction): FilterState { switch (action.type) { - case FilterActionType.SET_EXPANDED_GROUP: { + case 'SET_FILTER': { + return { + ...state, + activeFilters: { ...state.activeFilters, [action.payload.field]: action.payload.value } + } + } + case 'SET_EXPANDED_GROUP': { return { ...state, expandedGroup: action.payload } } default: { - throw new Error(`Unhandled action type: ${action.type}`) + throw new Error(`Unhandled action type`) } } } diff --git a/strudel-components/lib/components/FilterField.tsx b/strudel-components/lib/components/FilterField.tsx index a4cd2a33..f08e6b18 100644 --- a/strudel-components/lib/components/FilterField.tsx +++ b/strudel-components/lib/components/FilterField.tsx @@ -1,24 +1,153 @@ -import React, { ReactNode } from 'react'; +import React, { useState } from 'react'; import { Box, Stack, StackProps, Typography } from '@mui/material'; +import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; +import { CheckboxList } from './CheckboxList'; +import { RangeSlider } from './RangeSlider'; +import { DatePicker } from '@mui/x-date-pickers'; +import { useFilters } from './FilterContext'; interface FilterFieldProps extends StackProps { - label: ReactNode; - filter: ReactNode; + label: string; + field: string; + filterComponent: 'RangeSlider' | 'CheckboxList' | 'DateRange'; + filterProps: any; } +/** + * The type of the value should be dependent on the filterComponent + */ +type FilterValue = + T extends 'RangeSlider' ? number[] : + T extends 'CheckboxList' ? string[] | number[] | null : + T extends 'DateRange' ? [Date | null, Date | null] : + never; + +/** + * Determine if a value is truthy or falsy in the context of a filter. + * Values like 0 and false should be considered as having a value, + * and having an empty array should be considered not having a value. + */ +const hasValue = (value: any) => { + if (value === 0 || value === false) { + return true; + } else if (Array.isArray(value) && value.length === 0) { + return false; + } else { + return !!value; + } +} + +/** + * + */ export const FilterField: React.FC = ({ label, - filter, - color, + field, + filterComponent, + filterProps, ...rest }) => { - const defaultLabel = ( - - {label} - - ); - const labelComponent = typeof label === 'string' ? defaultLabel : label; - + const { state, dispatch } = useFilters(); + const [value, setValue] = useState>(null); + const isActive = hasValue(state.activeFilters[field]); + + /** + * When a filter is canceled, reset its value to the proper + * empty or base state depending on the filter type. + * In the activeFilters variable, empty filters will always be marked as null. + */ + const handleCancelFilter = () => { + switch (filterComponent) { + case 'CheckboxList': + setValue([]); + break; + case 'RangeSlider': + setValue([filterProps.min, filterProps.max]); + break; + case 'DateRange': + setValue([filterProps.min, filterProps.max]); + break; + default: + console.log('Unknown filter type'); + } + + dispatch({ type: 'SET_FILTER', payload: { field: field, value: null } }); + } + + /** + * Render filter component based on the `filterComponent` prop. + */ + const getFilterComponent = ( + field: FilterFieldProps['field'], + filterComponent: FilterFieldProps['filterComponent'], + filterProps: FilterFieldProps['filterProps'], + ) => { + switch (filterComponent) { + case 'CheckboxList': { + return ( + dispatch({ type: 'SET_FILTER', payload: { field: field, value: values } })} + /> + ); + } + case 'RangeSlider': { + const handleSliderChange = (event: Event | React.SyntheticEvent, values: number | number[]) => { + if (!Array.isArray(values)) { + return; + } + let newValues: number[] | null = [...values]; + /** Set to null if both ends of slider are at min/max */ + if (values[0] === filterProps.min && values[1] === filterProps.max) { + newValues = null + } + dispatch({ type: 'SET_FILTER', payload: { field: field, value: newValues } }) + }; + + return ( + field} + valueLabelDisplay="auto" + min={filterProps.min} + max={filterProps.max} + value={value || [filterProps.min, filterProps.max]} + onChange={(e, value) => setValue(value as number[])} + onChangeCommitted={handleSliderChange} + /> + ); + } + case 'DateRange': { + const currentDateRange = state.activeFilters[field]; + const hasValue = currentDateRange && Array.isArray(currentDateRange) && currentDateRange.length === 2; + const currentMin = hasValue && Array.isArray(currentDateRange) ? currentDateRange[0] : null; + const currentMax = hasValue && Array.isArray(currentDateRange) ? currentDateRange[1] : null; + + return ( + + dispatch({ type: 'SET_FILTER', payload: { field: field, value: [value, currentMax] } })} + /> + dispatch({ type: 'SET_FILTER', payload: { field: field, value: [currentMin, value] } })} + /> + + ); + } + } + } return ( = ({ }} {...rest} > - {labelComponent} + + handleCancelFilter()} + sx={{ + cursor: 'pointer', + display: 'inline-flex', + }} + > + + {label} + + {isActive && ( + + )} + + - {filter} + {getFilterComponent(field, filterComponent, filterProps)} ) diff --git a/strudel-components/lib/components/FilterGroup.tsx b/strudel-components/lib/components/FilterGroup.tsx index c5853227..b5205088 100644 --- a/strudel-components/lib/components/FilterGroup.tsx +++ b/strudel-components/lib/components/FilterGroup.tsx @@ -1,7 +1,7 @@ import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'; import { Accordion, AccordionDetails, AccordionSummary, Stack, Typography } from '@mui/material'; import React, { useState } from 'react'; -import { setExpandedGroup, useFilters } from './FilterContext'; +import { useFilters } from './FilterContext'; interface FilterGroupProps { label?: React.ReactNode; @@ -17,9 +17,11 @@ export const FilterGroup: React.FC = ({ const { state, dispatch } = useFilters(); const handleChange = (panel: string | number) => (event: React.SyntheticEvent, newExpanded: boolean) => { - dispatch(setExpandedGroup(newExpanded ? panel : false)); + dispatch({ type: 'SET_EXPANDED_GROUP', payload: newExpanded ? panel : false }); }; + console.log(state); + return ( = ({ sx={{ borderTop: '1px solid', borderTopColor: 'grey.300', - '&:first-child': { + '&:first-of-type': { borderTop: 'none', }, '&::before': { diff --git a/strudel-components/lib/components/StrudelSlider.tsx b/strudel-components/lib/components/RangeSlider.tsx similarity index 77% rename from strudel-components/lib/components/StrudelSlider.tsx rename to strudel-components/lib/components/RangeSlider.tsx index e7587584..e243e25b 100644 --- a/strudel-components/lib/components/StrudelSlider.tsx +++ b/strudel-components/lib/components/RangeSlider.tsx @@ -1,15 +1,15 @@ import React, { useState } from 'react'; import { Box, Slider, SliderProps } from '@mui/material'; -interface StrudelSliderProps extends SliderProps { +interface RangeSliderProps extends SliderProps { } /** - * Custom wrapper for the MUI Slider component. - * Enables advanced features such as value debounce. + * Custom wrapper for the MUI Slider components where + * the user is selecting a range of values. */ -export const StrudelSlider: React.FC = ({ +export const RangeSlider: React.FC = ({ min = 0, max = 100, ...rest diff --git a/strudel-components/src/App.tsx b/strudel-components/src/App.tsx index 8f270b71..65b1bf12 100644 --- a/strudel-components/src/App.tsx +++ b/strudel-components/src/App.tsx @@ -3,7 +3,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import { Filters } from '../lib/components/Filters' import { Box, CssBaseline, Stack, ThemeProvider, Typography, createTheme } from '@mui/material' import { CheckboxList } from '../lib/components/CheckboxList' -import { StrudelSlider } from '../lib/components/StrudelSlider' +import { RangeSlider } from '../lib/components/RangeSlider' import { FilterGroup } from '../lib/components/FilterGroup' import { FilterField } from '../lib/components/FilterField' import { FilterContext } from '../lib/components/FilterContext' @@ -23,6 +23,45 @@ function App() { > + + + {/* null} /> } - /> + /> */} - + {/* } /> - + */} - } /> - + */} diff --git a/strudel-components/src/examples/Filters/FiltersExGroups.tsx b/strudel-components/src/examples/Filters/FiltersExGroups.tsx index 7025a9ea..bd2b3275 100644 --- a/strudel-components/src/examples/Filters/FiltersExGroups.tsx +++ b/strudel-components/src/examples/Filters/FiltersExGroups.tsx @@ -2,7 +2,7 @@ import { Typography } from "@mui/material" import { FilterField } from "../../../lib/components/FilterField" import { FilterGroup } from "../../../lib/components/FilterGroup" import { Filters } from "../../../lib/components/Filters" -import { StrudelSlider } from "../../../lib/components/StrudelSlider" +import { StrudelSlider } from "../../../lib/components/RangeSlider" import { CheckboxList } from "../../../lib/components/CheckboxList" export const FiltersExGroups: React.FC = () => { diff --git a/strudel-components/src/examples/Filters/FiltersExNoGroups.tsx b/strudel-components/src/examples/Filters/FiltersExNoGroups.tsx index b1a0145e..539ba71a 100644 --- a/strudel-components/src/examples/Filters/FiltersExNoGroups.tsx +++ b/strudel-components/src/examples/Filters/FiltersExNoGroups.tsx @@ -1,7 +1,7 @@ import { CheckboxList } from "../../../lib/components/CheckboxList" import { FilterField } from "../../../lib/components/FilterField" import { Filters } from "../../../lib/components/Filters" -import { StrudelSlider } from "../../../lib/components/StrudelSlider" +import { StrudelSlider } from "../../../lib/components/RangeSlider" export const FiltersExNoGroups: React.FC = () => { return ( From 99fc867e65d783050ee47f6d68be0d52d09afc92 Mon Sep 17 00:00:00 2001 From: Cody O'Donnell Date: Mon, 22 Jul 2024 11:36:31 -0700 Subject: [PATCH 07/13] Implement active filter chip for filter groups --- .../lib/components/FilterField.tsx | 2 +- .../lib/components/FilterGroup.tsx | 34 ++++++++- strudel-components/src/App.tsx | 75 +++++++++---------- 3 files changed, 67 insertions(+), 44 deletions(-) diff --git a/strudel-components/lib/components/FilterField.tsx b/strudel-components/lib/components/FilterField.tsx index f08e6b18..e91252c6 100644 --- a/strudel-components/lib/components/FilterField.tsx +++ b/strudel-components/lib/components/FilterField.tsx @@ -27,7 +27,7 @@ type FilterValue = * Values like 0 and false should be considered as having a value, * and having an empty array should be considered not having a value. */ -const hasValue = (value: any) => { +export const hasValue = (value: any) => { if (value === 0 || value === false) { return true; } else if (Array.isArray(value) && value.length === 0) { diff --git a/strudel-components/lib/components/FilterGroup.tsx b/strudel-components/lib/components/FilterGroup.tsx index b5205088..0793eee0 100644 --- a/strudel-components/lib/components/FilterGroup.tsx +++ b/strudel-components/lib/components/FilterGroup.tsx @@ -1,7 +1,8 @@ import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'; -import { Accordion, AccordionDetails, AccordionSummary, Stack, Typography } from '@mui/material'; +import { Accordion, AccordionDetails, AccordionSummary, Chip, Stack, Typography } from '@mui/material'; import React, { useState } from 'react'; import { useFilters } from './FilterContext'; +import { hasValue } from './FilterField'; interface FilterGroupProps { label?: React.ReactNode; @@ -15,12 +16,28 @@ export const FilterGroup: React.FC = ({ children }) => { const { state, dispatch } = useFilters(); - + + /** + * Count the number of active filters in this group by using + * the `field` prop from the FilterField children to look up + * that filter in `activeFilters` + */ + let activeChildren = 0; + React.Children.forEach(children, (child) => { + if ( + React.isValidElement(child) && + child.props.field && + hasValue(state.activeFilters[child.props.field]) + ) { + return activeChildren++ + } + }) + const handleChange = (panel: string | number) => (event: React.SyntheticEvent, newExpanded: boolean) => { dispatch({ type: 'SET_EXPANDED_GROUP', payload: newExpanded ? panel : false }); }; - console.log(state); + console.log(children); return ( = ({ }, }} > - {label} + + {label} + {activeChildren > 0 && ( + + )} + diff --git a/strudel-components/src/App.tsx b/strudel-components/src/App.tsx index 65b1bf12..ea562647 100644 --- a/strudel-components/src/App.tsx +++ b/strudel-components/src/App.tsx @@ -61,50 +61,47 @@ function App() { ] }} /> - {/* + + null} - /> - } + field="field4" + filterComponent="RangeSlider" + filterProps={{ + min: 0, + max: 100 + }} /> null} - /> - } + field="field5" + filterComponent="RangeSlider" + filterProps={{ + min: 100, + max: 400 + }} + /> + - null} - /> - } - /> */} {/* Date: Mon, 22 Jul 2024 14:34:57 -0700 Subject: [PATCH 08/13] Move context inside of Filters component. Expose activeFilters and change event. --- .../lib/components/CheckboxList.tsx | 1 - .../lib/components/FilterContext.tsx | 37 ++- .../lib/components/FilterField.tsx | 20 +- .../lib/components/FilterGroup.tsx | 2 - strudel-components/lib/components/Filters.tsx | 95 +++--- strudel-components/src/App.tsx | 278 +++++++----------- 6 files changed, 210 insertions(+), 223 deletions(-) diff --git a/strudel-components/lib/components/CheckboxList.tsx b/strudel-components/lib/components/CheckboxList.tsx index 6eabe9ac..10b4ccfc 100644 --- a/strudel-components/lib/components/CheckboxList.tsx +++ b/strudel-components/lib/components/CheckboxList.tsx @@ -43,7 +43,6 @@ export const CheckboxList: React.FC = ({ }, [checkValues]); useEffect(() => { - console.log(values) setCheckValues(values); }, [values]); diff --git a/strudel-components/lib/components/FilterContext.tsx b/strudel-components/lib/components/FilterContext.tsx index a69cce08..86d2e7ae 100644 --- a/strudel-components/lib/components/FilterContext.tsx +++ b/strudel-components/lib/components/FilterContext.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren, useContext, useEffect } from 'react'; +import React, { PropsWithChildren, useContext, useEffect, useReducer } from 'react'; export interface FilterState { activeFilters: { [key: string]: any } @@ -14,6 +14,7 @@ const initialState: FilterState = { export type FilterAction = | { type: 'SET_FILTER', payload: { field: string, value: any } } + | { type: 'SET_ACTIVE_FILTERS', payload:FilterState['activeFilters'] } | { type: 'SET_EXPANDED_GROUP', payload: FilterState['expandedGroup']; } function filterReducer(state: FilterState, action: FilterAction): FilterState { @@ -24,6 +25,12 @@ function filterReducer(state: FilterState, action: FilterAction): FilterState { activeFilters: { ...state.activeFilters, [action.payload.field]: action.payload.value } } } + case 'SET_ACTIVE_FILTERS': { + return { + ...state, + activeFilters: action.payload + } + } case 'SET_EXPANDED_GROUP': { return { ...state, @@ -36,10 +43,34 @@ function filterReducer(state: FilterState, action: FilterAction): FilterState { } } -export const FilterContext: React.FC = ({ children }) => { - const [state, dispatch] = React.useReducer(filterReducer, initialState); +interface FilterContextProps extends PropsWithChildren { + activeFilters?: FilterState['activeFilters']; + onChange?: (filters: FilterState['activeFilters']) => void; +} + +export const FilterContext: React.FC = ({ + activeFilters = {}, + onChange = (filters) => null, + children +}) => { + const [state, dispatch] = useReducer(filterReducer, { ...initialState, activeFilters }); const value = { state, dispatch }; + /** + * Emit a change event when state.activeFilters changes + */ + useEffect(() => { + if (onChange) onChange(state.activeFilters); + }, [state.activeFilters]); + + /** + * If activeFilters is changed from outside the context (e.g. filters are reset) + * then the new value should be dispatched. + */ + useEffect(() => { + dispatch({ type: 'SET_ACTIVE_FILTERS', payload: activeFilters }); + }, [activeFilters]); + return ( {children} diff --git a/strudel-components/lib/components/FilterField.tsx b/strudel-components/lib/components/FilterField.tsx index e91252c6..9cf5d7ba 100644 --- a/strudel-components/lib/components/FilterField.tsx +++ b/strudel-components/lib/components/FilterField.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Box, Stack, StackProps, Typography } from '@mui/material'; import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; import { CheckboxList } from './CheckboxList'; @@ -148,6 +148,24 @@ export const FilterField: React.FC = ({ } } } + + /** + * When activeFilters changes, make sure the value changes accordingly. + * This primarily happens when filters are reset from the top. + */ + useEffect(() => { + if (isActive) { + setValue(state.activeFilters[field]); + } else if (filterComponent === 'RangeSlider') { + /** RangeSliders should be considered off if both values are min and max */ + if (value && (value[0] !== filterProps.min || value[1] !== filterProps.max)) { + handleCancelFilter(); + } + } else if (hasValue(value)) { + handleCancelFilter(); + } + },[state.activeFilters]); + return ( = ({ const handleChange = (panel: string | number) => (event: React.SyntheticEvent, newExpanded: boolean) => { dispatch({ type: 'SET_EXPANDED_GROUP', payload: newExpanded ? panel : false }); }; - - console.log(children); return ( void; onClose?: () => any; config?: any; header?: React.ReactNode; @@ -13,55 +15,64 @@ interface FilterPanelProps extends PaperProps { export const Filters: React.FC = ({ header = 'Filters', grouped = true, + onChange, onClose, config, children, ...rest }) => { + const [activeFilters, setActiveFilters] = useState({}); + + const handleReset = () => { + setActiveFilters({}); + } + return ( - - - {header && ( - - {header} - - - - )} - {grouped && ( - - {children} - - )} - {!grouped && ( - - {children} - - )} - - + {header} + + + + )} + {grouped && ( + + {children} + + )} + {!grouped && ( + + {children} + + )} + + + ) } \ No newline at end of file diff --git a/strudel-components/src/App.tsx b/strudel-components/src/App.tsx index ea562647..693ba368 100644 --- a/strudel-components/src/App.tsx +++ b/strudel-components/src/App.tsx @@ -6,203 +6,133 @@ import { CheckboxList } from '../lib/components/CheckboxList' import { RangeSlider } from '../lib/components/RangeSlider' import { FilterGroup } from '../lib/components/FilterGroup' import { FilterField } from '../lib/components/FilterField' -import { FilterContext } from '../lib/components/FilterContext' +import { FilterContext, FilterState } from '../lib/components/FilterContext' function App() { + const handleFilterChange = (filters: FilterState['activeFilters']) => { + console.log(filters); + } return ( - - My Filters} - sx={{ - maxWidth: '400px' - }} - > - - - - - - - - - - - {/* - null} - /> - } - /> - null} - /> - } - /> - */} - - - {/* + null} - /> - } + field="field1" + filterComponent="RangeSlider" + filterProps={{ + min: 0, + max: 100 + }} /> null} - /> - } + field="field2" + filterComponent="RangeSlider" + filterProps={{ + min: 100, + max: 400 + }} /> - null} - /> - } - /> + + + null} - /> - } + field="field4" + filterComponent="RangeSlider" + filterProps={{ + min: 0, + max: 100 + }} + /> + + + + + + null} - /> - } + field="field2" + filterComponent="RangeSlider" + filterProps={{ + min: 100, + max: 400 + }} /> - */} + From 2de7af26289d69c30f36f1f1ea877a65fb6012fa Mon Sep 17 00:00:00 2001 From: Cody O'Donnell Date: Mon, 22 Jul 2024 16:05:37 -0700 Subject: [PATCH 09/13] Add tooltip prop to FilterField --- .../lib/components/FilterField.tsx | 37 +++++++++++++++---- strudel-components/src/App.tsx | 1 + 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/strudel-components/lib/components/FilterField.tsx b/strudel-components/lib/components/FilterField.tsx index 9cf5d7ba..71d45bd2 100644 --- a/strudel-components/lib/components/FilterField.tsx +++ b/strudel-components/lib/components/FilterField.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Box, Stack, StackProps, Typography } from '@mui/material'; +import { Box, Stack, StackProps, Tooltip, Typography } from '@mui/material'; import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; import { CheckboxList } from './CheckboxList'; import { RangeSlider } from './RangeSlider'; @@ -9,6 +9,7 @@ import { useFilters } from './FilterContext'; interface FilterFieldProps extends StackProps { label: string; field: string; + tooltip?: string; filterComponent: 'RangeSlider' | 'CheckboxList' | 'DateRange'; filterProps: any; } @@ -43,6 +44,7 @@ export const hasValue = (value: any) => { export const FilterField: React.FC = ({ label, field, + tooltip, filterComponent, filterProps, ...rest @@ -89,6 +91,7 @@ export const FilterField: React.FC = ({ values={value as string[] | number[] | null} options={filterProps.options} onChange={(values) => dispatch({ type: 'SET_FILTER', payload: { field: field, value: values } })} + {...filterProps} /> ); } @@ -114,6 +117,7 @@ export const FilterField: React.FC = ({ value={value || [filterProps.min, filterProps.max]} onChange={(e, value) => setValue(value as number[])} onChangeCommitted={handleSliderChange} + {...filterProps} /> ); } @@ -186,16 +190,33 @@ export const FilterField: React.FC = ({ spacing={1} onClick={() => handleCancelFilter()} sx={{ - cursor: 'pointer', + cursor: isActive ? 'pointer' : 'default', display: 'inline-flex', }} > - - {label} - + {tooltip && ( + + + {label} + + + )} + {!tooltip && ( + + {label} + + )} {isActive && ( )} diff --git a/strudel-components/src/App.tsx b/strudel-components/src/App.tsx index 693ba368..87a7506e 100644 --- a/strudel-components/src/App.tsx +++ b/strudel-components/src/App.tsx @@ -117,6 +117,7 @@ function App() { Date: Mon, 22 Jul 2024 16:41:34 -0700 Subject: [PATCH 10/13] Add support for TextField filter --- .../lib/components/FilterField.tsx | 35 +++- strudel-components/src/App.tsx | 7 + .../src/examples/Filters/FiltersExGroups.tsx | 134 ++++++++------- .../examples/Filters/FiltersExNoGroups.tsx | 154 ++++++++++-------- 4 files changed, 196 insertions(+), 134 deletions(-) diff --git a/strudel-components/lib/components/FilterField.tsx b/strudel-components/lib/components/FilterField.tsx index 71d45bd2..42a9035f 100644 --- a/strudel-components/lib/components/FilterField.tsx +++ b/strudel-components/lib/components/FilterField.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Box, Stack, StackProps, Tooltip, Typography } from '@mui/material'; +import { Box, Stack, StackProps, TextField, Tooltip, Typography } from '@mui/material'; import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; import { CheckboxList } from './CheckboxList'; import { RangeSlider } from './RangeSlider'; @@ -10,8 +10,8 @@ interface FilterFieldProps extends StackProps { label: string; field: string; tooltip?: string; - filterComponent: 'RangeSlider' | 'CheckboxList' | 'DateRange'; - filterProps: any; + filterComponent: 'RangeSlider' | 'CheckboxList' | 'DateRange' | 'TextField'; + filterProps?: any; } /** @@ -20,7 +20,8 @@ interface FilterFieldProps extends StackProps { type FilterValue = T extends 'RangeSlider' ? number[] : T extends 'CheckboxList' ? string[] | number[] | null : - T extends 'DateRange' ? [Date | null, Date | null] : + T extends 'DateRange' ? [Date | null, Date | null] : + T extends 'TextField' ? string | null : never; /** @@ -69,6 +70,9 @@ export const FilterField: React.FC = ({ case 'DateRange': setValue([filterProps.min, filterProps.max]); break; + case 'TextField': + setValue(null); + break; default: console.log('Unknown filter type'); } @@ -150,6 +154,29 @@ export const FilterField: React.FC = ({ ); } + case 'TextField': { + + /** + * Debounce the dispatch so that activeFilters isn't rapidly updated. + */ + useEffect(() => { + const timeout = setTimeout(() => { + dispatch({ type: 'SET_FILTER', payload: { field: field, value: value } }) + }, 1000); + return () => { + clearTimeout(timeout); + } + }, [value]); + + return ( + setValue(e.target.value)} + fullWidth + {...filterProps} + /> + ); + } } } diff --git a/strudel-components/src/App.tsx b/strudel-components/src/App.tsx index 87a7506e..403795d8 100644 --- a/strudel-components/src/App.tsx +++ b/strudel-components/src/App.tsx @@ -109,11 +109,18 @@ function App() { + { +export const FiltersExGroups: React.FC = () => { + const handleFilterChange = (filters: FilterState['activeFilters']) => { + console.log(filters); + } return ( My Filters} + header="My Filters" + onChange={handleFilterChange} sx={{ maxWidth: '400px' }} > - + null} - /> - } + field="field1" + filterComponent="RangeSlider" + filterProps={{ + min: 0, + max: 100 + }} /> null} - /> - } + field="field2" + filterComponent="RangeSlider" + filterProps={{ + min: 100, + max: 400 + }} /> - null} - /> - } + - + null} - /> - } + field="field4" + filterComponent="RangeSlider" + filterProps={{ + min: 0, + max: 100 + }} /> null} - /> - } + field="field5" + filterComponent="RangeSlider" + filterProps={{ + min: 100, + max: 400 + }} + /> + diff --git a/strudel-components/src/examples/Filters/FiltersExNoGroups.tsx b/strudel-components/src/examples/Filters/FiltersExNoGroups.tsx index 539ba71a..6e1505eb 100644 --- a/strudel-components/src/examples/Filters/FiltersExNoGroups.tsx +++ b/strudel-components/src/examples/Filters/FiltersExNoGroups.tsx @@ -1,83 +1,97 @@ -import { CheckboxList } from "../../../lib/components/CheckboxList" +import { FilterState } from "../../../lib/components/FilterContext" import { FilterField } from "../../../lib/components/FilterField" import { Filters } from "../../../lib/components/Filters" -import { StrudelSlider } from "../../../lib/components/RangeSlider" -export const FiltersExNoGroups: React.FC = () => { +export const FiltersExGroups: React.FC = () => { + const handleFilterChange = (filters: FilterState['activeFilters']) => { + console.log(filters); + } return ( - null} - /> - } - /> - null} - /> - } - /> - null} - /> + + + - null} - /> - } - /> - null} - /> - } - /> + ] + }} + /> + + + ) } \ No newline at end of file From ff31025edf6ff32f9546c9906925269267bcf5d3 Mon Sep 17 00:00:00 2001 From: Cody O'Donnell Date: Thu, 1 Aug 2024 16:59:40 -0700 Subject: [PATCH 11/13] Add active label when no filter groups --- strudel-components/lib/components/Filters.tsx | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/strudel-components/lib/components/Filters.tsx b/strudel-components/lib/components/Filters.tsx index d34c6eed..950fba4e 100644 --- a/strudel-components/lib/components/Filters.tsx +++ b/strudel-components/lib/components/Filters.tsx @@ -1,7 +1,8 @@ import CloseIcon from '@mui/icons-material/Close'; -import { Box, Button, IconButton, Paper, PaperProps, Stack } from '@mui/material'; -import React, { useState } from 'react'; +import { Box, Button, Chip, IconButton, Paper, PaperProps, Stack } from '@mui/material'; +import React, { useEffect, useState } from 'react'; import { FilterContext, FilterState } from './FilterContext'; +import { hasValue } from './FilterField'; interface FilterPanelProps extends PaperProps { @@ -21,14 +22,38 @@ export const Filters: React.FC = ({ children, ...rest }) => { - const [activeFilters, setActiveFilters] = useState({}); + const [activeFilters, setActiveFilters] = useState({}); + + /** + * Count the number of active filters in this group by using + * the `field` prop from the FilterField children to look up + * that filter in `activeFilters` + */ + let activeChildren = 0; + React.Children.forEach(children, (child) => { + if ( + React.isValidElement(child) && + child.props.field && + hasValue(activeFilters[child.props.field]) + ) { + return activeChildren++ + } + }) + + const handleChange = (filters: FilterState['activeFilters']) => { + setActiveFilters(filters); + } const handleReset = () => { setActiveFilters({}); } + useEffect(() => { + if (onChange) onChange(activeFilters); + }, [activeFilters]) + return ( - + = ({ paddingBottom: 1, }} > - {header} + + {header} + {activeChildren > 0 && ( + + )} +