592 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Парус 8 - Панели мониторинга
Компонент: Таблица
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState, useReducer } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Pagination,
Paper,
IconButton,
Icon,
Stack,
Button,
Container,
Link
} from "@mui/material"; //Интерфейсные компоненты
import { P8PAppInlineError, P8PHintDialog } from "../p8p_app_message"; //Встраиваемое сообщение об ошибке
import { P8P_TABLE_AT, HEADER_INITIAL_STATE, p8pTableReducer } from "./p8p_table_reducer"; //Редьюсер состояния
import { P8PTableColumnToolBarLeft } from "./p8p_table_column_toolbar_left"; //Таблица - Панель инструментов столбца (левая)
import { P8PTableColumnToolBarRight } from "./p8p_table_column_toolbar_right"; //Таблица - Панель инструментов столбца (правая)
import { P8PTableColumnMenu } from "./p8p_table_column_menu"; //Таблица - Меню столбца
import { P8PTableColumnFilterDialog } from "./p8p_table_column_filter_dialog"; //Таблица - Диалог фильтра
import { P8PTableFiltersChips } from "./p8p_table_filters_chips"; //Таблица - Сводный фильтр
import {
P8P_TABLE_SIZE,
P8P_TABLE_DATA_TYPE,
P8P_TABLE_COLUMN_ORDER_DIRECTIONS,
P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS,
P8P_TABLE_COLUMN_MENU_ACTIONS,
P8P_TABLE_FILTER_SHAPE,
P8P_TABLE_ORDER_SHAPE,
P8P_TABLE_PAGINATOR_ALIGN,
P8P_TABLE_PAGINATOR_POSITION,
P8P_TABLE_MORE_HEIGHT,
P8P_TABLE_FILTERS_HEIGHT
} from "./p8p_table_constants"; //Константы таблицы
import { P8P_TABLE_VARIANT } from "../../theme/variants/p8p_table_variants"; //Варианты таблиц
import { P8P_TABLE_HEAD_VARIANT } from "../../theme/variants/p8p_table_head_variants"; //Варианты заголовков таблиц
import { P8P_TABLE_CELL_VARIANT } from "../../theme/variants/p8p_table_cell_variants"; //Варианты ячеек таблиц
import { P8P_TABLE_ROW_VARIANT } from "../../theme/variants/p8p_table_row_variants"; //Варианты строк таблиц
import { P8P_CONTAINER_VARIANT } from "../../theme/variants/p8p_container_variants"; //Варианты контейнеров
import { P8P_PAGINATION_VARIANT } from "../../theme/variants/p8p_pagination_variants"; //Варианты пагинаторов
import { P8P_TYPOGRAPHY_VARIANT } from "../../theme/p8p_typography"; //Варианты шрифтов
//-----------
//Тело модуля
//-----------
//Таблица
const P8PTable = ({
style = {},
tableStyle = {},
columnsDef = [],
groups = [],
rows = [],
orders,
filters,
size,
pageNumber = 1,
pagesCount = 0,
pagesAlign = P8P_TABLE_PAGINATOR_ALIGN.RIGHT,
pagesPosition = P8P_TABLE_PAGINATOR_POSITION.BOTTOM,
fixedHeader = false,
fixedColumns = 0,
morePages = false,
reloading = false,
expandable,
orderAscMenuItemCaption,
orderDescMenuItemCaption,
filterMenuItemCaption,
valueFilterCaption,
valueFromFilterCaption,
valueToFilterCaption,
okFilterBtnCaption,
clearFilterBtnCaption,
cancelFilterBtnCaption,
morePagesBtnCaption,
morePagesBtnProps,
noDataFoundText,
headCellRender,
dataCellRender,
groupCellRender,
rowExpandRender,
valueFormatter,
headExpandCellStyle,
onOrderChanged,
onFilterChanged,
onPagesCountChanged,
onPageChanged,
objectsCopier,
containerComponent,
containerComponentProps
}) => {
//Собственное состояние - описание заголовка
const [header, dispatchHeaderAction] = useReducer(p8pTableReducer, HEADER_INITIAL_STATE());
//Собственное состояние - фильтруемая колонка
const [filterColumn, setFilterColumn] = useState(null);
//Собственное состояние - развёрнутые строки
const [expanded, setExpanded] = useState({});
//Собственное состояния - развёрнутые группы
const [expandedGroups, setExpandedGroups] = useState(
Array.isArray(groups) && groups.length > 0 ? Object.assign({}, ...groups.map(g => ({ [g.name]: g.expanded }))) : {}
);
//Собственное состояние - колонка с отображаемой подсказкой
const [displayHintColumn, setDisplayHintColumn] = useState(null);
//Описание фильтруемой колонки
const filterColumnDef = filterColumn ? columnsDef.find(columnDef => columnDef.name == filterColumn) || null : null;
//Описание колонки с отображаемой подсказкой
const displayHintColumnDef = displayHintColumn ? columnsDef.find(columnDef => columnDef.name == displayHintColumn) || null : null;
//Значения фильтра фильтруемой колонки
const [filterColumnFrom, filterColumnTo] = filterColumn
? (() => {
const filter = filters.find(filter => filter.name == filterColumn);
return filter ? [filter.from == null ? "" : filter.from, filter.to == null ? "" : filter.to] : ["", ""];
})()
: ["", ""];
//Формирование заголовка таблицы
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, fixedColumns, objectsCopier } });
//Выравнивание в зависимости от типа данных
const getAlignByDataType = ({ dataType, hasChild }) =>
dataType === P8P_TABLE_DATA_TYPE.DATE || hasChild ? "center" : dataType === P8P_TABLE_DATA_TYPE.NUMB ? "right" : "left";
//Упорядочение содержимого в зависимости от типа данных
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) => {
switch (action) {
case P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.ORDER_TOGGLE: {
const colOrder = orders.find(o => o.name == columnName);
const newDirection =
colOrder?.direction == P8P_TABLE_COLUMN_ORDER_DIRECTIONS.ASC
? P8P_TABLE_COLUMN_ORDER_DIRECTIONS.DESC
: colOrder?.direction == P8P_TABLE_COLUMN_ORDER_DIRECTIONS.DESC
? null
: P8P_TABLE_COLUMN_ORDER_DIRECTIONS.ASC;
if (onOrderChanged) onOrderChanged({ columnName, direction: newDirection });
break;
}
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;
}
};
//Отработка нажатия на пункты меню
const handleMenuItemClick = (action, columnName) => {
switch (action) {
case P8P_TABLE_COLUMN_MENU_ACTIONS.ORDER_ASC:
onOrderChanged({ columnName, direction: P8P_TABLE_COLUMN_ORDER_DIRECTIONS.ASC });
break;
case P8P_TABLE_COLUMN_MENU_ACTIONS.ORDER_DESC:
onOrderChanged({ columnName, direction: P8P_TABLE_COLUMN_ORDER_DIRECTIONS.DESC });
break;
case P8P_TABLE_COLUMN_MENU_ACTIONS.FILTER:
setFilterColumn(columnName);
break;
}
};
//Отработка ввода значения фильтра колонки
const handleFilterOk = (columnName, from, to) => {
if (onFilterChanged) onFilterChanged({ columnName, from: from === "" ? null : from, to: to === "" ? null : to });
setFilterColumn(null);
};
//Отработка очистки значения фильтра колонки
const handleFilterClear = columnName => {
if (onFilterChanged) onFilterChanged({ columnName, from: null, to: null });
setFilterColumn(null);
};
//Отработка отмены ввода значения фильтра колонки
const handleFilterCancel = () => {
setFilterColumn(null);
};
//Отработка нажатия на элемент сводного фильтра
const handleFilterChipClick = columnName => setFilterColumn(columnName);
//Отработка удаления элемента сводного фильтра
const handleFilterChipDelete = columnName => (onFilterChanged ? onFilterChanged({ columnName, from: null, to: null }) : null);
//Отработка нажатия на кнопку догрузки страницы
const handleMorePagesBtnClick = () => {
if (onPagesCountChanged) onPagesCountChanged();
};
//Отработка нажатия на элемент отображения подсказки по колонке
const handleColumnShowHintClick = columnName => setDisplayHintColumn(columnName);
//Отработка сокрытия подсказки по колонке
const handleHintOk = () => setDisplayHintColumn(null);
//Отработка нажатия на кнопку раскрытия элемента
const handleExpandClick = rowIndex => {
if (expanded[rowIndex] === true)
setExpanded(pv => {
let res = { ...pv };
delete res[rowIndex];
return res;
});
else setExpanded(pv => ({ ...pv, [rowIndex]: true }));
};
//Отработка изменения страницы
const handlePageChange = (e, page) => onPageChanged && onPageChanged({ page });
//При перезагрузке данных
useEffect(() => {
if (reloading) setExpanded({});
}, [reloading]);
//При изменении описания колонок
useEffect(() => {
setHeader({ columnsDef, expandable, fixedColumns, objectsCopier });
}, [columnsDef, expandable, fixedColumns, objectsCopier]);
//Генерация заголовка группы
const renderGroupCell = group => {
let customRender = {};
if (groupCellRender) customRender = groupCellRender({ columnsDef: header.columnsDef, group }) || {};
return header.displayDataColumns.map((columnDef, i) => {
return (
<TableCell
variant={P8P_TABLE_CELL_VARIANT.GROUP_HEADER}
data-variant-props={{ width: columnDef.width, fixed: i == 0 && fixedColumns }}
key={`group-header-cell-${i}`}
{...customRender.cellProps}
sx={{ ...customRender.cellStyle }}
colSpan={expandable && rowExpandRender ? 2 : 1}
>
{i == 0 ? (
<Stack direction="row" alignItems="center">
{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>
) : null}
</TableCell>
);
});
};
//Генерация области страниц
const renderPagination = position => {
//Признак отображения в конкретной области
const isVisible = [
position === P8P_TABLE_PAGINATOR_POSITION.TOP ? P8P_TABLE_PAGINATOR_POSITION.TOP : P8P_TABLE_PAGINATOR_POSITION.BOTTOM,
P8P_TABLE_PAGINATOR_POSITION.BOTH
].includes(pagesPosition);
//Генерация содержимого
return (
<>
{pagesCount && pagesCount > 0 && isVisible ? (
<Pagination
variant={P8P_PAGINATION_VARIANT.TABLE_PAGINATION}
data-variant-props={{ pagesAlign, position }}
count={pagesCount}
defaultPage={1}
page={pageNumber}
size="medium"
onChange={handlePageChange}
/>
) : null}
</>
);
};
//Генерация содержимого
return (
<div style={{ ...(style || {}) }}>
{displayHintColumn ? <P8PHintDialog title={displayHintColumnDef.caption} hint={displayHintColumnDef.hint} onOk={handleHintOk} /> : null}
{filterColumn ? (
<P8PTableColumnFilterDialog
columnDef={filterColumnDef}
from={filterColumnFrom}
to={filterColumnTo}
valueCaption={valueFilterCaption}
valueFromCaption={valueFromFilterCaption}
valueToCaption={valueToFilterCaption}
okBtnCaption={okFilterBtnCaption}
clearBtnCaption={clearFilterBtnCaption}
cancelBtnCaption={cancelFilterBtnCaption}
valueFormatter={valueFormatter}
onOk={handleFilterOk}
onClear={handleFilterClear}
onCancel={handleFilterCancel}
/>
) : null}
{Array.isArray(filters) && filters.length > 0 ? (
<P8PTableFiltersChips
filters={filters}
columnsDef={columnsDef}
valueFromCaption={valueFromFilterCaption}
valueToCaption={valueToFilterCaption}
onFilterChipClick={handleFilterChipClick}
onFilterChipDelete={handleFilterChipDelete}
valueFormatter={valueFormatter}
/>
) : null}
{renderPagination(P8P_TABLE_PAGINATOR_POSITION.TOP)}
<TableContainer component={containerComponent ? containerComponent : Paper} {...(containerComponentProps ? containerComponentProps : {})}>
<Table
variant={P8P_TABLE_VARIANT.PRIMARY}
stickyHeader={fixedHeader}
sx={{ ...(tableStyle || {}) }}
size={size || P8P_TABLE_SIZE.MEDIUM}
>
<TableHead variant={fixedHeader ? P8P_TABLE_HEAD_VARIANT.STICKY : "primary"}>
{header.displayLevels.map((level, i) => (
<TableRow key={level}>
{expandable && rowExpandRender && i == 0 ? (
<TableCell
variant={P8P_TABLE_CELL_VARIANT.HEADER_EXPAND}
data-variant-props={{ fixed: fixedColumns }}
key="head-cell-expand-control"
align="center"
sx={{ ...headExpandCellStyle }}
rowSpan={header.displayLevelsColumns[level][0].rowSpan}
></TableCell>
) : null}
{header.displayLevelsColumns[level].map((columnDef, j) => {
let customRender = {};
if (headCellRender) customRender = headCellRender({ columnDef }) || {};
return (
<TableCell
variant={P8P_TABLE_CELL_VARIANT.HEADER_CELL}
data-variant-props={{ width: columnDef.width, fixed: columnDef.fixed, left: columnDef.fixedLeft }}
key={`head-cell-${j}`}
align={getAlignByDataType(columnDef)}
sx={{
...customRender.cellStyle
}}
rowSpan={columnDef.rowSpan}
colSpan={columnDef.colSpan}
{...customRender.cellProps}
>
<Stack
direction="row"
justifyContent={getJustifyContentByDataType(columnDef)}
alignItems="center"
sx={{ ...customRender.stackStyle }}
{...customRender.stackProps}
>
<P8PTableColumnToolBarLeft columnDef={columnDef} onItemClick={handleToolBarItemClick} />
{customRender.data ? (
customRender.data
) : columnDef.hint ? (
<Link
component="button"
variant={P8P_TYPOGRAPHY_VARIANT.COLUMN}
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 ? (
(Array.isArray(groups) && groups.length > 0 ? 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}`} variant={P8P_TABLE_ROW_VARIANT.PRIMARY}>
{expandable && rowExpandRender ? (
<TableCell
variant={P8P_TABLE_CELL_VARIANT.EXPAND}
data-variant-props={{ fixed: fixedColumns }}
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
variant={P8P_TABLE_CELL_VARIANT.CELL}
data-variant-props={{
width: columnDef.width,
fixed: columnDef.fixed,
left: columnDef.fixedLeft
}}
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
variant={P8P_TABLE_CELL_VARIANT.EXPAND_CONTAINER}
data-variant-props={{ fixed: fixedColumns }}
colSpan={fixedColumns ? header.displayFixedColumnsCount + 1 : 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>
);
})
) : noDataFoundText && !reloading ? (
<TableRow>
<TableCell colSpan={header.displayDataColumnsCount}>
<P8PAppInlineError text={noDataFoundText} />
</TableCell>
</TableRow>
) : null}
</TableBody>
</Table>
</TableContainer>
{renderPagination(P8P_TABLE_PAGINATOR_POSITION.BOTTOM)}
{morePages && (!pagesCount || pagesCount <= 0) ? (
<Container variant={P8P_CONTAINER_VARIANT.TABLE_MORE_BUTTON}>
<Button fullWidth onClick={handleMorePagesBtnClick} {...(morePagesBtnProps ? morePagesBtnProps : {})}>
{morePagesBtnCaption}
</Button>
</Container>
) : null}
</div>
);
};
//Контроль свойств - Таблица
P8PTable.propTypes = {
style: PropTypes.object,
tableStyle: PropTypes.object,
columnsDef: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
caption: PropTypes.string.isRequired,
order: PropTypes.bool.isRequired,
filter: PropTypes.bool.isRequired,
dataType: PropTypes.string.isRequired,
visible: PropTypes.bool.isRequired,
values: PropTypes.array,
parent: PropTypes.string,
expandable: PropTypes.bool.isRequired,
expanded: PropTypes.bool.isRequired,
width: PropTypes.number
})
).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(P8P_TABLE_ORDER_SHAPE).isRequired,
filters: PropTypes.arrayOf(P8P_TABLE_FILTER_SHAPE).isRequired,
size: PropTypes.string,
pageNumber: PropTypes.number,
pagesCount: PropTypes.number,
pagesAlign: PropTypes.string,
pagesPosition: PropTypes.string,
fixedHeader: PropTypes.bool,
fixedColumns: PropTypes.number,
morePages: PropTypes.bool,
reloading: PropTypes.bool,
expandable: PropTypes.bool,
orderAscMenuItemCaption: PropTypes.string.isRequired,
orderDescMenuItemCaption: PropTypes.string.isRequired,
filterMenuItemCaption: PropTypes.string.isRequired,
valueFilterCaption: PropTypes.string.isRequired,
valueFromFilterCaption: PropTypes.string.isRequired,
valueToFilterCaption: PropTypes.string.isRequired,
okFilterBtnCaption: PropTypes.string.isRequired,
clearFilterBtnCaption: PropTypes.string.isRequired,
cancelFilterBtnCaption: PropTypes.string.isRequired,
morePagesBtnCaption: PropTypes.string.isRequired,
morePagesBtnProps: PropTypes.object,
noDataFoundText: PropTypes.string,
headCellRender: PropTypes.func,
dataCellRender: PropTypes.func,
groupCellRender: PropTypes.func,
rowExpandRender: PropTypes.func,
valueFormatter: PropTypes.func,
headExpandCellStyle: PropTypes.object,
onOrderChanged: PropTypes.func,
onFilterChanged: PropTypes.func,
onPagesCountChanged: PropTypes.func,
onPageChanged: PropTypes.func,
objectsCopier: PropTypes.func.isRequired,
containerComponent: PropTypes.oneOfType([PropTypes.elementType, PropTypes.string]),
containerComponentProps: PropTypes.object
};
//----------------
//Интерфейс модуля
//----------------
export {
P8P_TABLE_DATA_TYPE,
P8P_TABLE_SIZE,
P8P_TABLE_FILTER_SHAPE,
P8P_TABLE_ORDER_SHAPE,
P8P_TABLE_MORE_HEIGHT,
P8P_TABLE_FILTERS_HEIGHT,
P8P_TABLE_PAGINATOR_ALIGN,
P8P_TABLE_PAGINATOR_POSITION,
P8PTable
};