WEB APP: P8PDataGrif - поддержка сложных заголовков с группами, группировка строк

This commit is contained in:
Mikhail Chechnev 2024-02-03 01:28:06 +03:00
parent afb72e475b
commit 88984a7804
4 changed files with 420 additions and 111 deletions

View File

@ -32,9 +32,10 @@ const P8P_DATA_GRID_FILTER_SHAPE = P8P_TABLE_FILTER_SHAPE;
const P8PDataGrid = ({ const P8PDataGrid = ({
columnsDef, columnsDef,
filtersInitial, filtersInitial,
groups,
rows, rows,
size, size,
morePages, morePages = false,
reloading, reloading,
expandable, expandable,
orderAscMenuItemCaption, orderAscMenuItemCaption,
@ -50,6 +51,7 @@ const P8PDataGrid = ({
noDataFoundText, noDataFoundText,
headCellRender, headCellRender,
dataCellRender, dataCellRender,
groupCellRender,
rowExpandRender, rowExpandRender,
valueFormatter, valueFormatter,
onOrderChanged, onOrderChanged,
@ -102,6 +104,7 @@ const P8PDataGrid = ({
return ( return (
<P8PTable <P8PTable
columnsDef={columnsDef} columnsDef={columnsDef}
groups={groups}
rows={rows} rows={rows}
orders={orders} orders={orders}
filters={filters} filters={filters}
@ -122,8 +125,10 @@ const P8PDataGrid = ({
noDataFoundText={noDataFoundText} noDataFoundText={noDataFoundText}
headCellRender={headCellRender} headCellRender={headCellRender}
dataCellRender={dataCellRender} dataCellRender={dataCellRender}
groupCellRender={groupCellRender}
rowExpandRender={rowExpandRender} rowExpandRender={rowExpandRender}
valueFormatter={valueFormatter} valueFormatter={valueFormatter}
objectsCopier={objectsCopier}
onOrderChanged={handleOrderChanged} onOrderChanged={handleOrderChanged}
onFilterChanged={handleFilterChanged} onFilterChanged={handleFilterChanged}
onPagesCountChanged={handlePagesCountChanged} onPagesCountChanged={handlePagesCountChanged}
@ -135,9 +140,10 @@ const P8PDataGrid = ({
P8PDataGrid.propTypes = { P8PDataGrid.propTypes = {
columnsDef: PropTypes.array.isRequired, columnsDef: PropTypes.array.isRequired,
filtersInitial: PropTypes.arrayOf(P8P_DATA_GRID_FILTER_SHAPE), filtersInitial: PropTypes.arrayOf(P8P_DATA_GRID_FILTER_SHAPE),
groups: PropTypes.array,
rows: PropTypes.array.isRequired, rows: PropTypes.array.isRequired,
size: PropTypes.string, size: PropTypes.string,
morePages: PropTypes.bool.isRequired, morePages: PropTypes.bool,
reloading: PropTypes.bool.isRequired, reloading: PropTypes.bool.isRequired,
expandable: PropTypes.bool, expandable: PropTypes.bool,
orderAscMenuItemCaption: PropTypes.string.isRequired, orderAscMenuItemCaption: PropTypes.string.isRequired,
@ -153,6 +159,7 @@ P8PDataGrid.propTypes = {
noDataFoundText: PropTypes.string, noDataFoundText: PropTypes.string,
headCellRender: PropTypes.func, headCellRender: PropTypes.func,
dataCellRender: PropTypes.func, dataCellRender: PropTypes.func,
groupCellRender: PropTypes.func,
rowExpandRender: PropTypes.func, rowExpandRender: PropTypes.func,
valueFormatter: PropTypes.func, valueFormatter: PropTypes.func,
onOrderChanged: PropTypes.func, onOrderChanged: PropTypes.func,

View File

@ -7,7 +7,7 @@
//Подключение библиотек //Подключение библиотек
//--------------------- //---------------------
import React, { useEffect, useState, useMemo } from "react"; //Классы React import React, { useEffect, useState, useReducer } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента import PropTypes from "prop-types"; //Контроль свойств компонента
import { import {
Table, Table,
@ -34,6 +34,7 @@ import {
Link Link
} from "@mui/material"; //Интерфейсные компоненты } from "@mui/material"; //Интерфейсные компоненты
import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
import { P8P_TABLE_AT, HEADER_INITIAL_STATE, hasValue, p8pTableReducer } from "./p8p_table_reducer"; //Редьюсер состояния
//--------- //---------
//Константы //Константы
@ -61,7 +62,8 @@ const P8P_TABLE_COLUMN_ORDER_DIRECTIONS = {
//Действия панели инструментов столбца //Действия панели инструментов столбца
const P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS = { const P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS = {
ORDER_TOGGLE: "ORDER_TOGGLE", ORDER_TOGGLE: "ORDER_TOGGLE",
FILTER_TOGGLE: "FILTER_TOGGLE" FILTER_TOGGLE: "FILTER_TOGGLE",
EXPAND_TOGGLE: "EXPAND_TOGGLE"
}; };
//Действия меню столбца //Действия меню столбца
@ -90,6 +92,9 @@ const STYLES = {
paddingBottom: 0, paddingBottom: 0,
paddingTop: 0 paddingTop: 0
}, },
TABLE_CELL_GROUP_HEADER: {
backgroundColor: "lightgray"
},
TABLE_COLUMN_STACK: { TABLE_COLUMN_STACK: {
alignItems: "center" alignItems: "center"
}, },
@ -110,11 +115,29 @@ const STYLES = {
//Вспомогательные классы и функции //Вспомогательные классы и функции
//-------------------------------- //--------------------------------
//Проверка существования значения //Панель инструментов столбца (левая)
const hasValue = value => typeof value !== "undefined" && value !== null && value !== ""; const P8PTableColumnToolBarLeft = ({ columnDef, onItemClick }) => {
//Кнопка развёртывания/свёртывания
let expButton = null;
if (columnDef.expandable)
expButton = (
<IconButton onClick={() => (onItemClick ? onItemClick(P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.EXPAND_TOGGLE, columnDef.name) : null)}>
<Icon>{columnDef.expanded ? "indeterminate_check_box" : "add_box"}</Icon>
</IconButton>
);
//Панель инструментов столбца //Генерация содержимого
const P8PTableColumnToolBar = ({ columnDef, orders, filters, onItemClick }) => { return <>{expButton}</>;
};
//Контроль свойств - Панель инструментов столбца (левая)
P8PTableColumnToolBarLeft.propTypes = {
columnDef: PropTypes.object.isRequired,
onItemClick: PropTypes.func
};
//Панель инструментов столбца (правая)
const P8PTableColumnToolBarRight = ({ columnDef, orders, filters, onItemClick }) => {
//Кнопка сортировки //Кнопка сортировки
const order = orders.find(o => o.name == columnDef.name); const order = orders.find(o => o.name == columnDef.name);
let orderButton = null; let orderButton = null;
@ -134,6 +157,7 @@ const P8PTableColumnToolBar = ({ columnDef, orders, filters, onItemClick }) => {
<Icon>filter_alt</Icon> <Icon>filter_alt</Icon>
</IconButton> </IconButton>
); );
//Генерация содержимого //Генерация содержимого
return ( return (
<> <>
@ -143,8 +167,8 @@ const P8PTableColumnToolBar = ({ columnDef, orders, filters, onItemClick }) => {
); );
}; };
//Контроль свойств - Панель инструментов столбца //Контроль свойств - Панель инструментов столбца (правая)
P8PTableColumnToolBar.propTypes = { P8PTableColumnToolBarRight.propTypes = {
columnDef: PropTypes.object.isRequired, columnDef: PropTypes.object.isRequired,
orders: PropTypes.array.isRequired, orders: PropTypes.array.isRequired,
filters: PropTypes.array.isRequired, filters: PropTypes.array.isRequired,
@ -431,11 +455,12 @@ P8PTableFiltersChips.propTypes = {
//Таблица //Таблица
const P8PTable = ({ const P8PTable = ({
columnsDef, columnsDef,
groups = [{}],
rows, rows,
orders, orders,
filters, filters,
size, size,
morePages, morePages = false,
reloading, reloading,
expandable, expandable,
orderAscMenuItemCaption, orderAscMenuItemCaption,
@ -451,18 +476,26 @@ const P8PTable = ({
noDataFoundText, noDataFoundText,
headCellRender, headCellRender,
dataCellRender, dataCellRender,
groupCellRender,
rowExpandRender, rowExpandRender,
valueFormatter, valueFormatter,
onOrderChanged, onOrderChanged,
onFilterChanged, onFilterChanged,
onPagesCountChanged onPagesCountChanged,
objectsCopier
}) => { }) => {
//Собственное состояние - описание заголовка
const [header, dispatchHeaderAction] = useReducer(p8pTableReducer, HEADER_INITIAL_STATE());
//Собственное состояние - фильтруемая колонка //Собственное состояние - фильтруемая колонка
const [filterColumn, setFilterColumn] = useState(null); const [filterColumn, setFilterColumn] = useState(null);
//Собственное состояние - развёрнутые строки //Собственное состояние - развёрнутые строки
const [expanded, setExpanded] = useState({}); const [expanded, setExpanded] = useState({});
//Собственное состояния - развёрнутые группы
const [expandedGroups, setExpandedGroups] = useState({});
//Собственное состояние - колонка с отображаемой подсказкой //Собственное состояние - колонка с отображаемой подсказкой
const [displayHintColumn, setDisplayHintColumn] = useState(null); const [displayHintColumn, setDisplayHintColumn] = useState(null);
@ -480,19 +513,21 @@ const P8PTable = ({
})() })()
: ["", ""]; : ["", ""];
//Определение списка видимых колонок //Формирование заголовка таблицы
const visibleColumns = useMemo(() => columnsDef.filter(columnDef => columnDef.visible === true), [columnsDef]); const setHeader = ({ columnsDef, expandable, objectsCopier }) =>
dispatchHeaderAction({ type: P8P_TABLE_AT.SET_HEADER, payload: { columnsDef, expandable, objectsCopier } });
//Определение количества видимых колонок //Сворачивание/разворачивание уровня заголовка таблицы
const visibleColumnsCount = useMemo(() => visibleColumns.length + (expandable === true ? 1 : 0), [visibleColumns, expandable]); const toggleHeaderExpand = ({ columnName, objectsCopier }) =>
dispatchHeaderAction({ type: P8P_TABLE_AT.TOGGLE_HEADER_EXPAND, payload: { columnName, expandable, objectsCopier } });
//Выравнивание в зависимости от типа данных //Выравнивание в зависимости от типа данных
const getAlignByDataType = dataType => const getAlignByDataType = ({ dataType, hasChild }) =>
dataType === P8P_TABLE_DATA_TYPE.DATE ? "center" : dataType === P8P_TABLE_DATA_TYPE.NUMB ? "right" : "left"; dataType === P8P_TABLE_DATA_TYPE.DATE || hasChild ? "center" : dataType === P8P_TABLE_DATA_TYPE.NUMB ? "right" : "left";
//Упорядочение содержимого в зависимости от типа данных //Упорядочение содержимого в зависимости от типа данных
const getJustifyContentByDataType = dataType => const getJustifyContentByDataType = ({ dataType, hasChild }) =>
dataType === P8P_TABLE_DATA_TYPE.DATE ? "center" : dataType === P8P_TABLE_DATA_TYPE.NUMB ? "flex-end" : "flex-start"; dataType === P8P_TABLE_DATA_TYPE.DATE || hasChild ? "center" : dataType === P8P_TABLE_DATA_TYPE.NUMB ? "flex-end" : "flex-start";
//Отработка нажатия на элемент пункта меню //Отработка нажатия на элемент пункта меню
const handleToolBarItemClick = (action, columnName) => { const handleToolBarItemClick = (action, columnName) => {
@ -511,6 +546,9 @@ const P8PTable = ({
case P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.FILTER_TOGGLE: case P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.FILTER_TOGGLE:
setFilterColumn(columnName); setFilterColumn(columnName);
break; break;
case P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.EXPAND_TOGGLE:
toggleHeaderExpand({ columnName, objectsCopier });
break;
} }
}; };
@ -579,6 +617,45 @@ const P8PTable = ({
if (reloading) setExpanded({}); if (reloading) setExpanded({});
}, [reloading]); }, [reloading]);
//При изменении описания колонок
useEffect(() => {
setHeader({ columnsDef, expandable, objectsCopier });
}, [columnsDef, expandable, objectsCopier]);
//При изменении состава групп
useEffect(() => {
let tmp = {};
groups.forEach(group => (!hasValue(expandedGroups[group.name]) ? (tmp[group.name] = group.expanded) : null));
setExpandedGroups(pv => ({ ...pv, ...tmp }));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [groups]);
//Генерация заголовка группы
const renderGroupCell = group => {
let customRender = {};
if (groupCellRender) customRender = groupCellRender({ columnsDef: header.columnsDef, group }) || {};
return (
<TableCell
colSpan={header.displayDataColumnsCount}
sx={{ ...STYLES.TABLE_CELL_GROUP_HEADER, ...customRender.cellStyle }}
{...customRender.cellProps}
>
<Stack direction="row" sx={STYLES.TABLE_COLUMN_STACK}>
{group.expandable ? (
<IconButton
onClick={() => {
setExpandedGroups(pv => ({ ...pv, ...{ [group.name]: !pv[group.name] } }));
}}
>
<Icon>{expandedGroups[group.name] ? "indeterminate_check_box" : "add_box"}</Icon>
</IconButton>
) : null}
{customRender.data ? customRender.data : group.caption}
</Stack>
</TableCell>
);
};
//Генерация содержимого //Генерация содержимого
return ( return (
<> <>
@ -616,98 +693,115 @@ const P8PTable = ({
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table sx={STYLES.TABLE} size={size || P8P_TABLE_SIZE.MEDIUM}> <Table sx={STYLES.TABLE} size={size || P8P_TABLE_SIZE.MEDIUM}>
<TableHead> <TableHead>
<TableRow> {header.displayLevels.map(level => (
{expandable && rowExpandRender ? <TableCell key="head-cell-expand-control" align="center"></TableCell> : null} <TableRow key={level}>
{visibleColumns.map((columnDef, j) => { {expandable && rowExpandRender ? <TableCell key="head-cell-expand-control" align="center"></TableCell> : null}
let customRender = {}; {header.displayLevelsColumns[level].map((columnDef, j) => {
if (headCellRender) customRender = headCellRender({ columnDef }) || {}; let customRender = {};
return ( if (headCellRender) customRender = headCellRender({ columnDef }) || {};
<TableCell return (
key={`head-cell-${j}`} <TableCell
align={getAlignByDataType(columnDef.dataType)} key={`head-cell-${j}`}
sx={{ ...customRender.cellStyle }} align={getAlignByDataType(columnDef)}
{...customRender.cellProps} sx={{ ...customRender.cellStyle }}
> rowSpan={columnDef.rowSpan}
<Stack colSpan={columnDef.colSpan}
direction="row" {...customRender.cellProps}
justifyContent={getJustifyContentByDataType(columnDef.dataType)}
sx={{ ...STYLES.TABLE_COLUMN_STACK, ...customRender.stackStyle }}
{...customRender.stackProps}
> >
{customRender.data ? ( <Stack
customRender.data direction="row"
) : columnDef.hint ? ( justifyContent={getJustifyContentByDataType(columnDef)}
<Link sx={{ ...STYLES.TABLE_COLUMN_STACK, ...customRender.stackStyle }}
component="button" {...customRender.stackProps}
variant="body2" >
align="left" <P8PTableColumnToolBarLeft columnDef={columnDef} onItemClick={handleToolBarItemClick} />
underline="always" {customRender.data ? (
onClick={() => handleColumnShowHintClick(columnDef.name)} customRender.data
> ) : columnDef.hint ? (
{columnDef.caption} <Link
</Link> component="button"
) : ( variant="body2"
columnDef.caption align="left"
)} underline="always"
<P8PTableColumnToolBar onClick={() => handleColumnShowHintClick(columnDef.name)}
columnDef={columnDef} >
orders={orders} {columnDef.caption}
filters={filters} </Link>
onItemClick={handleToolBarItemClick} ) : (
/> columnDef.caption
<P8PTableColumnMenu )}
columnDef={columnDef} <P8PTableColumnToolBarRight
orderAscItemCaption={orderAscMenuItemCaption} columnDef={columnDef}
orderDescItemCaption={orderDescMenuItemCaption} orders={orders}
filterItemCaption={filterMenuItemCaption} filters={filters}
onItemClick={handleMenuItemClick} onItemClick={handleToolBarItemClick}
/> />
</Stack> <P8PTableColumnMenu
</TableCell> columnDef={columnDef}
); orderAscItemCaption={orderAscMenuItemCaption}
})} orderDescItemCaption={orderDescMenuItemCaption}
</TableRow> filterItemCaption={filterMenuItemCaption}
onItemClick={handleMenuItemClick}
/>
</Stack>
</TableCell>
);
})}
</TableRow>
))}
</TableHead> </TableHead>
<TableBody> <TableBody>
{rows.length > 0 {rows.length > 0
? rows.map((row, i) => ( ? groups.map((group, g) => {
<React.Fragment key={`data-${i}`}> const rowsView = rows.map((row, i) =>
<TableRow key={`data-row-${i}`} sx={STYLES.TABLE_ROW}> !group?.name || group?.name == row.groupName ? (
{expandable && rowExpandRender ? ( <React.Fragment key={`data-${i}`}>
<TableCell key={`data-cell-expand-control-${i}`} align="center"> <TableRow key={`data-row-${i}`} sx={STYLES.TABLE_ROW}>
<IconButton onClick={() => handleExpandClick(i)}> {expandable && rowExpandRender ? (
<Icon>{expanded[i] === true ? "keyboard_arrow_down" : "keyboard_arrow_right"}</Icon> <TableCell key={`data-cell-expand-control-${i}`} align="center">
</IconButton> <IconButton onClick={() => handleExpandClick(i)}>
</TableCell> <Icon>{expanded[i] === true ? "keyboard_arrow_down" : "keyboard_arrow_right"}</Icon>
) : null} </IconButton>
{visibleColumns.map((columnDef, j) => { </TableCell>
let customRender = {}; ) : null}
if (dataCellRender) customRender = dataCellRender({ row, columnDef }) || {}; {header.displayDataColumns.map((columnDef, j) => {
return ( let customRender = {};
<TableCell if (dataCellRender) customRender = dataCellRender({ row, columnDef }) || {};
key={`data-cell-${j}`} return (
align={getAlignByDataType(columnDef.dataType)} <TableCell
sx={{ ...customRender.cellStyle }} key={`data-cell-${j}`}
{...customRender.cellProps} align={getAlignByDataType(columnDef)}
> sx={{ ...customRender.cellStyle }}
{customRender.data {...customRender.cellProps}
? customRender.data >
: valueFormatter {customRender.data
? valueFormatter({ value: row[columnDef.name], columnDef }) ? customRender.data
: row[columnDef.name]} : valueFormatter
</TableCell> ? valueFormatter({ value: row[columnDef.name], columnDef })
); : row[columnDef.name]}
})} </TableCell>
</TableRow> );
{expandable && rowExpandRender && expanded[i] === true ? ( })}
<TableRow key={`data-row-expand-${i}`}> </TableRow>
<TableCell sx={STYLES.TABLE_CELL_EXPAND_CONTAINER} colSpan={visibleColumnsCount}> {expandable && rowExpandRender && expanded[i] === true ? (
{rowExpandRender({ columnsDef, row })} <TableRow key={`data-row-expand-${i}`}>
</TableCell> <TableCell sx={STYLES.TABLE_CELL_EXPAND_CONTAINER} colSpan={header.displayDataColumnsCount}>
</TableRow> {rowExpandRender({ columnsDef, row })}
) : null} </TableCell>
</React.Fragment> </TableRow>
)) ) : null}
</React.Fragment>
) : null
);
return !group?.name ? (
rowsView
) : (
<React.Fragment key={`group-${g}`}>
<TableRow key={`group-header-${g}`}>{renderGroupCell(group)}</TableRow>
{!group.expandable || expandedGroups[group.name] === true ? rowsView : null}
</React.Fragment>
);
})
: null} : null}
</TableBody> </TableBody>
</Table> </Table>
@ -736,9 +830,21 @@ P8PTable.propTypes = {
order: PropTypes.bool.isRequired, order: PropTypes.bool.isRequired,
filter: PropTypes.bool.isRequired, filter: PropTypes.bool.isRequired,
dataType: PropTypes.string.isRequired, dataType: PropTypes.string.isRequired,
values: PropTypes.array visible: PropTypes.bool.isRequired,
values: PropTypes.array,
parent: PropTypes.string,
expandable: PropTypes.bool.isRequired,
expanded: PropTypes.bool.isRequired
}) })
).isRequired, ).isRequired,
groups: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
caption: PropTypes.string.isRequired,
expandable: PropTypes.bool.isRequired,
expanded: PropTypes.bool.isRequired
})
),
rows: PropTypes.array.isRequired, rows: PropTypes.array.isRequired,
orders: PropTypes.arrayOf( orders: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
@ -748,7 +854,7 @@ P8PTable.propTypes = {
).isRequired, ).isRequired,
filters: PropTypes.arrayOf(P8P_TABLE_FILTER_SHAPE).isRequired, filters: PropTypes.arrayOf(P8P_TABLE_FILTER_SHAPE).isRequired,
size: PropTypes.string, size: PropTypes.string,
morePages: PropTypes.bool.isRequired, morePages: PropTypes.bool,
reloading: PropTypes.bool.isRequired, reloading: PropTypes.bool.isRequired,
expandable: PropTypes.bool, expandable: PropTypes.bool,
orderAscMenuItemCaption: PropTypes.string.isRequired, orderAscMenuItemCaption: PropTypes.string.isRequired,
@ -764,11 +870,13 @@ P8PTable.propTypes = {
noDataFoundText: PropTypes.string, noDataFoundText: PropTypes.string,
headCellRender: PropTypes.func, headCellRender: PropTypes.func,
dataCellRender: PropTypes.func, dataCellRender: PropTypes.func,
groupCellRender: PropTypes.func,
rowExpandRender: PropTypes.func, rowExpandRender: PropTypes.func,
valueFormatter: PropTypes.func, valueFormatter: PropTypes.func,
onOrderChanged: PropTypes.func, onOrderChanged: PropTypes.func,
onFilterChanged: PropTypes.func, onFilterChanged: PropTypes.func,
onPagesCountChanged: PropTypes.func onPagesCountChanged: PropTypes.func,
objectsCopier: PropTypes.func.isRequired
}; };
//---------------- //----------------

View File

@ -0,0 +1,193 @@
/*
Парус 8 - Панели мониторинга
Компонент: Таблица - редьюсер состояния
*/
//---------
//Константы
//---------
//Типы действий
const P8P_TABLE_AT = {
SET_HEADER: "SET_HEADER", //Установка заголовка таблицы
TOGGLE_HEADER_EXPAND: "TOGGLE_HEADER_EXPAND" //Сворачивание/разворачивание уровня заголовка
};
//Состояние заголовка таблицы по умолчанию
const HEADER_INITIAL_STATE = () => ({
columnsDef: [],
displayLevels: [],
displayLevelsColumns: {},
displayDataColumnsCount: 0,
displayDataColumns: []
});
//Состояние описания ячейки заголовка таблицы по умолчанию
const HEADER_COLUMN_INITIAL_STATE = ({ columnDef, objectsCopier }) => {
const tmp = objectsCopier(columnDef);
if (!hasValue(tmp.parent)) tmp.parent = "";
if (!hasValue(tmp.expandable)) tmp.expandable = false;
if (!hasValue(tmp.expanded)) tmp.expanded = true;
return tmp;
};
//--------------------------------
//Вспомогательные классы и функции
//--------------------------------
//Проверка существования значения
const hasValue = value => typeof value !== "undefined" && value !== undefined && value !== null && value !== "";
//Определение высоты (в уровнях) ячейки заголовка
const getDisplayColumnRowSpan = (displayTree, maxLevel) => {
displayTree.forEach(columnDef => {
columnDef.rowSpan = columnDef.hasChild ? maxLevel - columnDef.childMaxLevel + 1 : maxLevel - columnDef.level + 1;
if (columnDef.hasChild) getDisplayColumnRowSpan(columnDef.child, maxLevel);
});
};
//Определение ширины (в колонках) ячейки заголовка
const getDisplayColumnColSpan = (displayTree, columnDef) => {
if (columnDef.hasChild) {
let colSpan = 0;
displayTree.forEach(cD => (cD.parent == columnDef.name ? (colSpan += getDisplayColumnColSpan(cD.child, cD)) : null));
return colSpan;
} else return 1;
};
//Формирование дерева отображаемых элементов заголовка
const buildDisplayTree = (columnsDef, parent, level) => {
const baseBuild = (columnsDef, parent, level) => {
let maxLevel = level - 1;
const res = columnsDef
.filter(columnDef => columnDef.parent == parent && columnDef.visible)
.map(columnDef => {
const [child, childMaxLevel] = columnDef.expanded ? baseBuild(columnsDef, columnDef.name, level + 1) : [[], level];
if (childMaxLevel > maxLevel) maxLevel = childMaxLevel;
const res = {
...columnDef,
child,
hasChild: child.length > 0 ? true : false,
level,
childMaxLevel: child.length > 0 ? childMaxLevel : 0
};
return { ...res, colSpan: getDisplayColumnColSpan(child, res), rowSpan: 1 };
});
return [res, maxLevel];
};
const [displayTree, maxLevel] = baseBuild(columnsDef, parent, level);
getDisplayColumnRowSpan(displayTree, maxLevel);
return [displayTree, maxLevel];
};
//Формирование коллекции отображаемых колонок уровня
const buildDisplayLevelsColumns = (displayTree, maxLevel) => {
const extractLevel = (displayTree, level) => {
let res = [];
displayTree.forEach(columnDef => {
if (columnDef.level == level) res.push(columnDef);
if (columnDef.hasChild) res = res.concat(extractLevel(columnDef.child, level));
});
return res;
};
const displayLevels = [...Array(maxLevel).keys()].map(i => i + 1);
const displayLevelsColumns = {};
displayLevels.forEach(level => (displayLevelsColumns[level] = extractLevel(displayTree, level)));
return [displayLevels, displayLevelsColumns];
};
//Формирование коллекции отображаемых колонок данных
const buildDisplayDataColumns = (displayTree, expandable) => {
const displayDataColumns = [];
const traverseTree = displayTree => {
displayTree.forEach(columnDef => (!columnDef.hasChild ? displayDataColumns.push(columnDef) : traverseTree(columnDef.child)));
};
traverseTree(displayTree);
return [displayDataColumns, displayDataColumns.length + (expandable === true ? 1 : 0)];
};
//Формирование описания отображаемых колонок
const buildDisplay = ({ columnsDef, expandable }) => {
//Сформируем дерево отображаемых колонок заголовка
const [displayTree, maxLevel] = buildDisplayTree(columnsDef, "", 1);
//Вытянем дерево в удобные для рендеринга структуры
const [displayLevels, displayLevelsColumns] = buildDisplayLevelsColumns(displayTree, maxLevel);
//Сформируем отображаемые колонки данных
const [displayDataColumns, displayDataColumnsCount] = buildDisplayDataColumns(displayTree, expandable);
//Вернём результат
return [displayLevels, displayLevelsColumns, displayDataColumns, displayDataColumnsCount];
};
//Формирование описания заголовка
const buildHeaderDef = ({ columnsDef, expandable, 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({
columnsDef: res.columnsDef,
expandable
});
//Сформируем дерево отображаемых колонок заголовка
//const [displayTree, maxLevel] = buildDisplayTree(res.columnsDef, "", 1);
//Вытянем дерево в удобные для рендеринга структуры
//[res.displayLevels, res.displayLevelsColumns] = buildDisplayLevelsColumns(displayTree, maxLevel);
//Сформируем отображаемые колонки данных
//[res.displayDataColumns, res.displayDataColumnsCount] = buildDisplayDataColumns(displayTree, expandable);
//Вернём результат
return res;
};
//-----------
//Тело модуля
//-----------
//Обработчики действий
const handlers = {
//Формирование заголовка
[P8P_TABLE_AT.SET_HEADER]: (state, { payload }) => {
const { columnsDef, expandable, objectsCopier } = payload;
return {
...state,
...buildHeaderDef({ columnsDef, expandable, objectsCopier })
};
},
[P8P_TABLE_AT.TOGGLE_HEADER_EXPAND]: (state, { payload }) => {
const { columnName, expandable, objectsCopier } = payload;
const columnsDef = objectsCopier(state.columnsDef);
columnsDef.forEach(columnDef => (columnDef.name == columnName ? (columnDef.expanded = !columnDef.expanded) : null));
const [displayLevels, displayLevelsColumns, displayDataColumns, displayDataColumnsCount] = buildDisplay({
columnsDef,
expandable
});
//const [displayTree, maxLevel] = buildDisplayTree(columnsDef, "", 1);
//const [displayLevels, displayLevelsColumns] = buildDisplayLevelsColumns(displayTree, maxLevel);
//const [displayDataColumns, displayDataColumnsCount] = buildDisplayDataColumns(displayTree, expandable);
return {
...state,
columnsDef,
displayLevels,
displayLevelsColumns,
displayDataColumns,
displayDataColumnsCount
};
},
//Обработчик по умолчанию
DEFAULT: state => state
};
//----------------
//Интерфейс модуля
//----------------
//Константы
export { P8P_TABLE_AT, HEADER_INITIAL_STATE, hasValue };
//Редьюсер состояния
export const p8pTableReducer = (state, action) => {
//Подберём обработчик
const handle = handlers[action.type] || handlers.DEFAULT;
//Исполним его
return handle(state, action);
};

View File

@ -39,6 +39,7 @@ const XML_ALWAYS_ARRAY_PATHS = [
"XRESPOND.XPAYLOAD.XROWS", "XRESPOND.XPAYLOAD.XROWS",
"XRESPOND.XPAYLOAD.XCOLUMNS_DEF", "XRESPOND.XPAYLOAD.XCOLUMNS_DEF",
"XRESPOND.XPAYLOAD.XCOLUMNS_DEF.values", "XRESPOND.XPAYLOAD.XCOLUMNS_DEF.values",
"XRESPOND.XPAYLOAD.XGROUPS",
"XRESPOND.XPAYLOAD.XGANTT_DEF.taskAttributes", "XRESPOND.XPAYLOAD.XGANTT_DEF.taskAttributes",
"XRESPOND.XPAYLOAD.XGANTT_DEF.taskColors", "XRESPOND.XPAYLOAD.XGANTT_DEF.taskColors",
"XRESPOND.XPAYLOAD.XGANTT_TASKS", "XRESPOND.XPAYLOAD.XGANTT_TASKS",