forked from CITKParus/P8-Panels
WEB APP: P8PDataGrif - поддержка сложных заголовков с группами, группировка строк
This commit is contained in:
parent
afb72e475b
commit
88984a7804
@ -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,
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
//----------------
|
//----------------
|
||||||
|
193
app/components/p8p_table_reducer.js
Normal file
193
app/components/p8p_table_reducer.js
Normal 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);
|
||||||
|
};
|
@ -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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user