ЦИТК-979 - Контроль типов данных при организации связи, дополнительные события диаграммы (нажатие на атрибут, нажатие на панель), подсветка сущности при нажатии на атрибут, подсветка некорректных приёмников связи, функции получения атрибутики состояния атрибута сущности

This commit is contained in:
Mikhail Chechnev 2025-08-14 14:13:50 +03:00
parent f6b29d5339
commit 4115961742
2 changed files with 105 additions and 21 deletions

View File

@ -19,11 +19,12 @@ import { DATA_TYPE } from "../../common"; //Общие ресурсы и кон
//Типовые цвета точек привязки
const HANDLE_BORDER_COLOR = "#69db7c";
const HANDLE_BORDER_COLOR_INVALID = "#ff0000";
const HANDLE_BORDER_COLOR_DISABLED = "#adb5bd";
//Стили
const STYLES = {
CONTAINER: { display: "flex", width: "100%", height: "100%" },
CONTAINER: { display: "flex", width: "100%", height: "100%", cursor: "default" },
HANDLE_SOURCE: isConnecting => ({
width: 14,
height: 14,
@ -32,17 +33,18 @@ const STYLES = {
borderRadius: 7,
background: "white"
}),
HANDLE_TARGET: isConnecting => ({
HANDLE_TARGET: (isConnecting, isValidConnection) => ({
width: isConnecting ? 14 : 0,
height: 14,
left: isConnecting ? -7 : 0,
border: `2px solid ${HANDLE_BORDER_COLOR}`,
border: `2px solid ${isValidConnection ? HANDLE_BORDER_COLOR : HANDLE_BORDER_COLOR_INVALID}`,
borderRadius: 7,
background: "white",
visibility: isConnecting ? "visible" : "hidden"
}),
CONTENT_STACK: { width: "100%" },
TITLE_NAME_STACK: { width: "100%", containerType: "inline-size" }
TITLE_NAME_STACK: { width: "100%", containerType: "inline-size" },
ATTR_PROP_ICON: { fontSize: "0.9rem" }
};
//Иконки
@ -55,11 +57,36 @@ const ICONS = {
//Структура данных об атрибуте сущности
const ATTRIBUTE_DATA_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
parentEntity: PropTypes.string,
name: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
dataType: PropTypes.number.isRequired
dataType: PropTypes.oneOf(Object.values(DATA_TYPE)),
agg: PropTypes.string,
alias: PropTypes.string,
use: PropTypes.oneOf([0, 1]).isRequired,
show: PropTypes.oneOf([0, 1]).isRequired
});
//-----------------------
//Вспомогательные функции
//-----------------------
//Получение атрибутики состояния включения атрибута в запрос
const attrGetUse = (attr, callToAction = false) => {
return [attr.use === 1, `${attr.use === 1 ? "Включен в запрос" : "Не включен в запрос"}${callToAction ? "- нажмите, чтобы изменить" : ""}`];
};
//Получение атрибутики состояния отображения атрибута в результатах запроса
const attrGetShow = (attr, callToAction = false) => {
return [
`${attr.show == 1 ? "Отображается в результатах запроса" : "Не отображается в результатах запроса"}${
callToAction ? "- нажмите, чтобы изменить" : ""
}`,
attr.show == 1 ? "visibility" : "visibility_off"
];
};
//-----------
//Тело модуля
//-----------
@ -67,25 +94,45 @@ const ATTRIBUTE_DATA_SHAPE = PropTypes.shape({
//Атрибут сущности
const Attribute = ({ data }) => {
//Поиск идентификатора соединяемого элемента
const connectionNodeId = useStore(state => state.connectionNodeId);
const [connectionNodeId, targetConnectionNode, connectionStatus] = useStore(state => [
state.connectionNodeId,
state?.connectionEndHandle?.nodeId,
state.connectionStatus
]);
//Флаг выполнения соединения сущностей
const isConnecting = Boolean(connectionNodeId);
//Флаг корректности соединения сущностей
const isValidConnection = !(data.id == targetConnectionNode && connectionStatus == "invalid");
//Получим атрибуты состояния отображения
const [showTitle, showIcon] = attrGetShow(data);
//Формирование представления
return (
<Box p={1} sx={STYLES.CONTAINER}>
<Handle type={"source"} position={Position.Right} style={STYLES.HANDLE_SOURCE(isConnecting)} />
<Handle type={"target"} position={Position.Left} isConnectableStart={false} style={STYLES.HANDLE_TARGET(isConnecting)} />
<Handle
type={"target"}
position={Position.Left}
isConnectableStart={false}
style={STYLES.HANDLE_TARGET(isConnecting, isValidConnection)}
/>
<Stack direction={"row"} alignItems={"center"} spacing={1} sx={STYLES.CONTENT_STACK}>
<Icon color={"action"}>{ICONS[data.dataType] || ICONS.DEFAULT}</Icon>
<Stack direction={"column"} alignItems={"left"} sx={STYLES.TITLE_NAME_STACK}>
<Typography variant={"body2"} noWrap title={data.title}>
{data.title}
</Typography>
<Typography variant={"caption"} color={"text.secondary"} noWrap title={data.name}>
{data.name}
</Typography>
<Stack direction={"row"} alignItems={"center"} spacing={0.5}>
<Typography component={"div"} variant={"caption"} color={"text.secondary"} noWrap title={data.name}>
{`${data.name},`}
</Typography>
<Icon color={"action"} sx={STYLES.ATTR_PROP_ICON} title={showTitle}>
{showIcon}
</Icon>
</Stack>
</Stack>
</Stack>
</Box>
@ -101,4 +148,4 @@ Attribute.propTypes = {
//Интерфейс модуля
//----------------
export { Attribute, ATTRIBUTE_DATA_SHAPE };
export { Attribute, ATTRIBUTE_DATA_SHAPE, attrGetUse, attrGetShow };

View File

@ -67,13 +67,17 @@ const hasCycle = (connection, target, nodes, edges, visited = new Set()) => {
const isValidConnection = (connection, nodes, edges) => {
//Должны быть заданы источник и приёмник
if (!connection.source || !connection.target) return false;
//Нельзя ссылаться на самого себя
const tableId = connection.source.split(".")[0];
const isSameTable = connection.target.startsWith(tableId);
if (isSameTable) return false;
//Приёмник должен быть среди элементов диаграммы и не должен быть источником
//Найдем источник и приёмник
const source = nodes.find(node => node.id === connection.source);
const target = nodes.find(node => node.id === connection.target);
if (!target || target.id === connection.source) return false;
//Приёмник и источник должны существовать
if (!target || !source) return false;
//Нельзя ссылаться на самого себя
if (source?.data?.parentEntity == target?.data?.parentEntity) return false;
//Типы данны источника и приёмника должны совпадать
if (source?.data?.dataType != target?.data?.dataType) return false;
//Приёмник должен не должен быть источником
if (target.id === connection.source) return false;
//Нельзя зацикливаться
return !hasCycle(connection, target, nodes, edges);
};
@ -87,11 +91,13 @@ const QueryDiagram = ({
entities = [],
relations = [],
onEntityClick,
onEntityAttrClick,
onEntityPositionChange,
onEntityRemove,
onRelactionClick,
onRelationAdd,
onRelationRemove
onRelationRemove,
onPaneClick
}) => {
//Собственное состояние - элементы
const [nodes, setNodes] = useState(entities);
@ -105,14 +111,31 @@ const QueryDiagram = ({
//При изменении элементов на диаграмме
const handleNodesChange = useCallback(
changes => {
setNodes(nodesSnapshot => applyNodeChanges(changes, nodesSnapshot));
//При выборе атрибута подсветим всю сущность
const tmpChanges = changes.reduce((prevChanges, curChanges) => {
const tmp = { ...curChanges };
if (tmp.type == "select") {
const chEnt = entities.find(e => e.id === tmp.id);
if (chEnt && chEnt?.data?.parentEntity) {
prevChanges.push({ ...curChanges, id: chEnt.data.parentEntity });
tmp.selected = false;
}
}
prevChanges.push(tmp);
return prevChanges;
}, []);
//Применим изменения в диаграмме
setNodes(nodesSnapshot => applyNodeChanges(tmpChanges, nodesSnapshot));
//Если двигали сущность - запомним начало движения
if (changes.length == 1 && changes[0].type == "position" && changes[0].dragging)
setMovedNode({ id: changes[0].id, position: { ...changes[0].position } });
//Если закончили двигать сущность и ранее запомнили начало движения - вызываем колбэки для нажатия и двиения сущности
if (changes.length == 1 && changes[0].type == "position" && !changes[0].dragging && movedNode) {
if (onEntityPositionChange) onEntityPositionChange(movedNode.id, movedNode.position);
if (onEntityClick) onEntityClick(movedNode.id);
setMovedNode(null);
}
//Если удалили сущность - вызываем колбэк для удаления
if (changes[0].type == "remove" && entities.find(e => e.id == changes[0].id && e.type == NODE_TYPE.ENTITY) && onEntityRemove)
onEntityRemove(changes[0].id);
},
@ -120,7 +143,13 @@ const QueryDiagram = ({
);
//При выборе элемента диаграммы
const handleNodeClick = useCallback((e, node) => onEntityClick && onEntityClick(node.id), [onEntityClick]);
const handleNodeClick = useCallback(
(e, node) =>
node?.type == NODE_TYPE.ENTITY
? onEntityClick && onEntityClick(node?.id)
: onEntityAttrClick && onEntityAttrClick(node?.parentId, node?.id),
[onEntityClick, onEntityAttrClick]
);
//При связывании элементов на диаграмме
const handleConnect = connection => {
@ -140,9 +169,14 @@ const QueryDiagram = ({
[onRelationRemove]
);
//При нажатии на холст диаграммы
const handlePaneClick = () => onPaneClick && onPaneClick();
//Валидация связи
const validateConnection = connection => isValidConnection(connection, nodes, edges);
//Подсветка выбранной сущности
//При изменении состава сущностей
useEffect(() => setNodes(entities), [entities]);
@ -159,6 +193,7 @@ const QueryDiagram = ({
onNodesChange={handleNodesChange}
onEdgeClick={handleEdgeClick}
onEdgesChange={handleEdgesChange}
onPaneClick={handlePaneClick}
defaultEdgeOptions={{
animated: true,
style: STYLES.EDGE
@ -178,11 +213,13 @@ QueryDiagram.propTypes = {
entities: PropTypes.arrayOf(ENTITY_SHAPE),
relations: PropTypes.arrayOf(RELATION_SHAPE),
onEntityClick: PropTypes.func,
onEntityAttrClick: PropTypes.func,
onEntityPositionChange: PropTypes.func,
onEntityRemove: PropTypes.func,
onRelactionClick: PropTypes.func,
onRelationAdd: PropTypes.func,
onRelationRemove: PropTypes.func
onRelationRemove: PropTypes.func,
onPaneClick: PropTypes.func
};
//----------------