Compare commits

...

7 Commits
main ... main

Author SHA1 Message Date
Mikhail Chechnev
a20de79a40 ЦИТК-979 - Установка атрибутов сущности (клиент) 2025-08-14 14:21:23 +03:00
Mikhail Chechnev
fa00940776 ЦИТК-979 - Установка атрибутов сущности (сервер) 2025-08-14 14:17:25 +03:00
Mikhail Chechnev
4115961742 ЦИТК-979 - Контроль типов данных при организации связи, дополнительные события диаграммы (нажатие на атрибут, нажатие на панель), подсветка сущности при нажатии на атрибут, подсветка некорректных приёмников связи, функции получения атрибутики состояния атрибута сущности 2025-08-14 14:13:50 +03:00
Mikhail Chechnev
f6b29d5339 WEBAPP: в utils.js функция конвертации JSON в XML вынесена в отдельную object2XML (ранее была в составе object2Base64XML) 2025-08-14 14:07:12 +03:00
Mikhail Chechnev
b3e7d7bf7b ЦИТК-979 - Косметика в списке запросов "QueriesList" (сделан более компактным) 2025-08-14 13:56:35 +03:00
Mikhail Chechnev
f9b2f1ae34 ЦИТК-979 - Компонент для настройки свойств запроса, настройка атрибутов сущностей (начало, клиент) 2025-08-08 19:16:30 +03:00
Mikhail Chechnev
836a3db5e2 ЦИТК-979 - Формирование списка атрибутов сущности (сервер) 2025-08-08 19:09:59 +03:00
14 changed files with 1067 additions and 144 deletions

View File

@ -105,11 +105,14 @@ const getDisplaySize = () => {
const deepCopyObject = obj => (structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj))); const deepCopyObject = obj => (structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj)));
//Конвертация объекта в Base64 XML //Конвертация объекта в Base64 XML
const object2Base64XML = (obj, builderOptions) => { const object2XML = (obj, builderOptions) => {
const builder = new XMLBuilder(builderOptions); const builder = new XMLBuilder(builderOptions);
return btoa(unescape(encodeURIComponent(builder.build(obj)))); return builder.build(obj);
}; };
//Конвертация объекта в Base64 XML
const object2Base64XML = (obj, builderOptions) => btoa(unescape(encodeURIComponent(object2XML(obj, builderOptions))));
//Конвертация XML в JSON //Конвертация XML в JSON
const xml2JSON = ({ xmlDoc, isArray, transformTagName, tagValueProcessor, attributeValueProcessor, useDefaultPatterns = true }) => { const xml2JSON = ({ xmlDoc, isArray, transformTagName, tagValueProcessor, attributeValueProcessor, useDefaultPatterns = true }) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -200,6 +203,7 @@ export {
hasValue, hasValue,
getDisplaySize, getDisplaySize,
deepCopyObject, deepCopyObject,
object2XML,
object2Base64XML, object2Base64XML,
xml2JSON, xml2JSON,
formatDateRF, formatDateRF,

View File

@ -19,11 +19,12 @@ import { DATA_TYPE } from "../../common"; //Общие ресурсы и кон
//Типовые цвета точек привязки //Типовые цвета точек привязки
const HANDLE_BORDER_COLOR = "#69db7c"; const HANDLE_BORDER_COLOR = "#69db7c";
const HANDLE_BORDER_COLOR_INVALID = "#ff0000";
const HANDLE_BORDER_COLOR_DISABLED = "#adb5bd"; const HANDLE_BORDER_COLOR_DISABLED = "#adb5bd";
//Стили //Стили
const STYLES = { const STYLES = {
CONTAINER: { display: "flex", width: "100%", height: "100%" }, CONTAINER: { display: "flex", width: "100%", height: "100%", cursor: "default" },
HANDLE_SOURCE: isConnecting => ({ HANDLE_SOURCE: isConnecting => ({
width: 14, width: 14,
height: 14, height: 14,
@ -32,17 +33,18 @@ const STYLES = {
borderRadius: 7, borderRadius: 7,
background: "white" background: "white"
}), }),
HANDLE_TARGET: isConnecting => ({ HANDLE_TARGET: (isConnecting, isValidConnection) => ({
width: isConnecting ? 14 : 0, width: isConnecting ? 14 : 0,
height: 14, height: 14,
left: isConnecting ? -7 : 0, left: isConnecting ? -7 : 0,
border: `2px solid ${HANDLE_BORDER_COLOR}`, border: `2px solid ${isValidConnection ? HANDLE_BORDER_COLOR : HANDLE_BORDER_COLOR_INVALID}`,
borderRadius: 7, borderRadius: 7,
background: "white", background: "white",
visibility: isConnecting ? "visible" : "hidden" visibility: isConnecting ? "visible" : "hidden"
}), }),
CONTENT_STACK: { width: "100%" }, 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({ const ATTRIBUTE_DATA_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
parentEntity: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
title: 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 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 isConnecting = Boolean(connectionNodeId);
//Флаг корректности соединения сущностей
const isValidConnection = !(data.id == targetConnectionNode && connectionStatus == "invalid");
//Получим атрибуты состояния отображения
const [showTitle, showIcon] = attrGetShow(data);
//Формирование представления //Формирование представления
return ( return (
<Box p={1} sx={STYLES.CONTAINER}> <Box p={1} sx={STYLES.CONTAINER}>
<Handle type={"source"} position={Position.Right} style={STYLES.HANDLE_SOURCE(isConnecting)} /> <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}> <Stack direction={"row"} alignItems={"center"} spacing={1} sx={STYLES.CONTENT_STACK}>
<Icon color={"action"}>{ICONS[data.dataType] || ICONS.DEFAULT}</Icon> <Icon color={"action"}>{ICONS[data.dataType] || ICONS.DEFAULT}</Icon>
<Stack direction={"column"} alignItems={"left"} sx={STYLES.TITLE_NAME_STACK}> <Stack direction={"column"} alignItems={"left"} sx={STYLES.TITLE_NAME_STACK}>
<Typography variant={"body2"} noWrap title={data.title}> <Typography variant={"body2"} noWrap title={data.title}>
{data.title} {data.title}
</Typography> </Typography>
<Typography variant={"caption"} color={"text.secondary"} noWrap title={data.name}> <Stack direction={"row"} alignItems={"center"} spacing={0.5}>
{data.name} <Typography component={"div"} variant={"caption"} color={"text.secondary"} noWrap title={data.name}>
</Typography> {`${data.name},`}
</Typography>
<Icon color={"action"} sx={STYLES.ATTR_PROP_ICON} title={showTitle}>
{showIcon}
</Icon>
</Stack>
</Stack> </Stack>
</Stack> </Stack>
</Box> </Box>
@ -101,4 +148,4 @@ Attribute.propTypes = {
//Интерфейс модуля //Интерфейс модуля
//---------------- //----------------
export { Attribute }; export { Attribute, ATTRIBUTE_DATA_SHAPE, attrGetUse, attrGetShow };

View File

@ -17,6 +17,7 @@ import "./entity.css"; //Стили компомнента
//Структура данных о сущности запроса //Структура данных о сущности запроса
const ENTITY_DATA_SHAPE = PropTypes.shape({ const ENTITY_DATA_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
title: PropTypes.string.isRequired title: PropTypes.string.isRequired
}); });
@ -26,7 +27,7 @@ const ENTITY_DATA_SHAPE = PropTypes.shape({
//----------- //-----------
//Сущность запроса //Сущность запроса
const Entity = ({ data, selected }) => { const Entity = ({ data, selected = false }) => {
return ( return (
<div className="entity__wrapper" data-selected={selected}> <div className="entity__wrapper" data-selected={selected}>
<div className="entity__title"> <div className="entity__title">
@ -47,4 +48,4 @@ Entity.propTypes = {
//Интерфейс модуля //Интерфейс модуля
//---------------- //----------------
export { Entity }; export { Entity, ENTITY_DATA_SHAPE };

View File

@ -0,0 +1,97 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Список атрибутов сущности
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, List, ListItem, IconButton, Icon, ListItemButton, ListItemText, ListItemIcon, Checkbox, Typography } from "@mui/material"; //Интерфейсные компоненты MUI
import { ATTRIBUTE_DATA_SHAPE, attrGetUse, attrGetShow } from "../attribute/attribute"; //Атрибут сущности
import { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения
//---------
//Константы
//---------
//Стили
const STYLES = {
SMALL_TOOL_ICON: {
fontSize: 20
},
LIST: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL }
};
//-----------
//Тело модуля
//-----------
//Список атрибутов сущности
const AttrsList = ({ attrs = [], filter, onSelect = null, onShow = null } = {}) => {
//При выборе элемента списка
const handleSelectClick = attr => {
onSelect && onSelect(attr);
};
//При нажатии на исправлении
const handleShowClick = (e, attr) => {
e.stopPropagation();
onShow && onShow(attr);
};
//Рег. выражение для фильтра
const filterRegExp = filter ? new RegExp(filter, "i") : null;
//Формирование представления
return (
<List sx={STYLES.LIST}>
{attrs &&
attrs
.filter(attr => (filterRegExp ? filterRegExp.test(attr.name) || filterRegExp.test(attr.title) : true))
.map((attr, i) => {
const [selected, selectedTitle] = attrGetUse(attr, true);
const [showTitle, showIcon] = attrGetShow(attr, true);
return (
<ListItem key={i} disablePadding>
<ListItemButton onClick={() => handleSelectClick(attr)} selected={selected} dense>
<ListItemIcon>
<Checkbox edge="start" checked={selected} tabIndex={-1} disableRipple title={selectedTitle} />
</ListItemIcon>
<ListItemText
primary={attr.title}
secondaryTypographyProps={{ component: "div" }}
secondary={
<Stack direction={"column"}>
<Typography variant={"caption"}>{`${attr.alias || attr.name}`}</Typography>
</Stack>
}
/>
<Stack direction={"row"}>
<IconButton onClick={e => handleShowClick(e, attr)} title={showTitle}>
<Icon>{showIcon}</Icon>
</IconButton>
</Stack>
</ListItemButton>
</ListItem>
);
})}
</List>
);
};
//Контроль свойств компонента - Список атрибутов сущности
AttrsList.propTypes = {
attrs: PropTypes.arrayOf(ATTRIBUTE_DATA_SHAPE),
filter: PropTypes.string,
onSelect: PropTypes.func,
onShow: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { AttrsList };

View File

@ -0,0 +1,111 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Диалог настройки атрибутов сущности
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { TextField, InputAdornment, Icon, IconButton } from "@mui/material"; //Интерфейсные элементы MUI
import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог
import { AttrsList } from "./attrs_list"; //Список атрибутов сущности
import { useEntityAttrs } from "./hooks"; //Хуки диалога настройки атрибутов сущности
//-----------
//Тело модуля
//-----------
//Диалог настройки атрибутов сущности
const EntityAttrsDialog = ({ query, id, title, onOk, onCancel }) => {
//Собственное состояние - фильтр атрибутов
const [filter, setFilter] = useState("");
//Собственное состояние - список атрибутов
const [attrs, setAttrs] = useState([]);
//Хук для взаимодействия с сервером
const [srvAttrs, saveAttrs] = useEntityAttrs(query, id);
//Нажатие на кнопку "Ok"
const handleOk = async () => {
await saveAttrs(attrs);
onOk && onOk();
};
//Нажатие на кнопку "Отмена"
const handleCancel = () => onCancel && onCancel();
//Выбор/исключение атрибута из запроса
const handleAttrSelect = attr =>
setAttrs(
attrs.map(a => ({
...(a.id === attr.id ? { ...a, use: a.use === 1 ? 0 : 1, show: a.use === 1 ? 0 : a.show } : a)
}))
);
//Отображение/сокрытие атрибута в запросе
const handleAttrShow = attr =>
setAttrs(
attrs.map(a => ({
...(a.id === attr.id ? { ...a, show: a.show === 1 ? 0 : 1, use: a.show === 0 ? 1 : a.use } : a)
}))
);
//При изменении значения фильтра
const handleFilterChange = e => setFilter(e.target.value);
//При очистке фильтра
const handleFilterClear = () => setFilter("");
//При загрузке данных с сервера
useEffect(() => {
if (srvAttrs) setAttrs(srvAttrs.map(srvAttr => ({ ...srvAttr })));
}, [srvAttrs]);
//Генерация содержимого
return (
<P8PDialog title={`Атрибуты сущности "${title}"`} onOk={handleOk} onCancel={handleCancel}>
<TextField
margin={"normal"}
variant={"standard"}
fullWidth
placeholder={"Поиск атрибута..."}
value={filter}
onChange={handleFilterChange}
InputProps={{
startAdornment: (
<InputAdornment position={"start"}>
<Icon>search</Icon>
</InputAdornment>
),
endAdornment: (
<InputAdornment position={"end"}>
<IconButton onClick={handleFilterClear}>
<Icon>clear</Icon>
</IconButton>
</InputAdornment>
)
}}
/>
<AttrsList attrs={attrs} filter={filter} onSelect={handleAttrSelect} onShow={handleAttrShow} />
</P8PDialog>
);
};
//Контроль свойств - Диалог настройки атрибутов сущности
EntityAttrsDialog.propTypes = {
query: PropTypes.number.isRequired,
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { EntityAttrsDialog };

View File

@ -0,0 +1,92 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Пользовательские хуки диалога настройки атрибутов сущности
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../../context/backend"; //Контекст взаимодействия с сервером
import { object2Base64XML } from "../../../../core/utils"; //Вспомогательные функции
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//-----------
//Тело модуля
//-----------
//Работа с атрибутами сущности
const useEntityAttrs = (query, entity) => {
//Собственное состояние - флаг загрузки
const [isLoading, setLoading] = useState(false);
//Собственное состояние - флаг необходимости обновления
const [refresh, setRefresh] = useState(true);
//Собственное состояние - данные
const [data, setData] = useState(null);
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//Обновление данных
const doRefresh = () => setRefresh(true);
//Установка атрибутов сущности
const setAttrs = useCallback(
async attrs => {
await executeStored({
stored: "PKG_P8PANELS_QE.QUERY_ENT_ATTRS_SET",
args: {
NRN: query,
SID: entity,
CATTRS: {
VALUE: object2Base64XML(attrs, { arrayNodeName: "XATTR", ignoreAttributes: false, attributeNamePrefix: "" }),
SDATA_TYPE: SERV_DATA_TYPE_CLOB
}
},
loader: false
});
},
[query, entity, executeStored, SERV_DATA_TYPE_CLOB]
);
//При необходимости получить/обновить данные
useEffect(() => {
//Загрузка данных с сервера
const loadData = async () => {
try {
setLoading(true);
const data = await executeStored({
stored: "PKG_P8PANELS_QE.QUERY_ENT_ATTRS_GET",
args: { NRN: query, SID: entity },
respArg: "COUT",
isArray: name => ["XATTR"].includes(name),
attributeValueProcessor: (name, val) => (["name", "title", "agg"].includes(name) ? undefined : val),
loader: true
});
setData(data?.XATTRS?.XATTR || []);
} finally {
setRefresh(false);
setLoading(false);
}
};
//Если надо обновить
if (refresh)
//Получим данные
loadData();
}, [refresh, query, entity, executeStored]);
//Возвращаем интерфейс хука
return [data, setAttrs, doRefresh, isLoading];
};
//----------------
//Интерфейс модуля
//----------------
export { useEntityAttrs };

View File

@ -10,6 +10,7 @@
import React from "react"; //Классы React import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, List, ListItem, IconButton, Icon, ListItemButton, ListItemText, Typography } from "@mui/material"; //Интерфейсные компоненты MUI import { Stack, List, ListItem, IconButton, Icon, ListItemButton, ListItemText, Typography } from "@mui/material"; //Интерфейсные компоненты MUI
import { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
//--------- //---------
@ -20,14 +21,15 @@ import { BUTTONS } from "../../../../../app.text"; //Общие текстовы
const STYLES = { const STYLES = {
SMALL_TOOL_ICON: { SMALL_TOOL_ICON: {
fontSize: 20 fontSize: 20
} },
LIST: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL }
}; };
//--------- //---------
//Константы //Константы
//--------- //---------
//Структура данных о сущности запроса //Структура элемента списка запросов
const QUERIES_LIST_ITEM_SHAPE = PropTypes.shape({ const QUERIES_LIST_ITEM_SHAPE = PropTypes.shape({
rn: PropTypes.number.isRequired, rn: PropTypes.number.isRequired,
code: PropTypes.string.isRequired, code: PropTypes.string.isRequired,
@ -43,7 +45,7 @@ const QUERIES_LIST_ITEM_SHAPE = PropTypes.shape({
//Тело модуля //Тело модуля
//----------- //-----------
//Диалог открытия запроса //Список запросов
const QueriesList = ({ queries = [], current = null, onSelect = null, onPbl = null, onReady = null, onEdit = null, onDelete = null } = {}) => { const QueriesList = ({ queries = [], current = null, onSelect = null, onPbl = null, onReady = null, onEdit = null, onDelete = null } = {}) => {
//При выборе элемента списка //При выборе элемента списка
const handleSelectClick = query => { const handleSelectClick = query => {
@ -76,7 +78,7 @@ const QueriesList = ({ queries = [], current = null, onSelect = null, onPbl = nu
//Формирование представления //Формирование представления
return ( return (
<List sx={{ height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto" }}> <List sx={STYLES.LIST}>
{queries.map((query, i) => { {queries.map((query, i) => {
const selected = query.rn === current; const selected = query.rn === current;
const disabled = !query.modify; const disabled = !query.modify;
@ -85,8 +87,8 @@ const QueriesList = ({ queries = [], current = null, onSelect = null, onPbl = nu
const readyTitle = `${query.ready === 1 ? "Готов" : "Не готов"}${!disabled ? " - нажмите, чтобы изменить" : ""}`; const readyTitle = `${query.ready === 1 ? "Готов" : "Не готов"}${!disabled ? " - нажмите, чтобы изменить" : ""}`;
const readyIcon = query.ready === 1 ? "touch_app" : "do_not_touch"; const readyIcon = query.ready === 1 ? "touch_app" : "do_not_touch";
return ( return (
<ListItem key={i}> <ListItem key={i} disablePadding>
<ListItemButton onClick={() => handleSelectClick(query)} selected={selected}> <ListItemButton onClick={() => handleSelectClick(query)} selected={selected} dense>
<ListItemText <ListItemText
primary={query.name} primary={query.name}
secondaryTypographyProps={{ component: "div" }} secondaryTypographyProps={{ component: "div" }}

View File

@ -36,7 +36,7 @@ const QueriesManager = ({ current = null, onQuerySelect = null, onCancel = null
const handleQueryAdd = () => setModQuery(true); const handleQueryAdd = () => setModQuery(true);
//При выборе запроса //При выборе запроса
const handleQuerySelect = query => onQuerySelect && onQuerySelect(query.rn); const handleQuerySelect = query => onQuerySelect && onQuerySelect({ ...query });
//При установке признака публичности //При установке признака публичности
const handleQueryPblSet = query => setQueryPbl(query.rn, query.pbl === 1 ? 0 : 1); const handleQueryPblSet = query => setQueryPbl(query.rn, query.pbl === 1 ? 0 : 1);

View File

@ -8,10 +8,11 @@
//--------------------- //---------------------
import React, { useState, useCallback, useEffect } from "react"; //Классы React import React, { useState, useCallback, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import ReactFlow, { addEdge, Controls, getOutgoers, applyNodeChanges, applyEdgeChanges } from "reactflow"; //Библиотека редактора диаграмм import ReactFlow, { addEdge, Controls, getOutgoers, applyNodeChanges, applyEdgeChanges } from "reactflow"; //Библиотека редактора диаграмм
import { NODE_TYPE } from "../../common"; //Общие ресурсы и константы редактора import { NODE_TYPE } from "../../common"; //Общие ресурсы и константы редактора
import { Entity } from "../entity/entity"; //Сущность запроса import { Entity, ENTITY_DATA_SHAPE } from "../entity/entity"; //Сущность запроса
import { Attribute } from "../attribute/attribute"; //Атрибут сущности import { Attribute, ATTRIBUTE_DATA_SHAPE } from "../attribute/attribute"; //Атрибут сущности
import "reactflow/dist/style.css"; //Типовые стили библиотеки редактора диаграмм import "reactflow/dist/style.css"; //Типовые стили библиотеки редактора диаграмм
import "./query_diagram.css"; //Стили компонента import "./query_diagram.css"; //Стили компонента
@ -36,42 +37,48 @@ const NODE_TYPES_COMPONENTS = {
[NODE_TYPE.ATTRIBUTE]: Attribute [NODE_TYPE.ATTRIBUTE]: Attribute
}; };
//Структура сущности запроса
const ENTITY_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
type: PropTypes.oneOf([NODE_TYPE.ENTITY, NODE_TYPE.ATTRIBUTE]).isRequired,
style: PropTypes.object,
position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }),
draggable: PropTypes.bool.isRequired,
data: PropTypes.oneOfType([ENTITY_DATA_SHAPE, ATTRIBUTE_DATA_SHAPE])
});
//Структура связи запроса
const RELATION_SHAPE = PropTypes.shape({ id: PropTypes.string.isRequired, source: PropTypes.string.isRequired, target: PropTypes.string.isRequired });
//------------------------------------ //------------------------------------
//Вспомогательные функции и компоненты //Вспомогательные функции и компоненты
//------------------------------------ //------------------------------------
//Проверка зацикленности связи
const hasCycle = (connection, target, nodes, edges, visited = new Set()) => { const hasCycle = (connection, target, nodes, edges, visited = new Set()) => {
if (visited.has(target.id)) { if (visited.has(target.id)) return false;
return false;
}
visited.add(target.id); visited.add(target.id);
for (const outgoer of getOutgoers(target, nodes, edges))
for (const outgoer of getOutgoers(target, nodes, edges)) { if (outgoer.id === connection.source || hasCycle(connection, outgoer, nodes, edges, visited)) return true;
if (outgoer.id === connection.source || hasCycle(connection, outgoer, nodes, edges, visited)) {
return true;
}
}
return false; return false;
}; };
//Проверка корректности связи
const isValidConnection = (connection, nodes, edges) => { const isValidConnection = (connection, nodes, edges) => {
if (!connection.source || !connection.target) { //Должны быть заданы источник и приёмник
return false; if (!connection.source || !connection.target) return false;
} //Найдем источник и приёмник
const source = nodes.find(node => node.id === connection.source);
const tableId = connection.source.split("-")[0];
const isSameTable = connection.target.startsWith(tableId);
if (isSameTable) {
return false;
}
const target = nodes.find(node => node.id === connection.target); 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); return !hasCycle(connection, target, nodes, edges);
}; };
@ -80,7 +87,18 @@ const isValidConnection = (connection, nodes, edges) => {
//----------- //-----------
//Диаграмма запроса //Диаграмма запроса
const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRemove, onRelationAdd, onRelationRemove }) => { const QueryDiagram = ({
entities = [],
relations = [],
onEntityClick,
onEntityAttrClick,
onEntityPositionChange,
onEntityRemove,
onRelactionClick,
onRelationAdd,
onRelationRemove,
onPaneClick
}) => {
//Собственное состояние - элементы //Собственное состояние - элементы
const [nodes, setNodes] = useState(entities); const [nodes, setNodes] = useState(entities);
@ -93,17 +111,44 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem
//При изменении элементов на диаграмме //При изменении элементов на диаграмме
const handleNodesChange = useCallback( const handleNodesChange = useCallback(
changes => { 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) if (changes.length == 1 && changes[0].type == "position" && changes[0].dragging)
setMovedNode({ id: changes[0].id, position: { ...changes[0].position } }); setMovedNode({ id: changes[0].id, position: { ...changes[0].position } });
//Если закончили двигать сущность и ранее запомнили начало движения - вызываем колбэки для нажатия и двиения сущности
if (changes.length == 1 && changes[0].type == "position" && !changes[0].dragging && movedNode) { if (changes.length == 1 && changes[0].type == "position" && !changes[0].dragging && movedNode) {
if (onEntityPositionChange) onEntityPositionChange(movedNode.id, movedNode.position); if (onEntityPositionChange) onEntityPositionChange(movedNode.id, movedNode.position);
if (onEntityClick) onEntityClick(movedNode.id);
setMovedNode(null); setMovedNode(null);
} }
//Если удалили сущность - вызываем колбэк для удаления
if (changes[0].type == "remove" && entities.find(e => e.id == changes[0].id && e.type == NODE_TYPE.ENTITY) && onEntityRemove) if (changes[0].type == "remove" && entities.find(e => e.id == changes[0].id && e.type == NODE_TYPE.ENTITY) && onEntityRemove)
onEntityRemove(changes[0].id); onEntityRemove(changes[0].id);
}, },
[movedNode, entities, onEntityPositionChange, onEntityRemove] [movedNode, entities, onEntityClick, onEntityPositionChange, onEntityRemove]
);
//При выборе элемента диаграммы
const handleNodeClick = useCallback(
(e, node) =>
node?.type == NODE_TYPE.ENTITY
? onEntityClick && onEntityClick(node?.id)
: onEntityAttrClick && onEntityAttrClick(node?.parentId, node?.id),
[onEntityClick, onEntityAttrClick]
); );
//При связывании элементов на диаграмме //При связывании элементов на диаграмме
@ -112,6 +157,9 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem
onRelationAdd && onRelationAdd(connection.source, connection.target); onRelationAdd && onRelationAdd(connection.source, connection.target);
}; };
//При выборе связи диаграммы
const handleEdgeClick = useCallback((e, edge) => onRelactionClick && onRelactionClick(edge.id), [onRelactionClick]);
//При изменении связей на диаграмме //При изменении связей на диаграмме
const handleEdgesChange = useCallback( const handleEdgesChange = useCallback(
changes => { changes => {
@ -121,9 +169,13 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem
[onRelationRemove] [onRelationRemove]
); );
const validateConnection = connection => { //При нажатии на холст диаграммы
return isValidConnection(connection, nodes, edges); const handlePaneClick = () => onPaneClick && onPaneClick();
};
//Валидация связи
const validateConnection = connection => isValidConnection(connection, nodes, edges);
//Подсветка выбранной сущности
//При изменении состава сущностей //При изменении состава сущностей
useEffect(() => setNodes(entities), [entities]); useEffect(() => setNodes(entities), [entities]);
@ -137,8 +189,11 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem
nodes={nodes} nodes={nodes}
nodeTypes={NODE_TYPES_COMPONENTS} nodeTypes={NODE_TYPES_COMPONENTS}
edges={edges} edges={edges}
onNodeClick={handleNodeClick}
onNodesChange={handleNodesChange} onNodesChange={handleNodesChange}
onEdgeClick={handleEdgeClick}
onEdgesChange={handleEdgesChange} onEdgesChange={handleEdgesChange}
onPaneClick={handlePaneClick}
defaultEdgeOptions={{ defaultEdgeOptions={{
animated: true, animated: true,
style: STYLES.EDGE style: STYLES.EDGE
@ -153,6 +208,20 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem
); );
}; };
//Контроль свойств компонента - Диаграмма запроса
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,
onPaneClick: PropTypes.func
};
//---------------- //----------------
//Интерфейс модуля //Интерфейс модуля
//---------------- //----------------

View File

@ -0,0 +1,122 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Свойства запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Button, Icon } from "@mui/material"; //Интерфейсные компоненты MUI
import { MessagingСtx } from "../../../../context/messaging"; //Контекст сообщений приложения
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
import { EntityAddDialog } from "../entity_add_dialog/entity_add_dialog"; //Диалог добавления сущности
import { EntityAttrsDialog } from "../entity_attrs_dialog/entity_attrs_dialog"; //Диалог настройки атрибутов сущности
import { P8PEditorBox } from "../../../../components/editors/p8p_editor_box"; //Контейнер параметров редактора
import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Подзаголовок группы параметров редактора
import { ENTITY_DATA_SHAPE } from "../entity/entity"; //Описание сущности
import { RELATION_DATA_SHAPE } from "../relation/relation"; //Описание связи
//-----------
//Тело модуля
//-----------
//Свойства запроса
const QueryOptions = ({ query, entity, relation, onEntityAdd, onEntityRemove, onRelationRemove, onQueryOptionsChanged }) => {
//Отображение диалога добавления сущности
const [openEntityAddDialog, setOpenEntityAddDialog] = useState(false);
//Отображение диалога настройки атрибутов сущности
const [openEntityAttrsDialog, setOpenEntityAttrsDialog] = useState(false);
//Подключение к контексту сообщений
const { showMsgWarn } = useContext(MessagingСtx);
//При нажатии на кнопку добавлении сущности в запрос
const handleEntityAddClick = () => setOpenEntityAddDialog(true);
//При нажатии на кнопку настройки атрибутов сущности
const handleEntityAttrsClick = () => setOpenEntityAttrsDialog(true);
//При нажатии на кнопку даления сущности из запроса
const handleEntityRemoveClick = () =>
showMsgWarn(`Удалить сущность "${entity.title}"?`, () => entity?.id && onEntityRemove && onEntityRemove(entity.id));
//При нажатии на кнопку даления связи из запроса
const handleRelationRemoveClick = () =>
showMsgWarn(
`Удалить связь "${relation.source}" - "${relation.target}"?`,
() => relation?.id && onRelationRemove && onRelationRemove(relation.id)
);
//Закрытие диалога добавления сущности по "Отмена"
const handleEntityAddDialogCancel = () => setOpenEntityAddDialog(false);
//Закрытие диалога добавления сущности по "ОК"
const handleEntityAddDialogOk = values => onEntityAdd && onEntityAdd(values.name, res => res && setOpenEntityAddDialog(false));
//Закрытие диалога настройки атрибутов сущности по "Отмена"
const handleEntityAttrsDialogCancel = () => setOpenEntityAttrsDialog(false);
//Закрытие диалога настройки атрибутов сущности по "ОК"
const handleEntityAttrsDialogOk = () => {
onQueryOptionsChanged && onQueryOptionsChanged();
setOpenEntityAttrsDialog();
};
//Генерация содержимого
return (
<>
{openEntityAddDialog && <EntityAddDialog onOk={handleEntityAddDialogOk} onCancel={handleEntityAddDialogCancel} />}
{openEntityAttrsDialog && (
<EntityAttrsDialog query={query} {...entity} onOk={handleEntityAttrsDialogOk} onCancel={handleEntityAttrsDialogCancel} />
)}
<P8PEditorBox title={"Настройки запроса"}>
<P8PEditorSubHeader title={"Параметры"} />
<P8PEditorSubHeader title={"Условия отбора"} />
<P8PEditorSubHeader title={"Сущности"} />
<Button startIcon={<Icon>add</Icon>} onClick={handleEntityAddClick}>
{BUTTONS.INSERT}
</Button>
{entity && (
<>
<P8PEditorSubHeader title={entity.title} />
<Button startIcon={<Icon>edit_attributes</Icon>} onClick={handleEntityAttrsClick}>
Атрибуты
</Button>
<Button startIcon={<Icon>delete</Icon>} onClick={handleEntityRemoveClick}>
{BUTTONS.DELETE}
</Button>
</>
)}
{relation && (
<>
<P8PEditorSubHeader title={"Связь"} />
<Button startIcon={<Icon>delete</Icon>} onClick={handleRelationRemoveClick}>
{BUTTONS.DELETE}
</Button>
</>
)}
</P8PEditorBox>
</>
);
};
//Контроль свойств компонента - Свойства запроса
QueryOptions.propTypes = {
query: PropTypes.number.isRequired,
entity: ENTITY_DATA_SHAPE,
relation: RELATION_DATA_SHAPE,
onEntityAdd: PropTypes.func,
onEntityRemove: PropTypes.func,
onRelationRemove: PropTypes.func,
onQueryOptionsChanged: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { QueryOptions };

View File

@ -0,0 +1,27 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компоненты: Связь сущностей запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import PropTypes from "prop-types"; //Контроль свойств компонента
//---------
//Константы
//---------
//Структура данных о связи сущностей запроса
const RELATION_DATA_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
source: PropTypes.string.isRequired,
target: PropTypes.string.isRequired
});
//----------------
//Интерфейс модуля
//----------------
export { RELATION_DATA_SHAPE };

View File

@ -7,22 +7,23 @@
//Подключение библиотек //Подключение библиотек
//--------------------- //---------------------
import React, { useState } from "react"; //Классы React import React, { useState, useContext } from "react"; //Классы React
import { Box, Grid, Button, Icon } from "@mui/material"; //Интерфейсные компоненты MUI import { Box, Grid } from "@mui/material"; //Интерфейсные компоненты MUI
import { ApplicationСtx } from "../../context/application"; //Контекст взаимодействия с приложением
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Компоненты рабочего стола import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Компоненты рабочего стола
import { P8PEditorToolBar } from "../../components/editors/p8p_editor_toolbar"; //Панель инструментов редактора import { P8PEditorToolBar } from "../../components/editors/p8p_editor_toolbar"; //Панель инструментов редактора
import { BUTTONS } from "../../../app.text"; //Общие текстовые ресурсы приложения
import { QueryDiagram } from "./components/query_diagram/query_diagram"; //Диаграмма запроса import { QueryDiagram } from "./components/query_diagram/query_diagram"; //Диаграмма запроса
import { QueryOptions } from "./components/query_options/query_options"; //Свойства запроса
import { QueriesManager } from "./components/queries_manager/queries_manager"; //Менеджер запросов import { QueriesManager } from "./components/queries_manager/queries_manager"; //Менеджер запросов
import { EntityAddDialog } from "./components/entity_add_dialog/entity_add_dialog"; //Диалог добавления сущности
import { P8PEditorBox } from "../../components/editors/p8p_editor_box"; //Контейнер параметров редактора
import { P8PEditorSubHeader } from "../../components/editors/p8p_editor_sub_header"; //Подзаголовок группы параметров редактора
import { useQueryDesc } from "./hooks"; //Пользовательские хуки import { useQueryDesc } from "./hooks"; //Пользовательские хуки
//--------- //---------
//Константы //Константы
//--------- //---------
//Заголовок панели по умолчанию
const APP_BAR_TITLE_DEFAULT = "Редактор запросов";
//Стили //Стили
const STYLES = { const STYLES = {
CONTAINER: { display: "flex" }, CONTAINER: { display: "flex" },
@ -39,26 +40,79 @@ const QueryEditor = () => {
//Текущий запрос //Текущий запрос
const [query, setQuery] = useState(null); const [query, setQuery] = useState(null);
//Текущая сущность
const [entity, setEntity] = useState(null);
//Текущая связь
const [relation, setRelation] = useState(null);
//Отображения менеджера запросов //Отображения менеджера запросов
const [openQueriesManager, setOpenQueriesManager] = useState(true); const [openQueriesManager, setOpenQueriesManager] = useState(true);
//Отображение диалога добавления сущности
const [openEntityAddDialog, setOpenEntityAddDialog] = useState(false);
//Получение данных запроса //Получение данных запроса
const [queryDiagram, addEnt, removeEnt, setEntPosition, addRl, removeRl] = useQueryDesc(query); const [queryDiagram, addEnt, removeEnt, setEntPosition, addRl, removeRl, doRefresh] = useQueryDesc(query);
//Подключение к контексту приложения
const { setAppBarTitle } = useContext(ApplicationСtx);
//Выбор сущности
const selectEntity = ent => {
setRelation(null);
const queryEnt = queryDiagram.entities.find(e => e.id === ent);
if (queryEnt) setEntity({ ...queryEnt.data });
};
//Выбор связи
const selectRelation = rl => {
setEntity(null);
const queryRl = queryDiagram.relations.find(r => r.id === rl);
if (queryRl) setRelation({ ...queryRl });
};
//Сброс выбора связи и сущности
const cleanupEnRlSelection = () => {
setRelation(null);
setEntity(null);
};
//Обработка изменения положения сущности на диаграмме //Обработка изменения положения сущности на диаграмме
const handleEntityPositionChange = (ent, position) => setEntPosition(ent, position.x, position.y); const handleEntityPositionChange = (ent, position) => setEntPosition(ent, position.x, position.y);
//Обработка добавления сущности в запрос
const handleEntityAdd = async (entName, cb) => {
await addEnt(entName, "VIEW");
cb(true);
};
//Обработка удаления сущности из запроса //Обработка удаления сущности из запроса
const handleEntityRemove = ent => removeEnt(ent); const handleEntityRemove = async ent => {
await removeEnt(ent);
if (entity && entity?.id === ent) cleanupEnRlSelection();
};
//Обработка выделения сущности
const handleEntityClick = ent => selectEntity(ent);
//Обработка выделения тарибута сущности
const handleEntityAttrClick = ent => selectEntity(ent);
//Обработка выделения связи
const handleRelationClick = rl => selectRelation(rl);
//Обработка добавления отношения cущностей //Обработка добавления отношения cущностей
const handleRelationAdd = (source, target) => addRl(source, target); const handleRelationAdd = (source, target) => {
cleanupEnRlSelection();
addRl(source, target);
};
//Обработка удаления отношения cущностей //Обработка удаления отношения cущностей
const handleRelationRemove = rl => removeRl(rl); const handleRelationRemove = async rl => {
await removeRl(rl);
if (relation && relation?.id === rl) cleanupEnRlSelection();
};
//При нажатии на панели (пустом месте) диаграммы запроса
const handlePaneClick = () => cleanupEnRlSelection();
//Открытие менеджера запросов //Открытие менеджера запросов
const handleOpenQueriesManager = () => setOpenQueriesManager(true); const handleOpenQueriesManager = () => setOpenQueriesManager(true);
@ -67,26 +121,25 @@ const QueryEditor = () => {
const handleCancelQueriesManager = () => setOpenQueriesManager(false); const handleCancelQueriesManager = () => setOpenQueriesManager(false);
//Закрытие запроса //Закрытие запроса
const handleQueryClose = () => setQuery(null); const handleQueryClose = () => {
setAppBarTitle(APP_BAR_TITLE_DEFAULT);
cleanupEnRlSelection();
setQuery(null);
};
//При выборе запроса //При выборе запроса
const handleQuerySelect = query => { const handleQuerySelect = ({ rn, name }) => {
setQuery(query); setAppBarTitle(`Запрос [${name}]`);
setQuery(rn);
setOpenQueriesManager(false); setOpenQueriesManager(false);
}; };
//При добавлении сущности в запрос //При изменении свойств запроса
const handleEntityAdd = () => setOpenEntityAddDialog(true); const handleQueryOptionsChanged = () => {
cleanupEnRlSelection();
//Закрытие диалога добавления сущности по "ОК" doRefresh();
const handleEntityAddDialogOk = async values => {
await addEnt(values.name, "VIEW");
setOpenEntityAddDialog(false);
}; };
//Закрытие диалога добавления сущности по "ОК"
const handleEntityAddDialogCancel = () => setOpenEntityAddDialog(false);
//Панель инструмментов //Панель инструмментов
const toolBar = ( const toolBar = (
<P8PEditorToolBar <P8PEditorToolBar
@ -101,28 +154,34 @@ const QueryEditor = () => {
return ( return (
<Box sx={STYLES.CONTAINER}> <Box sx={STYLES.CONTAINER}>
{openQueriesManager && <QueriesManager current={query} onQuerySelect={handleQuerySelect} onCancel={handleCancelQueriesManager} />} {openQueriesManager && <QueriesManager current={query} onQuerySelect={handleQuerySelect} onCancel={handleCancelQueriesManager} />}
{openEntityAddDialog && <EntityAddDialog onOk={handleEntityAddDialogOk} onCancel={handleEntityAddDialogCancel} />}
<Grid container sx={STYLES.GRID_CONTAINER} columns={25}> <Grid container sx={STYLES.GRID_CONTAINER} columns={25}>
<Grid item xs={20}> <Grid item xs={20}>
{queryDiagram && ( {queryDiagram && (
<QueryDiagram <QueryDiagram
{...queryDiagram} {...queryDiagram}
onEntityClick={handleEntityClick}
onEntityAttrClick={handleEntityAttrClick}
onEntityPositionChange={handleEntityPositionChange} onEntityPositionChange={handleEntityPositionChange}
onEntityRemove={handleEntityRemove} onEntityRemove={handleEntityRemove}
onRelactionClick={handleRelationClick}
onRelationAdd={handleRelationAdd} onRelationAdd={handleRelationAdd}
onRelationRemove={handleRelationRemove} onRelationRemove={handleRelationRemove}
onPaneClick={handlePaneClick}
/> />
)} )}
</Grid> </Grid>
<Grid item xs={5} sx={STYLES.GRID_ITEM_INSPECTOR}> <Grid item xs={5} sx={STYLES.GRID_ITEM_INSPECTOR}>
{toolBar} {toolBar}
{query && ( {query && (
<P8PEditorBox title={"Параметры запроса"}> <QueryOptions
<P8PEditorSubHeader title={"Сущности"} /> query={query}
<Button startIcon={<Icon>add</Icon>} onClick={handleEntityAdd}> entity={entity}
{BUTTONS.INSERT} relation={relation}
</Button> onEntityAdd={handleEntityAdd}
</P8PEditorBox> onEntityRemove={handleEntityRemove}
onRelationRemove={handleRelationRemove}
onQueryOptionsChanged={handleQueryOptionsChanged}
/>
)} )}
</Grid> </Grid>
</Grid> </Grid>

View File

@ -59,6 +59,22 @@ create or replace package PKG_P8PANELS_QE as
NY in number -- Координата по оси ординат NY in number -- Координата по оси ординат
); );
/* Получение состава атрибутов сущности */
procedure QUERY_ENT_ATTRS_GET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
COUT out clob -- Сериализованное описание атрибутов сущности
);
/* Установка состава атрибутов сущности */
procedure QUERY_ENT_ATTRS_SET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
CATTRS in clob -- Сериализованное описание атрибутов сущности (BASE64 XML)
);
/* Добавление связи в запрос */ /* Добавление связи в запрос */
procedure QUERY_RL_ADD procedure QUERY_RL_ADD
( (
@ -192,7 +208,6 @@ create or replace package body PKG_P8PANELS_QE as
RENTS PKG_P8PANELS_QE_BASE.TENTS; -- Коллекция существующих сущностей RENTS PKG_P8PANELS_QE_BASE.TENTS; -- Коллекция существующих сущностей
RENT PKG_P8PANELS_QE_BASE.TENT; -- Удаляемая сущность RENT PKG_P8PANELS_QE_BASE.TENT; -- Удаляемая сущность
RRLS PKG_P8PANELS_QE_BASE.TRLS; -- Коллекция существующих связей RRLS PKG_P8PANELS_QE_BASE.TRLS; -- Коллекция существующих связей
RRLS_TMP PKG_P8PANELS_QE_BASE.TRLS; -- Буфер для коллекции удаляемых связей
begin begin
/* Провим права доступа */ /* Провим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER()); PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
@ -218,20 +233,8 @@ create or replace package body PKG_P8PANELS_QE as
if ((RENT.RATTRS is not null) and (RENT.RATTRS.COUNT > 0)) then if ((RENT.RATTRS is not null) and (RENT.RATTRS.COUNT > 0)) then
for I in RENT.RATTRS.FIRST .. RENT.RATTRS.LAST for I in RENT.RATTRS.FIRST .. RENT.RATTRS.LAST
loop loop
/* Если атрибут есть в связях (как источник или как приёмник) */ /* Удаляем связи в которых он задействован */
for J in 0 .. 1 PKG_P8PANELS_QE_BASE.TRLS_CLEANUP_BY_ATTR(RRLS => RRLS, SATTR_ID => RENT.RATTRS(I).SID);
loop
RRLS_TMP := PKG_P8PANELS_QE_BASE.TRLS_LIST_BY_ST(RRLS => RRLS,
SSOURCE_TARGET => RENT.RATTRS(I).SID,
NLIST_TYPE => J);
/* То связь должна быть удалена */
if ((RRLS_TMP is not null) and (RRLS_TMP.COUNT > 0)) then
for K in RRLS_TMP.FIRST .. RRLS_TMP.LAST
loop
PKG_P8PANELS_QE_BASE.TRLS_REMOVE(RRLS => RRLS, SID => RRLS_TMP(K).SID);
end loop;
end if;
end loop;
end loop; end loop;
end if; end if;
/* Сохраняем обновленный набор сущностей */ /* Сохраняем обновленный набор сущностей */
@ -265,6 +268,77 @@ create or replace package body PKG_P8PANELS_QE as
end if; end if;
end QUERY_ENT_POSITION_SET; end QUERY_ENT_POSITION_SET;
/* Получение состава атрибутов сущности */
procedure QUERY_ENT_ATTRS_GET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
COUT out clob -- Сериализованное описание атрибутов сущности
)
is
begin
/* Провим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
/* Сформируем описание атрибутов */
PKG_P8PANELS_QE_BASE.QUERY_ENT_ATTRS_GET(NRN => NRN, SID => SID, COUT => COUT);
end QUERY_ENT_ATTRS_GET;
/* Установка состава атрибутов сущности */
procedure QUERY_ENT_ATTRS_SET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
CATTRS in clob -- Сериализованное описание атрибутов сущности (BASE64 XML)
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
RENTS PKG_P8PANELS_QE_BASE.TENTS; -- Коллекция существующих сущностей
NENT_INDEX PKG_STD.TNUMBER; -- Индекс изменяемой сущности
RATTRS PKG_P8PANELS_QE_BASE.TATTRS; -- Коллекция полученных атриубтов
RRLS PKG_P8PANELS_QE_BASE.TRLS; -- Коллекция существующих связей
begin
/* Провим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
/* Читаем запись запроса */
RQ := PKG_P8PANELS_QE_BASE.QUERY_GET(NRN => NRN);
/* Читаем существующие сущности */
RENTS := PKG_P8PANELS_QE_BASE.QUERY_ENTS_GET(CENTS => RQ.ENTS);
/* Читаем свзяи */
RRLS := PKG_P8PANELS_QE_BASE.QUERY_RLS_GET(CRLS => RQ.RLS);
/* Находим изменяемую сущность */
NENT_INDEX := PKG_P8PANELS_QE_BASE.TENTS_INDEX_BY_ID(RENTS => RENTS, SID => SID);
if (NENT_INDEX is null) then
P_EXCEPTION(0,
'Сущность с идентификатором "%s" в запросе "%s" не определена.',
COALESCE(SID, '<НЕ УКАЗАН>'),
TO_CHAR(NRN));
end if;
/* Десериализуем новый набор атрибутов */
RATTRS := PKG_P8PANELS_QE_BASE.TATTRS_FROM_XML_BASE64(CXML => CATTRS,
SCHARSET => PKG_CHARSET.CHARSET_UTF_(),
BADD_ROOT => true);
/* Сбросим текущие атрибуты сущности */
RENTS(NENT_INDEX).RATTRS := PKG_P8PANELS_QE_BASE.TATTRS();
/* Наполним полученными и зачистим связи */
if (RATTRS is not null) and (RATTRS.COUNT > 0) then
for I in RATTRS.FIRST .. RATTRS.LAST
loop
/* Если атрибут используется - кладём в обновленную коллекцию атрибутов сущности */
if (RATTRS(I).NUSE = 1) then
RENTS(NENT_INDEX).RATTRS.EXTEND();
RENTS(NENT_INDEX).RATTRS(RENTS(NENT_INDEX).RATTRS.LAST) := RATTRS(I);
else
/* Атрибут не используется - необходимо очистись связи в которых он задействован */
PKG_P8PANELS_QE_BASE.TRLS_CLEANUP_BY_ATTR(RRLS => RRLS, SATTR_ID => RATTRS(I).SID);
end if;
end loop;
end if;
/* Сохраняем обновленный набор сущностей */
PKG_P8PANELS_QE_BASE.QUERY_ENTS_SET(NRN => RQ.RN, RENTS => RENTS);
/* Сохраняем обновленный набор связей */
PKG_P8PANELS_QE_BASE.QUERY_RLS_SET(NRN => RQ.RN, RRLS => RRLS);
end QUERY_ENT_ATTRS_SET;
/* Добавление связи в запрос */ /* Добавление связи в запрос */
procedure QUERY_RL_ADD procedure QUERY_RL_ADD
( (

View File

@ -1,16 +1,16 @@
create or replace package PKG_P8PANELS_QE_BASE as create or replace package PKG_P8PANELS_QE_BASE as
/* Константы - Типы сущностей */
SENT_TYPE_TABLE constant PKG_STD.TSTRING := 'TABLE'; -- Таблица
SENT_TYPE_VIEW constant PKG_STD.TSTRING := 'VIEW'; -- Представление
/* Типы данных - Атрибут сущности */ /* Типы данных - Атрибут сущности */
type TATTR is record type TATTR is record
( (
SID PKG_STD.TSTRING, -- Уникальный идентификатор в запросе SID PKG_STD.TSTRING, -- Уникальный идентификатор в запросе
SNAME PKG_STD.TSTRING, -- Имя SNAME PKG_STD.TSTRING, -- Имя
STITLE PKG_STD.TSTRING, -- Заголовок STITLE PKG_STD.TSTRING, -- Заголовок
NDATA_TYPE PKG_STD.TNUMBER -- Тип данных (см. константы PKG_STD.DATA_TYPE_*) NDATA_TYPE PKG_STD.TNUMBER, -- Тип данных (см. константы PKG_STD.DATA_TYPE_*)
SAGG PKG_STD.TSTRING, -- Агрегатная функция
SALIAS PKG_STD.TSTRING, -- Псевдоним в выборке
NUSE PKG_STD.TNUMBER, -- Флаг применения в запросе (1 - да, 0 - нет)
NSHOW PKG_STD.TNUMBER -- Флаг отображения в выборке (1 - да, 0 - нет)
); );
/* Типы данных - Коллекция атрибутов сущности */ /* Типы данных - Коллекция атрибутов сущности */
@ -42,6 +42,14 @@ create or replace package PKG_P8PANELS_QE_BASE as
/* Типы данных - Коллекция отношений */ /* Типы данных - Коллекция отношений */
type TRLS is table of TRL; type TRLS is table of TRL;
/* Десериализация коллекции атрибутов сущности (из BASE64) */
function TATTRS_FROM_XML_BASE64
(
CXML in clob, -- XML-описание коллекции атрибутов сущности (BASE64)
SCHARSET in varchar2, -- Кодировка, в которой XML-данные были упакованы в BASE64
BADD_ROOT in boolean := false -- Флаг необходимости добавления корневого тэга (false - нет, true - да)
) return TATTRS; -- Коллекция атрибутов сущности
/* Поиск индекса сущности по идентификатору */ /* Поиск индекса сущности по идентификатору */
function TENTS_INDEX_BY_ID function TENTS_INDEX_BY_ID
( (
@ -73,14 +81,6 @@ create or replace package PKG_P8PANELS_QE_BASE as
NY in number -- Координата по оси ординат NY in number -- Координата по оси ординат
); );
/* Формирование коллекции связей по источнику/приёмнику */
function TRLS_LIST_BY_ST
(
RRLS in TRLS, -- Коллекция связей
SSOURCE_TARGET in varchar2, -- Идентификатор источника/приёмкника
NLIST_TYPE in number -- Тип формирования коллекции (0 - по источнику, 1 - по приёмнику
) return TRLS; -- Сформированная коллекция
/* Добавление связи в коллекцию */ /* Добавление связи в коллекцию */
procedure TRLS_APPEND procedure TRLS_APPEND
( (
@ -96,6 +96,13 @@ create or replace package PKG_P8PANELS_QE_BASE as
SID in varchar2 -- Идентификатор удялемой связи SID in varchar2 -- Идентификатор удялемой связи
); );
/* Подчистка коллекции связей по атрибуту */
procedure TRLS_CLEANUP_BY_ATTR
(
RRLS in out nocopy TRLS, -- Изменяемая коллекция
SATTR_ID in varchar2 -- Идентификатор атрибута
);
/* Считывание записи запроса */ /* Считывание записи запроса */
function QUERY_GET function QUERY_GET
( (
@ -177,6 +184,14 @@ create or replace package PKG_P8PANELS_QE_BASE as
RENTS in TENTS -- Коллекция сущностей RENTS in TENTS -- Коллекция сущностей
); );
/* Получение состава атрибутов сущности */
procedure QUERY_ENT_ATTRS_GET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
COUT out clob -- Сериализованное описание атрибутов сущности
);
/* Чтение связей запроса */ /* Чтение связей запроса */
function QUERY_RLS_GET function QUERY_RLS_GET
( (
@ -208,6 +223,10 @@ end PKG_P8PANELS_QE_BASE;
/ /
create or replace package body PKG_P8PANELS_QE_BASE as create or replace package body PKG_P8PANELS_QE_BASE as
/* Константы - Типы сущностей */
SENT_TYPE_TABLE constant PKG_STD.TSTRING := 'TABLE'; -- Таблица
SENT_TYPE_VIEW constant PKG_STD.TSTRING := 'VIEW'; -- Представление
/* Константы - Теги для сериализации */ /* Константы - Теги для сериализации */
STAG_DATA constant PKG_STD.TSTRING := 'XDATA'; -- Данные STAG_DATA constant PKG_STD.TSTRING := 'XDATA'; -- Данные
STAG_QUERIES constant PKG_STD.TSTRING := 'XQUERIES'; -- Запросы STAG_QUERIES constant PKG_STD.TSTRING := 'XQUERIES'; -- Запросы
@ -238,6 +257,10 @@ create or replace package body PKG_P8PANELS_QE_BASE as
SATTR_Y constant PKG_STD.TSTRING := 'y'; -- Координата по Y SATTR_Y constant PKG_STD.TSTRING := 'y'; -- Координата по Y
SATTR_SOURCE constant PKG_STD.TSTRING := 'source'; -- Источник SATTR_SOURCE constant PKG_STD.TSTRING := 'source'; -- Источник
SATTR_TARGET constant PKG_STD.TSTRING := 'target'; -- Приёмник SATTR_TARGET constant PKG_STD.TSTRING := 'target'; -- Приёмник
SATTR_AGG constant PKG_STD.TSTRING := 'agg'; -- Агрегатная функция
SATTR_ALIAS constant PKG_STD.TSTRING := 'alias'; -- Псевдоним
SATTR_USE constant PKG_STD.TSTRING := 'use'; -- Применение в запросе
SATTR_SHOW constant PKG_STD.TSTRING := 'show'; -- Отображение в запросе
/* Получение заголовка представления из метаданных */ /* Получение заголовка представления из метаданных */
function DMSCLVIEWS_TITLE_GET function DMSCLVIEWS_TITLE_GET
@ -328,6 +351,10 @@ create or replace package body PKG_P8PANELS_QE_BASE as
PKG_XFAST.ATTR(SNAME => SATTR_NAME, SVALUE => RATTR.SNAME); PKG_XFAST.ATTR(SNAME => SATTR_NAME, SVALUE => RATTR.SNAME);
PKG_XFAST.ATTR(SNAME => SATTR_TITLE, SVALUE => RATTR.STITLE); PKG_XFAST.ATTR(SNAME => SATTR_TITLE, SVALUE => RATTR.STITLE);
PKG_XFAST.ATTR(SNAME => SATTR_DATA_TYPE, NVALUE => RATTR.NDATA_TYPE); PKG_XFAST.ATTR(SNAME => SATTR_DATA_TYPE, NVALUE => RATTR.NDATA_TYPE);
PKG_XFAST.ATTR(SNAME => SATTR_AGG, SVALUE => RATTR.SAGG);
PKG_XFAST.ATTR(SNAME => SATTR_ALIAS, SVALUE => RATTR.SALIAS);
PKG_XFAST.ATTR(SNAME => SATTR_USE, NVALUE => RATTR.NUSE);
PKG_XFAST.ATTR(SNAME => SATTR_SHOW, NVALUE => RATTR.NSHOW);
/* Закрываем описание атрибута сущности */ /* Закрываем описание атрибута сущности */
PKG_XFAST.UP(); PKG_XFAST.UP();
end TATTR_TO_XML; end TATTR_TO_XML;
@ -356,6 +383,10 @@ create or replace package body PKG_P8PANELS_QE_BASE as
RRES.SNAME := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_NAME); RRES.SNAME := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_NAME);
RRES.STITLE := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_TITLE); RRES.STITLE := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_TITLE);
RRES.NDATA_TYPE := PKG_XPATH.ATTRIBUTE_NUM(RNODE => XNODE, SNAME => SATTR_DATA_TYPE); RRES.NDATA_TYPE := PKG_XPATH.ATTRIBUTE_NUM(RNODE => XNODE, SNAME => SATTR_DATA_TYPE);
RRES.SAGG := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_AGG);
RRES.SALIAS := PKG_XPATH.ATTRIBUTE(RNODE => XNODE, SNAME => SATTR_ALIAS);
RRES.NUSE := PKG_XPATH.ATTRIBUTE_NUM(RNODE => XNODE, SNAME => SATTR_USE);
RRES.NSHOW := PKG_XPATH.ATTRIBUTE_NUM(RNODE => XNODE, SNAME => SATTR_SHOW);
/* Освободим документ */ /* Освободим документ */
PKG_XPATH.FREE(RDOCUMENT => XDOC); PKG_XPATH.FREE(RDOCUMENT => XDOC);
exception exception
@ -371,6 +402,119 @@ create or replace package body PKG_P8PANELS_QE_BASE as
return RRES; return RRES;
end TATTR_FROM_XML; end TATTR_FROM_XML;
/* Поиск индекса атрибута по имени */
function TATTRS_INDEX_BY_NAME
(
RATTRS in TATTRS, -- Коллекция атрибутов
SNAME in varchar2 -- Искомое имя
) return number -- Индекс найденного атрибута (null - если не найдено)
is
begin
/* Обходим коллекцию */
if ((RATTRS is not null) and (RATTRS.COUNT > 0)) then
for I in RATTRS.FIRST .. RATTRS.LAST
loop
begin
/* Возвращаем найденный индекс */
if (RATTRS(I).SNAME = SNAME) then
return I;
end if;
exception
when NO_DATA_FOUND then
null;
end;
end loop;
end if;
/* Ничего не нашли */
return null;
end TATTRS_INDEX_BY_NAME;
/* Формирование списка атрибутов сущности по типу и наименованию */
function TATTRS_MAKE
(
RENT in TENT, -- Родительская сущность
NCOUNT in number := null -- Количество атрибутов (null - все)
) return TATTRS -- Коллекция атрибутов сущности
is
RRES TATTRS; -- Буфер для результата
RVIEW_FIELDS PKG_OBJECT_DESC.TCOLUMNS; -- Коллекция описаний полей представления
RVIEW_FIELD PKG_OBJECT_DESC.TCOLUMN; -- Описание поля представления
NATTR_INDEX PKG_STD.TNUMBER; -- Индекс поля в коллекции атрибутов сущности
BINIT boolean := false; -- Признак инициализации атрибутов сущности
begin
/* Проверим корректность типа сущности */
if (RENT.STYPE not in (SENT_TYPE_TABLE, SENT_TYPE_VIEW)) then
P_EXCEPTION(0,
'Сущности типа "%s" не поддерживаются.',
COALESCE(RENT.STYPE, '<НЕ УКАЗАН>'));
end if;
/* Проверим, что у сущности задан идентификатор */
if (RENT.SID is null) then
P_EXCEPTION(0,
'Ошибка формирования атрибутов - не задан идентификатор сущности.');
end if;
/* Проверим, что у сущности задано имя */
if (RENT.SNAME is null) then
P_EXCEPTION(0,
'Ошибка формирования атрибутов - не задано имя сущности.');
end if;
/* Инициализируем результат */
RRES := TATTRS();
/* Установим флаг инициализации - в сущности нет атрибутов и нас просили ограничить количество */
if (((RENT.RATTRS is null) or (RENT.RATTRS.COUNT = 0)) and (NCOUNT is not null)) then
BINIT := true;
end if;
/* Если сущность это представление */
if (RENT.STYPE = SENT_TYPE_VIEW) then
/* Получим список полей представления */
RVIEW_FIELDS := PKG_OBJECT_DESC.DESC_SEL_COLUMNS(SSELECT_NAME => RENT.SNAME, BRAISE_ERROR => true);
/* Собираем атрибуты в ответ */
for I in 1 .. PKG_OBJECT_DESC.COUNT_COLUMNS(RCOLUMNS => RVIEW_FIELDS)
loop
/* Считываем очередное поле из коллекции описания полей представления */
RVIEW_FIELD := PKG_OBJECT_DESC.FETCH_COLUMN(RCOLUMNS => RVIEW_FIELDS, IINDEX => I);
/* Если поле поддерживаемого типа */
if (RVIEW_FIELD.DATA_TYPE in (PKG_STD.DATA_TYPE_STR(), PKG_STD.DATA_TYPE_NUM(), PKG_STD.DATA_TYPE_DATE())) then
/* Добавляем элемент в результирующую коллекцию */
RRES.EXTEND();
/* Ищем такой атрибут в родительской сущности */
NATTR_INDEX := TATTRS_INDEX_BY_NAME(RATTRS => RENT.RATTRS, SNAME => RVIEW_FIELD.COLUMN_NAME);
/* Если это поле уже есть в коллекции атрибутов родительской сущности */
if (NATTR_INDEX is not null) then
/* Возьмём элемент оттуда */
RRES(RRES.LAST) := RENT.RATTRS(NATTR_INDEX);
else
/* Такого элемента нет - берем из представления */
RRES(RRES.LAST).SID := TATTR_ID_MAKE(SENT_ID => RENT.SID, SNAME => RVIEW_FIELD.COLUMN_NAME);
RRES(RRES.LAST).SNAME := RVIEW_FIELD.COLUMN_NAME;
RRES(RRES.LAST).STITLE := DMSCLVIEWSATTRS_TITLE_GET(SVIEW_NAME => RENT.SNAME,
SATTR_NAME => RRES(RRES.LAST).SNAME);
RRES(RRES.LAST).NDATA_TYPE := RVIEW_FIELD.DATA_TYPE;
/* Если это инициализиция сущности - установим флаг применения в запросе (тогда атрибут будет отображен в диаграмме) */
if (BINIT) then
RRES(RRES.LAST).NUSE := 1;
RRES(RRES.LAST).NSHOW := 1;
else
RRES(RRES.LAST).NUSE := 0;
RRES(RRES.LAST).NSHOW := 0;
end if;
end if;
/* Ограничим объем коллекции если необходимо */
if (NCOUNT is not null) then
exit when RRES.LAST = NCOUNT;
end if;
end if;
end loop;
end if;
/* Если сущность это таблица */
if (RENT.STYPE = SENT_TYPE_TABLE) then
P_EXCEPTION(0,
'Поддержка формирования атрибутов для сущностей типа "Таблица" ещё не реализована.');
end if;
/* Возвращаем результат */
return RRES;
end TATTRS_MAKE;
/* Сериализация коллекции атрибутов сущности */ /* Сериализация коллекции атрибутов сущности */
procedure TATTRS_TO_XML procedure TATTRS_TO_XML
( (
@ -439,6 +583,26 @@ create or replace package body PKG_P8PANELS_QE_BASE as
return RRES; return RRES;
end TATTRS_FROM_XML; end TATTRS_FROM_XML;
/* Десериализация коллекции атрибутов сущности (из BASE64) */
function TATTRS_FROM_XML_BASE64
(
CXML in clob, -- XML-описание коллекции атрибутов сущности (BASE64)
SCHARSET in varchar2, -- Кодировка, в которой XML-данные были упакованы в BASE64
BADD_ROOT in boolean := false -- Флаг необходимости добавления корневого тэга (false - нет, true - да)
) return TATTRS -- Коллекция атрибутов сущности
is
CTMP clob; -- Буфер для преобразований
begin
/* Избавимся от BASE64 */
CTMP := BLOB2CLOB(LBDATA => BASE64_DECODE(LCSRCE => CXML), SCHARSET => SCHARSET);
/* Если надо - добавим корень */
if (BADD_ROOT) then
CTMP := '<' || STAG_ATTRS || '>' || CTMP || '</' || STAG_ATTRS || '>';
end if;
/* Десериализуем */
return TATTRS_FROM_XML(CXML => CTMP);
end TATTRS_FROM_XML_BASE64;
/* Формирование идентификатора сущности */ /* Формирование идентификатора сущности */
function TENT_ID_MAKE function TENT_ID_MAKE
( (
@ -462,15 +626,13 @@ create or replace package body PKG_P8PANELS_QE_BASE as
/* Формирование описания сущности */ /* Формирование описания сущности */
function TENT_MAKE function TENT_MAKE
( (
SNAME in varchar2, -- Имя SNAME in varchar2, -- Имя
STYPE in varchar2, -- Тип (см. константы SENT_TYPE_*) STYPE in varchar2, -- Тип (см. константы SENT_TYPE_*)
NNUMB in number -- Номер сущности NNUMB in number -- Номер сущности
) return TENT -- Описание сущности ) return TENT -- Описание сущности
is is
RENT TENT; -- Буфер для результата RENT TENT; -- Буфер для результата
RVIEW PKG_OBJECT_DESC.TVIEW; -- Описание представления RVIEW PKG_OBJECT_DESC.TVIEW; -- Описание представления
RVIEW_FIELDS PKG_OBJECT_DESC.TCOLUMNS; -- Коллекция описаний полей представления
RVIEW_FIELD PKG_OBJECT_DESC.TCOLUMN; -- Описание поля представления
begin begin
/* Проверим корректность типа сущности */ /* Проверим корректность типа сущности */
if (STYPE not in (SENT_TYPE_TABLE, SENT_TYPE_VIEW)) then if (STYPE not in (SENT_TYPE_TABLE, SENT_TYPE_VIEW)) then
@ -482,30 +644,13 @@ create or replace package body PKG_P8PANELS_QE_BASE as
if (STYPE = SENT_TYPE_VIEW) then if (STYPE = SENT_TYPE_VIEW) then
/* Получим описание представления */ /* Получим описание представления */
RVIEW := PKG_OBJECT_DESC.DESC_VIEW(SVIEW_NAME => SNAME, BRAISE_ERROR => true); RVIEW := PKG_OBJECT_DESC.DESC_VIEW(SVIEW_NAME => SNAME, BRAISE_ERROR => true);
/* Получим список полей представления */
RVIEW_FIELDS := PKG_OBJECT_DESC.DESC_SEL_COLUMNS(SSELECT_NAME => SNAME, BRAISE_ERROR => true);
/* Собираем заголовок сущности */ /* Собираем заголовок сущности */
RENT.SID := TENT_ID_MAKE(SNAME => RVIEW.VIEW_NAME, NNUMB => NNUMB); RENT.SID := TENT_ID_MAKE(SNAME => RVIEW.VIEW_NAME, NNUMB => NNUMB);
RENT.SNAME := RVIEW.VIEW_NAME; RENT.SNAME := RVIEW.VIEW_NAME;
RENT.STITLE := DMSCLVIEWS_TITLE_GET(SVIEW_NAME => RENT.SNAME); RENT.STITLE := DMSCLVIEWS_TITLE_GET(SVIEW_NAME => RENT.SNAME);
RENT.STYPE := SENT_TYPE_VIEW; RENT.STYPE := SENT_TYPE_VIEW;
RENT.RATTRS := TATTRS(); /* Формируем набор атрибутов */
/* Собираем атрибуты в ответ */ RENT.RATTRS := TATTRS_MAKE(RENT => RENT, NCOUNT => 10);
for I in 1 .. PKG_OBJECT_DESC.COUNT_COLUMNS(RCOLUMNS => RVIEW_FIELDS)
loop
/* По умолчанию - первые 10 */
exit when I > 10;
/* Считываем очередное поле из коллекции описания полей представления */
RVIEW_FIELD := PKG_OBJECT_DESC.FETCH_COLUMN(RCOLUMNS => RVIEW_FIELDS, IINDEX => I);
/* Формируем описание поля и добавляем в коллекцию атрибутов сущности */
RENT.RATTRS.EXTEND();
RENT.RATTRS(RENT.RATTRS.LAST).SID := TATTR_ID_MAKE(SENT_ID => RENT.SID, SNAME => RVIEW_FIELD.COLUMN_NAME);
RENT.RATTRS(RENT.RATTRS.LAST).SNAME := RVIEW_FIELD.COLUMN_NAME;
RENT.RATTRS(RENT.RATTRS.LAST).STITLE := DMSCLVIEWSATTRS_TITLE_GET(SVIEW_NAME => RENT.SNAME,
SATTR_NAME => RENT.RATTRS(RENT.RATTRS.LAST)
.SNAME);
RENT.RATTRS(RENT.RATTRS.LAST).NDATA_TYPE := RVIEW_FIELD.DATA_TYPE;
end loop;
end if; end if;
/* Если сущность это таблица */ /* Если сущность это таблица */
if (STYPE = SENT_TYPE_TABLE) then if (STYPE = SENT_TYPE_TABLE) then
@ -945,6 +1090,29 @@ create or replace package body PKG_P8PANELS_QE_BASE as
end if; end if;
end TRLS_REMOVE; end TRLS_REMOVE;
/* Подчистка коллекции связей по атрибуту */
procedure TRLS_CLEANUP_BY_ATTR
(
RRLS in out nocopy TRLS, -- Изменяемая коллекция
SATTR_ID in varchar2 -- Идентификатор атрибута
)
is
RRLS_TMP PKG_P8PANELS_QE_BASE.TRLS; -- Буфер для коллекции удаляемых связей
begin
/* Если атрибут есть в связях (как источник или как приёмник) */
for J in 0 .. 1
loop
RRLS_TMP := TRLS_LIST_BY_ST(RRLS => RRLS, SSOURCE_TARGET => SATTR_ID, NLIST_TYPE => J);
/* То связь должна быть удалена */
if ((RRLS_TMP is not null) and (RRLS_TMP.COUNT > 0)) then
for K in RRLS_TMP.FIRST .. RRLS_TMP.LAST
loop
TRLS_REMOVE(RRLS => RRLS, SID => RRLS_TMP(K).SID);
end loop;
end if;
end loop;
end TRLS_CLEANUP_BY_ATTR;
/* Сериализация коллекции связей */ /* Сериализация коллекции связей */
procedure TRLS_TO_XML procedure TRLS_TO_XML
( (
@ -1484,6 +1652,56 @@ create or replace package body PKG_P8PANELS_QE_BASE as
QUERY_CH_DATE_SYNC(NRN => NRN); QUERY_CH_DATE_SYNC(NRN => NRN);
end QUERY_ENTS_SET; end QUERY_ENTS_SET;
/* Получение состава атрибутов сущности */
procedure QUERY_ENT_ATTRS_GET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
COUT out clob -- Сериализованное описание атрибутов сущности
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
NENT_INDEX PKG_STD.TNUMBER; -- Индекс изменяемой сущности в коллекции
RENTS TENTS; -- Коллекция существующих сущностей
RENT TENT; -- Изменяемая сущность
RATTRS TATTRS; -- Коллекция атрибутов изменяемой сущности
begin
/* Читаем запись запроса */
RQ := QUERY_GET(NRN => NRN);
/* Читаем существующие сущности */
RENTS := QUERY_ENTS_GET(CENTS => RQ.ENTS);
/* Читаем изменяемую сущность */
NENT_INDEX := TENTS_INDEX_BY_ID(RENTS => RENTS, SID => SID);
if (NENT_INDEX is not null) then
RENT := RENTS(NENT_INDEX);
else
P_EXCEPTION(0,
'Сущность с идентификатором "%s" не определена.',
COALESCE(SID, '<НЕ УКАЗАН>'));
end if;
/* Получим полный набор атрибутов сущности */
RATTRS := TATTRS_MAKE(RENT => RENT);
/* Начинаем формирование XML */
PKG_XFAST.PROLOGUE(ITYPE => PKG_XFAST.CONTENT_, BALINE => true, BINDENT => true);
/* Открываем корень */
PKG_XFAST.DOWN_NODE(SNAME => STAG_DATA);
/* Формируем описание тарибутов */
TATTRS_TO_XML(RATTRS => RATTRS);
/* Закрываем корень */
PKG_XFAST.UP();
/* Сериализуем */
COUT := PKG_XFAST.SERIALIZE_TO_CLOB();
/* Завершаем формирование XML */
PKG_XFAST.EPILOGUE();
exception
when others then
/* Завершаем формирование XML */
PKG_XFAST.EPILOGUE();
/* Вернем ошибку */
PKG_STATE.DIAGNOSTICS_STACKED();
P_EXCEPTION(0, PKG_STATE.SQL_ERRM());
end QUERY_ENT_ATTRS_GET;
/* Чтение связей запроса */ /* Чтение связей запроса */
function QUERY_RLS_GET function QUERY_RLS_GET
( (