Compare commits

...

29 Commits

Author SHA1 Message Date
Mikhail Chechnev
676a20dd32 ЦИТК-878 - Валидация PKG_P8PANELS_CLNTTSKBRD под релиз июль 2025 2025-10-10 17:42:35 +03:00
Mim
659dfc7c10 ЦИТК-878 - Добавление панели "Доски задач"
Reviewed-on: CITKParus/P8-Panels#37
2025-10-10 13:34:39 +03:00
Mikhail Chechnev
a46cd28656 ЦИТК-979 - Поддержка обязательности связи в запросе 2025-10-10 13:33:22 +03:00
Mikhail Chechnev
6a6e58e88e ЦИТК-979 - Диалог настройки атрибута, ввод и контроль псевдонима атрибутов сущностей, короткие псевдонимы сущностей 2025-10-09 19:17:59 +03:00
Mikhail Chechnev
0767a12fa6 ЦИТК-979 - Область просмотра сформированного запроса вынесена в отдельный компонент 2025-09-25 14:12:27 +03:00
Mikhail Chechnev
be22cde138 ЦИТК-979 - Поддержка возможности указания отладочного значения аргумента запроса 2025-09-25 14:11:32 +03:00
Mikhail Chechnev
dae416cd83 WEBAPP: Поддержка пересчёта диалогов для P8PDialog и P8PInput 2025-09-25 14:06:44 +03:00
Mikhail Chechnev
c66216e47b ЦИТК-979 - Отображение текста сформированного SQL-запроса и предупреждений формирования (клиент) 2025-09-11 18:35:02 +03:00
Mikhail Chechnev
fbb0db8179 ЦИТК-979 - Ограничение ширины заголовка при отображении выбранной сущности в инспекторе 2025-09-11 18:32:23 +03:00
Mikhail Chechnev
3b03f00cc2 ЦИТК-979 - P8PEditorSubHeader - возможность установки предельной ширины и всплывающая подсказка 2025-09-11 18:30:45 +03:00
Mikhail Chechnev
aa9568c2fa ЦИТК-979 - Формирование SQL-выражения для запроса (сервер, начало) 2025-09-11 18:29:06 +03:00
Mikhail Chechnev
5706d59a92 ЦИТК-979 - Список аргументов запроса - косметика в коде 2025-09-04 15:51:21 +03:00
Mikhail Chechnev
ef1a63b4b6 ЦИТК-979 - Диалог настройки атрибутов сущности - фиксированная ширина, сообщение об отсутствии данных при поиске 2025-09-04 15:50:33 +03:00
Mikhail Chechnev
538643591f ЦИТК-979 - Добавление сущности в запрос из списка с поиском по БД 2025-09-04 15:48:16 +03:00
Mikhail Chechnev
f698bc1789 WEBAPP: P8PDialog - поддержка настройки ширины (width) и полноразмерности (fullWidth) 2025-09-04 15:36:27 +03:00
Mikhail Chechnev
f96425c80d ЦИТК-979 - Настройка запроса - условия отбора (выбор компонентов условий, очистка условия) 2025-09-03 15:17:26 +03:00
Mikhail Chechnev
7515a9cce3 ЦИТК-979 - Рефакторинг понятий Сущность, Атрибут, Отношение, Элемент диаграммы, Связь диаграммы 2025-09-03 15:16:12 +03:00
Mikhail Chechnev
86aa639ca0 ЦИТК-979 - Управляемый отступ сверху для P8PEditorSybHeader 2025-09-03 15:13:56 +03:00
Mikhail Chechnev
b09ae4d83b ЦИТК-979 - Настройка запроса - условия отбора (сервер и клиент) 2025-08-22 18:08:45 +03:00
Mikhail Chechnev
eef77cbd2d ЦИТК-979 - Настройка запроса - аргументы (клиент) + рефакторинг инспектора свойств 2025-08-22 14:31:06 +03:00
Mikhail Chechnev
38af01c9ef ЦИТК-979 - Иконки типов данных вынесены в общий модуль 2025-08-22 14:24:53 +03:00
Mikhail Chechnev
abaf9455a9 ЦИТК-979 - Настройка запроса - аргументы (сервер) 2025-08-22 13:39:42 +03:00
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
43 changed files with 5007 additions and 803 deletions

View File

@ -15,7 +15,8 @@ export const TITLES = {
DEFAULT_PANELS_GROUP: "Без привязки к группе", //Заголовок группы панелей по умолчанию
DATA_SOURCE_CONFIG: "Настройка источника данных", //Заголовок для настройки источника данных
INSERT: "Добавление", //Заголовок для диалогов/форм добавления
UPDATE: "Исправление" //Заголовок для диалогов/форм исправления
UPDATE: "Исправление", //Заголовок для диалогов/форм исправления
CONFIG: "Настройка" //Заголовок для диалога настройки
};
//Текст

View File

@ -17,7 +17,8 @@ import { Divider, Chip } from "@mui/material"; //Интерфейсные ком
//Стили
const STYLES = {
DIVIDER: { paddingTop: "20px" }
DIVIDER: pt => ({ paddingTop: pt || pt === 0 ? `${pt}px` : "20px" }),
CHIP: maxWidth => ({ cursor: "default", ...(maxWidth ? { maxWidth } : {}) })
};
//-----------
@ -25,18 +26,20 @@ const STYLES = {
//-----------
//Заголовок раздела редактора
const P8PEditorSubHeader = ({ title }) => {
const P8PEditorSubHeader = ({ title, paddingTop, maxWidth }) => {
//Формирование представления
return (
<Divider sx={STYLES.DIVIDER}>
<Chip label={title} size={"small"} />
<Divider sx={STYLES.DIVIDER(paddingTop)}>
<Chip label={title} size={"small"} title={title} sx={STYLES.CHIP(maxWidth)} />
</Divider>
);
};
//Контроль свойств компонента - Заголовок раздела редактора
P8PEditorSubHeader.propTypes = {
title: PropTypes.string.isRequired
title: PropTypes.string.isRequired,
paddingTop: PropTypes.number,
maxWidth: PropTypes.string
};
//----------------

View File

@ -13,20 +13,49 @@ import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from "@mui/
import { BUTTONS } from "../../app.text"; //Общие текстовые ресурсы
import { P8P_INPUT, P8PInput } from "./p8p_input"; //Поле ввода
//---------
//Константы
//---------
//Типовая ширина диалога
const P8P_DIALOG_WIDTH = {
XS: "xs",
SM: "sm",
MD: "md",
LG: "lg",
XL: "xl"
};
//-----------------------
//Вспомогательные функции
//-----------------------
//Формирование объекта вида {ключ: значение} из текущего состояния элементов ввода формы
const buildFormValues = inputsState =>
inputsState.reduce((res, input) => ({ ...res, [input.name]: input.value == undefined ? null : input.value }), {});
//-----------
//Тело модуля
//-----------
//Диалог
const P8PDialog = ({ title, inputs = [], children, onOk, onCancel, onClose }) => {
//Состояние диалога
const [state, setState] = useState({});
const P8PDialog = ({ title, width, fullWidth, inputs, children, onOk, onCancel, onClose, onInputChange }) => {
//Состояние элементов ввода диалога
const [inputsState, setInputsState] = useState([]);
//При изменении элемента ввода
const handleInputChange = (name, value) => setState(pv => ({ ...pv, [name]: value }));
const handleInputChange = (name, value) => {
//Если есть функция пересчета формы - вызовем её
const doNotChangeInputsState = onInputChange ? onInputChange(name, value, inputsState) : false;
//И ориентируясь на то, пересчитала ли она элементы ввода обновим собственное состояние.
//Если функция пересчета вернула "true", значит она пересчитала что-то, тогда новые настройки элементов придут через свойство inputs и будут обработаны в useEffect ниже.
//Следовательно, и нам здесь не надо состояние выставлять, т.к. всё будет перезаписано useEffectом.
if (!doNotChangeInputsState)
setInputsState(pv => pv.reduce((accum, cur) => [...accum, { ...cur, value: cur.name === name ? value : cur.value }], []));
};
//При нажатии на "ОК" диалога
const handleOk = () => onOk && onOk(state);
const handleOk = () => onOk && onOk(buildFormValues(inputsState));
//При нажатии на "Отмена" диалога
const handleCancel = () => onCancel && onCancel();
@ -34,20 +63,23 @@ const P8PDialog = ({ title, inputs = [], children, onOk, onCancel, onClose }) =>
//При нажатии на "Закрыть" диалога
const handleClose = () => (onClose ? onClose() : onCancel ? onCancel() : null);
//При подключении к старнице
//При изменении полей для ввода
useEffect(() => {
setState(inputs.reduce((res, input) => ({ ...res, [input.name]: input.value == undefined ? null : input.value }), {}));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (inputs && Array.isArray(inputs) && inputs.length > 0) setInputsState(inputs.map(input => ({ ...input })));
}, [inputs]);
//Расчет объектного представления текущих значений формы
const formValues = buildFormValues(inputsState);
//Формирование представления
return (
<Dialog onClose={handleClose} open>
<Dialog onClose={handleClose} open {...{ ...(width ? { maxWidth: width } : {}), ...(fullWidth === true ? { fullWidth: true } : {}) }}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
{inputs.map((input, i) => (
<P8PInput key={i} {...input} value={state[input.name]} formValues={state} onChange={handleInputChange} />
{inputsState.map((input, i) => (
<P8PInput key={i} {...input} formValues={formValues} onChange={handleInputChange} />
))}
{children}
</DialogContent>
<DialogActions>
@ -62,15 +94,18 @@ const P8PDialog = ({ title, inputs = [], children, onOk, onCancel, onClose }) =>
//Контроль свойств - Диалог
P8PDialog.propTypes = {
title: PropTypes.string.isRequired,
width: PropTypes.oneOf(Object.values(P8P_DIALOG_WIDTH)),
fullWidth: PropTypes.bool,
inputs: PropTypes.arrayOf(PropTypes.shape(P8P_INPUT)),
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
onOk: PropTypes.func,
onCancel: PropTypes.func,
onClose: PropTypes.func
onClose: PropTypes.func,
onInputChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PDialog };
export { P8PDialog, P8P_DIALOG_WIDTH };

View File

@ -35,26 +35,24 @@ const P8P_INPUT = {
//Поле ввода
const P8PInput = ({ name, value, label, onChange, dictionary, list, type, freeSolo = false, disabled = false, formValues, ...other }) => {
//Значение элемента
const [currentValue, setCurrentValue] = useState(value);
//Значение и тип элемента
const [current, setCurrent] = useState({ type: undefined, value: "" });
//При получении нового значения из вне
useEffect(() => {
setCurrentValue(value);
}, [value]);
//При получении нового значения или типа из вне
useEffect(() => setCurrent({ value, type }), [type, value]);
//Выбор значения из словаря
const handleDictionaryClick = () => dictionary && dictionary(formValues, res => (res ? res.map(i => handleChangeByName(i.name, i.value)) : null));
//Изменение значения элемента (по событию)
const handleChange = e => {
setCurrentValue(e.target.value);
setCurrent(pv => ({ ...pv, value: e.target.value }));
if (onChange) onChange(e.target.name, e.target.value);
};
//Изменение значения элемента (по имени и значению)
const handleChangeByName = (targetName, value) => {
if (targetName === name) setCurrentValue(value);
if (targetName === name) setCurrent(pv => ({ ...pv, value }));
if (onChange) onChange(targetName, value);
};
@ -69,7 +67,7 @@ const P8PInput = ({ name, value, label, onChange, dictionary, list, type, freeSo
name={name}
freeSolo
disabled={disabled}
inputValue={currentValue ? currentValue : ""}
inputValue={current.value ? current.value : ""}
onChange={(event, newValue) => handleChangeByName(name, newValue)}
onInputChange={(event, newInputValue) => handleChangeByName(name, newInputValue)}
options={list}
@ -85,7 +83,7 @@ const P8PInput = ({ name, value, label, onChange, dictionary, list, type, freeSo
id={name}
name={name}
label={label}
value={[undefined, null].includes(currentValue) ? "" : currentValue}
value={[undefined, null].includes(current.value) ? "" : current.value}
onChange={handleChange}
disabled={disabled}
displayEmpty
@ -100,13 +98,13 @@ const P8PInput = ({ name, value, label, onChange, dictionary, list, type, freeSo
)
) : (
<>
<InputLabel {...(type == "date" ? { shrink: true } : {})} htmlFor={name}>
<InputLabel {...(current.type == "date" ? { shrink: true } : {})} htmlFor={name}>
{label}
</InputLabel>
<Input
id={name}
name={name}
value={currentValue ? currentValue : ""}
value={current.value ? current.value : ""}
endAdornment={
dictionary ? (
<InputAdornment position="end">
@ -116,7 +114,7 @@ const P8PInput = ({ name, value, label, onChange, dictionary, list, type, freeSo
</InputAdornment>
) : null
}
{...(type ? { type } : {})}
{...(current.type ? { type: current.type } : {})}
onChange={handleChange}
disabled={disabled}
/>

View File

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

View File

@ -10,14 +10,33 @@
//Типы данных
const DATA_TYPE = { STR: 0, NUMB: 1, DATE: 2 };
//Иконки типов данных
const DATA_TYPE_ICON = {
[DATA_TYPE.STR]: "format_align_left",
[DATA_TYPE.NUMB]: "pin",
[DATA_TYPE.DATE]: "calendar_month"
};
//Типы элементов диаграммы
const NODE_TYPE = {
ENTITY: "entity",
ATTRIBUTE: "attribute"
};
//Типы сущностей
const ENTITY_TYPE = {
VIEW: "VIEW",
TABLE: "TABLE"
};
//Иконки типов сущностей
const ENTITY_TYPE_ICON = {
[ENTITY_TYPE.VIEW]: "table_view",
[ENTITY_TYPE.TABLE]: "table_rows"
};
//----------------
//Интерфейс модуля
//----------------
export { DATA_TYPE, NODE_TYPE };
export { DATA_TYPE, DATA_TYPE_ICON, NODE_TYPE, ENTITY_TYPE, ENTITY_TYPE_ICON };

View File

@ -0,0 +1,89 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компоненты: Аргумент запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, ListItem, IconButton, Icon, ListItemButton, ListItemText, Typography, ListItemIcon, Chip } from "@mui/material"; //Компоненты UI
import { STYLES as COMMON_STYLES } from "../../../../components/editors/p8p_editors_common"; //Общие ресурсы редаторов
import { DATA_TYPE, DATA_TYPE_ICON } from "../../common"; //Общие ресурсы и константы редактора запросов
//---------
//Константы
//---------
//Варианты представления
const ARGUMENT_VARIANT = {
LIST_ITEM: "LIST_ITEM",
CHIP: "CHIP"
};
//Структура аргумента
const ARGUMENT_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
dataType: PropTypes.oneOf(Object.values(DATA_TYPE)),
mandatory: PropTypes.oneOf([0, 1]).isRequired
});
//Иконки
const ICONS = { ...DATA_TYPE_ICON, DEFAULT: "category" };
//-----------
//Тело модуля
//-----------
//Аргумент запроса
const Argument = ({ arg, variant, onClick = null, onDelete = null }) => {
//Заголовок аргумента
const title = `${arg.mandatory == 1 ? "*" : ""}${arg.title}`;
//Иконка аргумента
const icon = ICONS[arg.dataType] || ICONS.DEFAULT;
//Формирование представления
return variant == ARGUMENT_VARIANT.LIST_ITEM ? (
<ListItem disablePadding>
<ListItemButton onClick={() => onClick && onClick(arg)} dense>
<ListItemIcon>
<Icon>{icon}</Icon>
</ListItemIcon>
<ListItemText
primary={title}
secondaryTypographyProps={{ component: "div" }}
secondary={
<Stack direction={"column"}>
<Typography variant={"caption"}>{arg.name}</Typography>
</Stack>
}
/>
<Stack direction={"row"}>
<IconButton onClick={e => onDelete && onDelete(e, arg)} title={"Удалить"}>
<Icon>delete</Icon>
</IconButton>
</Stack>
</ListItemButton>
</ListItem>
) : variant == ARGUMENT_VARIANT.CHIP ? (
<Chip icon={<Icon>{icon}</Icon>} label={title} variant={"outlined"} sx={COMMON_STYLES.CHIP(true)} />
) : null;
};
//Контроль свойств компонента - Аргумент запроса
Argument.propTypes = {
arg: ARGUMENT_SHAPE,
variant: PropTypes.oneOf(Object.values(ARGUMENT_VARIANT)).isRequired,
onClick: PropTypes.func,
onDelete: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { Argument, ARGUMENT_VARIANT, ARGUMENT_SHAPE };

View File

@ -11,7 +11,7 @@ import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Handle, Position, useStore } from "reactflow"; //Библиотека редактора диаграмм
import { Box, Stack, Icon, Typography } from "@mui/material"; //Компоненты UI
import { DATA_TYPE } from "../../common"; //Общие ресурсы и константы редактора
import { DATA_TYPE, DATA_TYPE_ICON } from "../../common"; //Общие ресурсы и константы редактора запросов
//---------
//Константы
@ -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,34 +33,55 @@ 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" }
};
//Иконки
const ICONS = {
[DATA_TYPE.STR]: "format_align_left",
[DATA_TYPE.NUMB]: "pin",
[DATA_TYPE.DATE]: "calendar_month",
DEFAULT: "category"
};
const ICONS = { ...DATA_TYPE_ICON, DEFAULT: "category" };
//Структура данных об атрибуте сущности
const ATTRIBUTE_DATA_SHAPE = PropTypes.shape({
const ATTRIBUTE_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
parentEntity: PropTypes.string.isRequired,
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 +89,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>
@ -94,11 +136,11 @@ const Attribute = ({ data }) => {
//Контроль свойств компонента - Атрибут сущности
Attribute.propTypes = {
data: ATTRIBUTE_DATA_SHAPE
data: ATTRIBUTE_SHAPE
};
//----------------
//Интерфейс модуля
//----------------
export { Attribute };
export { Attribute, ATTRIBUTE_SHAPE, attrGetUse, attrGetShow };

View File

@ -1,33 +0,0 @@
.entity__wrapper {
width: 100%;
height: 100%;
border: 1px solid var(--border-color-dark);
border-radius: 6px;
box-shadow: var(--shadow-entity);
overflow: hidden;
background-color: white;
}
.entity__wrapper[data-selected="true"] {
outline: 1px solid var(--outline-color);
border-color: var(--outline-color);
}
.entity__title {
width: 100%;
height: 50px;
align-content: center;
border-bottom: 1px solid var(--border-color);
font-weight: 900;
text-align: center;
background-color: var(--entity-title-bg);
cursor: move;
}
.entity__name {
width: 100%;
align-content: center;
text-align: center;
font-size: 0.8rem;
color: gray;
}

View File

@ -9,42 +9,136 @@
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import "./entity.css"; //Стили компомнента
import { Box, Stack, ListItem, Icon, ListItemButton, Typography } from "@mui/material"; //Компоненты UI
import { ENTITY_TYPE, ENTITY_TYPE_ICON } from "../../common"; //Общие ресурсы и константы редактора запросов
import { ATTRIBUTE_SHAPE } from "../attribute/attribute"; //Описание атрибута сущности
//---------
//Константы
//---------
//Варианты представления сущности
const ENTITY_VARIANT = {
LIST_ITEM: "LIST_ITEM",
DIAGRAM: "DIAGRAM"
};
//Иконки
const ICONS = { ...ENTITY_TYPE_ICON, DEFAULT: "border_clear" };
//Структура данных о сущности запроса
const ENTITY_DATA_SHAPE = PropTypes.shape({
const ENTITY_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
title: PropTypes.string.isRequired
title: PropTypes.string.isRequired,
type: PropTypes.oneOf(Object.values(ENTITY_TYPE)).isRequired,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
attrs: PropTypes.arrayOf(ATTRIBUTE_SHAPE).isRequired
});
//Структура данных о сущности запроса (краткая)
const ENTITY_BRIEF_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.oneOf(Object.values(ENTITY_TYPE)).isRequired
});
//Стили
const STYLES = {
CONTAINER: selected => ({
width: "100%",
height: "100%",
border: "1px solid var(--border-color-dark)",
borderRadius: "6px",
boxShadow: "var(--shadow-entity)",
overflow: "hidden",
backgroundColor: "white",
cursor: "move",
...(selected
? {
outline: "1px solid var(--outline-color)",
borderColor: "var(--outline-color)"
}
: {})
}),
CONTENT_STACK: variant => ({
width: "100%",
backgroundColor: "var(--entity-title-bg)",
...(variant === ENTITY_VARIANT.DIAGRAM ? { height: "50px" } : {})
}),
CAPTIONS_TYPOGRAPHY: variant => ({ ...(variant === ENTITY_VARIANT.DIAGRAM ? { maxWidth: "100px", overflow: "hidden" } : {}) })
};
//-----------
//Тело модуля
//-----------
//Сущность запроса
const Entity = ({ data, selected }) => {
return (
<div className="entity__wrapper" data-selected={selected}>
<div className="entity__title">
<span>{data.title}</span>
<div className="entity__name">{data.name}</div>
</div>
</div>
const Entity = ({ data, variant = ENTITY_VARIANT.DIAGRAM, selected = false, onClick = null }) => {
//Иконка
const icon = ICONS[data.type] || ICONS.DEFAULT;
//Всплывающая подсказка
const iconTitle = data.type === ENTITY_TYPE.VIEW ? "Представление" : "Таблица";
//Содержимое самой сущности
const entContent = (
<Stack
direction={"row"}
alignItems={"center"}
justifyContent={variant === ENTITY_VARIANT.DIAGRAM ? "center" : "left"}
p={1}
sx={STYLES.CONTENT_STACK(variant)}
>
<Icon color={"action"} title={iconTitle}>
{icon}
</Icon>
<Stack direction={"column"} alignItems={"left"} pl={1}>
<Typography
variant={variant === ENTITY_VARIANT.DIAGRAM ? "subtitle2" : "body2"}
noWrap={variant === ENTITY_VARIANT.DIAGRAM ? true : false}
title={data.title}
sx={STYLES.CAPTIONS_TYPOGRAPHY(variant)}
>
{data.title}
</Typography>
<Typography
component={"div"}
variant={"caption"}
color={"text.secondary"}
noWrap={variant === ENTITY_VARIANT.DIAGRAM ? true : false}
title={data.name}
sx={STYLES.CAPTIONS_TYPOGRAPHY(variant)}
>
{data.name}
</Typography>
</Stack>
</Stack>
);
//Формирование представления
return variant == ENTITY_VARIANT.LIST_ITEM ? (
<ListItem disablePadding>
<ListItemButton onClick={() => onClick && onClick(data)} dense>
{entContent}
</ListItemButton>
</ListItem>
) : variant == ENTITY_VARIANT.DIAGRAM ? (
<Box sx={STYLES.CONTAINER(selected)}>{entContent}</Box>
) : null;
};
//Контроль свойств компонента - Сущность запроса
Entity.propTypes = {
data: ENTITY_DATA_SHAPE,
selected: PropTypes.bool.isRequired
data: PropTypes.oneOfType([ENTITY_SHAPE, ENTITY_BRIEF_SHAPE]).isRequired,
variant: PropTypes.string,
selected: PropTypes.bool,
onClick: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { Entity };
export { Entity, ENTITY_VARIANT, ENTITY_SHAPE, ENTITY_BRIEF_SHAPE };

View File

@ -1,43 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Диалог добавления сущности запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог
import { TITLES } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
//-----------
//Тело модуля
//-----------
//Диалог добавления сущности запроса
const EntityAddDialog = ({ onOk, onCancel }) => {
//Нажатие на кнопку "Ok"
const handleOk = values => onOk && onOk({ ...values });
//Нажатие на кнопку "Отмена"
const handleCancel = () => onCancel && onCancel();
//Генерация содержимого
return (
<P8PDialog title={`${TITLES.INSERT} сущности`} inputs={[{ name: "name", value: "", label: "Имя" }]} onOk={handleOk} onCancel={handleCancel} />
);
};
//Контроль свойств - Диалог добавления сущности запроса
EntityAddDialog.propTypes = {
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { EntityAddDialog };

View File

@ -0,0 +1,69 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Инспектор свойств
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { P8PEditorBox } from "../../../../components/editors/p8p_editor_box"; //Контейнер параметров редактора
import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Подзаголовок группы параметров редактора
import { ENTITY_SHAPE } from "../entity/entity"; //Описание сущности
import { RELATION_SHAPE } from "../relation/relation"; //Описание связи
import { ARGUMENT_SHAPE } from "../argument/argument"; //Описание аргумента запроса
import { InspectorQueryArguments } from "../inspector_query_args/inspector_query_args"; //Управление аргументами запроса
import { InspectorQueryConditions } from "../inspector_query_cond/inspector_query_cond"; //Управление условиями отбора запроса
import { InspectorQueryEntities } from "../inspector_query_ents/inspector_query_ents"; //Управление сущностями запроса
import { InspectorQueryRelations } from "../inspector_query_rls/inspector_query_rls"; //Управление связями запроса
import { QueryArea } from "./query_area"; //Область запроса
//-----------
//Тело модуля
//-----------
//Инспектор свойств
const Inspector = ({ query, entity, relation, entities = [], args = [], cond = null, qry = "", qryMsg = "", onOptionsChanged = null }) => {
//При изменении настроек запроса
const handleOptionsChanged = () => onOptionsChanged && onOptionsChanged();
//Генерация содержимого
return (
<P8PEditorBox title={"Настройки запроса"}>
<P8PEditorSubHeader title={"Аргументы"} />
<InspectorQueryArguments query={query} args={args} onOptionsChanged={handleOptionsChanged} />
<P8PEditorSubHeader title={"Условия отбора"} />
<InspectorQueryConditions query={query} cond={cond} entities={entities} args={args} onOptionsChanged={handleOptionsChanged} />
<P8PEditorSubHeader title={"Сущности"} />
<InspectorQueryEntities query={query} entity={entity} onOptionsChanged={handleOptionsChanged} />
{relation && (
<>
<P8PEditorSubHeader title={"Связь"} />
<InspectorQueryRelations query={query} relation={relation} onOptionsChanged={handleOptionsChanged} />
</>
)}
<QueryArea qry={qry} qryMsg={qryMsg} />
</P8PEditorBox>
);
};
//Контроль свойств компонента - Инспектор свойств
Inspector.propTypes = {
query: PropTypes.number.isRequired,
entity: ENTITY_SHAPE,
relation: RELATION_SHAPE,
entities: PropTypes.arrayOf(ENTITY_SHAPE),
args: PropTypes.arrayOf(ARGUMENT_SHAPE),
cond: PropTypes.string,
qry: PropTypes.string,
qryMsg: PropTypes.string,
onOptionsChanged: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { Inspector };

View File

@ -0,0 +1,152 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Область запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Fab, Icon, Drawer, IconButton, TextField, Stack, Box, Snackbar, Alert } from "@mui/material"; //Компоненты MUI
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
import { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения
//---------
//Константы
//---------
//Стили
const STYLES = {
SQL_FAB: {
position: "absolute",
bottom: 16,
right: 16
},
SQL_TEXT_FIELD: {
fontSize: "0.9rem",
...APP_STYLES.SCROLL
},
SNACKBAR_ALERT: { width: "100%" }
};
//Начальное состояние всплывающего сообщения
const SNACK_BAR_MESSAGE_INIT = { text: null, type: null };
//-----------
//Тело модуля
//-----------
//Область запроса
const QueryArea = ({ qry = "", qryMsg = "" }) => {
//Собственное состояние - текст всплывающего сообщения
const [snackBarMessage, setSnackBarMessage] = useState(SNACK_BAR_MESSAGE_INIT);
//Собственное состояние - отображение области SQL запроса
const [displaySQL, setDisplaySQL] = useState(true);
//Собственное состояние - развёрнутость
const [expanded, setExpanded] = useState(false);
//При нажатии на кнопку копирования текста запроса
const handleCopyClick = async () => {
try {
await navigator.clipboard.writeText(qry);
setSnackBarMessage({ text: `Текст запроса скопирован в буфер обмена` });
} catch (e) {
setSnackBarMessage({ text: `Ошибка копирования текста запроса в буфер обмена: ${e.message}`, type: "error" });
}
};
//При нажатии на кнопку развёртывания
const handleExpandClick = () => setExpanded(!expanded);
//При нажатии на кнопку отображения SQL запроса
const handleShowSQLClick = () => setDisplaySQL(true);
//При нажатии на кнопку сокрытия SQL запроса
const handleCloseSQLClick = () => setDisplaySQL(false);
//При закрытии всплывающего сообщения
const handleSnackBarClose = () => setSnackBarMessage(SNACK_BAR_MESSAGE_INIT);
//Расчет размеров тектовых полей
const [qryRows, qryMsgRows] = expanded ? [15, 6] : [5, 3];
//Генерация содержимого
return (
<>
{(qry || qryMsg) && (
<Fab color={qryMsg ? "warning" : "default"} sx={STYLES.SQL_FAB} title={"Показать текст SQL запроса"} onClick={handleShowSQLClick}>
<Icon>join_left</Icon>
</Fab>
)}
{displaySQL && (
<Drawer open onClose={handleCloseSQLClick} anchor={"bottom"}>
<Box p={2}>
<Stack direction={"row"} justifyContent={"right"} spacing={2}>
{qry && (
<IconButton onClick={handleCopyClick} title={"Скопировать текст запроса"}>
<Icon>content_copy</Icon>
</IconButton>
)}
<IconButton onClick={handleExpandClick} title={expanded ? "Свернуть" : "Развернуть"}>
<Icon>{expanded ? "expand_more" : "expand_less"}</Icon>
</IconButton>
<IconButton onClick={handleCloseSQLClick} title={BUTTONS.HIDE}>
<Icon>close</Icon>
</IconButton>
</Stack>
<Stack direction={"column"} spacing={2}>
{qry && (
<TextField
label={"Текст запроса"}
multiline
fullWidth
value={qry}
minRows={qryRows}
maxRows={qryRows}
variant={"standard"}
focused
inputProps={{ sx: STYLES.SQL_TEXT_FIELD, disabled: true }}
/>
)}
{qryMsg && (
<TextField
label={"Предупреждения"}
color={"warning"}
multiline
fullWidth
value={qryMsg}
minRows={qryMsgRows}
maxRows={qryMsgRows}
variant={"standard"}
focused
inputProps={{ sx: STYLES.SQL_TEXT_FIELD, disabled: true }}
/>
)}
</Stack>
</Box>
</Drawer>
)}
<Snackbar open={Boolean(snackBarMessage.text)} autoHideDuration={3000} onClose={handleSnackBarClose}>
<Alert severity={snackBarMessage.type || "success"} sx={STYLES.SNACKBAR_ALERT} onClose={handleSnackBarClose}>
{snackBarMessage.text}
</Alert>
</Snackbar>
</>
);
};
//Контроль свойств компонента - Область запроса
QueryArea.propTypes = {
qry: PropTypes.string,
qryMsg: PropTypes.string
};
//----------------
//Интерфейс модуля
//----------------
export { QueryArea };

View File

@ -0,0 +1,98 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Диалог добавления/исправления аргумента запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог
import { TITLES } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
import { DATA_TYPE } from "../../common"; //Общие константы редактора
//-----------
//Тело модуля
//-----------
//Диалог добавления/исправления аргумента запроса
const ArgIUDialog = ({ name = "", title = "", dataType = DATA_TYPE.NUMB, mandatory = 0, value = "", insert = true, onOk, onCancel }) => {
//Собственное состояние - текущее состояние аргумента
const [current, setCurrent] = useState({ value, dataType });
//Нажатие на кнопку "Ok"
const handleOk = values => onOk && onOk({ ...values });
//Нажатие на кнопку "Отмена"
const handleCancel = () => onCancel && onCancel();
//При изменении значений в поле ввода
const handleInputChange = (name, value) => {
//Если поменяли тип данных - надо пересмотреть поля для ввода
if (name === "dataType") {
//Сохраним в состоянии новый тип данных и сбросим "отладочное значение", ведь оно зависит от типа данных (от этого состояния зависит свойство inputs, передаваемое в P8PDialog ниже)
setCurrent({ dataType: value, value: "" });
//Мы сами пересчитали форму, туда придут новые настройки элементов ввода через свойство inputs, не надо делать пересчет состояния внутри диалога
return true;
}
};
//Генерация содержимого
return (
<P8PDialog
title={`${insert === true ? TITLES.INSERT : TITLES.UPDATE} аргумента`}
inputs={[
{ name: "name", value: name, label: "Имя", disabled: insert != true },
{ name: "title", value: title, label: "Приглашение" },
{
name: "dataType",
value: current.dataType,
label: "Тип данных",
list: [
{ name: "Строка", value: DATA_TYPE.STR },
{ name: "Число", value: DATA_TYPE.NUMB },
{ name: "Дата", value: DATA_TYPE.DATE }
]
},
{
name: "mandatory",
value: mandatory,
label: "Обязательный",
list: [
{ name: "Нет", value: 0 },
{ name: "Да", value: 1 }
]
},
{
name: "value",
value: current.value,
label: "Значение (для отладки)",
type: current.dataType === DATA_TYPE.NUMB ? "number" : current.dataType === DATA_TYPE.DATE ? "date" : "text"
}
]}
onOk={handleOk}
onCancel={handleCancel}
onInputChange={handleInputChange}
/>
);
};
//Контроль свойств - Диалог добавления/исправления аргумента запроса
ArgIUDialog.propTypes = {
name: PropTypes.string,
title: PropTypes.string,
dataType: PropTypes.number,
mandatory: PropTypes.number,
value: PropTypes.string,
insert: PropTypes.bool,
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { ArgIUDialog };

View File

@ -0,0 +1,62 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Список аргументов
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { List } from "@mui/material"; //Интерфейсные компоненты MUI
import { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения
import { Argument, ARGUMENT_VARIANT, ARGUMENT_SHAPE } from "../argument/argument"; //Аргумент запроса
//---------
//Константы
//---------
//Стили
const STYLES = {
LIST: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL }
};
//-----------
//Тело модуля
//-----------
//Список аргументов
const ArgsList = ({ args = [], onSelect = null, onDelete = null } = {}) => {
//При нажатии на элемент списка
const handleItemClick = arg => onSelect && onSelect(arg);
//При нажатии на удалении элемента списка
const handleItemDeleteClick = (e, arg) => {
e.stopPropagation();
onDelete && onDelete(arg);
};
//Формирование представления
return (
<List sx={STYLES.LIST}>
{args &&
args.map((arg, i) => (
<Argument key={i} arg={arg} variant={ARGUMENT_VARIANT.LIST_ITEM} onClick={handleItemClick} onDelete={handleItemDeleteClick} />
))}
</List>
);
};
//Контроль свойств компонента - Список аргументов
ArgsList.propTypes = {
args: PropTypes.arrayOf(ARGUMENT_SHAPE),
onSelect: PropTypes.func,
onDelete: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { ArgsList };

View File

@ -0,0 +1,66 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Пользовательские хуки настройки аргументов запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import { useContext, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../../context/backend"; //Контекст взаимодействия с сервером
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//-----------
//Тело модуля
//-----------
//Работа с аргументами запроса
const useQueryArgs = query => {
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Добавление аргумента запроса
const addArg = useCallback(
async (name, title, dataType, mandatory, value) => {
await executeStored({
stored: "PKG_P8PANELS_QE.QUERY_OPT_ARG_ADD",
args: { NRN: query, SNAME: name, STITLE: title, NDATA_TYPE: dataType, NMANDATORY: mandatory, SVALUE: value },
loader: false
});
},
[query, executeStored]
);
//Исправление аргумента запроса
const editArg = useCallback(
async (name, title, dataType, mandatory, value) => {
await executeStored({
stored: "PKG_P8PANELS_QE.QUERY_OPT_ARG_EDIT",
args: { NRN: query, SNAME: name, STITLE: title, NDATA_TYPE: dataType, NMANDATORY: mandatory, SVALUE: value },
loader: false
});
},
[query, executeStored]
);
//Удаление аргумента запроса
const removeArg = useCallback(
async name => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_OPT_ARG_REMOVE", args: { NRN: query, SNAME: name }, loader: false });
},
[query, executeStored]
);
//Возвращаем интерфейс хука
return { addArg, editArg, removeArg };
};
//----------------
//Интерфейс модуля
//----------------
export { useQueryArgs };

View File

@ -0,0 +1,107 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент инспектора - Аргументы запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, Icon, Button, Card, CardContent, CardActionArea } from "@mui/material"; //Интерфейсные элементы
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы
import { useQueryArgs } from "./hooks"; //Хуки для работы с аргументами запроса на сервере
import { QueryArgsDialog } from "./query_args_dialog"; //Диалог настройки состава атрибутов
import { Argument, ARGUMENT_SHAPE, ARGUMENT_VARIANT } from "../argument/argument"; //Аргумент запроса
//-----------
//Тело модуля
//-----------
//Компонент инспектора - Аргументы запроса
const InspectorQueryArguments = ({ query, args = [], onOptionsChanged = null }) => {
//Собственное состояние - отображение диалога настройки состава аргументов
const [openQueryArgsDialog, setOpenQueryArgsDialog] = useState(false);
//Хук для взаимодействия с сервером
const { addArg, editArg, removeArg } = useQueryArgs(query);
//Уведомление родителя об изменении свойств
const notifyOptionsChanged = () => onOptionsChanged && onOptionsChanged();
//При нажатии на настройку аргументов
const handleSetup = () => setOpenQueryArgsDialog(true);
//При добавлении аргумента
const handleArgAdd = async (arg, cb) => {
await addArg(arg.name, arg.title, arg.dataType, arg.mandatory, arg.value);
cb && cb();
notifyOptionsChanged();
};
//При изменении аргумента
const handleArgEdit = async (arg, cb) => {
await editArg(arg.name, arg.title, arg.dataType, arg.mandatory, arg.value);
cb && cb();
notifyOptionsChanged();
};
//При удалении аргумента
const handleArgRemove = async arg => {
await removeArg(arg.name);
notifyOptionsChanged();
};
//Закрытие диалога настройки состава аргументов по "Закрыть"
const handleQueryArgsDialogClose = () => setOpenQueryArgsDialog(false);
//Расчет флага "настроенности"
const configured = args && args.length > 0 ? true : false;
//Формирование представления
return (
<>
{openQueryArgsDialog && (
<QueryArgsDialog
args={args}
onArgAdd={handleArgAdd}
onArgEdit={handleArgEdit}
onArgRemove={handleArgRemove}
onClose={handleQueryArgsDialogClose}
/>
)}
{configured && (
<Card variant={"outlined"}>
<CardActionArea onClick={handleSetup}>
<CardContent>
<Stack direction={"column"} spacing={1}>
{args.map((arg, i) => (
<Argument key={i} arg={arg} variant={ARGUMENT_VARIANT.CHIP} />
))}
</Stack>
</CardContent>
</CardActionArea>
</Card>
)}
{!configured && (
<Button startIcon={<Icon>build</Icon>} onClick={handleSetup}>
{BUTTONS.CONFIG}
</Button>
)}
</>
);
};
//Контроль свойств компонента - Компонент инспектора - Аргументы запроса
InspectorQueryArguments.propTypes = {
query: PropTypes.number.isRequired,
args: PropTypes.arrayOf(ARGUMENT_SHAPE),
onOptionsChanged: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { InspectorQueryArguments };

View File

@ -0,0 +1,80 @@
/*
Парус 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 { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог
import { ARGUMENT_SHAPE } from "../argument/argument"; //Аргумент запроса
import { ArgsList } from "./args_list"; //Список аргументов запроса
import { ArgIUDialog } from "./arg_iu_dialog"; //Диалог добавления/исправления аргумента
//-----------
//Тело модуля
//-----------
//Диалог настройки аргументов запроса
const QueryArgsDialog = ({ args, onArgAdd, onArgEdit, onArgRemove, onClose }) => {
//Собственное состояние - изменяемый аргумент
const [modArg, setModArg] = useState(null);
//Подключение к контексту сообщений
const { showMsgWarn } = useContext(MessagingСtx);
//Нажатие на кнопку "Закрыть"
const handleClose = () => onClose && onClose();
//При выборе аргумента в списке
const handleArgSelect = arg => setModArg({ ...arg });
//При добавлении аргумента
const handleArgAdd = () => setModArg(true);
//Удаление аргумента
const handleArgRemove = arg => showMsgWarn("Удалить аргумент?", () => onArgRemove && onArgRemove(arg));
//При закрытии диалога добавления/исправления по "ОК"
const handleIUDialogOk = async values => {
if (modArg === true) onArgAdd && onArgAdd(values, handleIUDialogCancel);
else onArgEdit && onArgEdit(values, handleIUDialogCancel);
};
//При закрытии диалога добавления/исправления по "Отмена"
const handleIUDialogCancel = () => setModArg(null);
//Генерация содержимого
return (
<P8PDialog title={`Аргументы запроса`} onClose={handleClose}>
{modArg && (
<ArgIUDialog {...(modArg === true ? {} : modArg)} insert={modArg === true} onOk={handleIUDialogOk} onCancel={handleIUDialogCancel} />
)}
<Button startIcon={<Icon>add</Icon>} onClick={handleArgAdd}>
{BUTTONS.INSERT}
</Button>
<ArgsList args={args} onSelect={handleArgSelect} onDelete={handleArgRemove} />
</P8PDialog>
);
};
//Контроль свойств - Диалог настройки аргументов запроса
QueryArgsDialog.propTypes = {
args: PropTypes.arrayOf(ARGUMENT_SHAPE),
onArgAdd: PropTypes.func,
onArgEdit: PropTypes.func,
onArgRemove: PropTypes.func,
onClose: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { QueryArgsDialog };

View File

@ -0,0 +1,138 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Кнопка выбора компонента условия
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Button, Menu, MenuItem, Icon, ListItemIcon, Stack, Typography } from "@mui/material"; //Интерфейсные компоненты MUI
import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Подзаголовок
//---------
//Константы
//---------
//Типы компонентов
const COMPONENT_TYPE = {
DIVIDER: "divider",
ITEM: "item"
};
//Высота элемента меню для компонента
const ITEM_HEIGHT = 48;
//Количество одновременно отображаемых элементов (остальные - прокруткой)
const ITEM_DSPL_COUNT = 10;
//Стили
const STYLES = {
MENU_PAPER: {
maxHeight: ITEM_HEIGHT * ITEM_DSPL_COUNT,
maxWidth: "400px",
overflow: "auto"
}
};
//-----------------------
//Вспомогательные функции
//-----------------------
//Проверка строкового свойства, обязательного для компонента типа "ITEM"
const checkReqStringPropForItem = (props, propName, componentName) => {
if (props.type === COMPONENT_TYPE.ITEM && !props[propName]) {
return new Error(`Prop \`${propName}\` is required when \`type\` is \`${COMPONENT_TYPE.ITEM}\` in \`${componentName}\`.`);
}
if (props[propName] && typeof props[propName] !== "string" && !(props[propName] instanceof String)) {
return new Error(`Prop \`${propName}\` must be a string`);
}
return null;
};
//-----------
//Тело модуля
//-----------
//Кнопка выбора компонента условия
const CondComponentSelectButton = ({ caption, components = [], onSelect }) => {
//Собственное состояние - элемент привязки меню
const [anchorEl, setAnchorEl] = useState(null);
//При нажатии на кнопку
const handleButtonClick = event => setAnchorEl(event.currentTarget);
//При закрытии меню
const handleClose = () => setAnchorEl(null);
//При нажатии на элемент меню
const handleComponentClick = component => {
handleClose();
onSelect && onSelect(component.value);
};
//Расчет флага открытия меню
const open = Boolean(anchorEl);
//Формирование представления
return (
<>
<Button onClick={handleButtonClick} endIcon={<Icon>{open ? "arrow_drop_up" : "arrow_drop_down"}</Icon>}>
{caption}
</Button>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
PaperProps={{
style: STYLES.MENU_PAPER,
//Импорт общесистемного JS-стиля не работает здесь, только через общесистемный CSS-класс
className: "scroll"
}}
>
{components.map((component, i) =>
component.type === COMPONENT_TYPE.DIVIDER ? (
<P8PEditorSubHeader key={i} title={component.title} paddingTop={0} />
) : (
<MenuItem key={i} onClick={() => handleComponentClick(component)}>
<ListItemIcon>
<Icon fontSize={"small"}>{component.icon}</Icon>
</ListItemIcon>
<Stack direction={"column"}>
<Typography noWrap variant={"body2"} title={component.title}>
{component.title}
</Typography>
<Typography noWrap variant={"caption"} color={"text.secondary"} title={component.name}>
{component.name}
</Typography>
</Stack>
</MenuItem>
)
)}
</Menu>
</>
);
};
//Контроль свойств - Кнопка выбора компонента условия
CondComponentSelectButton.propTypes = {
caption: PropTypes.string.isRequired,
components: PropTypes.arrayOf(
PropTypes.shape({
type: PropTypes.oneOf(Object.values(COMPONENT_TYPE)).isRequired,
title: PropTypes.string.isRequired,
name: checkReqStringPropForItem,
icon: checkReqStringPropForItem,
value: PropTypes.any
})
),
onSelect: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { CondComponentSelectButton, COMPONENT_TYPE };

View File

@ -0,0 +1,44 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Кнопки операций условий отбора
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { ButtonGroup, Button } from "@mui/material"; //Интерфейсные компоненты MUI
//-----------
//Тело модуля
//-----------
//Кнопки операций условий отбора
const CondOperationButtons = ({ buttons = [], onClick }) => {
//Формирование представления
return (
<ButtonGroup size={"small"} variant={"outlined"}>
{buttons.map((button, i) => (
<Button key={i} onClick={() => onClick(button.value)} title={button.title}>
{button.caption}
</Button>
))}
</ButtonGroup>
);
};
//Контроль свойств - Кнопки операций условий отбора
CondOperationButtons.propTypes = {
buttons: PropTypes.arrayOf(
PropTypes.shape({ caption: PropTypes.string.isRequired, title: PropTypes.string.isRequired, value: PropTypes.string.isRequired })
),
onClick: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { CondOperationButtons };

View File

@ -0,0 +1,38 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Пользовательские хуки для работы с условиями отбора запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import { useContext, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../../context/backend"; //Контекст взаимодействия с сервером
//-----------
//Тело модуля
//-----------
//Работа с условиями отбора запроса
const useQueryConditions = query => {
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Установка условий отбора в запросе
const setCond = useCallback(
async cond => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_OPT_COND_SET", args: { NRN: query, SCOND: cond }, loader: false });
},
[query, executeStored]
);
//Возвращаем интерфейс хука
return { setCond };
};
//----------------
//Интерфейс модуля
//----------------
export { useQueryConditions };

View File

@ -0,0 +1,109 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент инспектора - Условия отбора запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Card, CardActionArea, CardContent, Stack, Chip, Icon, Button } from "@mui/material"; //Интерфейсные элементы
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы
import { STYLES as COMMON_STYLES } from "../../../../components/editors/p8p_editors_common"; //Общие ресурсы редаторов
import { useQueryConditions } from "./hooks"; //Хуки для работы с условиями отбора
import { QueryCondDialog } from "./query_cond_dialog"; //Диалог настройки условий отбора
import { ENTITY_SHAPE } from "../entity/entity"; //Описание сущности
import { ARGUMENT_SHAPE } from "../argument/argument"; //Описание аргумента запроса
//---------
//Константы
//---------
//Общий стиль многострочного компонента "Chip"
const COMMON_MULTILINE_CHIP = COMMON_STYLES.CHIP(true, true);
//Стили
const STYLES = {
COND_CHIP: {
...COMMON_MULTILINE_CHIP,
"& .MuiChip-label": {
...COMMON_MULTILINE_CHIP["& .MuiChip-label"],
paddingTop: "5px",
paddingBottom: "5px"
}
}
};
//-----------
//Тело модуля
//-----------
//Компонент инспектора - Условия отбора запроса
const InspectorQueryConditions = ({ query, cond, entities = [], args = [], onOptionsChanged }) => {
//Собственное состояние - отображение диалога настройки условий отбора
const [openQueryCondDialog, setOpenQueryCondDialog] = useState(false);
//Работа со связями на сервере
const { setCond } = useQueryConditions(query);
//Уведомление родителя об изменении свойств
const notifyOptionsChanged = () => onOptionsChanged && onOptionsChanged();
//При нажатии на кнопку настройки условий отбора
const handleSetup = () => setOpenQueryCondDialog(true);
//При закрытии диалога изменений условий отбора по "ОК"
const handleQueryCondDialogOk = async cond => {
await setCond(cond);
notifyOptionsChanged();
setOpenQueryCondDialog(false);
};
//При закрытии диалога изменений условий отбора по "Отмена"
const handleQueryCondDialogCancel = () => setOpenQueryCondDialog(false);
//Расчет флага сконфигурированности
const configured = cond ? true : false;
//Формирование представления
return (
<>
{openQueryCondDialog && (
<QueryCondDialog cond={cond} entities={entities} args={args} onOk={handleQueryCondDialogOk} onCancel={handleQueryCondDialogCancel} />
)}
{configured && (
<Card variant={"outlined"}>
<CardActionArea onClick={handleSetup}>
<CardContent>
<Stack direction={"column"} spacing={1}>
<Chip sx={STYLES.COND_CHIP} label={cond} />
</Stack>
</CardContent>
</CardActionArea>
</Card>
)}
{!configured && (
<Button startIcon={<Icon>build</Icon>} onClick={handleSetup}>
{BUTTONS.CONFIG}
</Button>
)}
</>
);
};
//Контроль свойств компонента - Компонент инспектора - Условия отбора запроса
InspectorQueryConditions.propTypes = {
query: PropTypes.number.isRequired,
cond: PropTypes.string,
entities: PropTypes.arrayOf(ENTITY_SHAPE),
args: PropTypes.arrayOf(ARGUMENT_SHAPE),
onOptionsChanged: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { InspectorQueryConditions };

View File

@ -0,0 +1,186 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Диалог настройки условий отбора запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useRef, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Button, Stack, TextField } from "@mui/material"; //Интерфейсные компоненты MUI
import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог
import { DATA_TYPE_ICON } from "../../common"; //Общие ресурсы и константы редактора запросов
import { ENTITY_SHAPE } from "../entity/entity"; //Описание сущности
import { ARGUMENT_SHAPE } from "../argument/argument"; //Описание аргумента запроса
import { CondOperationButtons } from "./cond_operation_buttons"; //Кнопки операций условия отбора
import { CondComponentSelectButton, COMPONENT_TYPE } from "./cond_component_select_button"; //Кнопка выбора компонента условия
//---------
//Константы
//---------
//Иконки
const ICONS = { ...DATA_TYPE_ICON, DEFAULT: "category" };
//Стили
const STYLE = {
BUTTONS_STACK: { width: "550px" }
};
//-----------
//Тело модуля
//-----------
//Диалог настройки условий отбора запроса
const QueryCondDialog = ({ cond, entities, args, onOk, onCancel }) => {
//Собственное состояние - условия отбора
const [conditions, setConditions] = useState(cond || "");
//Ссылка на элемент ввода условия
const coditionInputRef = useRef(null);
//Перемещение курсора в конец поля ввода условия
const moveCondCursorToEnd = () => {
if (coditionInputRef.current) {
const length = coditionInputRef.current.value.length;
coditionInputRef.current.setSelectionRange(length, length);
coditionInputRef.current.focus();
}
};
//Нажатие на кнопку "ОК"
const handleOk = () => onOk && onOk(conditions);
//Нажатие на кнопку "Отмена"
const handleCancel = () => onCancel && onCancel();
//При изменении условия через компонент
const handleChange = e => setConditions(e.target.value);
//При выборе компонента условия
const handleComponentSelected = value => {
setConditions(pv => pv + value);
moveCondCursorToEnd();
};
//При нажатии на кнопку операции
const handleOperationButtonClick = value => {
setConditions(pv => pv + value);
moveCondCursorToEnd();
};
//При нажатии на кнопку очистки условий
const handleClearClick = () => setConditions("");
//При подмонтировании компонента
useEffect(() => {
//Перевод курсора в конец обёрнут в setTimeout по тому, что Input подмонтируется позднее диалога (он внутри children)
setTimeout(moveCondCursorToEnd, 100);
}, []);
//Доступные атрибуты сущностей запроса
const entsAttributes =
entities && entities?.length > 0
? entities.reduce(
(components, e) => [
...components,
{ type: COMPONENT_TYPE.DIVIDER, title: e.title },
...e.attrs.map(a => ({
type: COMPONENT_TYPE.ITEM,
title: a.title,
name: a.name,
value: a.id,
icon: ICONS[a.dataType] || ICONS.DEFAULT
}))
],
[]
)
: [];
const entsAttributesExists = entsAttributes.find(a => a.type === COMPONENT_TYPE.ITEM) ? true : false;
//Доступные аргументы запроса
const queryArguments =
args && args?.length > 0
? args.reduce(
(components, a) => [
...components,
{
type: COMPONENT_TYPE.ITEM,
title: a.title,
name: a.name,
value: `:${a.name}`,
icon: ICONS[a.dataType] || ICONS.DEFAULT
}
],
[]
)
: [];
const queryArgumentsExists = queryArguments.length > 0 ? true : false;
//Генерация содержимого
return (
<P8PDialog title={`Условия отбора запроса`} onOk={handleOk} onCancel={handleCancel}>
<Stack sx={STYLE.BUTTONS_STACK} direction={"row"} spacing={1} pb={1}>
{entsAttributesExists && (
<CondComponentSelectButton caption={"Атрибут сущности"} components={entsAttributes} onSelect={handleComponentSelected} />
)}
{queryArgumentsExists && (
<CondComponentSelectButton caption={"Аргумент запроса"} components={queryArguments} onSelect={handleComponentSelected} />
)}
<Button onClick={handleClearClick}>Очистить</Button>
</Stack>
<Stack sx={STYLE.BUTTONS_STACK} direction={"row"} spacing={1} pb={1}>
<CondOperationButtons
buttons={[
{ caption: "=", title: "Равно", value: " = " },
{ caption: "<>", title: "Не равно", value: " <> " },
{ caption: ">", title: "Больше", value: " > " },
{ caption: ">=", title: "Больше или равно", value: " >= " },
{ caption: "<", title: "Меньше", value: " < " },
{ caption: "<=", title: "Меньше или равно", value: " <= " },
{ caption: "(", title: "Открывающая скобка", value: "(" },
{ caption: ")", title: "Закрывающая скобка", value: ")" }
]}
onClick={handleOperationButtonClick}
/>
<CondOperationButtons
buttons={[
{ caption: "И", title: "Логическое умножение (конъюнкция)", value: " and " },
{ caption: "ИЛИ", title: "Логическое сложение (дизъюнкция)", value: " or " },
{ caption: "НЕ", title: "Отрицание (инверсия)", value: " not " },
{ caption: "B", title: "Входит в диапазон", value: " in (0, 1, ...) " }
]}
onClick={handleOperationButtonClick}
/>
</Stack>
<TextField
inputRef={coditionInputRef}
focused={true}
autoFocus
value={conditions}
placeholder={"Настройте условия отбора..."}
multiline
minRows={10}
fullWidth
onChange={handleChange}
/>
</P8PDialog>
);
};
//Контроль свойств - Диалог настройки условий отбора запроса
QueryCondDialog.propTypes = {
cond: PropTypes.string,
onOk: PropTypes.func,
entities: PropTypes.arrayOf(ENTITY_SHAPE),
args: PropTypes.arrayOf(ARGUMENT_SHAPE),
onCancel: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { QueryCondDialog };

View File

@ -0,0 +1,73 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Диалог настройки атрибута сущности
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог
import { TITLES } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
import { DATA_TYPE } from "../../common"; //Общие константы редактора
import { ATTRIBUTE_SHAPE } from "../attribute/attribute"; //Описание атрибута
//-----------
//Тело модуля
//-----------
//Диалог добавления/исправления аргумента запроса
const AttrSetupDialog = ({ attr, onOk, onCancel }) => {
//Достанем необходимое из атрибута
const { name, title, dataType, alias } = attr;
//Нажатие на кнопку "Ok"
const handleOk = values => onOk && onOk({ ...values });
//Нажатие на кнопку "Отмена"
const handleCancel = () => onCancel && onCancel();
//Генерация содержимого
return (
<P8PDialog
title={`${TITLES.CONFIG} атрибута "${title}"`}
inputs={[
{ name: "name", value: name, label: "Имя", disabled: true },
{ name: "title", value: title, label: "Наименование", disabled: true },
{
name: "dataType",
value: dataType,
label: "Тип данных",
list: [
{ name: "Строка", value: DATA_TYPE.STR },
{ name: "Число", value: DATA_TYPE.NUMB },
{ name: "Дата", value: DATA_TYPE.DATE }
],
disabled: true
},
{
name: "alias",
value: alias,
label: "Псевдоним"
}
]}
onOk={handleOk}
onCancel={handleCancel}
/>
);
};
//Контроль свойств - Диалог настройки атрибута сущности
AttrSetupDialog.propTypes = {
attr: ATTRIBUTE_SHAPE.isRequired,
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { AttrSetupDialog };

View File

@ -0,0 +1,101 @@
/*
Парус 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_SHAPE, attrGetUse, attrGetShow } from "../attribute/attribute"; //Атрибут сущности
import { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения
//---------
//Константы
//---------
//Стили
const STYLES = {
SMALL_TOOL_ICON: {
fontSize: 20
},
LIST: { height: "100%", width: "100%", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL }
};
//-----------
//Тело модуля
//-----------
//Список атрибутов сущности
const AttrsList = ({ attrs = [], onSelect = null, onShow = null, onSetup = null } = {}) => {
//При выборе элемента списка
const handleSelectClick = attr => {
onSelect && onSelect(attr);
};
//При нажатии на кнопку изменения видимости атрибута
const handleShowClick = (e, attr) => {
e.stopPropagation();
onShow && onShow(attr);
};
//При нажатии на кнопку настройки атрибута
const handleSetupClick = (e, attr) => {
e.stopPropagation();
onSetup && onSetup(attr);
};
//Формирование представления
return (
<List sx={STYLES.LIST}>
{attrs &&
attrs.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.name}${attr.alias ? ` (${attr.alias})` : ""}`}</Typography>
</Stack>
}
/>
<Stack direction={"row"}>
<IconButton onClick={e => handleShowClick(e, attr)} title={showTitle}>
<Icon>{showIcon}</Icon>
</IconButton>
<IconButton onClick={e => handleSetupClick(e, attr)} title={"Настроить"}>
<Icon>settings</Icon>
</IconButton>
</Stack>
</ListItemButton>
</ListItem>
);
})}
</List>
);
};
//Контроль свойств компонента - Список атрибутов сущности
AttrsList.propTypes = {
attrs: PropTypes.arrayOf(ATTRIBUTE_SHAPE),
onSelect: PropTypes.func,
onShow: PropTypes.func,
onSetup: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { AttrsList };

View File

@ -0,0 +1,113 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Диалог добавления сущности запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, TextField, InputAdornment, Icon, IconButton } from "@mui/material"; //Интерфейсные элементы MUI
import { P8PDialog, P8P_DIALOG_WIDTH } from "../../../../components/p8p_dialog"; //Типовой диалог
import { P8PComponentInlineMessage } from "../../../../components/editors/p8p_component_inline_message"; //Типовое встраиваемое сообщение
import { TITLES } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
import { EntsList } from "./ents_list"; //Список сущостей
import { useEntities, useDebounce } from "./hooks"; //Хуки для работы с сущностями
//---------
//Константы
//---------
//Максимальная длина поисковой фразы для начала поиска (символов)
const FILTER_MIN_LENGTH = 3;
//Таймаут ожидания ввода поисковой фразы до начала поиска на сервере (мс)
const FILTER_TIMEOUT = 1000;
//Стили
const STYLES = {
LIST_CONTAINER: { width: "100%", height: "500px" }
};
//-----------
//Тело модуля
//-----------
//Диалог добавления сущности запроса
const EntityAddDialog = ({ onSelect, onClose }) => {
//Собственное состояние - фильтр сущностей
const [filter, setFilter] = useState("");
//Значение для отложенного поиска
const debouncedFilter = useDebounce(filter, FILTER_TIMEOUT);
//Доступные сущности
const [ents] = useEntities(debouncedFilter, FILTER_MIN_LENGTH);
//При выборе сущности в списке
const handleSelect = ent => onSelect && onSelect({ ...ent });
//Нажатие на кнопку "Закыть"
const handleClose = () => onClose && onClose();
//При изменении значения фильтра
const handleFilterChange = e => setFilter(e.target.value);
//При очистке фильтра
const handleFilterClear = () => setFilter("");
//Генерация содержимого
return (
<P8PDialog title={`${TITLES.INSERT} сущности`} width={P8P_DIALOG_WIDTH.SM} fullWidth onClose={handleClose}>
<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>
)
}}
/>
<Box sx={STYLES.LIST_CONTAINER} justifyContent={"center"} alignItems={"center"} display={"flex"}>
{ents && <EntsList ents={ents} onSelect={handleSelect} />}
{!ents && (
<P8PComponentInlineMessage
name={"Нет данных, соответствующих фильтру"}
message={
filter || String(filter).length > FILTER_MIN_LENGTH
? "Измените значение фильтра для повторного поиска"
: `Начните ввод в строку поиска (не менее ${FILTER_MIN_LENGTH} символов)`
}
/>
)}
</Box>
</P8PDialog>
);
};
//Контроль свойств - Диалог добавления сущности запроса
EntityAddDialog.propTypes = {
onSelect: PropTypes.func,
onClose: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { EntityAddDialog };

View File

@ -0,0 +1,160 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Диалог настройки атрибутов сущности
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, TextField, InputAdornment, Icon, IconButton } from "@mui/material"; //Интерфейсные элементы MUI
import { P8PDialog, P8P_DIALOG_WIDTH } from "../../../../components/p8p_dialog"; //Типовой диалог
import { P8PComponentInlineMessage } from "../../../../components/editors/p8p_component_inline_message"; //Типовое встраиваемое сообщение
import { AttrsList } from "./attrs_list"; //Список атрибутов сущности
import { AttrSetupDialog } from "./attr_setup_dialog"; //Диалог настройки атрибута
import { useEntityAttrs } from "./hooks"; //Хуки диалога настройки атрибутов сущности
//---------
//Константы
//---------
//Стили
const STYLES = {
LIST_CONTAINER: { width: "100%", height: "500px" }
};
//-----------
//Тело модуля
//-----------
//Диалог настройки атрибутов сущности
const EntityAttrsDialog = ({ query, id, title, onOk, onCancel }) => {
//Собственное состояние - фильтр атрибутов
const [filter, setFilter] = useState("");
//Собственное состояние - список атрибутов
const [attrs, setAttrs] = useState([]);
//Собственное состояние - настраиваемый атрибут
const [setupAttr, setSetupAttr] = useState(null);
//Хук для взаимодействия с сервером
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 handleAttrSetup = attr => setSetupAttr({ ...attr });
//При закрытии диалога настройки атрибута по "ОК"
const handleAttrSetupOk = values => {
setAttrs(
attrs.map(a => ({
...(a.id === setupAttr.id ? { ...a, use: 1, alias: values.alias } : a)
}))
);
setSetupAttr(null);
};
const handleAttrSetupCancel = () => setSetupAttr(null);
//При изменении значения фильтра
const handleFilterChange = e => setFilter(e.target.value);
//При очистке фильтра
const handleFilterClear = () => setFilter("");
//При загрузке данных с сервера
useEffect(() => {
if (srvAttrs) setAttrs(srvAttrs.map(srvAttr => ({ ...srvAttr })));
}, [srvAttrs]);
//Рег. выражение для фильтра
const filterRegExp = filter ? new RegExp(filter, "i") : null;
//Отфильтрованные для отображения атрибуты
const filteredAttrs = attrs.filter(attr => (filterRegExp ? filterRegExp.test(attr.name) || filterRegExp.test(attr.title) : true));
//Флаг отображения списка атрибутов
const displayAttrsList = attrs.length == 0 ? null : filteredAttrs.length > 0 ? true : false;
//Генерация содержимого
return (
<P8PDialog title={`Атрибуты сущности "${title}"`} width={P8P_DIALOG_WIDTH.SM} fullWidth onOk={handleOk} onCancel={handleCancel}>
{setupAttr && <AttrSetupDialog attr={setupAttr} onOk={handleAttrSetupOk} onCancel={handleAttrSetupCancel} />}
<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>
)
}}
/>
<Box sx={STYLES.LIST_CONTAINER} justifyContent={"center"} alignItems={"center"} display={"flex"}>
{displayAttrsList === true && (
<AttrsList attrs={filteredAttrs} filter={filter} onSelect={handleAttrSelect} onShow={handleAttrShow} onSetup={handleAttrSetup} />
)}
{displayAttrsList === false && (
<P8PComponentInlineMessage
name={"Нет данных, соответствующих фильтру"}
message={"Измените значение фильтра для повторного поиска"}
/>
)}
</Box>
</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,52 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Список сущностей
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { List } from "@mui/material"; //Интерфейсные компоненты MUI
import { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения
import { Entity, ENTITY_VARIANT, ENTITY_BRIEF_SHAPE } from "../entity/entity"; //Сущности
//---------
//Константы
//---------
//Стили
const STYLES = {
LIST: { height: "100%", width: "100%", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL }
};
//-----------
//Тело модуля
//-----------
//Список сущностей
const EntsList = ({ ents = [], onSelect = null } = {}) => {
//При нажатии на элемент списка
const handleItemClick = ent => onSelect && onSelect(ent);
//Формирование представления
return (
<List sx={STYLES.LIST}>
{ents && ents.map((ent, i) => <Entity key={i} data={ent} variant={ENTITY_VARIANT.LIST_ITEM} onClick={handleItemClick} />)}
</List>
);
};
//Контроль свойств компонента - Список сущностей
EntsList.propTypes = {
ents: PropTypes.arrayOf(ENTITY_BRIEF_SHAPE),
onSelect: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { EntsList };

View File

@ -0,0 +1,203 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Пользовательские хуки для работы с сущностями
*/
//---------------------
//Подключение библиотек
//---------------------
import { useRef, useContext, useCallback, useEffect, useState } from "react"; //Классы React
import { BackEndСtx } from "../../../../context/backend"; //Контекст взаимодействия с сервером
import { object2Base64XML } from "../../../../core/utils"; //Вспомогательные функции
//-----------
//Тело модуля
//-----------
//Работа с сущностями системы
const useEntities = (filter, minFilterLen) => {
//Контроллер для прерывания запросов
const abortController = useRef(null);
//Собственное состояние - флаг загрузки
const [isLoading, setLoading] = useState(false);
//Собственное состояние - флаг необходимости обновления
const [refresh, setRefresh] = useState(true);
//Собственное состояние - данные
const [data, setData] = useState(null);
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Обновление данных
const doRefresh = () => setRefresh(true);
//При необходимости получить/обновить данные
useEffect(() => {
//Загрузка данных с сервера
const loadData = async () => {
try {
setLoading(true);
abortController.current?.abort?.();
abortController.current = new AbortController();
const data = await executeStored({
stored: "PKG_P8PANELS_QE.ENTITY_LIST",
args: { SSEARCH: filter },
respArg: "COUT",
isArray: name => ["XENT"].includes(name),
attributeValueProcessor: (name, val) => (["name", "title"].includes(name) ? undefined : val),
loader: false,
signal: abortController.current.signal
});
setData(data?.XENTS?.XENT || null);
} finally {
setRefresh(false);
setLoading(false);
}
};
//Если надо обновить
if (refresh) {
//Если фильтр задан и он нас удовлетворяет - получим данные
if (filter && String(filter).length >= minFilterLen) loadData();
//Нет фильтра - нет данных
else {
setData(null);
}
}
//Сбрасываем запрос при смене зависимостей или отмонтировании
return () => abortController.current?.abort?.();
}, [refresh, filter, minFilterLen, executeStored]);
//При изменении входных свойств - поднимаем флаг обновления
useEffect(() => setRefresh(true), [filter, minFilterLen]);
//Возвращаем интерфейс хука
return [data, doRefresh, isLoading];
};
//Работа с сущностями запроса
const useQueryEntities = query => {
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Добавление сущности в запрос
const addEnt = useCallback(
async (name, type) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_ENT_ADD", args: { NRN: query, SNAME: name, STYPE: type }, loader: false });
},
[query, executeStored]
);
//Удаление сущности из запроса
const removeEnt = useCallback(
async ent => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_ENT_REMOVE", args: { NRN: query, SID: ent }, loader: false });
},
[query, executeStored]
);
//Сохранение координат сущности на диаграммем
const setEntPosition = useCallback(
async (ent, x, y) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_ENT_POSITION_SET", args: { NRN: query, SID: ent, NX: x, NY: y }, loader: false });
},
[query, executeStored]
);
//Возвращаем интерфейс хука
return { addEnt, removeEnt, setEntPosition };
};
//Работа с атрибутами сущности
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];
};
//Отложенное значение
const useDebounce = (value, delay) => {
//Собственное состояние
const [debouncedValue, setDebouncedValue] = useState(value);
//При изменении интервала или значения
useEffect(() => {
//Взводим таймер
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
//Сбрасываем таймер при отмонтировании или изменении зависимостей
return () => clearTimeout(handler);
}, [value, delay]);
//Возвращаем отложенное значение
return debouncedValue;
};
//----------------
//Интерфейс модуля
//----------------
export { useEntities, useQueryEntities, useEntityAttrs, useDebounce };

View File

@ -0,0 +1,112 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент инспектора - Сущности запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useContext, useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Icon, Button } from "@mui/material"; //Интерфейсные элементы
import { MessagingСtx } from "../../../../context/messaging"; //Контекст сообщений приложения
import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Подзаголовок группы параметров редактора
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы
import { ENTITY_SHAPE } from "../entity/entity"; //Описание сущности
import { EntityAddDialog } from "./entity_add_dialog"; //Диалог добавления сущности
import { EntityAttrsDialog } from "./entity_attrs_dialog"; //Диалог настройки атрибутов сущности
import { useQueryEntities } from "./hooks"; //Хуки для работы с сущностями на сервере
//-----------
//Тело модуля
//-----------
//Компонент инспектора - Сущности запроса
const InspectorQueryEntities = ({ query, entity, onOptionsChanged }) => {
//Отображение диалога добавления сущности
const [openEntityAddDialog, setOpenEntityAddDialog] = useState(false);
//Отображение диалога настройки атрибутов сущности
const [openEntityAttrsDialog, setOpenEntityAttrsDialog] = useState(false);
//Работа с сущностями на сервере
const { addEnt, removeEnt } = useQueryEntities(query);
//Подключение к контексту сообщений
const { showMsgWarn } = useContext(MessagingСtx);
//Уведомление родителя об изменении свойств
const notifyOptionsChanged = () => onOptionsChanged && onOptionsChanged();
//При нажатии на кнопку добавлении сущности в запрос
const handleEntityAddClick = () => setOpenEntityAddDialog(true);
//При нажатии на кнопку настройки атрибутов сущности
const handleEntityAttrsClick = () => setOpenEntityAttrsDialog(true);
//При нажатии на кнопку даления сущности из запроса
const handleEntityRemoveClick = () =>
showMsgWarn(`Удалить сущность "${entity.title}"?`, async () => {
if (entity?.id) {
await removeEnt(entity.id);
notifyOptionsChanged();
}
});
//Закрытие диалога добавления сущности по "Закрыть"
const handleEntityAddDialogClose = () => setOpenEntityAddDialog(false);
//Закрытие диалога добавления сущности по выбору сущности из списка
const handleEntityAddDialogSelect = async ent => {
await addEnt(ent.name, ent.type);
setOpenEntityAddDialog(false);
notifyOptionsChanged();
};
//Закрытие диалога настройки атрибутов сущности по "Отмена"
const handleEntityAttrsDialogCancel = () => setOpenEntityAttrsDialog(false);
//Закрытие диалога настройки атрибутов сущности по "ОК"
const handleEntityAttrsDialogOk = () => {
notifyOptionsChanged();
setOpenEntityAttrsDialog();
};
//Формирование представления
return (
<>
{openEntityAddDialog && <EntityAddDialog onSelect={handleEntityAddDialogSelect} onClose={handleEntityAddDialogClose} />}
{openEntityAttrsDialog && (
<EntityAttrsDialog query={query} {...entity} onOk={handleEntityAttrsDialogOk} onCancel={handleEntityAttrsDialogCancel} />
)}
<Button startIcon={<Icon>add</Icon>} onClick={handleEntityAddClick}>
{BUTTONS.INSERT}
</Button>
{entity && (
<>
<P8PEditorSubHeader title={entity.title} maxWidth={"200px"} />
<Button startIcon={<Icon>edit_attributes</Icon>} onClick={handleEntityAttrsClick}>
Атрибуты
</Button>
<Button startIcon={<Icon>delete</Icon>} onClick={handleEntityRemoveClick}>
{BUTTONS.DELETE}
</Button>
</>
)}
</>
);
};
//Контроль свойств компонента - Компонент инспектора - Сущности запроса
InspectorQueryEntities.propTypes = {
query: PropTypes.number.isRequired,
entity: ENTITY_SHAPE,
onOptionsChanged: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { InspectorQueryEntities };

View File

@ -0,0 +1,58 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Пользовательские хуки для работы со связями
*/
//---------------------
//Подключение библиотек
//---------------------
import { useContext, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../../context/backend"; //Контекст взаимодействия с сервером
//-----------
//Тело модуля
//-----------
//Работа со связами запроса
const useQueryRelations = query => {
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Добавление отношения сущностей в запрос
const addRl = useCallback(
async (source, target) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_RL_ADD", args: { NRN: query, SSOURCE: source, STARGET: target }, loader: false });
},
[query, executeStored]
);
//Удаление отношения сущностей из запроса
const removeRl = useCallback(
async rl => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_RL_REMOVE", args: { NRN: query, SID: rl }, loader: false });
},
[query, executeStored]
);
//Установка признака обязательности отношения сущности в запросе
const setRlMandatory = useCallback(
async (rl, mandatory) => {
await executeStored({
stored: "PKG_P8PANELS_QE.QUERY_RL_MANDATORY_SET",
args: { NRN: query, SID: rl, NMANDATORY: mandatory },
loader: false
});
},
[query, executeStored]
);
//Возвращаем интерфейс хука
return { addRl, removeRl, setRlMandatory };
};
//----------------
//Интерфейс модуля
//----------------
export { useQueryRelations };

View File

@ -0,0 +1,88 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент инспектора - Связи запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Icon, Button, FormControlLabel, Checkbox } from "@mui/material"; //Интерфейсные элементы
import { MessagingСtx } from "../../../../context/messaging"; //Контекст сообщений приложения
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы
import { RELATION_SHAPE } from "../relation/relation"; //Описание связи
import { useQueryRelations } from "./hooks"; //Хуки для работы со связями
//---------
//Константы
//---------
//Стили
const STYLES = {
MANDATORY_FORMCONTROLLABEL: {
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center"
}
};
//-----------
//Тело модуля
//-----------
//Компонент инспектора - Связи запроса
const InspectorQueryRelations = ({ query, relation, onOptionsChanged }) => {
//Подключение к контексту сообщений
const { showMsgWarn } = useContext(MessagingСtx);
//Работа со связями на сервере
const { removeRl, setRlMandatory } = useQueryRelations(query);
//Уведомление родителя об изменении свойств
const notifyOptionsChanged = () => onOptionsChanged && onOptionsChanged();
//При нажатии на кнопку даления связи из запроса
const handleRelationRemoveClick = () =>
showMsgWarn(`Удалить связь "${relation.source}" - "${relation.target}"?`, async () => {
if (relation?.id) {
await removeRl(relation.id);
notifyOptionsChanged();
}
});
//При изменении состояния обязательности связи в запросе
const handleRelationMandatoryChange = async e => {
await setRlMandatory(relation.id, e.target.checked === true ? 1 : 0);
notifyOptionsChanged();
};
//Формирование представления
return (
<>
<FormControlLabel
sx={STYLES.MANDATORY_FORMCONTROLLABEL}
control={<Checkbox checked={relation.mandatory === 1 ? true : false} onChange={handleRelationMandatoryChange} />}
label={"Обязательна"}
/>
<Button startIcon={<Icon>delete</Icon>} onClick={handleRelationRemoveClick}>
{BUTTONS.DELETE}
</Button>
</>
);
};
//Контроль свойств компонента - Компонент инспектора - Связи запроса
InspectorQueryRelations.propTypes = {
query: PropTypes.number.isRequired,
relation: RELATION_SHAPE.isRequired,
onOptionsChanged: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { InspectorQueryRelations };

View File

@ -0,0 +1,116 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Пользовательские хуки для работы с запросами
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../../context/backend"; //Контекст взаимодействия с сервером
//-----------
//Тело модуля
//-----------
//Работа с запросами
const useQueries = () => {
//Собственное состояние - флаг инициализированности
const [isInit, setInit] = useState(false);
//Собственное состояние - флаг загрузки
const [isLoading, setLoading] = useState(false);
//Собственное состояние - флаг необходимости обновления
const [refresh, setRefresh] = useState(true);
//Собственное состояние - данные
const [data, setData] = useState(null);
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Обновление данных
const doRefresh = () => setRefresh(true);
//Добавление запроса
const insertQuery = useCallback(
async (code, name) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_INSERT", args: { SCODE: code, SNAME: name }, loader: false });
setRefresh(true);
},
[executeStored]
);
//Изменение запроса
const updateQuery = useCallback(
async (query, code, name) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_UPDATE", args: { NRN: query, SCODE: code, SNAME: name }, loader: false });
setRefresh(true);
},
[executeStored]
);
//Удаление запроса
const deleteQuery = useCallback(
async query => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_DELETE", args: { NRN: query }, loader: false });
setRefresh(true);
},
[executeStored]
);
//Установка флага готовности запроса
const setQueryReady = useCallback(
async (query, ready) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_READY_SET", args: { NRN: query, NREADY: ready }, loader: false });
setRefresh(true);
},
[executeStored]
);
//Установка флага публичности запроса
const setQueryPbl = useCallback(
async (query, pbl) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_PBL_SET", args: { NRN: query, NPBL: pbl }, loader: false });
setRefresh(true);
},
[executeStored]
);
//При необходимости получить/обновить данные
useEffect(() => {
//Загрузка данных с сервера
const loadData = async () => {
try {
setLoading(true);
const data = await executeStored({
stored: "PKG_P8PANELS_QE.QUERY_LIST",
respArg: "COUT",
isArray: name => ["XQUERY"].includes(name),
attributeValueProcessor: (name, val) => (["code", "name"].includes(name) ? undefined : val),
loader: true
});
setData(data?.XQUERIES?.XQUERY || []);
setInit(true);
} finally {
setRefresh(false);
setLoading(false);
}
};
//Если надо обновить
if (refresh)
//Получим данные
loadData();
}, [refresh, executeStored]);
//Возвращаем интерфейс хука
return [data, insertQuery, updateQuery, deleteQuery, setQueryReady, setQueryPbl, doRefresh, isLoading, isInit];
};
//----------------
//Интерфейс модуля
//----------------
export { useQueries };

View File

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

View File

@ -13,7 +13,7 @@ import { Button, Icon } from "@mui/material"; //Интерфейсные ком
import { MessagingСtx } from "../../../../context/messaging"; //Контекст сообщений
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
import { P8PConfigDialog } from "../../../../components/editors/p8p_config_dialog"; //Типовой диалог настройки
import { useQuery } from "../../hooks"; //Пользовательские хуки
import { useQueries } from "./hooks"; //Хуки для работы с запросами
import { QueriesList } from "./queries_list"; //Список запросов
import { QueryIUDialog } from "./query_iu_dialog"; //Диалог добавления/исправления запроса
@ -27,7 +27,7 @@ const QueriesManager = ({ current = null, onQuerySelect = null, onCancel = null
const [modQuery, setModQuery] = useState(null);
//Работа со списком запросов
const [queries, insertQuery, updateQuery, deleteQuery, setQueryReady, setQueryPbl] = useQuery();
const [queries, insertQuery, updateQuery, deleteQuery, setQueryReady, setQueryPbl] = useQueries();
//Подключение к контексту сообщений
const { showMsgWarn } = useContext(MessagingСtx);
@ -36,7 +36,7 @@ const QueriesManager = ({ current = null, onQuerySelect = null, onCancel = null
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);

View File

@ -8,10 +8,11 @@
//---------------------
import React, { useState, useCallback, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import ReactFlow, { addEdge, Controls, getOutgoers, applyNodeChanges, applyEdgeChanges } from "reactflow"; //Библиотека редактора диаграмм
import { NODE_TYPE } from "../../common"; //Общие ресурсы и константы редактора
import { Entity } from "../entity/entity"; //Сущность запроса
import { Attribute } from "../attribute/attribute"; //Атрибут сущности
import { Entity, ENTITY_SHAPE } from "../entity/entity"; //Сущность запроса
import { Attribute, ATTRIBUTE_SHAPE } from "../attribute/attribute"; //Атрибут сущности
import "reactflow/dist/style.css"; //Типовые стили библиотеки редактора диаграмм
import "./query_diagram.css"; //Стили компонента
@ -36,42 +37,48 @@ const NODE_TYPES_COMPONENTS = {
[NODE_TYPE.ATTRIBUTE]: Attribute
};
//Структура элемента диаграммы
const NODE_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_SHAPE, ATTRIBUTE_SHAPE])
});
//Структура связи диаграммы
const EDGE_SHAPE = PropTypes.shape({ id: PropTypes.string.isRequired, source: PropTypes.string.isRequired, target: PropTypes.string.isRequired });
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Проверка зацикленности связи
const hasCycle = (connection, target, nodes, edges, visited = new Set()) => {
if (visited.has(target.id)) {
return false;
}
if (visited.has(target.id)) return false;
visited.add(target.id);
for (const outgoer of getOutgoers(target, nodes, edges)) {
if (outgoer.id === connection.source || hasCycle(connection, outgoer, nodes, edges, visited)) {
return true;
}
}
for (const outgoer of getOutgoers(target, nodes, edges))
if (outgoer.id === connection.source || hasCycle(connection, outgoer, nodes, edges, visited)) return true;
return false;
};
//Проверка корректности связи
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;
}
//Должны быть заданы источник и приёмник
if (!connection.source || !connection.target) 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);
};
@ -80,12 +87,23 @@ const isValidConnection = (connection, nodes, edges) => {
//-----------
//Диаграмма запроса
const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRemove, onRelationAdd, onRelationRemove }) => {
const QueryDiagram = ({
nodes = [],
edges = [],
onEntityClick,
onEntityAttrClick,
onEntityPositionChange,
onEntityRemove,
onRelactionClick,
onRelationAdd,
onRelationRemove,
onPaneClick
}) => {
//Собственное состояние - элементы
const [nodes, setNodes] = useState(entities);
const [nodesCurrent, setNodesCurrent] = useState(nodes);
//Собственное состояние - связи
const [edges, setEdges] = useState(relations);
const [edgesCurrent, setEdgesCurrent] = useState(edges);
//Собственное состояние - перемещённый элемент
const [movedNode, setMovedNode] = useState(null);
@ -93,56 +111,90 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem
//При изменении элементов на диаграмме
const handleNodesChange = useCallback(
changes => {
setNodes(nodesSnapshot => applyNodeChanges(changes, nodesSnapshot));
//При выборе атрибута подсветим всю сущность
const tmpChanges = changes.reduce((prevChanges, curChanges) => {
const tmp = { ...curChanges };
if (tmp.type == "select") {
const chEnt = nodes.find(n => n.id === tmp.id);
if (chEnt && chEnt?.data?.parentEntity) {
prevChanges.push({ ...curChanges, id: chEnt.data.parentEntity });
tmp.selected = false;
}
}
prevChanges.push(tmp);
return prevChanges;
}, []);
//Применим изменения в диаграмме
setNodesCurrent(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)
//Если удалили сущность - вызываем колбэк для удаления
if (changes[0].type == "remove" && nodes.find(n => n.id == changes[0].id && n.type == NODE_TYPE.ENTITY) && onEntityRemove)
onEntityRemove(changes[0].id);
},
[movedNode, entities, onEntityPositionChange, onEntityRemove]
[movedNode, nodes, 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]
);
//При связывании элементов на диаграмме
const handleConnect = connection => {
setEdges(state => addEdge({ ...connection, id: `${connection.source}-${connection.target}` }, state));
setEdgesCurrent(state => addEdge({ ...connection, id: `${connection.source}-${connection.target}` }, state));
onRelationAdd && onRelationAdd(connection.source, connection.target);
};
//При выборе связи диаграммы
const handleEdgeClick = useCallback((e, edge) => onRelactionClick && onRelactionClick(edge.id), [onRelactionClick]);
//При изменении связей на диаграмме
const handleEdgesChange = useCallback(
changes => {
setEdges(edgesSnapshot => applyEdgeChanges(changes, edgesSnapshot));
setEdgesCurrent(edgesSnapshot => applyEdgeChanges(changes, edgesSnapshot));
if (changes.length == 1 && changes[0].type == "remove" && onRelationRemove) onRelationRemove(changes[0].id);
},
[onRelationRemove]
);
const validateConnection = connection => {
return isValidConnection(connection, nodes, edges);
};
//При нажатии на холст диаграммы
const handlePaneClick = () => onPaneClick && onPaneClick();
//Валидация связи
const validateConnection = connection => isValidConnection(connection, nodesCurrent, edgesCurrent);
//Подсветка выбранной сущности
//При изменении состава сущностей
useEffect(() => setNodes(entities), [entities]);
useEffect(() => setNodesCurrent(nodes), [nodes]);
//При изменении состава связей
useEffect(() => setEdges(relations), [relations]);
useEffect(() => setEdgesCurrent(edges), [edges]);
//Генерация содержимого
return (
<ReactFlow
nodes={nodes}
nodes={nodesCurrent}
nodeTypes={NODE_TYPES_COMPONENTS}
edges={edges}
edges={edgesCurrent}
onNodeClick={handleNodeClick}
onNodesChange={handleNodesChange}
onEdgeClick={handleEdgeClick}
onEdgesChange={handleEdgesChange}
defaultEdgeOptions={{
animated: true,
style: STYLES.EDGE
}}
onPaneClick={handlePaneClick}
defaultEdgeOptions={{ style: STYLES.EDGE }}
connectionLineStyle={STYLES.CONNECTION_LINE}
onConnect={handleConnect}
isValidConnection={validateConnection}
@ -153,6 +205,20 @@ const QueryDiagram = ({ entities, relations, onEntityPositionChange, onEntityRem
);
};
//Контроль свойств компонента - Диаграмма запроса
QueryDiagram.propTypes = {
nodes: PropTypes.arrayOf(NODE_SHAPE),
edges: PropTypes.arrayOf(EDGE_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,28 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компоненты: Связь сущностей запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import PropTypes from "prop-types"; //Контроль свойств компонента
//---------
//Константы
//---------
//Структура данных о связи сущностей запроса
const RELATION_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
source: PropTypes.string.isRequired,
target: PropTypes.string.isRequired,
mandatory: PropTypes.number.isRequired
});
//----------------
//Интерфейс модуля
//----------------
export { RELATION_SHAPE };

View File

@ -1,86 +1,96 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Пользовательские хуки
Пользовательские хуки для работы с метаданными запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect, useCallback } from "react"; //Классы React
import { useState, useContext, useEffect } from "react"; //Классы React
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { NODE_TYPE } from "./common"; //Общие ресурсы и константы редактора
//---------
//Константы
//---------
//Ширина элемента диаграммы
const NODE_WIDTH = 250;
//Высота единицы состава группового элемента диаграммы
const GROUP_NODE_ITEM_HEIGHT = 50;
//Стили
const STYLES = {
ATTRIBUTE: isLast => ({ borderBottom: isLast ? "none" : "1px solid #dee2e6" })
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Конвертация серверного описания сущностей запроса в элементы диаграммы
const serverEntity2QueryDiagramNodes = entity => {
const groupWidth = 250;
const nameColumnHeight = 50;
const columns = entity?.XATTRS?.XATTR || [];
const columnsCount = columns.length;
const groupHeight = nameColumnHeight + columnsCount * 50;
const groupNode = {
//Ссылка на атрибуты
const attrs = entity.attrs || [];
//Количество атрибутов
const attrsCount = attrs.length;
//Высота группового элемента диаграммы
const entityNodeHeight = GROUP_NODE_ITEM_HEIGHT + attrsCount * GROUP_NODE_ITEM_HEIGHT;
//Элемент диаграммы для сущности (групповой элемент)
const entityNode = {
id: entity.id,
type: NODE_TYPE.ENTITY,
data: { ...entity },
position: { x: entity.x, y: entity.y },
style: {
width: groupWidth,
height: groupHeight
width: NODE_WIDTH,
height: entityNodeHeight
},
draggable: true
};
const columnNodes = columns.map((column, index, columns) => {
const x = 1;
const y = 50 * (index + 1);
const width = groupWidth - 2;
const height = 50;
const isLast = index === columns.length - 1;
const defaultColumnStyles = {
borderBottom: "1px solid #dee2e6"
};
const lastColumnStyles = {
borderBottom: "none"
};
const otherStyles = isLast ? lastColumnStyles : defaultColumnStyles;
return {
id: column.id,
type: NODE_TYPE.ATTRIBUTE,
data: {
...column,
included: false,
parentEntity: entity.id
},
position: { x, y },
parentId: entity.id,
extent: "parent",
style: {
width,
height,
...otherStyles
},
draggable: false,
selectable: true
};
});
return [groupNode, ...columnNodes];
//Элементы диаграммы для атрибутов сущности (состав группового элемента)
const attrsNodes = attrs.map((attr, index, attrs) => ({
id: attr.id,
type: NODE_TYPE.ATTRIBUTE,
data: { ...attr },
position: { x: 1, y: GROUP_NODE_ITEM_HEIGHT * (index + 1) },
parentId: entity.id,
extent: "parent",
style: {
width: NODE_WIDTH - 2,
height: GROUP_NODE_ITEM_HEIGHT,
...STYLES.ATTRIBUTE(index === attrs.length - 1)
},
draggable: false,
selectable: true
}));
//Возвращаем элемент для сущности (групповой) и элементы для атрибутов (состав группового)
return [entityNode, ...attrsNodes];
};
//Конвертация серверного описания запроса в данные для редактора диаграмм
const serverQueryData2QueryDiagram = (entities, relations) => {
const result = { entities: [], relations: [...relations] };
entities.forEach(entity => {
const nodes = serverEntity2QueryDiagramNodes(entity);
result.entities = [...result.entities, ...nodes];
//Инициализация результата
const result = { entities: [], relations: [], nodes: [], edges: [] };
//Сущности (почти как есть на сервере, только массив XATTRS.XATTR перемещается в attrs)
result.entities = entities.map(e => {
const tmp = { ...e };
tmp.attrs = tmp?.XATTRS?.XATTR?.map(a => ({ ...a })) || [];
delete tmp.XATTRS;
return tmp;
});
//Связи сущностей
result.relations = relations.map(r => ({ ...r }));
//Элементы для диаграммы
result.entities.forEach(entity => {
const nodes = serverEntity2QueryDiagramNodes(entity);
result.nodes = [...result.nodes, ...nodes];
});
//Грани для диаграммы
result.edges = relations.map(r => ({ ...r, animated: r.mandatory === 1 ? false : true }));
//Вернем итоговый результат
return result;
};
@ -88,8 +98,8 @@ const serverQueryData2QueryDiagram = (entities, relations) => {
//Тело модуля
//-----------
//Работа с запросами
const useQuery = () => {
//Работа с метаданными запроса
const useQuery = query => {
//Собственное состояние - флаг инициализированности
const [isInit, setInit] = useState(false);
@ -99,8 +109,14 @@ const useQuery = () => {
//Собственное состояние - флаг необходимости обновления
const [refresh, setRefresh] = useState(true);
//Собственное состояние - данные
const [data, setData] = useState(null);
//Собственное состояние - данные диаграммы
const [queryDiagram, setQueryDiagram] = useState(null);
//Собственное состояние - данные настроек
const [queryOption, setQueryOption] = useState(null);
//Собственное состояние - данные SQL-запроса
const [querySQL, setQuerySQL] = useState(null);
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
@ -108,51 +124,6 @@ const useQuery = () => {
//Обновление данных
const doRefresh = () => setRefresh(true);
//Добавление запроса
const insertQuery = useCallback(
async (code, name) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_INSERT", args: { SCODE: code, SNAME: name }, loader: false });
setRefresh(true);
},
[executeStored]
);
//Изменение запроса
const updateQuery = useCallback(
async (query, code, name) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_UPDATE", args: { NRN: query, SCODE: code, SNAME: name }, loader: false });
setRefresh(true);
},
[executeStored]
);
//Удаление запроса
const deleteQuery = useCallback(
async query => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_DELETE", args: { NRN: query }, loader: false });
setRefresh(true);
},
[executeStored]
);
//Установка флага готовности запроса
const setQueryReady = useCallback(
async (query, ready) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_READY_SET", args: { NRN: query, NREADY: ready }, loader: false });
setRefresh(true);
},
[executeStored]
);
//Установка флага публичности запроса
const setQueryPbl = useCallback(
async (query, pbl) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_PBL_SET", args: { NRN: query, NPBL: pbl }, loader: false });
setRefresh(true);
},
[executeStored]
);
//При необходимости получить/обновить данные
useEffect(() => {
//Загрузка данных с сервера
@ -160,107 +131,16 @@ const useQuery = () => {
try {
setLoading(true);
const data = await executeStored({
stored: "PKG_P8PANELS_QE.QUERY_LIST",
respArg: "COUT",
isArray: name => ["XQUERY"].includes(name),
attributeValueProcessor: (name, val) => (["code", "name"].includes(name) ? undefined : val),
loader: true
});
setData(data?.XQUERIES?.XQUERY || []);
setInit(true);
} finally {
setRefresh(false);
setLoading(false);
}
};
//Если надо обновить
if (refresh)
//Получим данные
loadData();
}, [refresh, executeStored]);
//Возвращаем интерфейс хука
return [data, insertQuery, updateQuery, deleteQuery, setQueryReady, setQueryPbl, doRefresh, isLoading, isInit];
};
//Работа с содержимым запроса
const useQueryDesc = query => {
//Собственное состояние - флаг инициализированности
const [isInit, setInit] = useState(false);
//Собственное состояние - флаг загрузки
const [isLoading, setLoading] = useState(false);
//Собственное состояние - флаг необходимости обновления
const [refresh, setRefresh] = useState(true);
//Собственное состояние - данные
const [data, setData] = useState(null);
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Обновление данных
const doRefresh = () => setRefresh(true);
//Добавление сущности в запрос
const addEnt = useCallback(
async (name, type) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_ENT_ADD", args: { NRN: query, SNAME: name, STYPE: type }, loader: false });
setRefresh(true);
},
[query, executeStored]
);
//Удаление сущности из запроса
const removeEnt = useCallback(
async ent => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_ENT_REMOVE", args: { NRN: query, SID: ent }, loader: false });
setRefresh(true);
},
[query, executeStored]
);
//Сохранение координат сущности на диаграммем
const setEntPosition = useCallback(
async (ent, x, y) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_ENT_POSITION_SET", args: { NRN: query, SID: ent, NX: x, NY: y }, loader: false });
},
[query, executeStored]
);
//Добавление отношения сущностей в запрос
const addRl = useCallback(
async (source, target) => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_RL_ADD", args: { NRN: query, SSOURCE: source, STARGET: target }, loader: false });
setRefresh(true);
},
[query, executeStored]
);
//Удаление отношения сущностей из запроса
const removeRl = useCallback(
async rl => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_RL_REMOVE", args: { NRN: query, SID: rl }, loader: false });
setRefresh(true);
},
[query, executeStored]
);
//При необходимости получить/обновить данные
useEffect(() => {
//Загрузка данных с сервера
const loadData = async () => {
try {
setLoading(true);
const data = await executeStored({
stored: "PKG_P8PANELS_QE.QUERY_DESC",
stored: "PKG_P8PANELS_QE.QUERY",
args: { NRN: query },
respArg: "COUT",
isArray: name => ["XENT", "XATTR", "XRL"].includes(name),
isArray: name => ["XENT", "XATTR", "XRL", "XARG"].includes(name),
attributeValueProcessor: (name, val) => (["value"].includes(name) ? undefined : val),
loader: true
});
setData(serverQueryData2QueryDiagram(data?.XENTS?.XENT || [], data?.XRLS?.XRL || []));
setQueryDiagram(serverQueryData2QueryDiagram(data?.XENTS?.XENT || [], data?.XRLS?.XRL || []));
setQueryOption({ args: data?.XOPT?.XARGS?.XARG || [], cond: data?.XOPT?.XCOND || null });
setQuerySQL({ qry: data?.XQRY, qryMsg: data?.XQRY_MSG });
setInit(true);
} finally {
setRefresh(false);
@ -273,18 +153,22 @@ const useQueryDesc = query => {
//Если есть для чего получать данные
loadData();
//Нет идентификатора запроса - нет данных
else setData(null);
else {
setQueryDiagram(null);
setQueryOption(null);
setQuerySQL(null);
}
}, [refresh, query, executeStored]);
//При изменении входных свойств - поднимаем флаг обновления
useEffect(() => setRefresh(true), [query]);
//Возвращаем интерфейс хука
return [data, addEnt, removeEnt, setEntPosition, addRl, removeRl, doRefresh, isLoading, isInit];
return [queryDiagram, queryOption, querySQL, doRefresh, isLoading, isInit];
};
//----------------
//Интерфейс модуля
//----------------
export { useQuery, useQueryDesc };
export { useQuery };

View File

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

View File

@ -11,10 +11,11 @@ create table P8PNL_QE_QUERY
CH_DATE date not null, -- Дата последнего изменения
READY number(1) default 0 not null, -- Флаг готовности к использованию (0 - нет, 1 - да)
PBL number(1) default 0 not null, -- Флаг публичности (0 - нет, 1 - да)
OPTS clob, -- Параметры запроса
OPT clob, -- Настройка запроса
ENTS clob, -- Сущности запроса
RLS clob, -- Отношения сущностей запроса
QRY clob, -- Запрос
QRY clob, -- Запрос (SQL выражение)
QRY_MSG clob, -- Сообщение при формировании запроса (предупреждения и ошибки формирования)
constraint C_P8PNL_QE_QUERY_RN_PK primary key (RN),
constraint C_P8PNL_QE_QUERY_CODE_NB check (rtrim(CODE) is not null),
constraint C_P8PNL_QE_QUERY_NAME_NB check (rtrim(NAME) is not null),

View File

@ -1789,6 +1789,9 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
)
is
NCOMPANY PKG_STD.TREF := GET_SESSION_COMPANY(); -- Организация сеанса
SEVENT PKG_STD.TSTRING; -- Полный номер события
NCONFIRM_REQUIRED PKG_STD.TNUMBER; -- Флаг необходимости подтверждения перехода
SCONFIRM_TEXT PKG_STD.TSTRING; -- Текст подтверждения перехода
NTMP PKG_STD.TREF; -- Заглушка
NSTEP_CURRENT PKG_STD.TREF; -- Номер текущего шага
NNOTE_RN PKG_STD.TREF; -- Рег. номер добавленного примечания
@ -1854,6 +1857,7 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
NIDENT => NIDENT,
NSTEP => NSTEP,
NEVENT => NEVENT,
SEVENT => SEVENT,
SEVENT_TYPE => SEVENT_TYPE,
SEVENT_STAT => SEVENT_STAT,
SINIT_PERSON => SINIT_PERSON,
@ -1863,6 +1867,8 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
NPOINT => NPOINT,
NPASS => NPASS,
NSELECT_EXEC => NSELECT_EXEC,
NCONFIRM_REQUIRED => NCONFIRM_REQUIRED,
SCONFIRM_TEXT => SCONFIRM_TEXT,
SEXECUTEMETHOD => SEXECUTEMETHOD,
SSEND_CLIENT => SSEND_CLIENT,
SSEND_DIVISION => SSEND_DIVISION,
@ -1881,6 +1887,7 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
NIDENT => NIDENT,
NSTEP => NSTEP,
NEVENT => NEVENT,
SEVENT => SEVENT,
SEVENT_TYPE => SEVENT_TYPE,
SEVENT_STAT => SEVENT_STAT,
SINIT_PERSON => SINIT_PERSON,
@ -1894,6 +1901,8 @@ create or replace package body PKG_P8PANELS_CLNTTSKBRD as
SEVENT_STAT => SEVENT_STAT,
SNEXT_STAT => SNEXT_STAT),
NSELECT_EXEC => NSELECT_EXEC,
NCONFIRM_REQUIRED => NCONFIRM_REQUIRED,
SCONFIRM_TEXT => SCONFIRM_TEXT,
SEXECUTEMETHOD => SEXECUTEMETHOD,
SSEND_CLIENT => SSEND_CLIENT,
SSEND_DIVISION => SSEND_DIVISION,

View File

@ -1,11 +1,25 @@
create or replace package PKG_P8PANELS_QE as
/* Получение списка доступных сущностей */
procedure ENTITY_LIST
(
SSEARCH in varchar2, -- Поисковый запрос
COUT out clob -- Сериализованный список доступных сущностей
);
/* Получение данных о запросе */
procedure QUERY
(
NRN in number, -- Рег. номер запроса
COUT out clob -- Сериализованное описание запроса
);
/* Получение списка запросов */
procedure QUERY_LIST
(
COUT out clob -- Сериализованный список запросов
);
/* Добавление запроса */
procedure QUERY_INSERT
(
@ -28,11 +42,18 @@ create or replace package PKG_P8PANELS_QE as
NRN in number -- Рег. номер запроса
);
/* Получение данных о запросе */
procedure QUERY_DESC
/* Установка признака "готовности" запроса */
procedure QUERY_READY_SET
(
NRN in number, -- Рег. номер запроса
COUT out clob -- Сериализованное описание запроса
NREADY in number -- Флаг готовности к использованию (0 - нет, 1 - да)
);
/* Установка признака "публичности" запроса */
procedure QUERY_PBL_SET
(
NRN in number, -- Рег. номер запроса
NPBL in number -- Флаг публичности (0 - приватный, 1 - публичный)
);
/* Добавление сущности в запрос */
@ -59,6 +80,22 @@ create or replace package PKG_P8PANELS_QE as
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
(
@ -74,24 +111,86 @@ create or replace package PKG_P8PANELS_QE as
SID in varchar2 -- Идентификатор связи
);
/* Установка признака "готовности" запроса */
procedure QUERY_READY_SET
/* Установка признака обязательности связи */
procedure QUERY_RL_MANDATORY_SET
(
NRN in number, -- Рег. номер запроса
NREADY in number -- Флаг готовности к использованию (0 - нет, 1 - да)
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор связи
NMANDATORY in number -- Флаг обязательности (1 - да, 0 - нет)
);
/* Установка признака "публичности" запроса */
procedure QUERY_PBL_SET
/* Добавление аргумента запроса */
procedure QUERY_OPT_ARG_ADD
(
NRN in number, -- Рег. номер запроса
SNAME in varchar2, -- Имя
STITLE in varchar2, -- Заголовок
NDATA_TYPE in number, -- Тип данных (см. константы PKG_STD.DATA_TYPE_*)
NMANDATORY in number, -- Флаг обязательности (1 - да, 0 - нет)
SVALUE in varchar2 -- Значение для отладки (строковое представление для всех типов данных)
);
/* Изменение аргумента запроса */
procedure QUERY_OPT_ARG_EDIT
(
NRN in number, -- Рег. номер запроса
SNAME in varchar2, -- Имя
STITLE in varchar2, -- Заголовок
NDATA_TYPE in number, -- Тип данных (см. константы PKG_STD.DATA_TYPE_*)
NMANDATORY in number, -- Флаг обязательности (1 - да, 0 - нет)
SVALUE in varchar2 -- Значение для отладки (строковое представление для всех типов данных)
);
/* Удаление аргумента запроса */
procedure QUERY_OPT_ARG_REMOVE
(
NRN in number, -- Рег. номер запроса
NPBL in number -- Флаг публичности (0 - приватный, 1 - публичный)
SNAME in varchar2 -- Имя
);
/* Установка условий отбора запроса */
procedure QUERY_OPT_COND_SET
(
NRN in number, -- Рег. номер запроса
SCOND in varchar2 -- Условия отбора
);
end PKG_P8PANELS_QE;
/
create or replace package body PKG_P8PANELS_QE as
/* Константы - Минимальная длина поисковой фразы для сущностей */
NENT_MIN_SEARCH_LEN constant PKG_STD.TNUMBER := 3; -- Количество символов
/* Получение списка доступных сущностей */
procedure ENTITY_LIST
(
SSEARCH in varchar2, -- Поисковый запрос
COUT out clob -- Сериализованный список доступных сущностей
)
is
begin
/* Если фильтр задан и он нас утраивает */
if ((SSEARCH is not null) and (LENGTH(SSEARCH) >= NENT_MIN_SEARCH_LEN)) then
/* Получим список доступных сущностей */
COUT := PKG_P8PANELS_QE_BASE.ENTITY_LIST(SSEARCH => SSEARCH, NBRIEF => 1, NINCLUDE_ATTRS => 0);
end if;
end ENTITY_LIST;
/* Получение описания запроса */
procedure QUERY
(
NRN in number, -- Рег. номер запроса
COUT out clob -- Сериализованное описание запроса
)
is
begin
/* Проверим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_VIEW(NRN => NRN, SUSER => UTILIZER());
/* Получим описание запроса */
COUT := PKG_P8PANELS_QE_BASE.QUERY(NRN => NRN);
end QUERY;
/* Получение списка запросов */
procedure QUERY_LIST
(
@ -100,7 +199,7 @@ create or replace package body PKG_P8PANELS_QE as
is
begin
/* Базово сформируем список */
COUT := PKG_P8PANELS_QE_BASE.QUERY_LIST_GET(SUSER => UTILIZER());
COUT := PKG_P8PANELS_QE_BASE.QUERY_LIST(SUSER => UTILIZER());
end QUERY_LIST;
/* Добавление запроса */
@ -144,172 +243,6 @@ create or replace package body PKG_P8PANELS_QE as
PKG_P8PANELS_QE_BASE.QUERY_DELETE(NRN => NRN);
end QUERY_DELETE;
/* Получение описания запроса */
procedure QUERY_DESC
(
NRN in number, -- Рег. номер запроса
COUT out clob -- Сериализованное описание запроса
)
is
begin
/* Проверим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_VIEW(NRN => NRN, SUSER => UTILIZER());
/* Получим описание запроса */
COUT := PKG_P8PANELS_QE_BASE.QUERY_DESC_GET(NRN => NRN);
end QUERY_DESC;
/* Добавление сущности в запрос */
procedure QUERY_ENT_ADD
(
NRN in number, -- Рег. номер запроса
SNAME in varchar2, -- Имя
STYPE in varchar2 -- Тип (см. константы SENT_TYPE_*)
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
RENTS PKG_P8PANELS_QE_BASE.TENTS; -- Коллекция существующих сущностей
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);
/* Формируем описание новой сущности и добавляем её в коллекцию */
PKG_P8PANELS_QE_BASE.TENTS_APPEND(RENTS => RENTS, SNAME => SNAME, STYPE => STYPE);
/* Сохраняем обновленный набор сущностей */
PKG_P8PANELS_QE_BASE.QUERY_ENTS_SET(NRN => RQ.RN, RENTS => RENTS);
end QUERY_ENT_ADD;
/* Удаление сущности из запроса */
procedure QUERY_ENT_REMOVE
(
NRN in number, -- Рег. номер запроса
SID in varchar2 -- Идентификатор сущности
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
RENTS PKG_P8PANELS_QE_BASE.TENTS; -- Коллекция существующих сущностей
RENT PKG_P8PANELS_QE_BASE.TENT; -- Удаляемая сущность
RRLS PKG_P8PANELS_QE_BASE.TRLS; -- Коллекция существующих связей
RRLS_TMP 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);
/* Находим удаляемую сущность */
begin
RENT := RENTS(PKG_P8PANELS_QE_BASE.TENTS_INDEX_BY_ID(RENTS => RENTS, SID => SID));
exception
when VALUE_ERROR then
P_EXCEPTION(0,
'Сущность с идентификатором "%s" в запросе "%s" не определена.',
COALESCE(SID, '<НЕ УКАЗАН>'),
TO_CHAR(NRN));
end;
/* Удаляем сущность из коллекции */
PKG_P8PANELS_QE_BASE.TENTS_REMOVE(RENTS => RENTS, SID => SID);
/* Обходим атрибуты сущности */
if ((RENT.RATTRS is not null) and (RENT.RATTRS.COUNT > 0)) then
for I in RENT.RATTRS.FIRST .. RENT.RATTRS.LAST
loop
/* Если атрибут есть в связях (как источник или как приёмник) */
for J in 0 .. 1
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 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_REMOVE;
/* Установка координат сущности в редакторе диаграммы запроса */
procedure QUERY_ENT_POSITION_SET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
NX in number, -- Координата по оси абсцисс
NY in number -- Координата по оси ординат
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
RENTS PKG_P8PANELS_QE_BASE.TENTS; -- Коллекция существующих сущностей
begin
/* Читаем запись запроса */
RQ := PKG_P8PANELS_QE_BASE.QUERY_GET(NRN => NRN);
/* Сохранять расположение будем только если это автор - так остальные могут перемещать на экране диаграмму, но не запоминать расположение сущностей */
if (RQ.AUTHOR = UTILIZER()) then
/* Читаем существующие сущности */
RENTS := PKG_P8PANELS_QE_BASE.QUERY_ENTS_GET(CENTS => RQ.ENTS);
/* Меняем координаты сущности в коллекции */
PKG_P8PANELS_QE_BASE.TENTS_POSITION_SET(RENTS => RENTS, SID => SID, NX => NX, NY => NY);
/* Сохраняем обновленный набор сущностей */
PKG_P8PANELS_QE_BASE.QUERY_ENTS_SET(NRN => RQ.RN, RENTS => RENTS);
end if;
end QUERY_ENT_POSITION_SET;
/* Добавление связи в запрос */
procedure QUERY_RL_ADD
(
NRN in number, -- Рег. номер запроса
SSOURCE in varchar2, -- Идентификатор атрибута-источника
STARGET in varchar2 -- Идентификатор атрибута-приёмника
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
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);
/* Читаем существующие связи */
RRLS := PKG_P8PANELS_QE_BASE.QUERY_RLS_GET(CRLS => RQ.RLS);
/* Формируем описание новой связи и добавляем её в коллекцию */
PKG_P8PANELS_QE_BASE.TRLS_APPEND(RRLS => RRLS, SSOURCE => SSOURCE, STARGET => STARGET);
/* Сохраняем обновленный набор связей */
PKG_P8PANELS_QE_BASE.QUERY_RLS_SET(NRN => RQ.RN, RRLS => RRLS);
end QUERY_RL_ADD;
/* Удаление связи из запроса */
procedure QUERY_RL_REMOVE
(
NRN in number, -- Рег. номер запроса
SID in varchar2 -- Идентификатор связи
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
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);
/* Читаем существующие связи */
RRLS := PKG_P8PANELS_QE_BASE.QUERY_RLS_GET(CRLS => RQ.RLS);
/* Удаляем связи из коллекции */
PKG_P8PANELS_QE_BASE.TRLS_REMOVE(RRLS => RRLS, SID => SID);
/* Сохраняем обновленный набор связей */
PKG_P8PANELS_QE_BASE.QUERY_RLS_SET(NRN => RQ.RN, RRLS => RRLS);
end QUERY_RL_REMOVE;
/* Установка признака "готовности" запроса */
procedure QUERY_READY_SET
(
@ -337,6 +270,401 @@ create or replace package body PKG_P8PANELS_QE as
/* Базовая установка признака публичности */
PKG_P8PANELS_QE_BASE.QUERY_PBL_SET(NRN => NRN, NPBL => NPBL);
end QUERY_PBL_SET;
/* Добавление сущности в запрос */
procedure QUERY_ENT_ADD
(
NRN in number, -- Рег. номер запроса
SNAME in varchar2, -- Имя
STYPE in varchar2 -- Тип (см. константы SENT_TYPE_*)
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
RENTS PKG_P8PANELS_QE_BASE.TENTS; -- Коллекция существующих сущностей
SQRY PKG_STD.TLSTRING; -- SQL-выражение запроса
SQRY_MSG PKG_STD.TLSTRING; -- Сообщение при формировании SQL-выражения
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);
/* Формируем описание новой сущности и добавляем её в коллекцию */
PKG_P8PANELS_QE_BASE.TENTS_ADD(RENTS => RENTS, SNAME => SNAME, STYPE => STYPE);
/* Сохраняем обновленный набор сущностей */
PKG_P8PANELS_QE_BASE.QUERY_ENTS_SET(NRN => RQ.RN, RENTS => RENTS);
/* Переформируем SQL-выражение */
PKG_P8PANELS_QE_BASE.QUERY_SQL_BUILD(NRN => RQ.RN, SQRY => SQRY, SQRY_MSG => SQRY_MSG);
PKG_P8PANELS_QE_BASE.QUERY_QRY_SET(NRN => RQ.RN, CQRY => SQRY, CQRY_MSG => SQRY_MSG);
end QUERY_ENT_ADD;
/* Удаление сущности из запроса */
procedure QUERY_ENT_REMOVE
(
NRN in number, -- Рег. номер запроса
SID in varchar2 -- Идентификатор сущности
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
RENTS PKG_P8PANELS_QE_BASE.TENTS; -- Коллекция существующих сущностей
RENT PKG_P8PANELS_QE_BASE.TENT; -- Удаляемая сущность
RRLS PKG_P8PANELS_QE_BASE.TRLS; -- Коллекция существующих связей
SQRY PKG_STD.TLSTRING; -- SQL-выражение запроса
SQRY_MSG PKG_STD.TLSTRING; -- Сообщение при формировании SQL-выражения
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);
/* Находим удаляемую сущность */
begin
RENT := RENTS(PKG_P8PANELS_QE_BASE.TENTS_INDEX_BY_ID(RENTS => RENTS, SID => SID));
exception
when VALUE_ERROR then
P_EXCEPTION(0,
'Сущность с идентификатором "%s" в запросе "%s" не определена.',
COALESCE(SID, '<НЕ УКАЗАН>'),
TO_CHAR(NRN));
end;
/* Удаляем сущность из коллекции */
PKG_P8PANELS_QE_BASE.TENTS_REMOVE(RENTS => RENTS, SID => SID);
/* Обходим атрибуты сущности */
if ((RENT.RATTRS is not null) and (RENT.RATTRS.COUNT > 0)) then
for I in RENT.RATTRS.FIRST .. RENT.RATTRS.LAST
loop
/* Удаляем связи в которых он задействован */
PKG_P8PANELS_QE_BASE.TRLS_CLEANUP_BY_ATTR(RRLS => RRLS, SATTR_ID => RENT.RATTRS(I).SID);
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);
/* Переформируем SQL-выражение */
PKG_P8PANELS_QE_BASE.QUERY_SQL_BUILD(NRN => RQ.RN, SQRY => SQRY, SQRY_MSG => SQRY_MSG);
PKG_P8PANELS_QE_BASE.QUERY_QRY_SET(NRN => RQ.RN, CQRY => SQRY, CQRY_MSG => SQRY_MSG);
end QUERY_ENT_REMOVE;
/* Установка координат сущности в редакторе диаграммы запроса */
procedure QUERY_ENT_POSITION_SET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор сущности
NX in number, -- Координата по оси абсцисс
NY in number -- Координата по оси ординат
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
RENTS PKG_P8PANELS_QE_BASE.TENTS; -- Коллекция существующих сущностей
begin
/* Читаем запись запроса */
RQ := PKG_P8PANELS_QE_BASE.QUERY_GET(NRN => NRN);
/* Сохранять расположение будем только если это автор - так остальные могут перемещать на экране диаграмму, но не запоминать расположение сущностей */
if (RQ.AUTHOR = UTILIZER()) then
/* Читаем существующие сущности */
RENTS := PKG_P8PANELS_QE_BASE.QUERY_ENTS_GET(CENTS => RQ.ENTS);
/* Меняем координаты сущности в коллекции */
PKG_P8PANELS_QE_BASE.TENTS_POSITION_SET(RENTS => RENTS, SID => SID, NX => NX, NY => NY);
/* Сохраняем обновленный набор сущностей */
PKG_P8PANELS_QE_BASE.QUERY_ENTS_SET(NRN => RQ.RN, RENTS => RENTS);
end if;
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; -- Коллекция существующих связей
SCHECK_MSG PKG_STD.TLSTRING; -- Сообщение при проверке атрибутов
SQRY PKG_STD.TLSTRING; -- SQL-выражение запроса
SQRY_MSG PKG_STD.TLSTRING; -- Сообщение при формировании SQL-выражения
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.TATTRS_CHECK(RATTRS => RENTS(NENT_INDEX).RATTRS, SCHECK_MSG => SCHECK_MSG);
if (SCHECK_MSG is not null) then
P_EXCEPTION(0, SCHECK_MSG);
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);
/* Переформируем SQL-выражение */
PKG_P8PANELS_QE_BASE.QUERY_SQL_BUILD(NRN => RQ.RN, SQRY => SQRY, SQRY_MSG => SQRY_MSG);
PKG_P8PANELS_QE_BASE.QUERY_QRY_SET(NRN => RQ.RN, CQRY => SQRY, CQRY_MSG => SQRY_MSG);
end QUERY_ENT_ATTRS_SET;
/* Добавление связи в запрос */
procedure QUERY_RL_ADD
(
NRN in number, -- Рег. номер запроса
SSOURCE in varchar2, -- Идентификатор атрибута-источника
STARGET in varchar2 -- Идентификатор атрибута-приёмника
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
RRLS PKG_P8PANELS_QE_BASE.TRLS; -- Коллекция существующих связей
SQRY PKG_STD.TLSTRING; -- SQL-выражение запроса
SQRY_MSG PKG_STD.TLSTRING; -- Сообщение при формировании SQL-выражения
begin
/* Провим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
/* Читаем запись запроса */
RQ := PKG_P8PANELS_QE_BASE.QUERY_GET(NRN => NRN);
/* Читаем существующие связи */
RRLS := PKG_P8PANELS_QE_BASE.QUERY_RLS_GET(CRLS => RQ.RLS);
/* Формируем описание новой связи и добавляем её в коллекцию */
PKG_P8PANELS_QE_BASE.TRLS_ADD(RRLS => RRLS, SSOURCE => SSOURCE, STARGET => STARGET, NMANDATORY => 0);
/* Сохраняем обновленный набор связей */
PKG_P8PANELS_QE_BASE.QUERY_RLS_SET(NRN => RQ.RN, RRLS => RRLS);
/* Переформируем SQL-выражение */
PKG_P8PANELS_QE_BASE.QUERY_SQL_BUILD(NRN => RQ.RN, SQRY => SQRY, SQRY_MSG => SQRY_MSG);
PKG_P8PANELS_QE_BASE.QUERY_QRY_SET(NRN => RQ.RN, CQRY => SQRY, CQRY_MSG => SQRY_MSG);
end QUERY_RL_ADD;
/* Удаление связи из запроса */
procedure QUERY_RL_REMOVE
(
NRN in number, -- Рег. номер запроса
SID in varchar2 -- Идентификатор связи
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
RRLS PKG_P8PANELS_QE_BASE.TRLS; -- Коллекция существующих связей
SQRY PKG_STD.TLSTRING; -- SQL-выражение запроса
SQRY_MSG PKG_STD.TLSTRING; -- Сообщение при формировании SQL-выражения
begin
/* Провим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
/* Читаем запись запроса */
RQ := PKG_P8PANELS_QE_BASE.QUERY_GET(NRN => NRN);
/* Читаем существующие связи */
RRLS := PKG_P8PANELS_QE_BASE.QUERY_RLS_GET(CRLS => RQ.RLS);
/* Удаляем связи из коллекции */
PKG_P8PANELS_QE_BASE.TRLS_REMOVE(RRLS => RRLS, SID => SID);
/* Сохраняем обновленный набор связей */
PKG_P8PANELS_QE_BASE.QUERY_RLS_SET(NRN => RQ.RN, RRLS => RRLS);
/* Переформируем SQL-выражение */
PKG_P8PANELS_QE_BASE.QUERY_SQL_BUILD(NRN => RQ.RN, SQRY => SQRY, SQRY_MSG => SQRY_MSG);
PKG_P8PANELS_QE_BASE.QUERY_QRY_SET(NRN => RQ.RN, CQRY => SQRY, CQRY_MSG => SQRY_MSG);
end QUERY_RL_REMOVE;
/* Установка признака обязательности связи */
procedure QUERY_RL_MANDATORY_SET
(
NRN in number, -- Рег. номер запроса
SID in varchar2, -- Идентификатор связи
NMANDATORY in number -- Флаг обязательности (1 - да, 0 - нет)
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
RRLS PKG_P8PANELS_QE_BASE.TRLS; -- Коллекция существующих связей
SQRY PKG_STD.TLSTRING; -- SQL-выражение запроса
SQRY_MSG PKG_STD.TLSTRING; -- Сообщение при формировании SQL-выражения
begin
/* Читаем запись запроса */
RQ := PKG_P8PANELS_QE_BASE.QUERY_GET(NRN => NRN);
/* Читаем существующие связи */
RRLS := PKG_P8PANELS_QE_BASE.QUERY_RLS_GET(CRLS => RQ.RLS);
/* Устанавливаем признакобязательности */
PKG_P8PANELS_QE_BASE.TRLS_MANDATORY_SET(RRLS => RRLS, SID => SID, NMANDATORY => NMANDATORY);
/* Сохраняем обновленный набор связей */
PKG_P8PANELS_QE_BASE.QUERY_RLS_SET(NRN => RQ.RN, RRLS => RRLS);
/* Переформируем SQL-выражение */
PKG_P8PANELS_QE_BASE.QUERY_SQL_BUILD(NRN => RQ.RN, SQRY => SQRY, SQRY_MSG => SQRY_MSG);
PKG_P8PANELS_QE_BASE.QUERY_QRY_SET(NRN => RQ.RN, CQRY => SQRY, CQRY_MSG => SQRY_MSG);
end QUERY_RL_MANDATORY_SET;
/* Добавление аргумента запроса */
procedure QUERY_OPT_ARG_ADD
(
NRN in number, -- Рег. номер запроса
SNAME in varchar2, -- Имя
STITLE in varchar2, -- Заголовок
NDATA_TYPE in number, -- Тип данных (см. константы PKG_STD.DATA_TYPE_*)
NMANDATORY in number, -- Флаг обязательности (1 - да, 0 - нет)
SVALUE in varchar2 -- Значение для отладки (строковое представление для всех типов данных)
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
ROPT PKG_P8PANELS_QE_BASE.TOPT; -- Настройка запроса
begin
/* Провим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
/* Читаем запись запроса */
RQ := PKG_P8PANELS_QE_BASE.QUERY_GET(NRN => NRN);
/* Читаем текущую настройку */
ROPT := PKG_P8PANELS_QE_BASE.QUERY_OPT_GET(COPT => RQ.OPT);
/* Добавляем аргумент в коллекцию */
PKG_P8PANELS_QE_BASE.TARGS_ADD(RARGS => ROPT.RARGS,
SNAME => SNAME,
STITLE => STITLE,
NDATA_TYPE => NDATA_TYPE,
NMANDATORY => NMANDATORY,
SVALUE => SVALUE);
/* Сохраняем обновленную настройку */
PKG_P8PANELS_QE_BASE.QUERY_OPT_SET(NRN => RQ.RN, ROPT => ROPT);
end QUERY_OPT_ARG_ADD;
/* Изменение аргумента запроса */
procedure QUERY_OPT_ARG_EDIT
(
NRN in number, -- Рег. номер запроса
SNAME in varchar2, -- Имя
STITLE in varchar2, -- Заголовок
NDATA_TYPE in number, -- Тип данных (см. константы PKG_STD.DATA_TYPE_*)
NMANDATORY in number, -- Флаг обязательности (1 - да, 0 - нет)
SVALUE in varchar2 -- Значение для отладки (строковое представление для всех типов данных)
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
ROPT PKG_P8PANELS_QE_BASE.TOPT; -- Настройка запроса
NINDEX PKG_STD.TNUMBER; -- Индекс изменяемого аргумента в коллекции
begin
/* Провим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
/* Читаем запись запроса */
RQ := PKG_P8PANELS_QE_BASE.QUERY_GET(NRN => NRN);
/* Читаем текущую настройку */
ROPT := PKG_P8PANELS_QE_BASE.QUERY_OPT_GET(COPT => RQ.OPT);
/* Находим изменяемый аргумент */
NINDEX := PKG_P8PANELS_QE_BASE.TARGS_INDEX_BY_NAME(RARGS => ROPT.RARGS, SNAME => SNAME);
if (NINDEX is null) then
P_EXCEPTION(0,
'Аргумент с именем "%s" в запросе "%s" не определен.',
COALESCE(SNAME, '<НЕ УКАЗАН>'),
TO_CHAR(NRN));
end if;
/* Изменяем аргумент в коллекции */
PKG_P8PANELS_QE_BASE.TARGS_EDIT(RARGS => ROPT.RARGS,
NINDEX => NINDEX,
SNAME => SNAME,
STITLE => STITLE,
NDATA_TYPE => NDATA_TYPE,
NMANDATORY => NMANDATORY,
SVALUE => SVALUE);
/* Сохраняем обновленную настройку */
PKG_P8PANELS_QE_BASE.QUERY_OPT_SET(NRN => RQ.RN, ROPT => ROPT);
end QUERY_OPT_ARG_EDIT;
/* Удаление аргумента запроса */
procedure QUERY_OPT_ARG_REMOVE
(
NRN in number, -- Рег. номер запроса
SNAME in varchar2 -- Имя
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
ROPT PKG_P8PANELS_QE_BASE.TOPT; -- Настройка запроса
begin
/* Провим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
/* Читаем запись запроса */
RQ := PKG_P8PANELS_QE_BASE.QUERY_GET(NRN => NRN);
/* Читаем текущую настройку */
ROPT := PKG_P8PANELS_QE_BASE.QUERY_OPT_GET(COPT => RQ.OPT);
/* Находим удаляемый аргумент */
if (PKG_P8PANELS_QE_BASE.TARGS_INDEX_BY_NAME(RARGS => ROPT.RARGS, SNAME => SNAME) is null) then
P_EXCEPTION(0,
'Аргумент с именем "%s" в запросе "%s" не определен.',
COALESCE(SNAME, '<НЕ УКАЗАН>'),
TO_CHAR(NRN));
end if;
/* Удаляем аргумент из коллекции */
PKG_P8PANELS_QE_BASE.TARGS_REMOVE(RARGS => ROPT.RARGS, SNAME => SNAME);
/* Сохраняем обновленную настройку */
PKG_P8PANELS_QE_BASE.QUERY_OPT_SET(NRN => RQ.RN, ROPT => ROPT);
end QUERY_OPT_ARG_REMOVE;
/* Установка условий отбора запроса */
procedure QUERY_OPT_COND_SET
(
NRN in number, -- Рег. номер запроса
SCOND in varchar2 -- Условия отбора
)
is
RQ P8PNL_QE_QUERY%rowtype; -- Запись запроса
ROPT PKG_P8PANELS_QE_BASE.TOPT; -- Настройка запроса
SQRY PKG_STD.TLSTRING; -- SQL-выражение запроса
SQRY_MSG PKG_STD.TLSTRING; -- Сообщение при формировании SQL-выражения
begin
/* Провим права доступа */
PKG_P8PANELS_QE_BASE.QUERY_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER());
/* Читаем запись запроса */
RQ := PKG_P8PANELS_QE_BASE.QUERY_GET(NRN => NRN);
/* Читаем текущую настройку */
ROPT := PKG_P8PANELS_QE_BASE.QUERY_OPT_GET(COPT => RQ.OPT);
/* Установим новые условия отбора */
ROPT.SCOND := SCOND;
/* Сохраняем обновленную настройку */
PKG_P8PANELS_QE_BASE.QUERY_OPT_SET(NRN => RQ.RN, ROPT => ROPT);
/* Переформируем SQL-выражение */
PKG_P8PANELS_QE_BASE.QUERY_SQL_BUILD(NRN => RQ.RN, SQRY => SQRY, SQRY_MSG => SQRY_MSG);
PKG_P8PANELS_QE_BASE.QUERY_QRY_SET(NRN => RQ.RN, CQRY => SQRY, CQRY_MSG => SQRY_MSG);
end QUERY_OPT_COND_SET;
end PKG_P8PANELS_QE;
/

File diff suppressed because it is too large Load Diff