From 4b5938e3b15dbf45f01731f3881e600778dc5ed5 Mon Sep 17 00:00:00 2001 From: Mikhail Chechnev Date: Sat, 4 May 2024 14:17:24 +0300 Subject: [PATCH] =?UTF-8?q?WEB=20APP:=20P8PDataGrid=20-=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B0=20=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D1=81=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=B8?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/p8p_data_grid.js | 6 + app/components/p8p_table.js | 164 ++++++++++++++++++++-------- app/components/p8p_table_reducer.js | 62 ++++++++--- 3 files changed, 174 insertions(+), 58 deletions(-) diff --git a/app/components/p8p_data_grid.js b/app/components/p8p_data_grid.js index 7ccfd73..51c1416 100644 --- a/app/components/p8p_data_grid.js +++ b/app/components/p8p_data_grid.js @@ -35,6 +35,8 @@ const P8PDataGrid = ({ groups, rows, size, + fixedHeader = false, + fixedColumns = 0, morePages = false, reloading, expandable, @@ -111,6 +113,8 @@ const P8PDataGrid = ({ orders={orders} filters={filters} size={size || P8P_DATA_GRID_SIZE.MEDIUM} + fixedHeader={fixedHeader} + fixedColumns={fixedColumns} morePages={morePages} reloading={reloading} expandable={expandable} @@ -147,6 +151,8 @@ P8PDataGrid.propTypes = { groups: PropTypes.array, rows: PropTypes.array.isRequired, size: PropTypes.string, + fixedHeader: PropTypes.bool, + fixedColumns: PropTypes.number, morePages: PropTypes.bool, reloading: PropTypes.bool.isRequired, expandable: PropTypes.bool, diff --git a/app/components/p8p_table.js b/app/components/p8p_table.js index b81947d..9d77a0a 100644 --- a/app/components/p8p_table.js +++ b/app/components/p8p_table.js @@ -33,6 +33,7 @@ import { Container, Link } from "@mui/material"; //Интерфейсные компоненты +import { useTheme } from "@mui/material/styles"; //Взаимодействие со стилями MUI import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке import { P8P_TABLE_AT, HEADER_INITIAL_STATE, hasValue, p8pTableReducer } from "./p8p_table_reducer"; //Редьюсер состояния @@ -85,9 +86,30 @@ const STYLES = { TABLE: { with: "100%" }, + TABLE_HEAD_STICKY: { + position: "sticky", + top: 0, + zIndex: 1000 + }, + TABLE_HEAD_CELL_STICKY: (theme, left) => ({ + position: "sticky", + left, + backgroundColor: theme.palette.background.default, + zIndex: 1000 + }), TABLE_ROW: { "&:last-child td, &:last-child th": { border: 0 } }, + TABLE_CELL_STICKY: (theme, left) => ({ + position: "sticky", + left, + backgroundColor: theme.palette.background.default, + zIndex: 500 + }), + TABLE_CELL_EXPAND_CONTROL: { + minWidth: "60px", + maxWidth: "60px" + }, TABLE_CELL_EXPAND_CONTAINER: { paddingBottom: 0, paddingTop: 0 @@ -95,6 +117,10 @@ const STYLES = { TABLE_CELL_GROUP_HEADER: { backgroundColor: "lightgray" }, + TABLE_CELL_GROUP_HEADER_STICKY: { + position: "sticky", + left: 0 + }, TABLE_COLUMN_STACK: { alignItems: "center" }, @@ -460,6 +486,8 @@ const P8PTable = ({ orders, filters, size, + fixedHeader = false, + fixedColumns = 0, morePages = false, reloading, expandable, @@ -501,6 +529,9 @@ const P8PTable = ({ //Собственное состояние - колонка с отображаемой подсказкой const [displayHintColumn, setDisplayHintColumn] = useState(null); + //Стили + const theme = useTheme(); + //Описание фильтруемой колонки const filterColumnDef = filterColumn ? columnsDef.find(columnDef => columnDef.name == filterColumn) || null : null; @@ -516,12 +547,12 @@ const P8PTable = ({ : ["", ""]; //Формирование заголовка таблицы - const setHeader = ({ columnsDef, expandable, objectsCopier }) => - dispatchHeaderAction({ type: P8P_TABLE_AT.SET_HEADER, payload: { columnsDef, expandable, objectsCopier } }); + const setHeader = ({ columnsDef, expandable, fixedColumns, objectsCopier }) => + dispatchHeaderAction({ type: P8P_TABLE_AT.SET_HEADER, payload: { columnsDef, expandable, fixedColumns, objectsCopier } }); //Сворачивание/разворачивание уровня заголовка таблицы const toggleHeaderExpand = ({ columnName, objectsCopier }) => - dispatchHeaderAction({ type: P8P_TABLE_AT.TOGGLE_HEADER_EXPAND, payload: { columnName, expandable, objectsCopier } }); + dispatchHeaderAction({ type: P8P_TABLE_AT.TOGGLE_HEADER_EXPAND, payload: { columnName, expandable, fixedColumns, objectsCopier } }); //Выравнивание в зависимости от типа данных const getAlignByDataType = ({ dataType, hasChild }) => @@ -621,38 +652,46 @@ const P8PTable = ({ //При изменении описания колонок useEffect(() => { - setHeader({ columnsDef, expandable, objectsCopier }); - }, [columnsDef, expandable, objectsCopier]); + setHeader({ columnsDef, expandable, fixedColumns, objectsCopier }); + }, [columnsDef, expandable, fixedColumns, objectsCopier]); //Генерация заголовка группы const renderGroupCell = group => { let customRender = {}; if (groupCellRender) customRender = groupCellRender({ columnsDef: header.columnsDef, group }) || {}; - return ( + return header.displayDataColumns.map((columnDef, i) => ( - - {group.expandable ? ( - { - setExpandedGroups(pv => ({ ...pv, ...{ [group.name]: !pv[group.name] } })); - }} - > - {expandedGroups[group.name] ? "indeterminate_check_box" : "add_box"} - - ) : null} - {customRender.data ? customRender.data : group.caption} - + {i == 0 ? ( + + {group.expandable ? ( + { + setExpandedGroups(pv => ({ ...pv, ...{ [group.name]: !pv[group.name] } })); + }} + > + {expandedGroups[group.name] ? "indeterminate_check_box" : "add_box"} + + ) : null} + {customRender.data ? customRender.data : group.caption} + + ) : null} - ); + )); }; //Генерация содержимого return ( - <> +
{displayHintColumn ? ( ) : null} @@ -684,13 +723,22 @@ const P8PTable = ({ valueFormatter={valueFormatter} /> ) : null} - - - - {header.displayLevels.map(level => ( +
+ + {header.displayLevels.map((level, i) => ( - {expandable && rowExpandRender ? : null} + {expandable && rowExpandRender && i == 0 ? ( + + ) : null} {header.displayLevelsColumns[level].map((columnDef, j) => { let customRender = {}; if (headCellRender) customRender = headCellRender({ columnDef }) || {}; @@ -698,7 +746,11 @@ const P8PTable = ({ {expandable && rowExpandRender ? ( - + handleExpandClick(i)}> {expanded[i] === true ? "keyboard_arrow_down" : "keyboard_arrow_right"} @@ -766,7 +825,13 @@ const P8PTable = ({ {customRender.data @@ -780,7 +845,15 @@ const P8PTable = ({ {expandable && rowExpandRender && expanded[i] === true ? ( - + {rowExpandRender({ columnsDef, row })} @@ -800,19 +873,19 @@ const P8PTable = ({ : null}
- {rows.length == 0 ? ( - noDataFoundText && !reloading ? ( - - ) : null - ) : morePages ? ( - - - - ) : null}
- + {rows.length == 0 ? ( + noDataFoundText && !reloading ? ( + + ) : null + ) : morePages ? ( + + + + ) : null} +
); }; @@ -829,7 +902,8 @@ P8PTable.propTypes = { values: PropTypes.array, parent: PropTypes.string, expandable: PropTypes.bool.isRequired, - expanded: PropTypes.bool.isRequired + expanded: PropTypes.bool.isRequired, + width: PropTypes.number }) ).isRequired, groups: PropTypes.arrayOf( @@ -849,6 +923,8 @@ P8PTable.propTypes = { ).isRequired, filters: PropTypes.arrayOf(P8P_TABLE_FILTER_SHAPE).isRequired, size: PropTypes.string, + fixedHeader: PropTypes.bool, + fixedColumns: PropTypes.number, morePages: PropTypes.bool, reloading: PropTypes.bool.isRequired, expandable: PropTypes.bool, diff --git a/app/components/p8p_table_reducer.js b/app/components/p8p_table_reducer.js index 9dc4721..9d2a10a 100644 --- a/app/components/p8p_table_reducer.js +++ b/app/components/p8p_table_reducer.js @@ -19,7 +19,8 @@ const HEADER_INITIAL_STATE = () => ({ displayLevels: [], displayLevelsColumns: {}, displayDataColumnsCount: 0, - displayDataColumns: [] + displayDataColumns: [], + displayFixedColumnsCount: 0 }); //Состояние описания ячейки заголовка таблицы по умолчанию @@ -28,6 +29,8 @@ const HEADER_COLUMN_INITIAL_STATE = ({ columnDef, objectsCopier }) => { if (!hasValue(tmp.parent)) tmp.parent = ""; if (!hasValue(tmp.expandable)) tmp.expandable = false; if (!hasValue(tmp.expanded)) tmp.expanded = true; + if (!hasValue(tmp.fixed)) tmp.fixed = false; + if (!hasValue(tmp.fixedLeft)) tmp.fixedLeft = 0; return tmp; }; @@ -55,8 +58,23 @@ const getDisplayColumnColSpan = (displayTree, columnDef) => { } else return 1; }; +//Определения признака зафиксированности колонки +const getFixedColumns = (displayTree, parentFixed, parentLeft, fixedColumns) => { + if (fixedColumns) { + let left = parentLeft; + displayTree.forEach((columnDef, i) => { + left += columnDef.width; + if ((columnDef.level == 1 && i + 1 <= fixedColumns) || (columnDef.level > 1 && parentFixed)) { + columnDef.fixed = true; + columnDef.fixedLeft = left - columnDef.width; + } else columnDef.fixed = false; + if (columnDef.hasChild) getFixedColumns(columnDef.child, columnDef.fixed, columnDef.fixedLeft, fixedColumns); + }); + } +}; + //Формирование дерева отображаемых элементов заголовка -const buildDisplayTree = (columnsDef, parent, level) => { +const buildDisplayTree = (columnsDef, parent, level, expandable, fixedColumns) => { const baseBuild = (columnsDef, parent, level) => { let maxLevel = level - 1; const res = columnsDef @@ -77,6 +95,7 @@ const buildDisplayTree = (columnsDef, parent, level) => { }; const [displayTree, maxLevel] = baseBuild(columnsDef, parent, level); getDisplayColumnRowSpan(displayTree, maxLevel); + getFixedColumns(displayTree, false, expandable ? 60 : 0, fixedColumns); return [displayTree, maxLevel]; }; @@ -106,28 +125,41 @@ const buildDisplayDataColumns = (displayTree, expandable) => { return [displayDataColumns, displayDataColumns.length + (expandable === true ? 1 : 0)]; }; +//Подсчёт количества отображаемых фиксированных колонок +const getDisplayFixedColumnsCount = displayTree => { + let res = 0; + const traverseTree = displayTree => { + displayTree.forEach(columnDef => (columnDef.hasChild ? traverseTree(columnDef.child) : columnDef.fixed ? res++ : null)); + }; + traverseTree(displayTree); + return res; +}; + //Формирование описания отображаемых колонок -const buildDisplay = ({ columnsDef, expandable }) => { +const buildDisplay = ({ columnsDef, expandable, fixedColumns }) => { //Сформируем дерево отображаемых колонок заголовка - const [displayTree, maxLevel] = buildDisplayTree(columnsDef, "", 1); + const [displayTree, maxLevel] = buildDisplayTree(columnsDef, "", 1, expandable, fixedColumns); //Вытянем дерево в удобные для рендеринга структуры const [displayLevels, displayLevelsColumns] = buildDisplayLevelsColumns(displayTree, maxLevel); //Сформируем отображаемые колонки данных const [displayDataColumns, displayDataColumnsCount] = buildDisplayDataColumns(displayTree, expandable); + //Подсчитаем количество отображаемых фиксированных колонок + const displayFixedColumnsCount = getDisplayFixedColumnsCount(displayTree); //Вернём результат - return [displayLevels, displayLevelsColumns, displayDataColumns, displayDataColumnsCount]; + return [displayLevels, displayLevelsColumns, displayDataColumns, displayDataColumnsCount, displayFixedColumnsCount]; }; //Формирование описания заголовка -const buildHeaderDef = ({ columnsDef, expandable, objectsCopier }) => { +const buildHeaderDef = ({ columnsDef, expandable, fixedColumns, objectsCopier }) => { //Инициализируем результат const res = HEADER_INITIAL_STATE(); //Инициализируем внутренне описание колонок и поместим его в результат columnsDef.forEach(columnDef => res.columnsDef.push(HEADER_COLUMN_INITIAL_STATE({ columnDef, objectsCopier }))); //Добавим в результат сведения об отображаемых данных - [res.displayLevels, res.displayLevelsColumns, res.displayDataColumns, res.displayDataColumnsCount] = buildDisplay({ + [res.displayLevels, res.displayLevelsColumns, res.displayDataColumns, res.displayDataColumnsCount, res.displayFixedColumnsCount] = buildDisplay({ columnsDef: res.columnsDef, - expandable + expandable, + fixedColumns }); //Сформируем дерево отображаемых колонок заголовка //const [displayTree, maxLevel] = buildDisplayTree(res.columnsDef, "", 1); @@ -147,19 +179,20 @@ const buildHeaderDef = ({ columnsDef, expandable, objectsCopier }) => { const handlers = { //Формирование заголовка [P8P_TABLE_AT.SET_HEADER]: (state, { payload }) => { - const { columnsDef, expandable, objectsCopier } = payload; + const { columnsDef, expandable, fixedColumns, objectsCopier } = payload; return { ...state, - ...buildHeaderDef({ columnsDef, expandable, objectsCopier }) + ...buildHeaderDef({ columnsDef, expandable, fixedColumns, objectsCopier }) }; }, [P8P_TABLE_AT.TOGGLE_HEADER_EXPAND]: (state, { payload }) => { - const { columnName, expandable, objectsCopier } = payload; + const { columnName, expandable, fixedColumns, objectsCopier } = payload; const columnsDef = objectsCopier(state.columnsDef); columnsDef.forEach(columnDef => (columnDef.name == columnName ? (columnDef.expanded = !columnDef.expanded) : null)); - const [displayLevels, displayLevelsColumns, displayDataColumns, displayDataColumnsCount] = buildDisplay({ + const [displayLevels, displayLevelsColumns, displayDataColumns, displayDataColumnsCount, displayFixedColumnsCount] = buildDisplay({ columnsDef, - expandable + expandable, + fixedColumns }); //const [displayTree, maxLevel] = buildDisplayTree(columnsDef, "", 1); //const [displayLevels, displayLevelsColumns] = buildDisplayLevelsColumns(displayTree, maxLevel); @@ -170,7 +203,8 @@ const handlers = { displayLevels, displayLevelsColumns, displayDataColumns, - displayDataColumnsCount + displayDataColumnsCount, + displayFixedColumnsCount }; }, //Обработчик по умолчанию