-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[docs] Add recipe for save and manage filters from the panel #19361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+730
−0
Merged
Changes from 2 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
a7e72a5
add filter panel recipe
siriwatknp 8327e04
update title
siriwatknp 76419e0
add filter panel recipe
siriwatknp 59a9f7d
update title
siriwatknp 08a8faf
apply suggestion
siriwatknp 5aa6a57
Merge branch 'docs/filter-recipe' of https://github.com/siriwatknp/ma…
siriwatknp e472c89
Merge branch 'master' of https://github.com/mui-org/material-ui-x int…
siriwatknp d543a4d
trigger ci
siriwatknp 6b6ba71
Apply suggestions from code review
siriwatknp bf09b65
Merge branch 'master' into docs/filter-recipe
siriwatknp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
352 changes: 352 additions & 0 deletions
352
docs/data/data-grid/filtering-recipes/FilterPresetsPanel.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,352 @@ | ||
| import * as React from 'react'; | ||
| import { | ||
| DataGridPro, | ||
| GridFilterPanel, | ||
| useGridApiContext, | ||
| } from '@mui/x-data-grid-pro'; | ||
| import { useDemoData } from '@mui/x-data-grid-generator'; | ||
| import Box from '@mui/material/Box'; | ||
| import Button from '@mui/material/Button'; | ||
| import TextField from '@mui/material/TextField'; | ||
| import IconButton from '@mui/material/IconButton'; | ||
| import Dialog from '@mui/material/Dialog'; | ||
| import DialogTitle from '@mui/material/DialogTitle'; | ||
| import DialogContent from '@mui/material/DialogContent'; | ||
| import DialogActions from '@mui/material/DialogActions'; | ||
| import List from '@mui/material/List'; | ||
| import ListItem from '@mui/material/ListItem'; | ||
| import ListItemButton from '@mui/material/ListItemButton'; | ||
| import ListItemText from '@mui/material/ListItemText'; | ||
| import Typography from '@mui/material/Typography'; | ||
| import Alert from '@mui/material/Alert'; | ||
| import Stack from '@mui/material/Stack'; | ||
| import Tooltip from '@mui/material/Tooltip'; | ||
| import Divider from '@mui/material/Divider'; | ||
| import SaveIcon from '@mui/icons-material/Save'; | ||
| import DeleteIcon from '@mui/icons-material/Delete'; | ||
| import AddIcon from '@mui/icons-material/Add'; | ||
|
|
||
| const STORAGE_KEY = 'dataGridFilterPresets'; | ||
| const EMPTY_STORE = { presets: [], activePresetId: null }; | ||
|
|
||
| const createPresetsStore = () => { | ||
| let listeners = []; | ||
|
|
||
| return { | ||
| subscribe: (callback) => { | ||
| listeners.push(callback); | ||
| return () => { | ||
| listeners = listeners.filter((listener) => listener !== callback); | ||
| }; | ||
| }, | ||
| getSnapshot: () => { | ||
| try { | ||
| const saved = localStorage.getItem(STORAGE_KEY); | ||
| return saved || JSON.stringify(EMPTY_STORE); | ||
| } catch { | ||
| return JSON.stringify(EMPTY_STORE); | ||
| } | ||
| }, | ||
| getServerSnapshot: () => { | ||
| return JSON.stringify(EMPTY_STORE); | ||
| }, | ||
| update: (store) => { | ||
| try { | ||
| localStorage.setItem(STORAGE_KEY, JSON.stringify(store)); | ||
| } catch { | ||
| // Silently fail if localStorage is not available | ||
| } | ||
| listeners.forEach((listener) => listener()); | ||
| }, | ||
| }; | ||
| }; | ||
|
|
||
| const usePersistedPresets = () => { | ||
| const [presetsStore] = React.useState(createPresetsStore); | ||
|
|
||
| const storeString = React.useSyncExternalStore( | ||
| presetsStore.subscribe, | ||
| presetsStore.getSnapshot, | ||
| presetsStore.getServerSnapshot, | ||
| ); | ||
|
|
||
| const store = React.useMemo(() => { | ||
| try { | ||
| return JSON.parse(storeString); | ||
| } catch { | ||
| return EMPTY_STORE; | ||
| } | ||
| }, [storeString]); | ||
|
|
||
| return React.useMemo( | ||
| () => [store, presetsStore.update], | ||
| [store, presetsStore.update], | ||
| ); | ||
| }; | ||
|
|
||
| function CustomFilterPanel(props) { | ||
| const apiRef = useGridApiContext(); | ||
| const [store, setStore] = usePersistedPresets(); | ||
| const [createFilterDialogOpen, setCreateFilterDialogOpen] = React.useState(false); | ||
| const [createFilterName, setCreateFilterName] = React.useState(''); | ||
|
|
||
| const getCurrentFilterModel = () => apiRef.current.state.filter.filterModel; | ||
| const hasActiveFilters = () => getCurrentFilterModel().items.length > 0; | ||
|
|
||
| const handleSavePreset = () => { | ||
| if (!store.activePresetId) { | ||
| return; | ||
| } | ||
|
|
||
| const currentFilterModel = getCurrentFilterModel(); | ||
|
|
||
| setStore({ | ||
| ...store, | ||
| presets: store.presets.map((p) => | ||
| p.id === store.activePresetId | ||
| ? { | ||
| ...p, | ||
| filterModel: currentFilterModel, | ||
| } | ||
| : p, | ||
| ), | ||
| }); | ||
| }; | ||
|
|
||
| const handleCreateFilter = () => { | ||
| if (!createFilterName.trim()) { | ||
| return; | ||
| } | ||
|
|
||
| const newPreset = { | ||
| id: `preset_${Date.now()}`, | ||
| name: createFilterName.trim(), | ||
| filterModel: getCurrentFilterModel(), | ||
| createdAt: new Date().toISOString(), | ||
| }; | ||
|
|
||
| setStore({ | ||
| ...store, | ||
| presets: [...store.presets, newPreset], | ||
| activePresetId: newPreset.id, | ||
| }); | ||
|
|
||
| setCreateFilterDialogOpen(false); | ||
| setCreateFilterName(''); | ||
| }; | ||
|
|
||
| const handleLoadPreset = (presetId) => { | ||
| const preset = store.presets.find((p) => p.id === presetId); | ||
| if (preset) { | ||
| apiRef.current.setFilterModel(preset.filterModel); | ||
|
|
||
| setStore({ | ||
| ...store, | ||
| activePresetId: presetId, | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| const handleDeletePreset = (presetId) => { | ||
| setStore({ | ||
| ...store, | ||
| presets: store.presets.filter((p) => p.id !== presetId), | ||
| activePresetId: | ||
| store.activePresetId === presetId ? null : store.activePresetId, | ||
| }); | ||
| }; | ||
|
|
||
| return ( | ||
| <React.Fragment> | ||
| <Box | ||
| sx={{ | ||
| minWidth: 256, | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| 'div:has(&)': { flexWrap: 'wrap' }, | ||
| }} | ||
| > | ||
| <Box | ||
| sx={{ | ||
| p: 2, | ||
| pb: 1, | ||
| display: 'flex', | ||
| justifyContent: 'space-between', | ||
| alignItems: 'center', | ||
| }} | ||
| > | ||
| <Typography variant="subtitle2" sx={{ fontWeight: 'bold' }}> | ||
| Saved Filters | ||
| </Typography> | ||
| <Stack direction="row" spacing={0.5}> | ||
| <Tooltip title="Create new filter"> | ||
| <IconButton | ||
| onClick={() => setCreateFilterDialogOpen(true)} | ||
| size="small" | ||
| color="primary" | ||
| > | ||
| <AddIcon fontSize="small" /> | ||
| </IconButton> | ||
| </Tooltip> | ||
| <Tooltip title="Update selected filter"> | ||
| <span> | ||
| <IconButton | ||
| onClick={handleSavePreset} | ||
| disabled={!store.activePresetId} | ||
| size="small" | ||
| color="primary" | ||
| > | ||
| <SaveIcon fontSize="small" /> | ||
| </IconButton> | ||
| </span> | ||
| </Tooltip> | ||
| </Stack> | ||
| </Box> | ||
|
|
||
| {store.presets.length === 0 ? ( | ||
| <Box sx={{ p: 2, pt: 0, flexGrow: 1, display: 'flex' }}> | ||
| <Box | ||
| sx={(theme) => ({ | ||
| flex: 1, | ||
| border: '1px solid', | ||
| borderColor: 'divider', | ||
| borderRadius: 1, | ||
| bgcolor: 'grey.100', | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| p: 2, | ||
| color: 'text.secondary', | ||
| ...theme.applyStyles('dark', { | ||
| bgcolor: 'grey.900', | ||
| }), | ||
| })} | ||
| > | ||
| <Typography variant="body2" align="center"> | ||
| No saved filters yet | ||
| </Typography> | ||
| </Box> | ||
| </Box> | ||
| ) : ( | ||
| <List dense sx={{ pt: 0 }}> | ||
| {store.presets.map((preset) => ( | ||
| <ListItem | ||
| key={preset.id} | ||
| disablePadding | ||
| secondaryAction={ | ||
| <IconButton | ||
| edge="end" | ||
| size="small" | ||
| onClick={(event) => { | ||
| event.stopPropagation(); | ||
| handleDeletePreset(preset.id); | ||
| }} | ||
| > | ||
| <DeleteIcon fontSize="small" /> | ||
| </IconButton> | ||
| } | ||
| > | ||
| <ListItemButton | ||
| selected={preset.id === store.activePresetId} | ||
| onClick={() => handleLoadPreset(preset.id)} | ||
| sx={{ | ||
| '&.Mui-selected': { | ||
| bgcolor: 'action.selected', | ||
| position: 'relative', | ||
| '&::after': { | ||
| content: '""', | ||
| position: 'absolute', | ||
| right: 0, | ||
| top: '50%', | ||
| transform: 'translateY(-50%)', | ||
| width: 3, | ||
| height: '60%', | ||
| bgcolor: 'primary.main', | ||
| borderRadius: '3px 0 0 3px', | ||
| }, | ||
| }, | ||
| }} | ||
| > | ||
| <ListItemText | ||
| primary={preset.name} | ||
| secondary={ | ||
| <Typography variant="caption" color="text.secondary"> | ||
| {preset.filterModel.items.length} filter | ||
| {preset.filterModel.items.length > 1 ? 's' : ''} | ||
| </Typography> | ||
| } | ||
| /> | ||
| </ListItemButton> | ||
| </ListItem> | ||
| ))} | ||
| </List> | ||
| )} | ||
| </Box> | ||
|
|
||
| <Divider orientation="vertical" sx={{ height: 'auto' }} /> | ||
| <GridFilterPanel {...props} /> | ||
| <Dialog | ||
| open={createFilterDialogOpen} | ||
| onClose={() => setCreateFilterDialogOpen(false)} | ||
| maxWidth="sm" | ||
| > | ||
| <DialogTitle>Create new filter</DialogTitle> | ||
| <DialogContent> | ||
| <Stack spacing={2} sx={{ mt: 1 }}> | ||
| <TextField | ||
| autoFocus | ||
| label="Filter Name" | ||
| value={createFilterName} | ||
| onChange={(event) => setCreateFilterName(event.target.value)} | ||
| fullWidth | ||
| required | ||
| /> | ||
| <Alert severity="info"> | ||
| {hasActiveFilters() | ||
| ? `This will save the current ${getCurrentFilterModel().items.length} active filter${getCurrentFilterModel().items.length !== 1 ? 's' : ''} as a new preset.` | ||
| : 'This will create an empty filter preset that you can configure later.'} | ||
| </Alert> | ||
| </Stack> | ||
| </DialogContent> | ||
| <DialogActions> | ||
| <Button | ||
| variant="outlined" | ||
| onClick={() => setCreateFilterDialogOpen(false)} | ||
| > | ||
| Cancel | ||
| </Button> | ||
| <Button | ||
| onClick={handleCreateFilter} | ||
| variant="contained" | ||
| disabled={!createFilterName.trim()} | ||
| startIcon={<AddIcon />} | ||
| > | ||
| Create Filter | ||
| </Button> | ||
| </DialogActions> | ||
| </Dialog> | ||
| </React.Fragment> | ||
| ); | ||
| } | ||
|
|
||
| export default function FilterPresetsPanel() { | ||
| const { data, loading } = useDemoData({ | ||
| dataSet: 'Commodity', | ||
| rowLength: 100, | ||
| maxColumns: 10, | ||
| }); | ||
|
|
||
| return ( | ||
| <Box sx={{ height: 400, width: '100%' }}> | ||
| <DataGridPro | ||
| {...data} | ||
| loading={loading} | ||
| slots={{ | ||
| filterPanel: CustomFilterPanel, | ||
| }} | ||
| showToolbar | ||
| /> | ||
| </Box> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.