forked from CITKParus/P8-Panels
820 lines
34 KiB
JavaScript
820 lines
34 KiB
JavaScript
/*
|
||
Парус 8 - Панели мониторинга
|
||
Компонент: Циклограмма
|
||
*/
|
||
|
||
//---------------------
|
||
//Подключение библиотек
|
||
//---------------------
|
||
|
||
import React, { useEffect, useState, useRef } from "react"; //Классы React
|
||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||
import {
|
||
Box,
|
||
Typography,
|
||
Dialog,
|
||
DialogActions,
|
||
DialogContent,
|
||
Button,
|
||
List,
|
||
ListItem,
|
||
ListItemText,
|
||
Link,
|
||
Divider,
|
||
IconButton,
|
||
Icon
|
||
} from "@mui/material"; //Интерфейсные компоненты
|
||
import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
|
||
import { hasValue } from "../core/utils"; //Вспомогательный функции
|
||
|
||
//---------
|
||
//Константы
|
||
//---------
|
||
|
||
//Уровни масштаба
|
||
const P8P_CYCLOGRAM_ZOOM = [0.2, 0.4, 0.7, 1, 1.5, 2, 2.5];
|
||
|
||
//Параметры элементов циклограммы
|
||
const NDEFAULT_LINE_HEIGHT = 20;
|
||
const NDEFAULT_HEADER_HEIGHT = 35;
|
||
|
||
//Высота заголовка
|
||
const TITLE_HEIGHT = "44px";
|
||
|
||
//Высота панели масштабирования
|
||
const ZOOM_HEIGHT = "56px";
|
||
|
||
//Стили
|
||
const STYLES = {
|
||
CYCLOGRAM_TITLE: { height: TITLE_HEIGHT },
|
||
CYCLOGRAM_ZOOM: { height: ZOOM_HEIGHT },
|
||
HEADER_COLUMN: {
|
||
fontSize: "12px",
|
||
textOverflow: "ellipsis",
|
||
overflow: "hidden",
|
||
whiteSpace: "pre",
|
||
textAlign: "center",
|
||
lineHeight: "3",
|
||
padding: "0px 5px"
|
||
},
|
||
CYCLOGRAM_BOX: (noData, title, zoomBar) => ({
|
||
position: "relative",
|
||
overflow: "auto",
|
||
padding: "0px 8px",
|
||
height: `calc(100% - ${zoomBar ? ZOOM_HEIGHT : "0px"} - ${title ? TITLE_HEIGHT : "0px"})`,
|
||
display: noData ? "none" : ""
|
||
}),
|
||
GRID_ROW: index => (index % 2 === 0 ? { backgroundColor: "#ffffff" } : { backgroundColor: "#f5f5f5" }),
|
||
GROUP_HEADER_BOX: {
|
||
border: "1px solid",
|
||
backgroundColor: "#ebebeb",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center"
|
||
},
|
||
GROUP_HEADER: {
|
||
fontSize: "14px",
|
||
textAlign: "center",
|
||
wordWrap: "break-word"
|
||
},
|
||
TASK_EDITOR_CONTENT: { minWidth: 400, overflowX: "auto" },
|
||
TASK_EDITOR_LIST: { width: "100%", minWidth: 300, maxWidth: 700, bgcolor: "background.paper" },
|
||
TASK_BOX: (lineHeight, bgColor, textColor, highlightColor) => ({
|
||
display: "flex",
|
||
alignItems: "center",
|
||
backgroundColor: bgColor ? bgColor : "#b4b9bf",
|
||
...(textColor ? { color: textColor } : {}),
|
||
height: lineHeight,
|
||
"&:hover": {
|
||
...(highlightColor
|
||
? { backgroundColor: `${highlightColor} !important`, filter: "brightness(1) !important" }
|
||
: { filter: "brightness(1.25) !important" }),
|
||
cursor: "pointer !important"
|
||
}
|
||
}),
|
||
TASK: lineHeight => {
|
||
const availableLines = Math.floor(lineHeight / 18);
|
||
return {
|
||
width: "100%",
|
||
fontSize: "12px",
|
||
overflowWrap: "break-word",
|
||
wordBreak: "break-all",
|
||
overflow: "hidden",
|
||
textOverflow: "ellipsis",
|
||
display: "-webkit-box",
|
||
lineHeight: "18px",
|
||
maxHeight: lineHeight,
|
||
WebkitLineClamp: availableLines < 1 ? 1 : availableLines,
|
||
WebkitBoxOrient: "vertical"
|
||
};
|
||
}
|
||
};
|
||
|
||
//Структура колонки
|
||
const P8P_CYCLOGRAM_COLUMN_SHAPE = PropTypes.shape({
|
||
name: PropTypes.string.isRequired,
|
||
start: PropTypes.number.isRequired,
|
||
end: PropTypes.number.isRequired
|
||
});
|
||
|
||
//Структура группы
|
||
const P8P_CYCLOGRAM_GROUP_SHAPE = PropTypes.shape({
|
||
name: PropTypes.string.isRequired,
|
||
height: PropTypes.number.isRequired,
|
||
width: PropTypes.number.isRequired,
|
||
visible: PropTypes.bool.isRequired
|
||
});
|
||
|
||
//Структура задачи
|
||
const P8P_CYCLOGRAM_TASK_SHAPE = PropTypes.shape({
|
||
id: PropTypes.string.isRequired,
|
||
rn: PropTypes.number.isRequired,
|
||
name: PropTypes.string.isRequired,
|
||
fullName: PropTypes.string.isRequired,
|
||
lineNumb: PropTypes.number.isRequired,
|
||
start: PropTypes.number.isRequired,
|
||
end: PropTypes.number.isRequired,
|
||
group: PropTypes.string,
|
||
bgColor: PropTypes.string,
|
||
textColor: PropTypes.string,
|
||
highlightColor: PropTypes.string
|
||
});
|
||
|
||
//Структура динамического атрибута задачи
|
||
const P8P_CYCLOGRAM_TASK_ATTRIBUTE_SHAPE = PropTypes.shape({
|
||
name: PropTypes.string.isRequired,
|
||
caption: PropTypes.string.isRequired,
|
||
visible: PropTypes.bool.isRequired
|
||
});
|
||
|
||
//--------------------------------
|
||
//Вспомогательные классы и функции
|
||
//--------------------------------
|
||
|
||
//Определение сдвига для максимальной ширины колонок
|
||
const getShift = (columns, currentColumnsMaxWidth, maxCyclogramWidth) => {
|
||
//Определяем доступное пространство для расширения
|
||
let maxWidthDiff = maxCyclogramWidth - currentColumnsMaxWidth;
|
||
//Инициализируем значение сдвига
|
||
let shift = 1;
|
||
//Если доступно больше ширины и есть пространство для расширения
|
||
if (maxCyclogramWidth > currentColumnsMaxWidth && maxCyclogramWidth - maxWidthDiff > columns.length) {
|
||
//Определяем доступный сдвиг колонок
|
||
shift = maxCyclogramWidth / currentColumnsMaxWidth;
|
||
}
|
||
//Возвращаем сдвиг
|
||
return shift;
|
||
};
|
||
|
||
//Формирование стилей для группы
|
||
const getGroupStyles = (indexGrp, highlightColor) => {
|
||
return `.main .TaskGrp${indexGrp}:hover .TaskGrp${indexGrp} {
|
||
${highlightColor ? `background: ${highlightColor};` : `filter: brightness(1.15);`}
|
||
}
|
||
.main:has(.TaskGrp${indexGrp}:hover) .TaskGrpHeader${indexGrp} {
|
||
display: block;
|
||
}
|
||
`;
|
||
//cursor: pointer;
|
||
};
|
||
|
||
//Фон строк таблицы
|
||
const P8PCyclogramRowsGrid = ({ rows, maxWidth, lineHeight }) => {
|
||
return (
|
||
<g>
|
||
{rows.map((el, index) => (
|
||
<foreignObject x="0" y={NDEFAULT_HEADER_HEIGHT + index * lineHeight} width={maxWidth} height={lineHeight} key={index}>
|
||
<Box sx={STYLES.GRID_ROW(index)} height={lineHeight} />
|
||
</foreignObject>
|
||
))}
|
||
</g>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Фон строк таблицы
|
||
P8PCyclogramRowsGrid.propTypes = {
|
||
rows: PropTypes.array.isRequired,
|
||
maxWidth: PropTypes.number.isRequired,
|
||
lineHeight: PropTypes.number.isRequired
|
||
};
|
||
|
||
//Линии строк таблицы
|
||
const P8PCyclogramRowsLines = ({ rows, maxWidth, lineHeight }) => {
|
||
return (
|
||
<g>
|
||
{rows.map((el, index) => (
|
||
<line
|
||
x1="0"
|
||
y1={NDEFAULT_HEADER_HEIGHT + lineHeight + index * lineHeight}
|
||
x2={maxWidth}
|
||
y2={NDEFAULT_HEADER_HEIGHT + lineHeight + index * lineHeight}
|
||
key={index}
|
||
></line>
|
||
))}
|
||
</g>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Линии строк таблицы
|
||
P8PCyclogramRowsLines.propTypes = {
|
||
rows: PropTypes.array.isRequired,
|
||
maxWidth: PropTypes.number.isRequired,
|
||
lineHeight: PropTypes.number.isRequired
|
||
};
|
||
|
||
//Линии колонок таблицы
|
||
const P8PCyclogramColumnsLines = ({ columns, shift, y1, y2 }) => {
|
||
//Инициализируем старт текущей колонки
|
||
let prevColumnEnd = 0;
|
||
return (
|
||
<g>
|
||
{columns.map((column, index) => {
|
||
//Аккумулируем окончание последней колонки с учетом сдвига
|
||
prevColumnEnd = index !== 0 ? prevColumnEnd + (columns[index - 1].end - columns[index - 1].start) * shift : 0;
|
||
return <line x1={prevColumnEnd} y1={y1} x2={prevColumnEnd} y2={y2} stroke="#e0e0e0" key={index} />;
|
||
})}
|
||
<line
|
||
x1={prevColumnEnd + (columns[columns.length - 1].end - columns[columns.length - 1].start) * shift}
|
||
y1={y1}
|
||
x2={prevColumnEnd + (columns[columns.length - 1].end - columns[columns.length - 1].start) * shift}
|
||
y2={y2}
|
||
stroke="#e0e0e0"
|
||
/>
|
||
</g>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Линии колонок таблицы
|
||
P8PCyclogramColumnsLines.propTypes = {
|
||
columns: PropTypes.array.isRequired,
|
||
shift: PropTypes.number.isRequired,
|
||
y1: PropTypes.number.isRequired,
|
||
y2: PropTypes.number.isRequired
|
||
};
|
||
|
||
//Фон таблицы циклограммы
|
||
const P8PCyclogramGrid = ({ tasks, columns, shift, maxWidth, maxHeight, lineHeight }) => {
|
||
//Формируем массив строк исходя из максимального значения строки задачи
|
||
const rows = Array.from(Array(Math.max(...tasks.map(o => o.lineNumb)) + 1).keys());
|
||
return (
|
||
<g className="grid">
|
||
<rect x="0" y="0" width={maxWidth} height={maxHeight}></rect>
|
||
<P8PCyclogramRowsGrid rows={rows} maxWidth={maxWidth} lineHeight={lineHeight} />
|
||
<P8PCyclogramRowsLines rows={rows} maxWidth={maxWidth} lineHeight={lineHeight} />
|
||
<P8PCyclogramColumnsLines columns={columns} shift={shift} y1={NDEFAULT_HEADER_HEIGHT} y2={maxHeight} />
|
||
</g>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Фон таблицы циклограммы
|
||
P8PCyclogramGrid.propTypes = {
|
||
tasks: PropTypes.array.isRequired,
|
||
columns: PropTypes.array.isRequired,
|
||
shift: PropTypes.number.isRequired,
|
||
maxWidth: PropTypes.number.isRequired,
|
||
maxHeight: PropTypes.number.isRequired,
|
||
lineHeight: PropTypes.number.isRequired
|
||
};
|
||
|
||
//Колонка заголовка циклограммы
|
||
const P8PCyclogramHeaderColumn = ({ column, start, shift, columnRenderer }) => {
|
||
//Рассчитываем ширину колонки
|
||
const columnWidth = column.end - column.start;
|
||
//Формируем собственное отображение, если требуется
|
||
const customView = columnRenderer ? columnRenderer({ column }) : null;
|
||
return (
|
||
<>
|
||
<foreignObject x={start} y="0" width={columnWidth * shift} height={NDEFAULT_HEADER_HEIGHT}>
|
||
{customView ? (
|
||
customView
|
||
) : (
|
||
<Typography sx={{ ...STYLES.HEADER_COLUMN, height: NDEFAULT_HEADER_HEIGHT }} title={column.name}>
|
||
{column.name}
|
||
</Typography>
|
||
)}
|
||
</foreignObject>
|
||
</>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Колонка заголовка циклограммы
|
||
P8PCyclogramHeaderColumn.propTypes = {
|
||
column: PropTypes.object.isRequired,
|
||
start: PropTypes.number.isRequired,
|
||
shift: PropTypes.number.isRequired,
|
||
maxHeight: PropTypes.number.isRequired,
|
||
lastElement: PropTypes.bool,
|
||
columnRenderer: PropTypes.func
|
||
};
|
||
|
||
//Заголовок циклограммы
|
||
const P8PCyclogramHeader = ({ columns, shift, maxWidth, maxHeight, columnRenderer, headerBlock }) => {
|
||
//Инициализируем старт текущей колонки
|
||
let prevColumnEnd = 0;
|
||
return (
|
||
<g className="header" ref={headerBlock}>
|
||
<rect x="0" y="0" width={maxWidth} height={NDEFAULT_HEADER_HEIGHT} fill="#ffffff" stroke="#e0e0e0" strokeWidth="1.4"></rect>
|
||
{columns.map((column, index) => {
|
||
//Аккумулируем окончание последней колонки с учетом сдвига
|
||
prevColumnEnd = index !== 0 ? prevColumnEnd + (columns[index - 1].end - columns[index - 1].start) * shift : 0;
|
||
return (
|
||
<P8PCyclogramHeaderColumn
|
||
column={column}
|
||
shift={shift}
|
||
start={prevColumnEnd}
|
||
maxHeight={maxHeight}
|
||
lastElement={columns.length - 1 === index}
|
||
columnRenderer={columnRenderer}
|
||
key={index}
|
||
/>
|
||
);
|
||
})}
|
||
<g className="columnsDividers">
|
||
<P8PCyclogramColumnsLines columns={columns} shift={shift} y1={0} y2={NDEFAULT_HEADER_HEIGHT} />
|
||
</g>
|
||
</g>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Заголовок циклограммы
|
||
P8PCyclogramHeader.propTypes = {
|
||
columns: PropTypes.array.isRequired,
|
||
shift: PropTypes.number.isRequired,
|
||
maxWidth: PropTypes.number.isRequired,
|
||
maxHeight: PropTypes.number.isRequired,
|
||
columnRenderer: PropTypes.func,
|
||
headerBlock: PropTypes.object
|
||
};
|
||
|
||
//Задача циклограммы
|
||
const P8PCyclogramTask = ({ task, indexGrp, shift, lineHeight, openTaskEditor, taskRenderer }) => {
|
||
//Рассчитываем ширину задачи
|
||
const width = task.end !== 0 ? (task.end - task.start) * shift : 0;
|
||
//Формируем собственное отображение, если требуется
|
||
const customView = taskRenderer ? taskRenderer({ task, taskHeight: lineHeight, taskWidth: width }) || {} : {};
|
||
return (
|
||
<foreignObject
|
||
x={task.start !== 0 ? task.start * shift : 0}
|
||
y={NDEFAULT_HEADER_HEIGHT + task.lineNumb * lineHeight}
|
||
width={width}
|
||
height={lineHeight}
|
||
>
|
||
<Box
|
||
className={hasValue(indexGrp) ? `TaskGrp${indexGrp}` : null}
|
||
sx={{ ...STYLES.TASK_BOX(lineHeight, task.bgColor, task.textColor, task.highlightColor), ...customView.taskStyle }}
|
||
{...customView.taskProps}
|
||
onClick={() => openTaskEditor(task)}
|
||
>
|
||
{customView.data ? (
|
||
customView.data
|
||
) : (
|
||
<Typography sx={STYLES.TASK(lineHeight)} title={task.name}>
|
||
{task.name}
|
||
</Typography>
|
||
)}
|
||
</Box>
|
||
</foreignObject>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Группы циклограммы
|
||
P8PCyclogramTask.propTypes = {
|
||
task: PropTypes.object.isRequired,
|
||
indexGrp: PropTypes.number,
|
||
shift: PropTypes.number.isRequired,
|
||
lineHeight: PropTypes.number.isRequired,
|
||
openTaskEditor: PropTypes.func.isRequired,
|
||
taskRenderer: PropTypes.func
|
||
};
|
||
|
||
//Основная информация циклограммы
|
||
const P8PCyclogramMain = ({
|
||
columns,
|
||
groups,
|
||
tasks,
|
||
shift,
|
||
lineHeight,
|
||
maxWidth,
|
||
maxHeight,
|
||
openTaskEditor,
|
||
groupHeaderRenderer,
|
||
taskRenderer,
|
||
columnRenderer,
|
||
headerBlock
|
||
}) => {
|
||
//Инициализируем коллекцию тасков с группами
|
||
const tasksWithGroup = tasks.filter(task => hasValue(task.groupName));
|
||
//Инициализируем коллекцию тасков без групп
|
||
const tasksWithoutGroup = tasks.filter(task => !hasValue(task.groupName));
|
||
//Инициализируем коллекцию отображаемых групп
|
||
const visibleGroups = groups ? groups.filter(group => group.visible) : [];
|
||
return (
|
||
<g className="main">
|
||
<g className="tasks">
|
||
{visibleGroups.length !== 0
|
||
? visibleGroups.map((grp, indexGrp) => {
|
||
//Считываем задачи группы
|
||
let groupTasks = tasksWithGroup.filter(task => task.groupName === grp.name);
|
||
//Если по данной группе нет тасков - ничего не выводим
|
||
if (groupTasks.length === 0) {
|
||
return null;
|
||
}
|
||
return (
|
||
<g className={`TaskGrp${indexGrp}`} key={indexGrp}>
|
||
{groupTasks.map((task, index) => (
|
||
<P8PCyclogramTask
|
||
task={task}
|
||
indexGrp={indexGrp}
|
||
shift={shift}
|
||
lineHeight={lineHeight}
|
||
openTaskEditor={openTaskEditor}
|
||
taskRenderer={taskRenderer}
|
||
key={index}
|
||
/>
|
||
))}
|
||
<style>{getGroupStyles(indexGrp, grp.highlightColor)}</style>
|
||
</g>
|
||
);
|
||
})
|
||
: null}
|
||
<g className={`TasksWithoutGroups`}>
|
||
{tasksWithoutGroup.map((task, index) => {
|
||
return (
|
||
<P8PCyclogramTask
|
||
task={task}
|
||
shift={shift}
|
||
lineHeight={lineHeight}
|
||
openTaskEditor={openTaskEditor}
|
||
taskRenderer={taskRenderer}
|
||
key={index}
|
||
/>
|
||
);
|
||
})}
|
||
</g>
|
||
</g>
|
||
<P8PCyclogramHeader
|
||
columns={columns}
|
||
shift={shift}
|
||
maxWidth={maxWidth}
|
||
maxHeight={maxHeight}
|
||
columnRenderer={columnRenderer}
|
||
headerBlock={headerBlock}
|
||
/>
|
||
{visibleGroups.length !== 0 ? (
|
||
<g className="groups">
|
||
{visibleGroups.map((grp, indexGrp) => {
|
||
//Инициализируем параметры группы
|
||
let defaultView = null;
|
||
let customView = null;
|
||
let groupHeaderX = 0;
|
||
let groupHeaderY = 0;
|
||
let groupTasks = tasksWithGroup.filter(task => task.groupName === grp.name);
|
||
//Если по данной группе нет тасков - ничего не выводим
|
||
if (groupTasks.length === 0) {
|
||
return null;
|
||
}
|
||
//Если требуется отображать заголовок группы
|
||
if (grp.visible) {
|
||
//Формируем отображение по умолчанию
|
||
defaultView = (
|
||
<Box sx={{ ...STYLES.GROUP_HEADER_BOX, height: grp.height }}>
|
||
<Typography sx={{ ...STYLES.GROUP_HEADER, maxWidth: grp.width, maxHeight: grp.height }}>{grp.name}</Typography>
|
||
</Box>
|
||
);
|
||
//Формируем собственное отображение, если требуется
|
||
customView = groupHeaderRenderer ? groupHeaderRenderer({ group: grp }) : null;
|
||
//Рассчитываем координаты заголовка группы
|
||
groupHeaderX = Math.min(...groupTasks.map(o => o.start)) * shift;
|
||
groupHeaderY = NDEFAULT_HEADER_HEIGHT + Math.min(...groupTasks.map(o => o.lineNumb)) * lineHeight - grp.height - 5;
|
||
}
|
||
return (
|
||
<foreignObject
|
||
x={groupHeaderX}
|
||
y={groupHeaderY}
|
||
width={grp.width}
|
||
height={grp.height}
|
||
className={`TaskGrpHeader${indexGrp}`}
|
||
display="none"
|
||
key={indexGrp}
|
||
>
|
||
{customView ? customView : defaultView}
|
||
</foreignObject>
|
||
);
|
||
})}
|
||
</g>
|
||
) : null}
|
||
</g>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Основная информация циклограммы
|
||
P8PCyclogramMain.propTypes = {
|
||
columns: PropTypes.array.isRequired,
|
||
groups: PropTypes.array,
|
||
tasks: PropTypes.array.isRequired,
|
||
shift: PropTypes.number.isRequired,
|
||
lineHeight: PropTypes.number.isRequired,
|
||
maxWidth: PropTypes.number.isRequired,
|
||
maxHeight: PropTypes.number.isRequired,
|
||
openTaskEditor: PropTypes.func.isRequired,
|
||
groupHeaderRenderer: PropTypes.func,
|
||
taskRenderer: PropTypes.func,
|
||
columnRenderer: PropTypes.func,
|
||
headerBlock: PropTypes.object
|
||
};
|
||
|
||
//Редактор задачи
|
||
const P8PCyclogramTaskEditor = ({
|
||
task,
|
||
taskAttributes,
|
||
onOk,
|
||
onCancel,
|
||
taskAttributeRenderer,
|
||
taskDialogRenderer,
|
||
nameCaption,
|
||
okBtnCaption,
|
||
cancelBtnCaption
|
||
}) => {
|
||
//Собственное состояние
|
||
const [state] = useState({
|
||
start: task.start,
|
||
end: task.end
|
||
});
|
||
|
||
//Отображаемые атрибуты
|
||
const dispTaskAttributes =
|
||
Array.isArray(taskAttributes) && taskAttributes.length > 0 ? taskAttributes.filter(attr => attr.visible && hasValue(task[attr.name])) : [];
|
||
|
||
//При сохранении
|
||
const handleOk = () => (onOk && state.start && state.end ? onOk() : null);
|
||
|
||
//При отмене
|
||
const handleCancel = () => (onCancel ? onCancel() : null);
|
||
|
||
//Генерация содержимого
|
||
return (
|
||
<Dialog open onClose={handleCancel}>
|
||
{taskDialogRenderer ? (
|
||
taskDialogRenderer({ task, taskAttributes, close: handleCancel })
|
||
) : (
|
||
<>
|
||
<DialogContent sx={STYLES.TASK_EDITOR_CONTENT}>
|
||
<List sx={STYLES.TASK_EDITOR_LIST}>
|
||
<ListItem alignItems="flex-start">
|
||
<ListItemText primary={nameCaption} secondary={task.fullName} />
|
||
</ListItem>
|
||
{dispTaskAttributes.length > 0 ? <Divider component="li" /> : null}
|
||
{dispTaskAttributes.length > 0
|
||
? dispTaskAttributes.map((attr, i) => {
|
||
const defaultView = task[attr.name];
|
||
const customView = taskAttributeRenderer ? taskAttributeRenderer({ task, attribute: attr }) : null;
|
||
return (
|
||
<React.Fragment key={i}>
|
||
<ListItem alignItems="flex-start">
|
||
<ListItemText
|
||
primary={attr.caption}
|
||
secondaryTypographyProps={{ component: "span" }}
|
||
secondary={customView ? customView : defaultView}
|
||
/>
|
||
</ListItem>
|
||
{i < dispTaskAttributes.length - 1 ? <Divider component="li" /> : null}
|
||
</React.Fragment>
|
||
);
|
||
})
|
||
: null}
|
||
</List>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={handleOk}>{okBtnCaption}</Button>
|
||
<Button onClick={handleCancel}>{cancelBtnCaption}</Button>
|
||
</DialogActions>
|
||
</>
|
||
)}
|
||
</Dialog>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Редактор задачи
|
||
P8PCyclogramTaskEditor.propTypes = {
|
||
task: P8P_CYCLOGRAM_TASK_SHAPE,
|
||
taskAttributes: PropTypes.arrayOf(P8P_CYCLOGRAM_TASK_ATTRIBUTE_SHAPE),
|
||
onOk: PropTypes.func,
|
||
onCancel: PropTypes.func,
|
||
taskAttributeRenderer: PropTypes.func,
|
||
taskDialogRenderer: PropTypes.func,
|
||
nameCaption: PropTypes.string.isRequired,
|
||
okBtnCaption: PropTypes.string.isRequired,
|
||
cancelBtnCaption: PropTypes.string.isRequired
|
||
};
|
||
|
||
//Циклограмма
|
||
const P8PCyclogram = ({
|
||
containerStyle,
|
||
lineHeight,
|
||
title,
|
||
titleStyle,
|
||
onTitleClick,
|
||
zoomBar,
|
||
zoom,
|
||
columns,
|
||
columnRenderer,
|
||
groups,
|
||
groupHeaderRenderer,
|
||
tasks,
|
||
taskRenderer,
|
||
taskAttributes,
|
||
taskAttributeRenderer,
|
||
taskDialogRenderer,
|
||
noDataFoundText,
|
||
nameTaskEditorCaption,
|
||
okTaskEditorBtnCaption,
|
||
cancelTaskEditorBtnCaption
|
||
}) => {
|
||
//Хук основного блока (для последующего определения доступной ширины)
|
||
const mainBlock = useRef(null);
|
||
//Хук для заголовка таблицы
|
||
const headerBlock = useRef(null);
|
||
//Собственное состояние
|
||
const [state, setState] = useState({
|
||
noData: true,
|
||
loaded: false,
|
||
lineHeight: NDEFAULT_LINE_HEIGHT,
|
||
maxWidth: 0,
|
||
maxHeight: 0,
|
||
shift: 0,
|
||
zoom: P8P_CYCLOGRAM_ZOOM.includes(zoom) ? zoom : 1,
|
||
tasks: [],
|
||
editTask: null
|
||
});
|
||
|
||
//Обновление масштаба циклограммы
|
||
const handleZoomChange = direction => {
|
||
//Считываем текущий индекс
|
||
const currentIndex = P8P_CYCLOGRAM_ZOOM.indexOf(state.zoom);
|
||
setState(pv => ({
|
||
...pv,
|
||
zoom:
|
||
currentIndex + direction !== P8P_CYCLOGRAM_ZOOM.length && currentIndex + direction !== -1
|
||
? P8P_CYCLOGRAM_ZOOM[currentIndex + direction]
|
||
: pv.zoom
|
||
}));
|
||
};
|
||
|
||
//Открытие редактора задачи
|
||
const openTaskEditor = task => setState(pv => ({ ...pv, editTask: { ...task } }));
|
||
|
||
//При сохранении задачи в редакторе
|
||
const handleTaskEditorSave = () => {
|
||
setState(pv => ({ ...pv, editTask: null }));
|
||
};
|
||
|
||
//При закрытии редактора задачи без сохранения
|
||
const handleTaskEditorCancel = () => setState(pv => ({ ...pv, editTask: null }));
|
||
|
||
//При скролле блока
|
||
const handleScroll = e => {
|
||
//Изменяем позицию заголовка таблицы относительно скролла
|
||
headerBlock.current.setAttribute("transform", "translate(0," + e.currentTarget.scrollTop + ")");
|
||
};
|
||
|
||
//При изменении данных
|
||
useEffect(() => {
|
||
//Если есть колонки и задачи
|
||
if (Array.isArray(columns) && columns.length > 0 && Array.isArray(tasks) && tasks.length > 0) {
|
||
//Определяем текущую максимальную ширину колонок
|
||
let currentColumnsMaxWidth = Math.max(...columns.map(o => o.end));
|
||
//Определяем доступный сдвиг для ширины колонок (16 - паддинг по бокам)
|
||
let columnShift = getShift(columns, currentColumnsMaxWidth, mainBlock.current.offsetWidth - 16) * state.zoom;
|
||
//Устанавливаем значения исходя из колонок/задач
|
||
setState(pv => ({
|
||
...pv,
|
||
loaded: true,
|
||
lineHeight: lineHeight ? lineHeight : NDEFAULT_LINE_HEIGHT,
|
||
maxWidth: columnShift !== 0 ? currentColumnsMaxWidth * columnShift : currentColumnsMaxWidth,
|
||
maxHeight: NDEFAULT_HEADER_HEIGHT + (Math.max(...tasks.map(o => o.lineNumb)) + 1) * (lineHeight ? lineHeight : NDEFAULT_LINE_HEIGHT),
|
||
shift: columnShift,
|
||
tasks: tasks,
|
||
noData: false
|
||
}));
|
||
} else {
|
||
//Устанавливаем значения исходя из колонок/задач
|
||
setState(pv => ({
|
||
...pv,
|
||
noData: true
|
||
}));
|
||
}
|
||
}, [columns, lineHeight, state.zoom, tasks]);
|
||
|
||
//Генерация содержимого
|
||
return (
|
||
<>
|
||
<div ref={mainBlock} style={{ ...(containerStyle ? containerStyle : {}) }}>
|
||
{state.noData ? <P8PAppInlineError text={noDataFoundText} /> : null}
|
||
{state.loaded ? (
|
||
<>
|
||
{title ? (
|
||
<Typography
|
||
p={1}
|
||
sx={{ ...STYLES.CYCLOGRAM_TITLE, ...(titleStyle ? titleStyle : {}) }}
|
||
align="center"
|
||
color="textSecondary"
|
||
variant="subtitle1"
|
||
>
|
||
{onTitleClick ? (
|
||
<Link component="button" variant="body2" underline="hover" onClick={() => onTitleClick()}>
|
||
{title}
|
||
</Link>
|
||
) : (
|
||
title
|
||
)}
|
||
</Typography>
|
||
) : null}
|
||
{zoomBar ? (
|
||
<Box p={1} sx={STYLES.CYCLOGRAM_ZOOM}>
|
||
<IconButton
|
||
onClick={() => handleZoomChange(1)}
|
||
disabled={state.zoom == P8P_CYCLOGRAM_ZOOM[P8P_CYCLOGRAM_ZOOM.length - 1]}
|
||
>
|
||
<Icon>zoom_in</Icon>
|
||
</IconButton>
|
||
<IconButton onClick={() => handleZoomChange(-1)} disabled={state.zoom == P8P_CYCLOGRAM_ZOOM[0]}>
|
||
<Icon>zoom_out</Icon>
|
||
</IconButton>
|
||
</Box>
|
||
) : null}
|
||
<Box className="scroll" sx={STYLES.CYCLOGRAM_BOX(state.noData, title, zoomBar)} onScroll={handleScroll}>
|
||
<svg id="cyclogram" width={state.maxWidth} height={state.maxHeight}>
|
||
<P8PCyclogramGrid
|
||
tasks={state.tasks}
|
||
columns={columns}
|
||
shift={state.shift}
|
||
maxWidth={state.maxWidth}
|
||
maxHeight={state.maxHeight}
|
||
lineHeight={state.lineHeight}
|
||
/>
|
||
<P8PCyclogramMain
|
||
columns={columns}
|
||
groups={groups}
|
||
tasks={state.tasks}
|
||
shift={state.shift}
|
||
lineHeight={state.lineHeight}
|
||
maxWidth={state.maxWidth}
|
||
maxHeight={state.maxHeight}
|
||
groupHeaderRenderer={groupHeaderRenderer}
|
||
openTaskEditor={openTaskEditor}
|
||
taskRenderer={taskRenderer}
|
||
columnRenderer={columnRenderer}
|
||
headerBlock={headerBlock}
|
||
/>
|
||
</svg>
|
||
</Box>
|
||
</>
|
||
) : null}
|
||
{state.editTask ? (
|
||
<P8PCyclogramTaskEditor
|
||
task={state.editTask}
|
||
taskAttributes={taskAttributes}
|
||
onOk={handleTaskEditorSave}
|
||
onCancel={handleTaskEditorCancel}
|
||
taskAttributeRenderer={taskAttributeRenderer}
|
||
taskDialogRenderer={taskDialogRenderer}
|
||
nameCaption={nameTaskEditorCaption}
|
||
okBtnCaption={okTaskEditorBtnCaption}
|
||
cancelBtnCaption={cancelTaskEditorBtnCaption}
|
||
/>
|
||
) : null}
|
||
</div>
|
||
</>
|
||
);
|
||
};
|
||
|
||
//Контроль свойств - Циклограмма
|
||
P8PCyclogram.propTypes = {
|
||
containerStyle: PropTypes.object,
|
||
lineHeight: PropTypes.number,
|
||
title: PropTypes.string,
|
||
titleStyle: PropTypes.object,
|
||
onTitleClick: PropTypes.func,
|
||
zoomBar: PropTypes.bool,
|
||
zoom: PropTypes.number,
|
||
columns: PropTypes.arrayOf(P8P_CYCLOGRAM_COLUMN_SHAPE).isRequired,
|
||
columnRenderer: PropTypes.func,
|
||
groups: PropTypes.arrayOf(P8P_CYCLOGRAM_GROUP_SHAPE),
|
||
groupHeaderRenderer: PropTypes.func,
|
||
tasks: PropTypes.arrayOf(P8P_CYCLOGRAM_TASK_SHAPE).isRequired,
|
||
taskRenderer: PropTypes.func,
|
||
taskAttributes: PropTypes.arrayOf(P8P_CYCLOGRAM_TASK_ATTRIBUTE_SHAPE),
|
||
taskAttributeRenderer: PropTypes.func,
|
||
taskDialogRenderer: PropTypes.func,
|
||
noDataFoundText: PropTypes.string.isRequired,
|
||
nameTaskEditorCaption: PropTypes.string.isRequired,
|
||
okTaskEditorBtnCaption: PropTypes.string.isRequired,
|
||
cancelTaskEditorBtnCaption: PropTypes.string.isRequired
|
||
};
|
||
|
||
//----------------
|
||
//Интерфейс модуля
|
||
//----------------
|
||
|
||
export { P8PCyclogram };
|