/* * 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 . */ 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( 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, 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) => { changeFilterText(event.target.value); }; return ( filterTextChanged(event)} > { filterText != "" && { changeFilterText(""); document.getElementById("findColumn").focus(); }}>close } {tables.map((table: QTableMetaData) => ( toggleColumnGroupOpen(table.name)} sx={{width: "100%", justifyContent: "flex-start", fontSize: "0.875rem", pt: 0.5}} disableRipple={true} > {filterText != "" ? "horizontal_rule" : openGroups[table.name] ? "expand_more" : "expand_less"} 0} onClick={(event) => onTableVisibilityClick(event, table.name)} size="small" /> {table.label} fields  ({noOfVisibleColumnsByTable[table.name]} / {noOfColumnsByTable[table.name]}) {(openGroups[table.name] || openGroupsBecauseOfFilter[table.name]) && tableFields[table.name].map((gridColumn: any) => { if (doesColumnMatchFilterText(gridColumn)) { return ( onColumnVisibilityChange(gridColumn.field)} size="small" />} label={{gridColumn.headerName.replace(/.*: /, "")}} /> ); } } )} ))} ); } );