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 = ({
|
||||
columnsDef,
|
||||
filtersInitial,
|
||||
groups,
|
||||
rows,
|
||||
size,
|
||||
morePages,
|
||||
morePages = false,
|
||||
reloading,
|
||||
expandable,
|
||||
orderAscMenuItemCaption,
|
||||
@ -50,6 +51,7 @@ const P8PDataGrid = ({
|
||||
noDataFoundText,
|
||||
headCellRender,
|
||||
dataCellRender,
|
||||
groupCellRender,
|
||||
rowExpandRender,
|
||||
valueFormatter,
|
||||
onOrderChanged,
|
||||
@ -102,6 +104,7 @@ const P8PDataGrid = ({
|
||||
return (
|
||||
<P8PTable
|
||||
columnsDef={columnsDef}
|
||||
groups={groups}
|
||||
rows={rows}
|
||||
orders={orders}
|
||||
filters={filters}
|
||||
@ -122,8 +125,10 @@ const P8PDataGrid = ({
|
||||
noDataFoundText={noDataFoundText}
|
||||
headCellRender={headCellRender}
|
||||
dataCellRender={dataCellRender}
|
||||
groupCellRender={groupCellRender}
|
||||
rowExpandRender={rowExpandRender}
|
||||
valueFormatter={valueFormatter}
|
||||
objectsCopier={objectsCopier}
|
||||
onOrderChanged={handleOrderChanged}
|
||||
onFilterChanged={handleFilterChanged}
|
||||
onPagesCountChanged={handlePagesCountChanged}
|
||||
@ -135,9 +140,10 @@ const P8PDataGrid = ({
|
||||
P8PDataGrid.propTypes = {
|
||||
columnsDef: PropTypes.array.isRequired,
|
||||
filtersInitial: PropTypes.arrayOf(P8P_DATA_GRID_FILTER_SHAPE),
|
||||
groups: PropTypes.array,
|
||||
rows: PropTypes.array.isRequired,
|
||||
size: PropTypes.string,
|
||||
morePages: PropTypes.bool.isRequired,
|
||||
morePages: PropTypes.bool,
|
||||
reloading: PropTypes.bool.isRequired,
|
||||
expandable: PropTypes.bool,
|
||||
orderAscMenuItemCaption: PropTypes.string.isRequired,
|
||||
@ -153,6 +159,7 @@ P8PDataGrid.propTypes = {
|
||||
noDataFoundText: PropTypes.string,
|
||||
headCellRender: PropTypes.func,
|
||||
dataCellRender: PropTypes.func,
|
||||
groupCellRender: PropTypes.func,
|
||||
rowExpandRender: PropTypes.func,
|
||||
valueFormatter: 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 {
|
||||
Table,
|
||||
@ -34,6 +34,7 @@ import {
|
||||
Link
|
||||
} from "@mui/material"; //Интерфейсные компоненты
|
||||
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 = {
|
||||
ORDER_TOGGLE: "ORDER_TOGGLE",
|
||||
FILTER_TOGGLE: "FILTER_TOGGLE"
|
||||
FILTER_TOGGLE: "FILTER_TOGGLE",
|
||||
EXPAND_TOGGLE: "EXPAND_TOGGLE"
|
||||
};
|
||||
|
||||
//Действия меню столбца
|
||||
@ -90,6 +92,9 @@ const STYLES = {
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0
|
||||
},
|
||||
TABLE_CELL_GROUP_HEADER: {
|
||||
backgroundColor: "lightgray"
|
||||
},
|
||||
TABLE_COLUMN_STACK: {
|
||||
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);
|
||||
let orderButton = null;
|
||||
@ -134,6 +157,7 @@ const P8PTableColumnToolBar = ({ columnDef, orders, filters, onItemClick }) => {
|
||||
<Icon>filter_alt</Icon>
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<>
|
||||
@ -143,8 +167,8 @@ const P8PTableColumnToolBar = ({ columnDef, orders, filters, onItemClick }) => {
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Панель инструментов столбца
|
||||
P8PTableColumnToolBar.propTypes = {
|
||||
//Контроль свойств - Панель инструментов столбца (правая)
|
||||
P8PTableColumnToolBarRight.propTypes = {
|
||||
columnDef: PropTypes.object.isRequired,
|
||||
orders: PropTypes.array.isRequired,
|
||||
filters: PropTypes.array.isRequired,
|
||||
@ -431,11 +455,12 @@ P8PTableFiltersChips.propTypes = {
|
||||
//Таблица
|
||||
const P8PTable = ({
|
||||
columnsDef,
|
||||
groups = [{}],
|
||||
rows,
|
||||
orders,
|
||||
filters,
|
||||
size,
|
||||
morePages,
|
||||
morePages = false,
|
||||
reloading,
|
||||
expandable,
|
||||
orderAscMenuItemCaption,
|
||||
@ -451,18 +476,26 @@ const P8PTable = ({
|
||||
noDataFoundText,
|
||||
headCellRender,
|
||||
dataCellRender,
|
||||
groupCellRender,
|
||||
rowExpandRender,
|
||||
valueFormatter,
|
||||
onOrderChanged,
|
||||
onFilterChanged,
|
||||
onPagesCountChanged
|
||||
onPagesCountChanged,
|
||||
objectsCopier
|
||||
}) => {
|
||||
//Собственное состояние - описание заголовка
|
||||
const [header, dispatchHeaderAction] = useReducer(p8pTableReducer, HEADER_INITIAL_STATE());
|
||||
|
||||
//Собственное состояние - фильтруемая колонка
|
||||
const [filterColumn, setFilterColumn] = useState(null);
|
||||
|
||||
//Собственное состояние - развёрнутые строки
|
||||
const [expanded, setExpanded] = useState({});
|
||||
|
||||
//Собственное состояния - развёрнутые группы
|
||||
const [expandedGroups, setExpandedGroups] = useState({});
|
||||
|
||||
//Собственное состояние - колонка с отображаемой подсказкой
|
||||
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 =>
|
||||
dataType === P8P_TABLE_DATA_TYPE.DATE ? "center" : dataType === P8P_TABLE_DATA_TYPE.NUMB ? "right" : "left";
|
||||
const getAlignByDataType = ({ dataType, hasChild }) =>
|
||||
dataType === P8P_TABLE_DATA_TYPE.DATE || hasChild ? "center" : dataType === P8P_TABLE_DATA_TYPE.NUMB ? "right" : "left";
|
||||
|
||||
//Упорядочение содержимого в зависимости от типа данных
|
||||
const getJustifyContentByDataType = dataType =>
|
||||
dataType === P8P_TABLE_DATA_TYPE.DATE ? "center" : dataType === P8P_TABLE_DATA_TYPE.NUMB ? "flex-end" : "flex-start";
|
||||
const getJustifyContentByDataType = ({ dataType, hasChild }) =>
|
||||
dataType === P8P_TABLE_DATA_TYPE.DATE || hasChild ? "center" : dataType === P8P_TABLE_DATA_TYPE.NUMB ? "flex-end" : "flex-start";
|
||||
|
||||
//Отработка нажатия на элемент пункта меню
|
||||
const handleToolBarItemClick = (action, columnName) => {
|
||||
@ -511,6 +546,9 @@ const P8PTable = ({
|
||||
case P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.FILTER_TOGGLE:
|
||||
setFilterColumn(columnName);
|
||||
break;
|
||||
case P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.EXPAND_TOGGLE:
|
||||
toggleHeaderExpand({ columnName, objectsCopier });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@ -579,6 +617,45 @@ const P8PTable = ({
|
||||
if (reloading) setExpanded({});
|
||||
}, [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 (
|
||||
<>
|
||||
@ -616,98 +693,115 @@ const P8PTable = ({
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={STYLES.TABLE} size={size || P8P_TABLE_SIZE.MEDIUM}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{expandable && rowExpandRender ? <TableCell key="head-cell-expand-control" align="center"></TableCell> : null}
|
||||
{visibleColumns.map((columnDef, j) => {
|
||||
let customRender = {};
|
||||
if (headCellRender) customRender = headCellRender({ columnDef }) || {};
|
||||
return (
|
||||
<TableCell
|
||||
key={`head-cell-${j}`}
|
||||
align={getAlignByDataType(columnDef.dataType)}
|
||||
sx={{ ...customRender.cellStyle }}
|
||||
{...customRender.cellProps}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent={getJustifyContentByDataType(columnDef.dataType)}
|
||||
sx={{ ...STYLES.TABLE_COLUMN_STACK, ...customRender.stackStyle }}
|
||||
{...customRender.stackProps}
|
||||
{header.displayLevels.map(level => (
|
||||
<TableRow key={level}>
|
||||
{expandable && rowExpandRender ? <TableCell key="head-cell-expand-control" align="center"></TableCell> : null}
|
||||
{header.displayLevelsColumns[level].map((columnDef, j) => {
|
||||
let customRender = {};
|
||||
if (headCellRender) customRender = headCellRender({ columnDef }) || {};
|
||||
return (
|
||||
<TableCell
|
||||
key={`head-cell-${j}`}
|
||||
align={getAlignByDataType(columnDef)}
|
||||
sx={{ ...customRender.cellStyle }}
|
||||
rowSpan={columnDef.rowSpan}
|
||||
colSpan={columnDef.colSpan}
|
||||
{...customRender.cellProps}
|
||||
>
|
||||
{customRender.data ? (
|
||||
customRender.data
|
||||
) : columnDef.hint ? (
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
align="left"
|
||||
underline="always"
|
||||
onClick={() => handleColumnShowHintClick(columnDef.name)}
|
||||
>
|
||||
{columnDef.caption}
|
||||
</Link>
|
||||
) : (
|
||||
columnDef.caption
|
||||
)}
|
||||
<P8PTableColumnToolBar
|
||||
columnDef={columnDef}
|
||||
orders={orders}
|
||||
filters={filters}
|
||||
onItemClick={handleToolBarItemClick}
|
||||
/>
|
||||
<P8PTableColumnMenu
|
||||
columnDef={columnDef}
|
||||
orderAscItemCaption={orderAscMenuItemCaption}
|
||||
orderDescItemCaption={orderDescMenuItemCaption}
|
||||
filterItemCaption={filterMenuItemCaption}
|
||||
onItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent={getJustifyContentByDataType(columnDef)}
|
||||
sx={{ ...STYLES.TABLE_COLUMN_STACK, ...customRender.stackStyle }}
|
||||
{...customRender.stackProps}
|
||||
>
|
||||
<P8PTableColumnToolBarLeft columnDef={columnDef} onItemClick={handleToolBarItemClick} />
|
||||
{customRender.data ? (
|
||||
customRender.data
|
||||
) : columnDef.hint ? (
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
align="left"
|
||||
underline="always"
|
||||
onClick={() => handleColumnShowHintClick(columnDef.name)}
|
||||
>
|
||||
{columnDef.caption}
|
||||
</Link>
|
||||
) : (
|
||||
columnDef.caption
|
||||
)}
|
||||
<P8PTableColumnToolBarRight
|
||||
columnDef={columnDef}
|
||||
orders={orders}
|
||||
filters={filters}
|
||||
onItemClick={handleToolBarItemClick}
|
||||
/>
|
||||
<P8PTableColumnMenu
|
||||
columnDef={columnDef}
|
||||
orderAscItemCaption={orderAscMenuItemCaption}
|
||||
orderDescItemCaption={orderDescMenuItemCaption}
|
||||
filterItemCaption={filterMenuItemCaption}
|
||||
onItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.length > 0
|
||||
? rows.map((row, i) => (
|
||||
<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">
|
||||
<IconButton onClick={() => handleExpandClick(i)}>
|
||||
<Icon>{expanded[i] === true ? "keyboard_arrow_down" : "keyboard_arrow_right"}</Icon>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
) : null}
|
||||
{visibleColumns.map((columnDef, j) => {
|
||||
let customRender = {};
|
||||
if (dataCellRender) customRender = dataCellRender({ row, columnDef }) || {};
|
||||
return (
|
||||
<TableCell
|
||||
key={`data-cell-${j}`}
|
||||
align={getAlignByDataType(columnDef.dataType)}
|
||||
sx={{ ...customRender.cellStyle }}
|
||||
{...customRender.cellProps}
|
||||
>
|
||||
{customRender.data
|
||||
? customRender.data
|
||||
: valueFormatter
|
||||
? valueFormatter({ value: row[columnDef.name], columnDef })
|
||||
: row[columnDef.name]}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
{expandable && rowExpandRender && expanded[i] === true ? (
|
||||
<TableRow key={`data-row-expand-${i}`}>
|
||||
<TableCell sx={STYLES.TABLE_CELL_EXPAND_CONTAINER} colSpan={visibleColumnsCount}>
|
||||
{rowExpandRender({ columnsDef, row })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
))
|
||||
? groups.map((group, g) => {
|
||||
const rowsView = rows.map((row, i) =>
|
||||
!group?.name || group?.name == row.groupName ? (
|
||||
<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">
|
||||
<IconButton onClick={() => handleExpandClick(i)}>
|
||||
<Icon>{expanded[i] === true ? "keyboard_arrow_down" : "keyboard_arrow_right"}</Icon>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
) : null}
|
||||
{header.displayDataColumns.map((columnDef, j) => {
|
||||
let customRender = {};
|
||||
if (dataCellRender) customRender = dataCellRender({ row, columnDef }) || {};
|
||||
return (
|
||||
<TableCell
|
||||
key={`data-cell-${j}`}
|
||||
align={getAlignByDataType(columnDef)}
|
||||
sx={{ ...customRender.cellStyle }}
|
||||
{...customRender.cellProps}
|
||||
>
|
||||
{customRender.data
|
||||
? customRender.data
|
||||
: valueFormatter
|
||||
? valueFormatter({ value: row[columnDef.name], columnDef })
|
||||
: row[columnDef.name]}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
{expandable && rowExpandRender && expanded[i] === true ? (
|
||||
<TableRow key={`data-row-expand-${i}`}>
|
||||
<TableCell sx={STYLES.TABLE_CELL_EXPAND_CONTAINER} colSpan={header.displayDataColumnsCount}>
|
||||
{rowExpandRender({ columnsDef, row })}
|
||||
</TableCell>
|
||||
</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}
|
||||
</TableBody>
|
||||
</Table>
|
||||
@ -736,9 +830,21 @@ P8PTable.propTypes = {
|
||||
order: PropTypes.bool.isRequired,
|
||||
filter: PropTypes.bool.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,
|
||||
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,
|
||||
orders: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
@ -748,7 +854,7 @@ P8PTable.propTypes = {
|
||||
).isRequired,
|
||||
filters: PropTypes.arrayOf(P8P_TABLE_FILTER_SHAPE).isRequired,
|
||||
size: PropTypes.string,
|
||||
morePages: PropTypes.bool.isRequired,
|
||||
morePages: PropTypes.bool,
|
||||
reloading: PropTypes.bool.isRequired,
|
||||
expandable: PropTypes.bool,
|
||||
orderAscMenuItemCaption: PropTypes.string.isRequired,
|
||||
@ -764,11 +870,13 @@ P8PTable.propTypes = {
|
||||
noDataFoundText: PropTypes.string,
|
||||
headCellRender: PropTypes.func,
|
||||
dataCellRender: PropTypes.func,
|
||||
groupCellRender: PropTypes.func,
|
||||
rowExpandRender: PropTypes.func,
|
||||
valueFormatter: PropTypes.func,
|
||||
onOrderChanged: 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.XCOLUMNS_DEF",
|
||||
"XRESPOND.XPAYLOAD.XCOLUMNS_DEF.values",
|
||||
"XRESPOND.XPAYLOAD.XGROUPS",
|
||||
"XRESPOND.XPAYLOAD.XGANTT_DEF.taskAttributes",
|
||||
"XRESPOND.XPAYLOAD.XGANTT_DEF.taskColors",
|
||||
"XRESPOND.XPAYLOAD.XGANTT_TASKS",
|
||||
|
Loading…
x
Reference in New Issue
Block a user