From 88984a78040863e7d6891a6146bec1b871a882e0 Mon Sep 17 00:00:00 2001 From: Mikhail Chechnev Date: Sat, 3 Feb 2024 01:28:06 +0300 Subject: [PATCH] =?UTF-8?q?WEB=20APP:=20P8PDataGrif=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=81=D0=BB=D0=BE?= =?UTF-8?q?=D0=B6=D0=BD=D1=8B=D1=85=20=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=BE=D0=B2=20=D1=81=20=D0=B3=D1=80=D1=83=D0=BF?= =?UTF-8?q?=D0=BF=D0=B0=D0=BC=D0=B8,=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/p8p_data_grid.js | 11 +- app/components/p8p_table.js | 326 ++++++++++++++++++---------- app/components/p8p_table_reducer.js | 193 ++++++++++++++++ app/core/client.js | 1 + 4 files changed, 420 insertions(+), 111 deletions(-) create mode 100644 app/components/p8p_table_reducer.js diff --git a/app/components/p8p_data_grid.js b/app/components/p8p_data_grid.js index 87d20db..98525be 100644 --- a/app/components/p8p_data_grid.js +++ b/app/components/p8p_data_grid.js @@ -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 ( typeof value !== "undefined" && value !== null && value !== ""; +//Панель инструментов столбца (левая) +const P8PTableColumnToolBarLeft = ({ columnDef, onItemClick }) => { + //Кнопка развёртывания/свёртывания + let expButton = null; + if (columnDef.expandable) + expButton = ( + (onItemClick ? onItemClick(P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.EXPAND_TOGGLE, columnDef.name) : null)}> + {columnDef.expanded ? "indeterminate_check_box" : "add_box"} + + ); -//Панель инструментов столбца -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 }) => { filter_alt ); + //Генерация содержимого 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 ( + + + {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} + + + ); + }; + //Генерация содержимого return ( <> @@ -616,98 +693,115 @@ const P8PTable = ({ - - {expandable && rowExpandRender ? : null} - {visibleColumns.map((columnDef, j) => { - let customRender = {}; - if (headCellRender) customRender = headCellRender({ columnDef }) || {}; - return ( - - ( + + {expandable && rowExpandRender ? : null} + {header.displayLevelsColumns[level].map((columnDef, j) => { + let customRender = {}; + if (headCellRender) customRender = headCellRender({ columnDef }) || {}; + return ( + - {customRender.data ? ( - customRender.data - ) : columnDef.hint ? ( - handleColumnShowHintClick(columnDef.name)} - > - {columnDef.caption} - - ) : ( - columnDef.caption - )} - - - - - ); - })} - + + + {customRender.data ? ( + customRender.data + ) : columnDef.hint ? ( + handleColumnShowHintClick(columnDef.name)} + > + {columnDef.caption} + + ) : ( + columnDef.caption + )} + + + + + ); + })} + + ))} {rows.length > 0 - ? rows.map((row, i) => ( - - - {expandable && rowExpandRender ? ( - - handleExpandClick(i)}> - {expanded[i] === true ? "keyboard_arrow_down" : "keyboard_arrow_right"} - - - ) : null} - {visibleColumns.map((columnDef, j) => { - let customRender = {}; - if (dataCellRender) customRender = dataCellRender({ row, columnDef }) || {}; - return ( - - {customRender.data - ? customRender.data - : valueFormatter - ? valueFormatter({ value: row[columnDef.name], columnDef }) - : row[columnDef.name]} - - ); - })} - - {expandable && rowExpandRender && expanded[i] === true ? ( - - - {rowExpandRender({ columnsDef, row })} - - - ) : null} - - )) + ? groups.map((group, g) => { + const rowsView = rows.map((row, i) => + !group?.name || group?.name == row.groupName ? ( + + + {expandable && rowExpandRender ? ( + + handleExpandClick(i)}> + {expanded[i] === true ? "keyboard_arrow_down" : "keyboard_arrow_right"} + + + ) : null} + {header.displayDataColumns.map((columnDef, j) => { + let customRender = {}; + if (dataCellRender) customRender = dataCellRender({ row, columnDef }) || {}; + return ( + + {customRender.data + ? customRender.data + : valueFormatter + ? valueFormatter({ value: row[columnDef.name], columnDef }) + : row[columnDef.name]} + + ); + })} + + {expandable && rowExpandRender && expanded[i] === true ? ( + + + {rowExpandRender({ columnsDef, row })} + + + ) : null} + + ) : null + ); + return !group?.name ? ( + rowsView + ) : ( + + {renderGroupCell(group)} + {!group.expandable || expandedGroups[group.name] === true ? rowsView : null} + + ); + }) : null}
@@ -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 }; //---------------- diff --git a/app/components/p8p_table_reducer.js b/app/components/p8p_table_reducer.js new file mode 100644 index 0000000..9dc4721 --- /dev/null +++ b/app/components/p8p_table_reducer.js @@ -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); +}; diff --git a/app/core/client.js b/app/core/client.js index 07eead5..b60ebf8 100644 --- a/app/core/client.js +++ b/app/core/client.js @@ -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",