WEB APP: P8PDataGrid - поддержка фиксированных заголовка и колонок

This commit is contained in:
Mikhail Chechnev 2024-05-04 14:17:24 +03:00
parent 44069b0bc9
commit 4b5938e3b1
3 changed files with 174 additions and 58 deletions

View File

@ -35,6 +35,8 @@ const P8PDataGrid = ({
groups,
rows,
size,
fixedHeader = false,
fixedColumns = 0,
morePages = false,
reloading,
expandable,
@ -111,6 +113,8 @@ const P8PDataGrid = ({
orders={orders}
filters={filters}
size={size || P8P_DATA_GRID_SIZE.MEDIUM}
fixedHeader={fixedHeader}
fixedColumns={fixedColumns}
morePages={morePages}
reloading={reloading}
expandable={expandable}
@ -147,6 +151,8 @@ P8PDataGrid.propTypes = {
groups: PropTypes.array,
rows: PropTypes.array.isRequired,
size: PropTypes.string,
fixedHeader: PropTypes.bool,
fixedColumns: PropTypes.number,
morePages: PropTypes.bool,
reloading: PropTypes.bool.isRequired,
expandable: PropTypes.bool,

View File

@ -33,6 +33,7 @@ import {
Container,
Link
} from "@mui/material"; //Интерфейсные компоненты
import { useTheme } from "@mui/material/styles"; //Взаимодействие со стилями MUI
import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
import { P8P_TABLE_AT, HEADER_INITIAL_STATE, hasValue, p8pTableReducer } from "./p8p_table_reducer"; //Редьюсер состояния
@ -85,9 +86,30 @@ const STYLES = {
TABLE: {
with: "100%"
},
TABLE_HEAD_STICKY: {
position: "sticky",
top: 0,
zIndex: 1000
},
TABLE_HEAD_CELL_STICKY: (theme, left) => ({
position: "sticky",
left,
backgroundColor: theme.palette.background.default,
zIndex: 1000
}),
TABLE_ROW: {
"&:last-child td, &:last-child th": { border: 0 }
},
TABLE_CELL_STICKY: (theme, left) => ({
position: "sticky",
left,
backgroundColor: theme.palette.background.default,
zIndex: 500
}),
TABLE_CELL_EXPAND_CONTROL: {
minWidth: "60px",
maxWidth: "60px"
},
TABLE_CELL_EXPAND_CONTAINER: {
paddingBottom: 0,
paddingTop: 0
@ -95,6 +117,10 @@ const STYLES = {
TABLE_CELL_GROUP_HEADER: {
backgroundColor: "lightgray"
},
TABLE_CELL_GROUP_HEADER_STICKY: {
position: "sticky",
left: 0
},
TABLE_COLUMN_STACK: {
alignItems: "center"
},
@ -460,6 +486,8 @@ const P8PTable = ({
orders,
filters,
size,
fixedHeader = false,
fixedColumns = 0,
morePages = false,
reloading,
expandable,
@ -501,6 +529,9 @@ const P8PTable = ({
//Собственное состояние - колонка с отображаемой подсказкой
const [displayHintColumn, setDisplayHintColumn] = useState(null);
//Стили
const theme = useTheme();
//Описание фильтруемой колонки
const filterColumnDef = filterColumn ? columnsDef.find(columnDef => columnDef.name == filterColumn) || null : null;
@ -516,12 +547,12 @@ const P8PTable = ({
: ["", ""];
//Формирование заголовка таблицы
const setHeader = ({ columnsDef, expandable, objectsCopier }) =>
dispatchHeaderAction({ type: P8P_TABLE_AT.SET_HEADER, payload: { columnsDef, expandable, objectsCopier } });
const setHeader = ({ columnsDef, expandable, fixedColumns, objectsCopier }) =>
dispatchHeaderAction({ type: P8P_TABLE_AT.SET_HEADER, payload: { columnsDef, expandable, fixedColumns, objectsCopier } });
//Сворачивание/разворачивание уровня заголовка таблицы
const toggleHeaderExpand = ({ columnName, objectsCopier }) =>
dispatchHeaderAction({ type: P8P_TABLE_AT.TOGGLE_HEADER_EXPAND, payload: { columnName, expandable, objectsCopier } });
dispatchHeaderAction({ type: P8P_TABLE_AT.TOGGLE_HEADER_EXPAND, payload: { columnName, expandable, fixedColumns, objectsCopier } });
//Выравнивание в зависимости от типа данных
const getAlignByDataType = ({ dataType, hasChild }) =>
@ -621,19 +652,26 @@ const P8PTable = ({
//При изменении описания колонок
useEffect(() => {
setHeader({ columnsDef, expandable, objectsCopier });
}, [columnsDef, expandable, objectsCopier]);
setHeader({ columnsDef, expandable, fixedColumns, objectsCopier });
}, [columnsDef, expandable, fixedColumns, objectsCopier]);
//Генерация заголовка группы
const renderGroupCell = group => {
let customRender = {};
if (groupCellRender) customRender = groupCellRender({ columnsDef: header.columnsDef, group }) || {};
return (
return header.displayDataColumns.map((columnDef, i) => (
<TableCell
colSpan={header.displayDataColumnsCount}
sx={{ ...STYLES.TABLE_CELL_GROUP_HEADER, ...customRender.cellStyle }}
key={`group-header-cell-${i}`}
{...customRender.cellProps}
sx={{
...STYLES.TABLE_CELL_GROUP_HEADER,
...customRender.cellStyle,
...(columnDef.width ? { minWidth: columnDef.width, maxWidth: columnDef.width } : {}),
...(i == 0 && fixedColumns ? STYLES.TABLE_CELL_GROUP_HEADER_STICKY : {})
}}
colSpan={expandable && rowExpandRender ? 2 : 1}
>
{i == 0 ? (
<Stack direction="row" sx={STYLES.TABLE_COLUMN_STACK}>
{group.expandable ? (
<IconButton
@ -646,13 +684,14 @@ const P8PTable = ({
) : null}
{customRender.data ? customRender.data : group.caption}
</Stack>
) : null}
</TableCell>
);
));
};
//Генерация содержимого
return (
<>
<div>
{displayHintColumn ? (
<P8PTableColumnHintDialog columnDef={displayHintColumnDef} okBtnCaption={okFilterBtnCaption} onOk={handleHintOk} />
) : null}
@ -684,13 +723,22 @@ const P8PTable = ({
valueFormatter={valueFormatter}
/>
) : null}
<TableContainer component={containerComponent ? containerComponent : Paper} {...(containerComponentProps ? containerComponentProps : {})}>
<Table sx={STYLES.TABLE} size={size || P8P_TABLE_SIZE.MEDIUM}>
<TableHead>
{header.displayLevels.map(level => (
<Table stickyHeader={fixedHeader} sx={STYLES.TABLE} size={size || P8P_TABLE_SIZE.MEDIUM}>
<TableHead sx={fixedHeader ? STYLES.TABLE_HEAD_STICKY : {}}>
{header.displayLevels.map((level, i) => (
<TableRow key={level}>
{expandable && rowExpandRender ? <TableCell key="head-cell-expand-control" align="center"></TableCell> : null}
{expandable && rowExpandRender && i == 0 ? (
<TableCell
key="head-cell-expand-control"
align="center"
sx={{
...STYLES.TABLE_CELL_EXPAND_CONTROL,
...(fixedColumns ? STYLES.TABLE_HEAD_CELL_STICKY(theme, 0) : {})
}}
rowSpan={header.displayLevelsColumns[level][0].rowSpan}
></TableCell>
) : null}
{header.displayLevelsColumns[level].map((columnDef, j) => {
let customRender = {};
if (headCellRender) customRender = headCellRender({ columnDef }) || {};
@ -698,7 +746,11 @@ const P8PTable = ({
<TableCell
key={`head-cell-${j}`}
align={getAlignByDataType(columnDef)}
sx={{ ...customRender.cellStyle }}
sx={{
...customRender.cellStyle,
...(columnDef.width ? { minWidth: columnDef.width, maxWidth: columnDef.width } : {}),
...(columnDef.fixed ? STYLES.TABLE_HEAD_CELL_STICKY(theme, columnDef.fixedLeft) : {})
}}
rowSpan={columnDef.rowSpan}
colSpan={columnDef.colSpan}
{...customRender.cellProps}
@ -753,7 +805,14 @@ const P8PTable = ({
<React.Fragment key={`data-${i}`}>
<TableRow key={`data-row-${i}`} sx={STYLES.TABLE_ROW}>
{expandable && rowExpandRender ? (
<TableCell key={`data-cell-expand-control-${i}`} align="center">
<TableCell
key={`data-cell-expand-control-${i}`}
align="center"
sx={{
...STYLES.TABLE_CELL_EXPAND_CONTROL,
...(fixedColumns ? STYLES.TABLE_CELL_STICKY(theme, 0) : {})
}}
>
<IconButton onClick={() => handleExpandClick(i)}>
<Icon>{expanded[i] === true ? "keyboard_arrow_down" : "keyboard_arrow_right"}</Icon>
</IconButton>
@ -766,7 +825,13 @@ const P8PTable = ({
<TableCell
key={`data-cell-${j}`}
align={getAlignByDataType(columnDef)}
sx={{ ...customRender.cellStyle }}
sx={{
...customRender.cellStyle,
...(columnDef.width
? { minWidth: columnDef.width, maxWidth: columnDef.width }
: {}),
...(columnDef.fixed ? STYLES.TABLE_CELL_STICKY(theme, columnDef.fixedLeft) : {})
}}
{...customRender.cellProps}
>
{customRender.data
@ -780,7 +845,15 @@ const P8PTable = ({
</TableRow>
{expandable && rowExpandRender && expanded[i] === true ? (
<TableRow key={`data-row-expand-${i}`}>
<TableCell sx={STYLES.TABLE_CELL_EXPAND_CONTAINER} colSpan={header.displayDataColumnsCount}>
<TableCell
sx={{
...STYLES.TABLE_CELL_EXPAND_CONTAINER,
...(fixedColumns ? STYLES.TABLE_CELL_STICKY(theme, 0) : {})
}}
colSpan={
fixedColumns ? header.displayFixedColumnsCount + 1 : header.displayDataColumnsCount
}
>
{rowExpandRender({ columnsDef, row })}
</TableCell>
</TableRow>
@ -800,6 +873,7 @@ const P8PTable = ({
: null}
</TableBody>
</Table>
</TableContainer>
{rows.length == 0 ? (
noDataFoundText && !reloading ? (
<P8PAppInlineError text={noDataFoundText} />
@ -811,8 +885,7 @@ const P8PTable = ({
</Button>
</Container>
) : null}
</TableContainer>
</>
</div>
);
};
@ -829,7 +902,8 @@ P8PTable.propTypes = {
values: PropTypes.array,
parent: PropTypes.string,
expandable: PropTypes.bool.isRequired,
expanded: PropTypes.bool.isRequired
expanded: PropTypes.bool.isRequired,
width: PropTypes.number
})
).isRequired,
groups: PropTypes.arrayOf(
@ -849,6 +923,8 @@ P8PTable.propTypes = {
).isRequired,
filters: PropTypes.arrayOf(P8P_TABLE_FILTER_SHAPE).isRequired,
size: PropTypes.string,
fixedHeader: PropTypes.bool,
fixedColumns: PropTypes.number,
morePages: PropTypes.bool,
reloading: PropTypes.bool.isRequired,
expandable: PropTypes.bool,

View File

@ -19,7 +19,8 @@ const HEADER_INITIAL_STATE = () => ({
displayLevels: [],
displayLevelsColumns: {},
displayDataColumnsCount: 0,
displayDataColumns: []
displayDataColumns: [],
displayFixedColumnsCount: 0
});
//Состояние описания ячейки заголовка таблицы по умолчанию
@ -28,6 +29,8 @@ const HEADER_COLUMN_INITIAL_STATE = ({ columnDef, objectsCopier }) => {
if (!hasValue(tmp.parent)) tmp.parent = "";
if (!hasValue(tmp.expandable)) tmp.expandable = false;
if (!hasValue(tmp.expanded)) tmp.expanded = true;
if (!hasValue(tmp.fixed)) tmp.fixed = false;
if (!hasValue(tmp.fixedLeft)) tmp.fixedLeft = 0;
return tmp;
};
@ -55,8 +58,23 @@ const getDisplayColumnColSpan = (displayTree, columnDef) => {
} else return 1;
};
//Определения признака зафиксированности колонки
const getFixedColumns = (displayTree, parentFixed, parentLeft, fixedColumns) => {
if (fixedColumns) {
let left = parentLeft;
displayTree.forEach((columnDef, i) => {
left += columnDef.width;
if ((columnDef.level == 1 && i + 1 <= fixedColumns) || (columnDef.level > 1 && parentFixed)) {
columnDef.fixed = true;
columnDef.fixedLeft = left - columnDef.width;
} else columnDef.fixed = false;
if (columnDef.hasChild) getFixedColumns(columnDef.child, columnDef.fixed, columnDef.fixedLeft, fixedColumns);
});
}
};
//Формирование дерева отображаемых элементов заголовка
const buildDisplayTree = (columnsDef, parent, level) => {
const buildDisplayTree = (columnsDef, parent, level, expandable, fixedColumns) => {
const baseBuild = (columnsDef, parent, level) => {
let maxLevel = level - 1;
const res = columnsDef
@ -77,6 +95,7 @@ const buildDisplayTree = (columnsDef, parent, level) => {
};
const [displayTree, maxLevel] = baseBuild(columnsDef, parent, level);
getDisplayColumnRowSpan(displayTree, maxLevel);
getFixedColumns(displayTree, false, expandable ? 60 : 0, fixedColumns);
return [displayTree, maxLevel];
};
@ -106,28 +125,41 @@ const buildDisplayDataColumns = (displayTree, expandable) => {
return [displayDataColumns, displayDataColumns.length + (expandable === true ? 1 : 0)];
};
//Подсчёт количества отображаемых фиксированных колонок
const getDisplayFixedColumnsCount = displayTree => {
let res = 0;
const traverseTree = displayTree => {
displayTree.forEach(columnDef => (columnDef.hasChild ? traverseTree(columnDef.child) : columnDef.fixed ? res++ : null));
};
traverseTree(displayTree);
return res;
};
//Формирование описания отображаемых колонок
const buildDisplay = ({ columnsDef, expandable }) => {
const buildDisplay = ({ columnsDef, expandable, fixedColumns }) => {
//Сформируем дерево отображаемых колонок заголовка
const [displayTree, maxLevel] = buildDisplayTree(columnsDef, "", 1);
const [displayTree, maxLevel] = buildDisplayTree(columnsDef, "", 1, expandable, fixedColumns);
//Вытянем дерево в удобные для рендеринга структуры
const [displayLevels, displayLevelsColumns] = buildDisplayLevelsColumns(displayTree, maxLevel);
//Сформируем отображаемые колонки данных
const [displayDataColumns, displayDataColumnsCount] = buildDisplayDataColumns(displayTree, expandable);
//Подсчитаем количество отображаемых фиксированных колонок
const displayFixedColumnsCount = getDisplayFixedColumnsCount(displayTree);
//Вернём результат
return [displayLevels, displayLevelsColumns, displayDataColumns, displayDataColumnsCount];
return [displayLevels, displayLevelsColumns, displayDataColumns, displayDataColumnsCount, displayFixedColumnsCount];
};
//Формирование описания заголовка
const buildHeaderDef = ({ columnsDef, expandable, objectsCopier }) => {
const buildHeaderDef = ({ columnsDef, expandable, fixedColumns, objectsCopier }) => {
//Инициализируем результат
const res = HEADER_INITIAL_STATE();
//Инициализируем внутренне описание колонок и поместим его в результат
columnsDef.forEach(columnDef => res.columnsDef.push(HEADER_COLUMN_INITIAL_STATE({ columnDef, objectsCopier })));
//Добавим в результат сведения об отображаемых данных
[res.displayLevels, res.displayLevelsColumns, res.displayDataColumns, res.displayDataColumnsCount] = buildDisplay({
[res.displayLevels, res.displayLevelsColumns, res.displayDataColumns, res.displayDataColumnsCount, res.displayFixedColumnsCount] = buildDisplay({
columnsDef: res.columnsDef,
expandable
expandable,
fixedColumns
});
//Сформируем дерево отображаемых колонок заголовка
//const [displayTree, maxLevel] = buildDisplayTree(res.columnsDef, "", 1);
@ -147,19 +179,20 @@ const buildHeaderDef = ({ columnsDef, expandable, objectsCopier }) => {
const handlers = {
//Формирование заголовка
[P8P_TABLE_AT.SET_HEADER]: (state, { payload }) => {
const { columnsDef, expandable, objectsCopier } = payload;
const { columnsDef, expandable, fixedColumns, objectsCopier } = payload;
return {
...state,
...buildHeaderDef({ columnsDef, expandable, objectsCopier })
...buildHeaderDef({ columnsDef, expandable, fixedColumns, objectsCopier })
};
},
[P8P_TABLE_AT.TOGGLE_HEADER_EXPAND]: (state, { payload }) => {
const { columnName, expandable, objectsCopier } = payload;
const { columnName, expandable, fixedColumns, objectsCopier } = payload;
const columnsDef = objectsCopier(state.columnsDef);
columnsDef.forEach(columnDef => (columnDef.name == columnName ? (columnDef.expanded = !columnDef.expanded) : null));
const [displayLevels, displayLevelsColumns, displayDataColumns, displayDataColumnsCount] = buildDisplay({
const [displayLevels, displayLevelsColumns, displayDataColumns, displayDataColumnsCount, displayFixedColumnsCount] = buildDisplay({
columnsDef,
expandable
expandable,
fixedColumns
});
//const [displayTree, maxLevel] = buildDisplayTree(columnsDef, "", 1);
//const [displayLevels, displayLevelsColumns] = buildDisplayLevelsColumns(displayTree, maxLevel);
@ -170,7 +203,8 @@ const handlers = {
displayLevels,
displayLevelsColumns,
displayDataColumns,
displayDataColumnsCount
displayDataColumnsCount,
displayFixedColumnsCount
};
},
//Обработчик по умолчанию