ЦИТК-979 - Настройка запроса - условия отбора (выбор компонентов условий, очистка условия)

This commit is contained in:
Mikhail Chechnev 2025-09-03 15:17:26 +03:00
parent 7515a9cce3
commit f96425c80d
4 changed files with 238 additions and 12 deletions

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

@ -17,6 +17,7 @@ import { ButtonGroup, Button } from "@mui/material"; //Интерфейсные
//Кнопки операций условий отбора
const CondOperationButtons = ({ buttons = [], onClick }) => {
//Формирование представления
return (
<ButtonGroup size={"small"} variant={"outlined"}>
{buttons.map((button, i) => (

View File

@ -14,6 +14,8 @@ 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"; //Описание аргумента запроса
//---------
//Константы
@ -39,7 +41,7 @@ const STYLES = {
//-----------
//Компонент инспектора - Условия отбора запроса
const InspectorQueryConditions = ({ query, cond, onOptionsChanged }) => {
const InspectorQueryConditions = ({ query, cond, entities = [], args = [], onOptionsChanged }) => {
//Собственное состояние - отображение диалога настройки условий отбора
const [openQueryCondDialog, setOpenQueryCondDialog] = useState(false);
@ -68,7 +70,9 @@ const InspectorQueryConditions = ({ query, cond, onOptionsChanged }) => {
//Формирование представления
return (
<>
{openQueryCondDialog && <QueryCondDialog cond={cond} onOk={handleQueryCondDialogOk} onCancel={handleQueryCondDialogCancel} />}
{openQueryCondDialog && (
<QueryCondDialog cond={cond} entities={entities} args={args} onOk={handleQueryCondDialogOk} onCancel={handleQueryCondDialogCancel} />
)}
{configured && (
<Card variant={"outlined"}>
<CardActionArea onClick={handleSetup}>
@ -93,6 +97,8 @@ const InspectorQueryConditions = ({ query, cond, onOptionsChanged }) => {
InspectorQueryConditions.propTypes = {
query: PropTypes.number.isRequired,
cond: PropTypes.string,
entities: PropTypes.arrayOf(ENTITY_SHAPE),
args: PropTypes.arrayOf(ARGUMENT_SHAPE),
onOptionsChanged: PropTypes.func
};

View File

@ -7,30 +7,46 @@
//Подключение библиотек
//---------------------
import React, { useState, useRef } from "react"; //Классы React
import React, { useState, useRef, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, TextField } from "@mui/material"; //Интерфейсные компоненты MUI
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, onOk, onCancel }) => {
const QueryCondDialog = ({ cond, entities, args, onOk, onCancel }) => {
//Собственное состояние - условия отбора
const [conditions, setConditions] = useState(cond || "");
//Ссылка на элемент ввода условия
const condInputRef = useRef(null);
const coditionInputRef = useRef(null);
//Перемещение курсора в конец поля ввода условия
const moveCondCursorToEnd = () => {
if (condInputRef.current) {
const length = condInputRef.current.value.length;
condInputRef.current.setSelectionRange(length, length);
condInputRef.current.focus();
if (coditionInputRef.current) {
const length = coditionInputRef.current.value.length;
coditionInputRef.current.setSelectionRange(length, length);
coditionInputRef.current.focus();
}
};
@ -43,16 +59,79 @@ const QueryCondDialog = ({ cond, onOk, 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={{ width: "550px" }} direction={"row"} spacing={1} pb={1}>
<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: " = " },
@ -77,7 +156,7 @@ const QueryCondDialog = ({ cond, onOk, onCancel }) => {
/>
</Stack>
<TextField
inputRef={condInputRef}
inputRef={coditionInputRef}
focused={true}
autoFocus
value={conditions}
@ -95,6 +174,8 @@ const QueryCondDialog = ({ cond, onOk, onCancel }) => {
QueryCondDialog.propTypes = {
cond: PropTypes.string,
onOk: PropTypes.func,
entities: PropTypes.arrayOf(ENTITY_SHAPE),
args: PropTypes.arrayOf(ARGUMENT_SHAPE),
onCancel: PropTypes.func
};