From 4115961742466e1285cc73afeab98ef0ba955915 Mon Sep 17 00:00:00 2001 From: Mikhail Chechnev Date: Thu, 14 Aug 2025 14:13:50 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A6=D0=98=D0=A2=D0=9A-979=20-=20=D0=9A=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D1=80=D0=BE=D0=BB=D1=8C=20=D1=82=D0=B8=D0=BF=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D0=BE=D1=80=D0=B3=D0=B0=D0=BD=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B8,=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B4=D0=B8=D0=B0=D0=B3=D1=80=D0=B0=D0=BC=D0=BC=D1=8B=20(?= =?UTF-8?q?=D0=BD=D0=B0=D0=B6=D0=B0=D1=82=D0=B8=D0=B5=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B0=D1=82=D1=80=D0=B8=D0=B1=D1=83=D1=82,=20=D0=BD=D0=B0?= =?UTF-8?q?=D0=B6=D0=B0=D1=82=D0=B8=D0=B5=20=D0=BD=D0=B0=20=D0=BF=D0=B0?= =?UTF-8?q?=D0=BD=D0=B5=D0=BB=D1=8C),=20=D0=BF=D0=BE=D0=B4=D1=81=D0=B2?= =?UTF-8?q?=D0=B5=D1=82=D0=BA=D0=B0=20=D1=81=D1=83=D1=89=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=BF=D1=80=D0=B8=20=D0=BD=D0=B0=D0=B6=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D0=B8=20=D0=BD=D0=B0=20=D0=B0=D1=82=D1=80=D0=B8?= =?UTF-8?q?=D0=B1=D1=83=D1=82,=20=D0=BF=D0=BE=D0=B4=D1=81=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D0=BD=D0=B5=D0=BA=D0=BE=D1=80=D1=80=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D0=BD=D1=8B=D1=85=20=D0=BF=D1=80=D0=B8=D1=91=D0=BC?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B8?= =?UTF-8?q?,=20=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B0=D1=82?= =?UTF-8?q?=D1=80=D0=B8=D0=B1=D1=83=D1=82=D0=B8=D0=BA=D0=B8=20=D1=81=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F=20=D0=B0=D1=82=D1=80?= =?UTF-8?q?=D0=B8=D0=B1=D1=83=D1=82=D0=B0=20=D1=81=D1=83=D1=89=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/attribute/attribute.js | 69 ++++++++++++++++--- .../components/query_diagram/query_diagram.js | 57 ++++++++++++--- 2 files changed, 105 insertions(+), 21 deletions(-) diff --git a/app/panels/query_editor/components/attribute/attribute.js b/app/panels/query_editor/components/attribute/attribute.js index cfb4b9a..a3e9508 100644 --- a/app/panels/query_editor/components/attribute/attribute.js +++ b/app/panels/query_editor/components/attribute/attribute.js @@ -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 ( - + {ICONS[data.dataType] || ICONS.DEFAULT} {data.title} - - {data.name} - + + + {`${data.name},`} + + + {showIcon} + + @@ -101,4 +148,4 @@ Attribute.propTypes = { //Интерфейс модуля //---------------- -export { Attribute, ATTRIBUTE_DATA_SHAPE }; +export { Attribute, ATTRIBUTE_DATA_SHAPE, attrGetUse, attrGetShow }; diff --git a/app/panels/query_editor/components/query_diagram/query_diagram.js b/app/panels/query_editor/components/query_diagram/query_diagram.js index bf8f6dd..71f5b2c 100644 --- a/app/panels/query_editor/components/query_diagram/query_diagram.js +++ b/app/panels/query_editor/components/query_diagram/query_diagram.js @@ -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 }; //----------------