2025-05-20 11:56:29 +03:00

435 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Парус 8 - Панели мониторинга - Редактор панелей
Общие компоненты редакторов свойств
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
Box,
Stack,
IconButton,
Icon,
Typography,
Divider,
Chip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
InputAdornment,
MenuItem,
Menu,
Card,
CardContent,
CardActions,
CardActionArea
} from "@mui/material"; //Интерфейсные элементы
import client from "../../../core/client"; //Клиент БД
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
import { BUTTONS } from "../../../../app.text"; //Общие текстовые ресурсы
import { useUserProcDesc } from "./components_hooks"; //Общие хуки компонентов
import "../panels_editor.css"; //Стили редактора
//---------
//Константы
//---------
//Стили
const STYLES = {
CHIP: (fullWidth = false, multiLine = false) => ({
...(multiLine ? { height: "auto" } : {}),
"& .MuiChip-label": {
...(multiLine
? {
display: "block",
whiteSpace: "normal"
}
: {}),
...(fullWidth ? { width: "100%" } : {})
}
})
};
//Типы даных аргументов
const ARGUMENT_DATA_TYPE = {
STR: client.SERV_DATA_TYPE_STR,
NUMB: client.SERV_DATA_TYPE_NUMB,
DATE: client.SERV_DATA_TYPE_DATE
};
//Типы источников данных
const DATA_SOURCE_TYPE = {
USER_PROC: "USER_PROC",
QUERY: "QUERY"
};
//Типы источников данных (наименования)
const DATA_SOURCE_TYPE_NAME = {
[DATA_SOURCE_TYPE.USER_PROC]: "Пользовательская процедура",
[DATA_SOURCE_TYPE.QUERY]: "Запрос"
};
//Структура аргумента источника данных
const DATA_SOURCE_ARGUMENT_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
caption: PropTypes.string.isRequired,
dataType: PropTypes.oneOf(Object.values(ARGUMENT_DATA_TYPE)),
req: PropTypes.bool.isRequired,
value: PropTypes.any,
valueSource: PropTypes.string
});
//Начальное состояние аргумента источника данных
const DATA_SOURCE_ARGUMENT_INITIAL = {
name: "",
caption: "",
dataType: "",
req: false,
value: "",
valueSource: ""
};
//Структура источника данных
const DATA_SOURCE_SHAPE = PropTypes.shape({
type: PropTypes.oneOf([...Object.values(DATA_SOURCE_TYPE), ""]),
userProc: PropTypes.string,
stored: PropTypes.string,
respArg: PropTypes.string,
arguments: PropTypes.arrayOf(DATA_SOURCE_ARGUMENT_SHAPE)
});
//Начальное состояние истоника данных
const DATA_SOURCE_INITIAL = {
type: "",
userProc: "",
stored: "",
respArg: "",
arguments: []
};
//-----------
//Тело модуля
//-----------
//Контейнер редактора
const EditorBox = ({ title, children, onSave }) => {
//При нажатии на "Сохранить"
const handleSaveClick = (closeEditor = false) => onSave && onSave(closeEditor);
//Формирование представления
return (
<Box className={"component-editor__container"}>
<Divider>{title}</Divider>
<Stack direction={"column"} spacing={1}>
{children}
</Stack>
<Stack direction={"row"} justifyContent={"right"} p={1}>
<IconButton onClick={() => handleSaveClick(false)} title={BUTTONS.APPLY}>
<Icon>done</Icon>
</IconButton>
<IconButton onClick={() => handleSaveClick(true)} title={BUTTONS.SAVE}>
<Icon>done_all</Icon>
</IconButton>
</Stack>
</Box>
);
};
//Контроль свойств компонента - контейнер редактора
EditorBox.propTypes = {
title: PropTypes.string.isRequired,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
onSave: PropTypes.func
};
//Заголовок раздела редактора
const EditorSubHeader = ({ title }) => {
//Формирование представления
return (
<Divider className={"component-editor__divider"}>
<Chip label={title} size={"small"} />
</Divider>
);
};
//Контроль свойств компонента - заголовок раздела редактора
EditorSubHeader.propTypes = {
title: PropTypes.string.isRequired
};
//Диалог настройки
const ConfigDialog = ({ title, children, onOk, onCancel }) => {
//Формирование представления
return (
<Dialog onClose={onCancel} open>
<DialogTitle>{title}</DialogTitle>
<DialogContent>{children}</DialogContent>
<DialogActions>
<Button onClick={() => onOk && onOk()}>{BUTTONS.OK}</Button>
<Button onClick={() => onCancel && onCancel()}>{BUTTONS.CANCEL}</Button>
</DialogActions>
</Dialog>
);
};
//Контроль свойств компонента - диалог настройки
ConfigDialog.propTypes = {
title: PropTypes.string.isRequired,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//Диалог настройки источника данных
const ConfigDataSourceDialog = ({ dataSource = null, valueProviders = {}, onOk = null, onCancel = null } = {}) => {
//Собственное состояние - параметры элемента формы
const [state, setState] = useState({ ...DATA_SOURCE_INITIAL, ...dataSource });
//Собственное состояние - флаги обновление данных
const [refresh, setRefresh] = useState({ userProcDesc: 0 });
//Собственное состояние - элемент привязки меню выбора источника
const [valueProvidersMenuAnchorEl, setValueProvidersMenuAnchorEl] = useState(null);
//Описание выбранной пользовательской процедуры
const [userProcDesc] = useUserProcDesc({ code: state.userProc, refresh: refresh.userProcDesc });
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//Установка значения/привязки аргумента
const setArgumentValueSource = (index, value, valueSource) =>
setState(pv => ({
...pv,
arguments: pv.arguments.map((argument, i) => ({ ...argument, ...(i == index ? { value, valueSource } : {}) }))
}));
//Открытие/сокрытие меню выбора источника
const toggleValueProvidersMenu = target => setValueProvidersMenuAnchorEl(target instanceof Element ? target : null);
//При нажатии на очистку наименования пользовательской процедуры
const handleUserProcClearClick = () => setState({ ...DATA_SOURCE_INITIAL });
//При нажатии на выбор пользовательской процедуры в качестве источника данных
const handleUserProcSelectClick = () => {
pOnlineShowDictionary({
unitCode: "UserProcedures",
showMethod: "main",
inputParameters: [{ name: "in_CODE", value: state.userProc }],
callBack: res => {
if (res.success) {
setState(pv => ({ ...pv, type: DATA_SOURCE_TYPE.USER_PROC, userProc: res.outParameters.out_CODE }));
setRefresh(pv => ({ ...pv, userProcDesc: pv.userProcDesc + 1 }));
}
}
});
};
//При закрытии дилога с сохранением
const handleOk = () => onOk && onOk({ ...state });
//При закртии диалога отменой
const handleCancel = () => onCancel && onCancel();
//При очистке значения/связывания аргумента
const handleArgumentClearClick = index => setArgumentValueSource(index, "", "");
//При отображении меню связывания аргумента с поставщиком данных
const handleArgumentLinkMenuClick = e => setValueProvidersMenuAnchorEl(e.currentTarget);
//При выборе элемента меню связывания аргумента с поставщиком данных
const handleArgumentLinkClick = valueSource => {
setArgumentValueSource(valueProvidersMenuAnchorEl.id, "", valueSource);
toggleValueProvidersMenu();
};
//При вводе значения аргумента
const handleArgumentChange = (index, value) => setArgumentValueSource(index, value, "");
//При изменении описания пользовательской процедуры
useEffect(() => {
if (userProcDesc)
setState(pv => ({
...pv,
stored: userProcDesc?.stored?.name,
respArg: userProcDesc?.stored?.respArg,
arguments: (userProcDesc?.arguments || []).map(argument => ({ ...DATA_SOURCE_ARGUMENT_INITIAL, ...argument }))
}));
}, [userProcDesc]);
//Список значений
const values = Object.keys(valueProviders).reduce((res, key) => [...res, ...Object.keys(valueProviders[key])], []);
//Наличие значений
const isValues = values && values.length > 0 ? true : false;
//Меню привязки к поставщикам значений
const valueProvidersMenu = isValues && (
<Menu anchorEl={valueProvidersMenuAnchorEl} open={Boolean(valueProvidersMenuAnchorEl)} onClose={toggleValueProvidersMenu}>
{values.map((value, i) => (
<MenuItem key={i} onClick={() => handleArgumentLinkClick(value)}>
{value}
</MenuItem>
))}
</Menu>
);
//Формирование представления
return (
<ConfigDialog title="Настройка источника данных" onOk={handleOk} onCancel={handleCancel}>
<Stack direction={"column"} spacing={1}>
{valueProvidersMenu}
<TextField
type={"text"}
variant={"standard"}
value={state.userProc}
label={"Пользовательская процедура"}
InputLabelProps={{ shrink: true }}
InputProps={{
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleUserProcClearClick}>
<Icon>clear</Icon>
</IconButton>
<IconButton onClick={handleUserProcSelectClick}>
<Icon>list</Icon>
</IconButton>
</InputAdornment>
)
}}
/>
{Array.isArray(state?.arguments) &&
state.arguments.map((argument, i) => (
<TextField
key={i}
type={"text"}
variant={"standard"}
value={argument.value || argument.valueSource}
label={argument.caption}
onChange={e => handleArgumentChange(i, e.target.value)}
InputLabelProps={{ shrink: true }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => handleArgumentClearClick(i)}>
<Icon>clear</Icon>
</IconButton>
{isValues && (
<IconButton id={i} onClick={handleArgumentLinkMenuClick}>
<Icon>settings_ethernet</Icon>
</IconButton>
)}
</InputAdornment>
)
}}
/>
))}
</Stack>
</ConfigDialog>
);
};
//Контроль свойств компонента - Диалог настройки источника данных
ConfigDataSourceDialog.propTypes = {
dataSource: DATA_SOURCE_SHAPE,
valueProviders: PropTypes.object,
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//Источник данных
const DataSource = ({ dataSource = null, valueProviders = {}, onChange = null } = {}) => {
//Собственное состояние - отображение диалога настройки
const [configDlg, setConfigDlg] = useState(false);
//Уведомление родителя о смене настроек источника данных
const notifyChange = settings => onChange && onChange(settings);
//При нажатии на настройку источника данных
const handleSetup = () => setConfigDlg(true);
//При нажатии на настройку источника данных
const handleSetupOk = dataSource => {
setConfigDlg(false);
notifyChange(dataSource);
};
//При нажатии на настройку источника данных
const handleSetupCancel = () => setConfigDlg(false);
//При удалении настроек источника данных
const handleDelete = () => notifyChange({ ...DATA_SOURCE_INITIAL });
//Расчет флага "настроенности"
const configured = dataSource?.type ? true : false;
//Список аргументов
const args =
configured &&
dataSource.arguments.map((argument, i) => (
<Chip
key={i}
label={`:${argument.name} = ${argument.valueSource || argument.value || "NULL"}`}
variant={"outlined"}
sx={STYLES.CHIP(true)}
/>
));
//Формирование представления
return (
<>
{configDlg && (
<ConfigDataSourceDialog dataSource={dataSource} valueProviders={valueProviders} onOk={handleSetupOk} onCancel={handleSetupCancel} />
)}
{configured && (
<Card variant={"outlined"}>
<CardActionArea onClick={handleSetup}>
<CardContent>
<Typography variant={"subtitle1"} noWrap={true}>
{dataSource.type === DATA_SOURCE_TYPE.USER_PROC ? dataSource.userProc : "Источник без наименования"}
</Typography>
<Typography variant={"caption"} color={"text.secondary"} noWrap={true}>
{DATA_SOURCE_TYPE_NAME[dataSource.type] || "Неизвестный тип источника"}
</Typography>
<Stack direction={"column"} spacing={1} pt={2}>
{args}
</Stack>
</CardContent>
</CardActionArea>
<CardActions>
<IconButton onClick={handleDelete}>
<Icon>delete</Icon>
</IconButton>
</CardActions>
</Card>
)}
{!configured && (
<Button startIcon={<Icon>build</Icon>} onClick={handleSetup}>
Настроить
</Button>
)}
</>
);
};
//Контроль свойств компонента - Источник данных
DataSource.propTypes = {
dataSource: DATA_SOURCE_SHAPE,
valueProviders: PropTypes.object,
onChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { STYLES, ARGUMENT_DATA_TYPE, DATA_SOURCE_TYPE, DATA_SOURCE_SHAPE, DATA_SOURCE_INITIAL, EditorBox, EditorSubHeader, ConfigDialog, DataSource };