mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 05:10:45 +00:00
SPRINT-13: local storage of pinned columns, added ability to clear all filters, bug fix, added carat thing to rows per page dropdown
This commit is contained in:
@ -284,7 +284,7 @@ function CustomIsAnyInput(type: "number" | "text", props: GridFilterInputValuePr
|
|||||||
///////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////
|
||||||
// if numeric, check that first before pushing as a chip //
|
// if numeric, check that first before pushing as a chip //
|
||||||
///////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////
|
||||||
if(type === "number" || Number.isNaN(Number(part)))
|
if(type === "number" && Number.isNaN(Number(part)))
|
||||||
{
|
{
|
||||||
setErrorText("Some values are not numbers");
|
setErrorText("Some values are not numbers");
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,11 @@ import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryF
|
|||||||
import {Alert, TablePagination} from "@mui/material";
|
import {Alert, TablePagination} from "@mui/material";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import DialogContentText from "@mui/material/DialogContentText";
|
||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import LinearProgress from "@mui/material/LinearProgress";
|
import LinearProgress from "@mui/material/LinearProgress";
|
||||||
@ -37,7 +42,8 @@ import ListItemIcon from "@mui/material/ListItemIcon";
|
|||||||
import Menu from "@mui/material/Menu";
|
import Menu from "@mui/material/Menu";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Modal from "@mui/material/Modal";
|
import Modal from "@mui/material/Modal";
|
||||||
import {DataGridPro, getGridDateOperators, GridCallbackDetails, GridColDef, GridColumnOrderChangeParams, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterModel, GridLinkOperator, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, MuiEvent, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import {DataGridPro, getGridDateOperators, GridCallbackDetails, GridColDef, GridColumnOrderChangeParams, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterModel, GridLinkOperator, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, MuiEvent, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro";
|
||||||
import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
|
import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
|
||||||
import React, {useContext, useEffect, useReducer, useRef, useState} from "react";
|
import React, {useContext, useEffect, useReducer, useRef, useState} from "react";
|
||||||
import {Link, useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
import {Link, useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||||
@ -59,6 +65,7 @@ const COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT = "qqq.columnVisibility";
|
|||||||
const COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT = "qqq.columnSort";
|
const COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT = "qqq.columnSort";
|
||||||
const FILTER_LOCAL_STORAGE_KEY_ROOT = "qqq.filter";
|
const FILTER_LOCAL_STORAGE_KEY_ROOT = "qqq.filter";
|
||||||
const ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage";
|
const ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage";
|
||||||
|
const PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT = "qqq.pinnedColumns";
|
||||||
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
|
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
@ -161,12 +168,14 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
////////////////////////////////////////////
|
////////////////////////////////////////////
|
||||||
const sortLocalStorageKey = `${COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const sortLocalStorageKey = `${COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const rowsPerPageLocalStorageKey = `${ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const rowsPerPageLocalStorageKey = `${ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
|
const pinnedColumnsLocalStorageKey = `${PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const columnVisibilityLocalStorageKey = `${COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const columnVisibilityLocalStorageKey = `${COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const filterLocalStorageKey = `${FILTER_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const filterLocalStorageKey = `${FILTER_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
let defaultSort = [] as GridSortItem[];
|
let defaultSort = [] as GridSortItem[];
|
||||||
let defaultVisibility = {};
|
let defaultVisibility = {};
|
||||||
let defaultRowsPerPage = 10;
|
let defaultRowsPerPage = 10;
|
||||||
let defaultDensity = "standard";
|
let defaultDensity = "standard" as GridDensity;
|
||||||
|
let defaultPinnedColumns = {left: ["__check__", "id"]} as GridPinnedColumns;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
// set the to be not per table (do as above if we want per table) at a later port //
|
// set the to be not per table (do as above if we want per table) at a later port //
|
||||||
@ -181,6 +190,12 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
defaultVisibility = JSON.parse(localStorage.getItem(columnVisibilityLocalStorageKey));
|
defaultVisibility = JSON.parse(localStorage.getItem(columnVisibilityLocalStorageKey));
|
||||||
}
|
}
|
||||||
|
if (localStorage.getItem(pinnedColumnsLocalStorageKey))
|
||||||
|
{
|
||||||
|
defaultPinnedColumns = JSON.parse(localStorage.getItem(pinnedColumnsLocalStorageKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (localStorage.getItem(rowsPerPageLocalStorageKey))
|
if (localStorage.getItem(rowsPerPageLocalStorageKey))
|
||||||
{
|
{
|
||||||
defaultRowsPerPage = JSON.parse(localStorage.getItem(rowsPerPageLocalStorageKey));
|
defaultRowsPerPage = JSON.parse(localStorage.getItem(rowsPerPageLocalStorageKey));
|
||||||
@ -194,20 +209,12 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
const [columnSortModel, setColumnSortModel] = useState(defaultSort);
|
const [columnSortModel, setColumnSortModel] = useState(defaultSort);
|
||||||
const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility);
|
const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility);
|
||||||
const [rowsPerPage, setRowsPerPage] = useState(defaultRowsPerPage);
|
const [rowsPerPage, setRowsPerPage] = useState(defaultRowsPerPage);
|
||||||
const [density, setDensity] = useState(defaultDensity as GridDensity);
|
const [density, setDensity] = useState(defaultDensity);
|
||||||
|
const [pinnedColumns, setPinnedColumns] = useState(defaultPinnedColumns);
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// for some reason, if we set the filterModel to what is in local storage, an onChange event //
|
|
||||||
// fires on the grid anyway with an empty filter, so be aware of the first onchange, and //
|
|
||||||
// when that happens put the default back - it needs to be in state //
|
|
||||||
// const [defaultFilter1] = useState(defaultFilter); //
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
const [ defaultFilter ] = useState({items: []} as GridFilterModel);
|
|
||||||
|
|
||||||
const [tableState, setTableState] = useState("");
|
const [tableState, setTableState] = useState("");
|
||||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||||
const [defaultFilterLoaded, setDefaultFilterLoaded] = useState(false);
|
const [defaultFilterLoaded, setDefaultFilterLoaded] = useState(false);
|
||||||
const [, setFiltersMenu] = useState(null);
|
|
||||||
const [actionsMenu, setActionsMenu] = useState(null);
|
const [actionsMenu, setActionsMenu] = useState(null);
|
||||||
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
|
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
|
||||||
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
|
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
|
||||||
@ -222,8 +229,9 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
const [tableLabel, setTableLabel] = useState("");
|
const [tableLabel, setTableLabel] = useState("");
|
||||||
const [gridMouseDownX, setGridMouseDownX] = useState(0);
|
const [gridMouseDownX, setGridMouseDownX] = useState(0);
|
||||||
const [gridMouseDownY, setGridMouseDownY] = useState(0);
|
const [gridMouseDownY, setGridMouseDownY] = useState(0);
|
||||||
const [pinnedColumns, setPinnedColumns] = useState({left: ["__check__", "id"]});
|
|
||||||
const [gridPreferencesWindow, setGridPreferencesWindow] = useState(undefined);
|
const [gridPreferencesWindow, setGridPreferencesWindow] = useState(undefined);
|
||||||
|
const [showClearFiltersWarning, setShowClearFiltersWarning] = useState(false);
|
||||||
|
const [hasValidFilters, setHasValidFilters] = useState(false);
|
||||||
|
|
||||||
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
|
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
|
||||||
const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
|
const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
|
||||||
@ -241,8 +249,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
const [ queryErrors, setQueryErrors ] = useState({} as any);
|
const [ queryErrors, setQueryErrors ] = useState({} as any);
|
||||||
const [ receivedQueryErrorTimestamp, setReceivedQueryErrorTimestamp ] = useState(new Date());
|
const [ receivedQueryErrorTimestamp, setReceivedQueryErrorTimestamp ] = useState(new Date());
|
||||||
|
|
||||||
const {pageHeader, setPageHeader} = useContext(QContext);
|
const {setPageHeader} = useContext(QContext);
|
||||||
|
|
||||||
const [ , forceUpdate ] = useReducer((x) => x + 1, 0);
|
const [ , forceUpdate ] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
|
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
|
||||||
@ -302,12 +309,13 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
if (filterModel)
|
if (filterModel)
|
||||||
{
|
{
|
||||||
|
let foundFilter = false;
|
||||||
filterModel.items.forEach((item) =>
|
filterModel.items.forEach((item) =>
|
||||||
{
|
{
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// if no value set and not 'empty' or 'not empty' operators, skip this filter //
|
// if no value set and not 'empty' or 'not empty' operators, skip this filter //
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
if(! item.value && item.operatorValue !== "isEmpty" && item.operatorValue !== "isNotEmpty")
|
if((! item.value || item.value.length == 0) && item.operatorValue !== "isEmpty" && item.operatorValue !== "isNotEmpty")
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -315,7 +323,9 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
const operator = QFilterUtils.gridCriteriaOperatorToQQQ(item.operatorValue);
|
const operator = QFilterUtils.gridCriteriaOperatorToQQQ(item.operatorValue);
|
||||||
const values = QFilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue);
|
const values = QFilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue);
|
||||||
qFilter.addCriteria(new QFilterCriteria(item.columnField, operator, values));
|
qFilter.addCriteria(new QFilterCriteria(item.columnField, operator, values));
|
||||||
|
foundFilter = true;
|
||||||
});
|
});
|
||||||
|
setHasValidFilters(foundFilter);
|
||||||
|
|
||||||
qFilter.booleanOperator = "AND";
|
qFilter.booleanOperator = "AND";
|
||||||
if (filterModel.linkOperator == "or")
|
if (filterModel.linkOperator == "or")
|
||||||
@ -362,7 +372,6 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
});
|
});
|
||||||
setColumnSortModel(columnSortModel);
|
setColumnSortModel(columnSortModel);
|
||||||
}
|
}
|
||||||
setPinnedColumns({left: [ "__check__", tableMetaData.primaryKeyField ]});
|
|
||||||
|
|
||||||
const qFilter = buildQFilter(localFilterModel);
|
const qFilter = buildQFilter(localFilterModel);
|
||||||
|
|
||||||
@ -621,6 +630,12 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
localStorage.setItem(rowsPerPageLocalStorageKey, JSON.stringify(size));
|
localStorage.setItem(rowsPerPageLocalStorageKey, JSON.stringify(size));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePinnedColumnsChange = (pinnedColumns: GridPinnedColumns) =>
|
||||||
|
{
|
||||||
|
setPinnedColumns(pinnedColumns)
|
||||||
|
localStorage.setItem(pinnedColumnsLocalStorageKey, JSON.stringify(pinnedColumns));
|
||||||
|
};
|
||||||
|
|
||||||
const handleStateChange = (state: GridState, event: MuiEvent, details: GridCallbackDetails) =>
|
const handleStateChange = (state: GridState, event: MuiEvent, details: GridCallbackDetails) =>
|
||||||
{
|
{
|
||||||
if (state && state.density && state.density.value !== density)
|
if (state && state.density && state.density.value !== density)
|
||||||
@ -710,26 +725,6 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
const handleFilterChange = (filterModel: GridFilterModel) =>
|
const handleFilterChange = (filterModel: GridFilterModel) =>
|
||||||
{
|
{
|
||||||
////////////////////////////////////////////////////////
|
|
||||||
// remove any items in the filter that are incomplete //
|
|
||||||
////////////////////////////////////////////////////////
|
|
||||||
/*
|
|
||||||
if(filterModel && filterModel.items)
|
|
||||||
{
|
|
||||||
filterModel.items.forEach((item: GridFilterItem, index: number, arr: GridFilterItem[]) =>
|
|
||||||
{
|
|
||||||
if(! item.value)
|
|
||||||
{
|
|
||||||
if( item.operatorValue !== "isEmpty" && item.operatorValue !== "isNotEmpty")
|
|
||||||
{
|
|
||||||
arr.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
setFilterModel(filterModel);
|
setFilterModel(filterModel);
|
||||||
if (filterLocalStorageKey)
|
if (filterLocalStorageKey)
|
||||||
{
|
{
|
||||||
@ -754,7 +749,6 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
setTableState(tableName);
|
setTableState(tableName);
|
||||||
setFiltersMenu(null);
|
|
||||||
const metaData = await qController.loadMetaData();
|
const metaData = await qController.loadMetaData();
|
||||||
QValueUtils.qInstance = metaData;
|
QValueUtils.qInstance = metaData;
|
||||||
|
|
||||||
@ -1044,12 +1038,38 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<GridToolbarColumnsButton />
|
<GridToolbarColumnsButton />
|
||||||
<GridToolbarFilterButton />
|
<div style={{position: "relative"}}>
|
||||||
<GridToolbarDensitySelector />
|
<GridToolbarFilterButton />
|
||||||
<GridToolbarExportContainer>
|
{
|
||||||
<ExportMenuItem format="csv" />
|
hasValidFilters && (
|
||||||
<ExportMenuItem format="xlsx" />
|
|
||||||
</GridToolbarExportContainer>
|
<div id="clearFiltersButton" style={{position: "absolute", left: "84px", top: "6px"}}>
|
||||||
|
<Tooltip title="Clear All Filters">
|
||||||
|
<Icon sx={{cursor: "pointer"}} onClick={() => setShowClearFiltersWarning(true)}>clear</Icon>
|
||||||
|
</Tooltip>
|
||||||
|
<Dialog open={showClearFiltersWarning} onClose={() => setShowClearFiltersWarning(false)}>
|
||||||
|
<DialogTitle id="alert-dialog-title">Confirm </DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>Are you sure you want to clear all filters?</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setShowClearFiltersWarning(false)}>No</Button>
|
||||||
|
<Button onClick={() =>
|
||||||
|
{
|
||||||
|
setShowClearFiltersWarning(false);
|
||||||
|
handleFilterChange({items: []} as GridFilterModel);
|
||||||
|
}}>Yes</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<GridToolbarDensitySelector />
|
||||||
|
<GridToolbarExportContainer>
|
||||||
|
<ExportMenuItem format="csv" />
|
||||||
|
<ExportMenuItem format="xlsx" />
|
||||||
|
</GridToolbarExportContainer>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
selectFullFilterState === "checked" && (
|
selectFullFilterState === "checked" && (
|
||||||
@ -1140,7 +1160,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
setTotalRecords(null);
|
setTotalRecords(null);
|
||||||
updateTable();
|
updateTable();
|
||||||
}, [ tableState, filterModel ]);
|
}, [ tableState, filterModel]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
@ -1189,7 +1209,8 @@ function EntityList({table, launchProcess}: Props): JSX.Element
|
|||||||
<MDBox height="100%">
|
<MDBox height="100%">
|
||||||
<DataGridPro
|
<DataGridPro
|
||||||
components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading}}
|
components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading}}
|
||||||
initialState={{pinnedColumns: pinnedColumns}}
|
pinnedColumns={pinnedColumns}
|
||||||
|
onPinnedColumnsChange={handlePinnedColumnsChange}
|
||||||
pagination
|
pagination
|
||||||
paginationMode="server"
|
paginationMode="server"
|
||||||
sortingMode="server"
|
sortingMode="server"
|
||||||
|
@ -212,3 +212,16 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
|||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.MuiDataGrid-toolbarContainer .MuiBadge-badge
|
||||||
|
{
|
||||||
|
right: 7px;
|
||||||
|
top: 5px;
|
||||||
|
background: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiTablePagination-root .MuiSvgIcon-root
|
||||||
|
{
|
||||||
|
display: inline;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user