mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 21:00:45 +00:00
Merge pull request #19 from Kingsrook/feature/custom-columns-panel
Custom columns panel - for showing join tables hierarchically
This commit is contained in:
399
src/qqq/components/query/CustomColumnsPanel.tsx
Normal file
399
src/qqq/components/query/CustomColumnsPanel.tsx
Normal file
@ -0,0 +1,399 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
|
import {Box, FormControlLabel, FormGroup} from "@mui/material";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Icon from "@mui/material/Icon";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Switch from "@mui/material/Switch";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import {GridColDef, GridSlotsComponentsProps, useGridApiContext, useGridSelector} from "@mui/x-data-grid-pro";
|
||||||
|
import {GridColumnsPanelProps} from "@mui/x-data-grid/components/panel/GridColumnsPanel";
|
||||||
|
import {gridColumnDefinitionsSelector, gridColumnVisibilityModelSelector} from "@mui/x-data-grid/hooks/features/columns/gridColumnsSelector";
|
||||||
|
import React, {createRef, forwardRef, useEffect, useReducer, useState} from "react";
|
||||||
|
|
||||||
|
declare module "@mui/x-data-grid"
|
||||||
|
{
|
||||||
|
interface ColumnsPanelPropsOverrides
|
||||||
|
{
|
||||||
|
tableMetaData: QTableMetaData;
|
||||||
|
initialOpenedGroups: { [name: string]: boolean };
|
||||||
|
openGroupsChanger: (openedGroups: { [name: string]: boolean }) => void;
|
||||||
|
initialFilterText: string;
|
||||||
|
filterTextChanger: (filterText: string) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomColumnsPanel = forwardRef<any, GridColumnsPanelProps>(
|
||||||
|
function MyCustomColumnsPanel(props: GridSlotsComponentsProps["columnsPanel"], ref)
|
||||||
|
{
|
||||||
|
const apiRef = useGridApiContext();
|
||||||
|
const columns = useGridSelector(apiRef, gridColumnDefinitionsSelector);
|
||||||
|
const columnVisibilityModel = useGridSelector(apiRef, gridColumnVisibilityModelSelector);
|
||||||
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
const someRef = createRef();
|
||||||
|
|
||||||
|
const [openGroups, setOpenGroups] = useState(props.initialOpenedGroups || {});
|
||||||
|
const openGroupsBecauseOfFilter = {} as { [name: string]: boolean };
|
||||||
|
const [lastScrollTop, setLastScrollTop] = useState(0);
|
||||||
|
const [filterText, setFilterText] = useState(props.initialFilterText);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
// set up the list of tables - e.g., main table plus exposed joins //
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
const tables: QTableMetaData[] = [];
|
||||||
|
tables.push(props.tableMetaData);
|
||||||
|
|
||||||
|
console.log(`Open groups: ${JSON.stringify(openGroups)}`);
|
||||||
|
|
||||||
|
if (props.tableMetaData.exposedJoins)
|
||||||
|
{
|
||||||
|
for (let i = 0; i < props.tableMetaData.exposedJoins.length; i++)
|
||||||
|
{
|
||||||
|
tables.push(props.tableMetaData.exposedJoins[i].joinTable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCheckboxColumn = (column: GridColDef): boolean =>
|
||||||
|
{
|
||||||
|
return (column.headerName == "Checkbox selection");
|
||||||
|
};
|
||||||
|
|
||||||
|
const doesColumnMatchFilterText = (column: GridColDef): boolean =>
|
||||||
|
{
|
||||||
|
if (isCheckboxColumn(column))
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////
|
||||||
|
// let's never show the checkbox column //
|
||||||
|
//////////////////////////////////////////
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterText == "")
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnLabelMinusTable = column.headerName.replace(/.*: /, "");
|
||||||
|
if (columnLabelMinusTable.toLowerCase().startsWith(filterText.toLowerCase()))
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
// try to match word-boundary followed by the filter text //
|
||||||
|
// e.g., "name" would match "First Name" or "Last Name" //
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
const re = new RegExp("\\b" + filterText.toLowerCase());
|
||||||
|
if (columnLabelMinusTable.toLowerCase().match(re))
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// in case text is an invalid regex... well, at least do a starts-with match... //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (columnLabelMinusTable.toLowerCase().startsWith(filterText.toLowerCase()))
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false);
|
||||||
|
};
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// build the map of list of fields, plus counts of columns & visible columns //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
const tableFields: { [tableName: string]: GridColDef[] } = {};
|
||||||
|
const noOfColumnsByTable: { [name: string]: number } = {};
|
||||||
|
const noOfVisibleColumnsByTable: { [name: string]: number } = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < tables.length; i++)
|
||||||
|
{
|
||||||
|
const tableName = tables[i].name;
|
||||||
|
tableFields[tableName] = [];
|
||||||
|
noOfColumnsByTable[tableName] = 0;
|
||||||
|
noOfVisibleColumnsByTable[tableName] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// always sort columns by label. note, in future may offer different sorts - here's where to do it. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const sortedColumns = [... columns];
|
||||||
|
sortedColumns.sort((a, b): number =>
|
||||||
|
{
|
||||||
|
return a.headerName.localeCompare(b.headerName);
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < sortedColumns.length; i++)
|
||||||
|
{
|
||||||
|
const column = sortedColumns[i];
|
||||||
|
if (isCheckboxColumn(column))
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
// don't count the checkbox or put it in the list for display //
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tableName = props.tableMetaData.name;
|
||||||
|
const fieldName = column.field;
|
||||||
|
if (fieldName.indexOf(".") > -1)
|
||||||
|
{
|
||||||
|
tableName = fieldName.split(".", 2)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
tableFields[tableName].push(column);
|
||||||
|
|
||||||
|
if (doesColumnMatchFilterText(column))
|
||||||
|
{
|
||||||
|
noOfColumnsByTable[tableName]++;
|
||||||
|
if (columnVisibilityModel[column.field] !== false)
|
||||||
|
{
|
||||||
|
noOfVisibleColumnsByTable[tableName]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterText != "")
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if there's a filter, then force open any groups (tables) with a field that matches it //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (doesColumnMatchFilterText(column))
|
||||||
|
{
|
||||||
|
openGroupsBecauseOfFilter[tableName] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
if (someRef && someRef.current)
|
||||||
|
{
|
||||||
|
console.log(`Trying to set scroll top to: ${lastScrollTop}`);
|
||||||
|
// @ts-ignore
|
||||||
|
someRef.current.scrollTop = lastScrollTop;
|
||||||
|
}
|
||||||
|
}, [lastScrollTop]);
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** event handler for toggling the open/closed status of a group (table)
|
||||||
|
*******************************************************************************/
|
||||||
|
const toggleColumnGroupOpen = (groupName: string) =>
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
// if there's a filter, we don't do the normal toggling... //
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
if (filterText != "")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openGroups[groupName] = !!!openGroups[groupName];
|
||||||
|
|
||||||
|
const newOpenGroups = JSON.parse(JSON.stringify(openGroups));
|
||||||
|
setOpenGroups(newOpenGroups);
|
||||||
|
props.openGroupsChanger(newOpenGroups);
|
||||||
|
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** event handler for toggling visibility state of one column
|
||||||
|
*******************************************************************************/
|
||||||
|
const onColumnVisibilityChange = (fieldName: string) =>
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
setLastScrollTop(someRef.current.scrollTop);
|
||||||
|
|
||||||
|
apiRef.current.setColumnVisibility(fieldName, columnVisibilityModel[fieldName] === false);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** event handler for clicking table-visibility switch
|
||||||
|
*******************************************************************************/
|
||||||
|
const onTableVisibilityClick = (event: React.MouseEvent<HTMLButtonElement>, tableName: string) =>
|
||||||
|
{
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
setLastScrollTop(someRef.current.scrollTop);
|
||||||
|
|
||||||
|
let newValue = true;
|
||||||
|
if (noOfVisibleColumnsByTable[tableName] == noOfColumnsByTable[tableName])
|
||||||
|
{
|
||||||
|
newValue = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < columns.length; i++)
|
||||||
|
{
|
||||||
|
const column = columns[i];
|
||||||
|
if (isCheckboxColumn(column))
|
||||||
|
{
|
||||||
|
/////////////////////////////////
|
||||||
|
// never turn the checkbox off //
|
||||||
|
/////////////////////////////////
|
||||||
|
columnVisibilityModel[column.field] = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const fieldName = column.field;
|
||||||
|
if (fieldName.indexOf(".") > -1)
|
||||||
|
{
|
||||||
|
if (tableName === fieldName.split(".", 2)[0] && doesColumnMatchFilterText(column))
|
||||||
|
{
|
||||||
|
columnVisibilityModel[fieldName] = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (tableName == props.tableMetaData.name && doesColumnMatchFilterText(column))
|
||||||
|
{
|
||||||
|
columnVisibilityModel[fieldName] = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// not too sure what this is doing... kinda got it from toggleAllColumns in //
|
||||||
|
// ./@mui/x-data-grid/components/panel/GridColumnsPanel.js //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
const currentModel = gridColumnVisibilityModelSelector(apiRef);
|
||||||
|
const newModel = JSON.parse(JSON.stringify(currentModel));
|
||||||
|
apiRef.current.setColumnVisibilityModel(newModel);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** event handler for reset button - turn on only all columns from main table
|
||||||
|
*******************************************************************************/
|
||||||
|
const resetClicked = () =>
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
setLastScrollTop(someRef.current.scrollTop);
|
||||||
|
|
||||||
|
for (let i = 0; i < columns.length; i++)
|
||||||
|
{
|
||||||
|
const column = columns[i];
|
||||||
|
const fieldName = column.field;
|
||||||
|
if (fieldName.indexOf(".") > -1)
|
||||||
|
{
|
||||||
|
columnVisibilityModel[fieldName] = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
columnVisibilityModel[fieldName] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentModel = gridColumnVisibilityModelSelector(apiRef);
|
||||||
|
const newModel = JSON.parse(JSON.stringify(currentModel));
|
||||||
|
apiRef.current.setColumnVisibilityModel(newModel);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeFilterText = (newValue: string) =>
|
||||||
|
{
|
||||||
|
setFilterText(newValue);
|
||||||
|
props.filterTextChanger(newValue)
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterTextChanged = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) =>
|
||||||
|
{
|
||||||
|
changeFilterText(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="custom-columns-panel" style={{width: "350px", height: "450px"}}>
|
||||||
|
<Box height="55px" padding="5px" display="flex">
|
||||||
|
<TextField id="findColumn" label="Find column" placeholder="Column title" variant="standard" fullWidth={true}
|
||||||
|
value={filterText}
|
||||||
|
onChange={(event) => filterTextChanged(event)}
|
||||||
|
></TextField>
|
||||||
|
{
|
||||||
|
filterText != "" && <IconButton sx={{position: "absolute", right: "0", top: "1rem"}} onClick={() =>
|
||||||
|
{
|
||||||
|
changeFilterText("");
|
||||||
|
document.getElementById("findColumn").focus();
|
||||||
|
}}><Icon fontSize="small">close</Icon></IconButton>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
<Box ref={someRef} overflow="auto" height="calc( 100% - 105px )">
|
||||||
|
|
||||||
|
<Stack direction="column" spacing={1} pl="0.5rem">
|
||||||
|
<FormGroup>
|
||||||
|
{tables.map((table: QTableMetaData) =>
|
||||||
|
(
|
||||||
|
<React.Fragment key={table.name}>
|
||||||
|
<IconButton
|
||||||
|
key={table.name}
|
||||||
|
size="small"
|
||||||
|
onClick={() => toggleColumnGroupOpen(table.name)}
|
||||||
|
sx={{width: "100%", justifyContent: "flex-start", fontSize: "0.875rem", pt: 0.5}}
|
||||||
|
disableRipple={true}
|
||||||
|
>
|
||||||
|
<Icon>{filterText != "" ? "horizontal_rule" : openGroups[table.name] ? "expand_more" : "expand_less"}</Icon>
|
||||||
|
<Box pl={"4px"} position="relative" top="-2px">
|
||||||
|
<Switch
|
||||||
|
checked={noOfVisibleColumnsByTable[table.name] == noOfColumnsByTable[table.name] && noOfVisibleColumnsByTable[table.name] > 0}
|
||||||
|
onClick={(event) => onTableVisibilityClick(event, table.name)}
|
||||||
|
size="small" />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{pl: "0.125rem", fontWeight: "bold"}} textAlign="left">
|
||||||
|
{table.label} fields
|
||||||
|
<Box display="inline" fontWeight="200">({noOfVisibleColumnsByTable[table.name]} / {noOfColumnsByTable[table.name]})</Box>
|
||||||
|
</Box>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
{(openGroups[table.name] || openGroupsBecauseOfFilter[table.name]) && tableFields[table.name].map((gridColumn: any) =>
|
||||||
|
{
|
||||||
|
if (doesColumnMatchFilterText(gridColumn))
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<Box key={gridColumn.field} pl={6}>
|
||||||
|
<FormControlLabel
|
||||||
|
sx={{fontWeight: "500 !important", display: "flex", paddingBottom: "0.25rem", alignItems: "flex-start"}}
|
||||||
|
control={<Switch
|
||||||
|
checked={columnVisibilityModel[gridColumn.field] !== false}
|
||||||
|
onChange={() => onColumnVisibilityChange(gridColumn.field)}
|
||||||
|
size="small" />}
|
||||||
|
label={<Box pt="0.25rem" lineHeight="1.4">{gridColumn.headerName.replace(/.*: /, "")}</Box>} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</FormGroup>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
<Box height="50px" padding="5px" display="flex" justifyContent="space-between">
|
||||||
|
<Button onClick={resetClicked}>Reset</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
@ -46,12 +46,9 @@ 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 Stack from "@mui/material/Stack";
|
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowProps, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro";
|
import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowProps, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro";
|
||||||
import {GridColumnsPanelProps} from "@mui/x-data-grid/components/panel/GridColumnsPanel";
|
|
||||||
import {gridColumnDefinitionsSelector, gridColumnVisibilityModelSelector} from "@mui/x-data-grid/hooks/features/columns/gridColumnsSelector";
|
|
||||||
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
||||||
import FormData from "form-data";
|
import FormData from "form-data";
|
||||||
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
||||||
@ -60,6 +57,7 @@ import QContext from "QContext";
|
|||||||
import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
import MenuButton from "qqq/components/buttons/MenuButton";
|
import MenuButton from "qqq/components/buttons/MenuButton";
|
||||||
import SavedFilters from "qqq/components/misc/SavedFilters";
|
import SavedFilters from "qqq/components/misc/SavedFilters";
|
||||||
|
import {CustomColumnsPanel} from "qqq/components/query/CustomColumnsPanel";
|
||||||
import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
|
import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
|
||||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||||
@ -163,6 +161,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
const [density, setDensity] = useState(defaultDensity);
|
const [density, setDensity] = useState(defaultDensity);
|
||||||
const [pinnedColumns, setPinnedColumns] = useState(defaultPinnedColumns);
|
const [pinnedColumns, setPinnedColumns] = useState(defaultPinnedColumns);
|
||||||
|
|
||||||
|
const initialColumnChooserOpenGroups = {} as { [name: string]: boolean };
|
||||||
|
initialColumnChooserOpenGroups[tableName] = true;
|
||||||
|
const [columnChooserOpenGroups, setColumnChooserOpenGroups] = useState(initialColumnChooserOpenGroups);
|
||||||
|
const [columnChooserFilterText, setColumnChooserFilterText] = useState("");
|
||||||
|
|
||||||
const [tableState, setTableState] = useState("");
|
const [tableState, setTableState] = useState("");
|
||||||
const [metaData, setMetaData] = useState(null as QInstance);
|
const [metaData, setMetaData] = useState(null as QInstance);
|
||||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||||
@ -316,7 +319,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
///////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////
|
||||||
if (tableMetaData && tableMetaData.name !== tableName)
|
if (tableMetaData && tableMetaData.name !== tableName)
|
||||||
{
|
{
|
||||||
console.log(" it looks like we changed tables - try to reload the things");
|
|
||||||
setTableMetaData(null);
|
setTableMetaData(null);
|
||||||
setColumnSortModel([]);
|
setColumnSortModel([]);
|
||||||
setColumnVisibilityModel({});
|
setColumnVisibilityModel({});
|
||||||
@ -833,10 +835,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
setColumnVisibilityModel(columnVisibilityModel);
|
setColumnVisibilityModel(columnVisibilityModel);
|
||||||
if (columnVisibilityLocalStorageKey)
|
if (columnVisibilityLocalStorageKey)
|
||||||
{
|
{
|
||||||
localStorage.setItem(
|
localStorage.setItem(columnVisibilityLocalStorageKey, JSON.stringify(columnVisibilityModel));
|
||||||
columnVisibilityLocalStorageKey,
|
|
||||||
JSON.stringify(columnVisibilityModel),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1357,95 +1356,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
// this is a WIP example of how we could do a custom "columns" panel/menu //
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
const CustomColumnsPanel = forwardRef<any, GridColumnsPanelProps>(
|
|
||||||
function MyCustomColumnsPanel(props: GridColumnsPanelProps, ref)
|
|
||||||
{
|
|
||||||
const apiRef = useGridApiContext();
|
|
||||||
const columns = useGridSelector(apiRef, gridColumnDefinitionsSelector);
|
|
||||||
const columnVisibilityModel = useGridSelector(apiRef, gridColumnVisibilityModelSelector);
|
|
||||||
|
|
||||||
const [openGroups, setOpenGroups] = useState({} as { [name: string]: boolean });
|
|
||||||
|
|
||||||
const groups = ["Order", "Line Item"];
|
|
||||||
|
|
||||||
const onColumnVisibilityChange = (fieldName: string) =>
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
if(columnVisibilityModel[fieldName] === undefined)
|
|
||||||
{
|
|
||||||
columnVisibilityModel[fieldName] = true;
|
|
||||||
}
|
|
||||||
columnVisibilityModel[fieldName] = !columnVisibilityModel[fieldName];
|
|
||||||
setColumnVisibilityModel(JSON.parse(JSON.stringify(columnVisibilityModel)))
|
|
||||||
*/
|
|
||||||
|
|
||||||
console.log(`${fieldName} = ${columnVisibilityModel[fieldName]}`);
|
|
||||||
// columnVisibilityModel[fieldName] = Math.random() < 0.5;
|
|
||||||
apiRef.current.setColumnVisibility(fieldName, columnVisibilityModel[fieldName] === false);
|
|
||||||
// handleColumnVisibilityChange(JSON.parse(JSON.stringify(columnVisibilityModel)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleColumnGroup = (groupName: string) =>
|
|
||||||
{
|
|
||||||
if (openGroups[groupName] === undefined)
|
|
||||||
{
|
|
||||||
openGroups[groupName] = true;
|
|
||||||
}
|
|
||||||
openGroups[groupName] = !openGroups[groupName];
|
|
||||||
setOpenGroups(JSON.parse(JSON.stringify(openGroups)));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref} className="custom-columns-panel" style={{width: "350px", height: "450px"}}>
|
|
||||||
<Box height="55px" padding="5px">
|
|
||||||
<TextField label="Find column" placeholder="Column title" variant="standard" fullWidth={true}></TextField>
|
|
||||||
</Box>
|
|
||||||
<Box overflow="auto" height="calc( 100% - 105px )">
|
|
||||||
|
|
||||||
<Stack direction="column" spacing={1} pl="0.5rem">
|
|
||||||
|
|
||||||
{groups.map((groupName: string) =>
|
|
||||||
(
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
key={groupName}
|
|
||||||
size="small"
|
|
||||||
onClick={() => toggleColumnGroup(groupName)}
|
|
||||||
sx={{width: "100%", justifyContent: "flex-start", fontSize: "0.875rem"}}
|
|
||||||
disableRipple={true}
|
|
||||||
>
|
|
||||||
<Icon>{openGroups[groupName] === false ? "expand_less" : "expand_more"}</Icon>
|
|
||||||
<Box sx={{pl: "0.25rem", fontWeight: "bold"}} textAlign="left">{groupName} fields</Box>
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
{openGroups[groupName] !== false && columnsModel.map((gridColumn: any) => (
|
|
||||||
<IconButton
|
|
||||||
key={gridColumn.field}
|
|
||||||
size="small"
|
|
||||||
onClick={() => onColumnVisibilityChange(gridColumn.field)}
|
|
||||||
sx={{width: "100%", justifyContent: "flex-start", fontSize: "0.875rem", pl: "1.375rem"}}
|
|
||||||
disableRipple={true}
|
|
||||||
>
|
|
||||||
<Icon>{columnVisibilityModel[gridColumn.field] === false ? "visibility_off" : "visibility"}</Icon>
|
|
||||||
<Box sx={{pl: "0.25rem"}} textAlign="left">{gridColumn.headerName}</Box>
|
|
||||||
</IconButton>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
<Box height="50px" padding="5px" display="flex" justifyContent="space-between">
|
|
||||||
<Button>hide all</Button>
|
|
||||||
<Button>show all</Button>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const safeToLocaleString = (n: Number): string =>
|
const safeToLocaleString = (n: Number): string =>
|
||||||
{
|
{
|
||||||
@ -1853,7 +1763,23 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
<Card>
|
<Card>
|
||||||
<Box height="100%">
|
<Box height="100%">
|
||||||
<DataGridPro
|
<DataGridPro
|
||||||
components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading, ColumnMenu: CustomColumnMenu/*, ColumnsPanel: CustomColumnsPanel*/}}
|
components={{
|
||||||
|
Toolbar: CustomToolbar,
|
||||||
|
Pagination: CustomPagination,
|
||||||
|
LoadingOverlay: Loading,
|
||||||
|
ColumnMenu: CustomColumnMenu,
|
||||||
|
ColumnsPanel: CustomColumnsPanel
|
||||||
|
}}
|
||||||
|
componentsProps={{
|
||||||
|
columnsPanel:
|
||||||
|
{
|
||||||
|
tableMetaData: tableMetaData,
|
||||||
|
initialOpenedGroups: columnChooserOpenGroups,
|
||||||
|
openGroupsChanger: setColumnChooserOpenGroups,
|
||||||
|
initialFilterText: columnChooserFilterText,
|
||||||
|
filterTextChanger: setColumnChooserFilterText
|
||||||
|
}
|
||||||
|
}}
|
||||||
pinnedColumns={pinnedColumns}
|
pinnedColumns={pinnedColumns}
|
||||||
onPinnedColumnsChange={handlePinnedColumnsChange}
|
onPinnedColumnsChange={handlePinnedColumnsChange}
|
||||||
pagination
|
pagination
|
||||||
|
@ -398,3 +398,11 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
|||||||
top: -5px;
|
top: -5px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-columns-panel .MuiSwitch-thumb
|
||||||
|
{
|
||||||
|
width: 15px !important;
|
||||||
|
height: 15px !important;
|
||||||
|
position: relative;
|
||||||
|
top: 3px;
|
||||||
|
}
|
Reference in New Issue
Block a user