ЦИТК-979 - Установка атрибутов сущности (клиент)

This commit is contained in:
Mikhail Chechnev 2025-08-14 14:21:23 +03:00
parent fa00940776
commit a20de79a40
5 changed files with 317 additions and 32 deletions

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ import { RELATION_DATA_SHAPE } from "../relation/relation"; //Описание
//-----------
//Свойства запроса
const QueryOptions = ({ onEntityAdd, onEntityRemove, onRelationRemove, onQueryOptionsChanged, entity, relation }) => {
const QueryOptions = ({ query, entity, relation, onEntityAdd, onEntityRemove, onRelationRemove, onQueryOptionsChanged }) => {
//Отображение диалога добавления сущности
const [openEntityAddDialog, setOpenEntityAddDialog] = useState(false);
@ -70,8 +70,12 @@ const QueryOptions = ({ onEntityAdd, onEntityRemove, onRelationRemove, onQueryOp
return (
<>
{openEntityAddDialog && <EntityAddDialog onOk={handleEntityAddDialogOk} onCancel={handleEntityAddDialogCancel} />}
{openEntityAttrsDialog && <EntityAttrsDialog {...entity} onOk={handleEntityAttrsDialogOk} onCancel={handleEntityAttrsDialogCancel} />}
<P8PEditorBox title={"Параметры запроса"}>
{openEntityAttrsDialog && (
<EntityAttrsDialog query={query} {...entity} onOk={handleEntityAttrsDialogOk} onCancel={handleEntityAttrsDialogCancel} />
)}
<P8PEditorBox title={"Настройки запроса"}>
<P8PEditorSubHeader title={"Параметры"} />
<P8PEditorSubHeader title={"Условия отбора"} />
<P8PEditorSubHeader title={"Сущности"} />
<Button startIcon={<Icon>add</Icon>} onClick={handleEntityAddClick}>
{BUTTONS.INSERT}
@ -102,12 +106,13 @@ const QueryOptions = ({ onEntityAdd, onEntityRemove, onRelationRemove, onQueryOp
//Контроль свойств компонента - Свойства запроса
QueryOptions.propTypes = {
query: PropTypes.number.isRequired,
entity: ENTITY_DATA_SHAPE,
relation: RELATION_DATA_SHAPE,
onEntityAdd: PropTypes.func,
onEntityRemove: PropTypes.func,
onRelationRemove: PropTypes.func,
onQueryOptionsChanged: PropTypes.func,
entity: ENTITY_DATA_SHAPE,
relation: RELATION_DATA_SHAPE
onQueryOptionsChanged: PropTypes.func
};
//----------------

View File

@ -55,6 +55,26 @@ const QueryEditor = () => {
//Подключение к контексту приложения
const { setAppBarTitle } = useContext(ApplicationСtx);
//Выбор сущности
const selectEntity = ent => {
setRelation(null);
const queryEnt = queryDiagram.entities.find(e => e.id === ent);
if (queryEnt) setEntity({ ...queryEnt.data });
};
//Выбор связи
const selectRelation = rl => {
setEntity(null);
const queryRl = queryDiagram.relations.find(r => r.id === rl);
if (queryRl) setRelation({ ...queryRl });
};
//Сброс выбора связи и сущности
const cleanupEnRlSelection = () => {
setRelation(null);
setEntity(null);
};
//Обработка изменения положения сущности на диаграмме
const handleEntityPositionChange = (ent, position) => setEntPosition(ent, position.x, position.y);
@ -67,36 +87,33 @@ const QueryEditor = () => {
//Обработка удаления сущности из запроса
const handleEntityRemove = async ent => {
await removeEnt(ent);
if (entity && entity?.id === ent) setEntity(null);
if (entity && entity?.id === ent) cleanupEnRlSelection();
};
//Обработка выделения сущности
const handleEntityClick = ent => {
setRelation(null);
const queryEnt = queryDiagram.entities.find(e => e.id === ent);
if (queryEnt)
if (entity?.id == queryEnt.id) setEntity(null);
else setEntity({ ...queryEnt.data });
};
const handleEntityClick = ent => selectEntity(ent);
//Обработка выделения тарибута сущности
const handleEntityAttrClick = ent => selectEntity(ent);
//Обработка выделения связи
const handleRelationClick = rl => {
setEntity(null);
const queryRl = queryDiagram.relations.find(r => r.id === rl);
if (queryRl)
if (relation?.id == queryRl.id) setRelation(null);
else setRelation({ ...queryRl });
};
const handleRelationClick = rl => selectRelation(rl);
//Обработка добавления отношения cущностей
const handleRelationAdd = (source, target) => addRl(source, target);
const handleRelationAdd = (source, target) => {
cleanupEnRlSelection();
addRl(source, target);
};
//Обработка удаления отношения cущностей
const handleRelationRemove = async rl => {
await removeRl(rl);
if (relation && relation?.id === rl) setRelation(null);
if (relation && relation?.id === rl) cleanupEnRlSelection();
};
//При нажатии на панели (пустом месте) диаграммы запроса
const handlePaneClick = () => cleanupEnRlSelection();
//Открытие менеджера запросов
const handleOpenQueriesManager = () => setOpenQueriesManager(true);
@ -106,8 +123,7 @@ const QueryEditor = () => {
//Закрытие запроса
const handleQueryClose = () => {
setAppBarTitle(APP_BAR_TITLE_DEFAULT);
setEntity(null);
setRelation(null);
cleanupEnRlSelection();
setQuery(null);
};
@ -119,7 +135,10 @@ const QueryEditor = () => {
};
//При изменении свойств запроса
const handleQueryOptionsChanged = () => doRefresh();
const handleQueryOptionsChanged = () => {
cleanupEnRlSelection();
doRefresh();
};
//Панель инструмментов
const toolBar = (
@ -141,11 +160,13 @@ const QueryEditor = () => {
<QueryDiagram
{...queryDiagram}
onEntityClick={handleEntityClick}
onEntityAttrClick={handleEntityAttrClick}
onEntityPositionChange={handleEntityPositionChange}
onEntityRemove={handleEntityRemove}
onRelactionClick={handleRelationClick}
onRelationAdd={handleRelationAdd}
onRelationRemove={handleRelationRemove}
onPaneClick={handlePaneClick}
/>
)}
</Grid>
@ -153,12 +174,13 @@ const QueryEditor = () => {
{toolBar}
{query && (
<QueryOptions
query={query}
entity={entity}
relation={relation}
onEntityAdd={handleEntityAdd}
onEntityRemove={handleEntityRemove}
onRelationRemove={handleRelationRemove}
onQueryOptionsChanged={handleQueryOptionsChanged}
entity={entity}
relation={relation}
/>
)}
</Grid>