diff --git a/app/components/editors/p8p_actions.js b/app/components/editors/p8p_actions.js new file mode 100644 index 0000000..20d7257 --- /dev/null +++ b/app/components/editors/p8p_actions.js @@ -0,0 +1,121 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Компонент: Действия +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React, { useState } from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { Icon, Button } from "@mui/material"; //Интерфейсные элементы +import { P8PCAEditor } from "./p8p_component_action/editor"; //Редактор действия +import { P8PChipList } from "./p8p_chip_list"; //Дополнительные настройки редактора +import { P8P_CA_SHAPE, P8P_CAS_INITIAL, P8P_CA_TYPE } from "./p8p_component_action/common"; //Общие ресурсы действий + +//--------- +//Константы +//--------- + +//------------------------------------ +//Вспомогательные функции и компоненты +//------------------------------------ + +const getActionChipText = (type, params) => { + //Определяем от типа + switch (type) { + //Открыть раздел + case P8P_CA_TYPE.openUnit.code: + return params.unitName; + //Открыть панель + case P8P_CA_TYPE.openPanel.code: + return params.panelValue.value; + //Установить переменную + case P8P_CA_TYPE.setVariable.code: + return params.reduce((prev, cur) => [...prev, cur.variableSource], []).join(", "); + //Для всех остальных + default: + return "Не определен"; + } +}; + +//----------- +//Тело модуля +//----------- + +//Действия +const P8PActions = ({ actions = P8P_CAS_INITIAL, valueProviders = {}, areas = [], valueTypes = [], onChange = null } = {}) => { + //Собственное состояние - редактор действий + const [actionEditor, setActionEditor] = useState({ display: false, index: null }); + + //При изменении действий + const handleActionsChange = actions => onChange && onChange(actions); + + //При добавлении действия + const handleActionAdd = () => setActionEditor({ display: true, index: null }); + + //При нажатии на действие + const handleActionClick = i => setActionEditor({ display: true, index: i }); + + //При удалении действия + const handleActionDelete = i => { + const newActions = [...actions]; + newActions.splice(i, 1); + handleActionsChange(newActions); + }; + + //При отмене сохранения изменений действия + const handleActionCancel = () => setActionEditor({ display: false, index: null }); + + //При сохранении изменений действия + const handleActionSave = action => { + const newActions = [...actions]; + actionEditor.index == null ? newActions.push({ ...action }) : (newActions[actionEditor.index] = { ...action }); + handleActionsChange(newActions); + setActionEditor({ display: false, index: null }); + }; + + //Определяем структуру действий для отображения + const actionChips = actions.map(item => { + //Собираем текст действия + let text = getActionChipText(item.type, item.params); + //Формируем структуру для отображения карточки действия + return { text: text, title: text, icon: P8P_CA_TYPE[item.type]?.icon, iconTitle: P8P_CA_TYPE[item.type]?.name }; + }); + + //Формирование представления + return ( + <> + {actionEditor.display && ( + + )} + + + + ); +}; + +//Контроль свойств компонента - действия +P8PActions.propTypes = { + actions: PropTypes.arrayOf(P8P_CA_SHAPE), + valueProviders: PropTypes.object, + areas: PropTypes.array, + valueTypes: PropTypes.array, + onChange: PropTypes.func +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8PActions }; diff --git a/app/components/editors/p8p_chip_list.js b/app/components/editors/p8p_chip_list.js new file mode 100644 index 0000000..f652b1f --- /dev/null +++ b/app/components/editors/p8p_chip_list.js @@ -0,0 +1,88 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Компонент: Список элементов +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { Chip, Typography, Stack, Icon } from "@mui/material"; //Интерфейсные элементы +import { STYLES as COMMON_STYLES } from "./p8p_editors_common"; //Общие ресурсы редакторов + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + CHIP_ITEM: { ...COMMON_STYLES.CHIP(true, false) }, + STACK_ITEM: { alignItems: "center" }, + TYPOGRAPHY_INFO: { overflow: "hidden" } +}; + +//----------- +//Тело модуля +//----------- + +//Список элементов +const P8PChipList = ({ items, labelRender, onClick, onDelete }) => { + //При нажатии на элемент + const handleClick = index => onClick && onClick(index); + + //При удалении элемента + const handleDelete = index => onDelete && onDelete(index); + + //Формирование представления + return ( + <> + {Array.isArray(items) && + items.length > 0 && + items.map((item, i) => ( + + {item.icon ? ( + <> + + {item.icon} + + - + + ) : null} + + {item.text} + + + ) + } + variant={"outlined"} + onClick={onClick && !item.disableClick ? () => handleClick(i) : null} + onDelete={onDelete && !item.disableDelete ? () => handleDelete(i) : null} + deleteIcon={item.deleteIcon ? {item.deleteIcon} : null} + sx={STYLES.CHIP_ITEM} + /> + ))} + + ); +}; + +//Контроль свойств - список элементов +P8PChipList.propTypes = { + items: PropTypes.array, + labelRender: PropTypes.func, + onClick: PropTypes.func, + onDelete: PropTypes.func +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8PChipList }; diff --git a/app/components/editors/p8p_component_action/common.js b/app/components/editors/p8p_component_action/common.js new file mode 100644 index 0000000..9dab4f8 --- /dev/null +++ b/app/components/editors/p8p_component_action/common.js @@ -0,0 +1,148 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Общие ресурсы компонента "Редактор действия" +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import PropTypes from "prop-types"; //Контроль свойств компонента + +//--------- +//Константы +//--------- + +//Значения типа по умолчанию +const P8P_CA_DEF_TYPE_VALUE = { + TEXT_VALUE: "Значение", + VARIABLE: "Переменная" +}; + +//Типы значений по умолчанию +const P8P_CA_DEF_VALUE_TYPES = [...Object.values(P8P_CA_DEF_TYPE_VALUE)]; + +//Типы действия +const P8P_CA_TYPE = { + openUnit: { code: "openUnit", name: "Открыть раздел", icon: "article" }, + openPanel: { code: "openPanel", name: "Открыть панель", icon: "storage" }, + setVariable: { code: "setVariable", name: "Установить переменную", icon: "mediation" } +}; + +//Список полей связываемых с проводником значений +const P8P_CA_SOURCE_FIELDS = ["variableSource"]; + +//Список полей хранящих объект типа {"Тип", "Значение"}, где может храниться связь с проводником значений +const P8P_CA_OBJECT_SOURCE_FIELDS = ["resultValue", "tabValue", "panelValue"]; + +//Структура значения с типом +const P8P_CA_OBJECT_VALUE_SHAPE = PropTypes.shape({ + type: PropTypes.string, + value: PropTypes.string +}); + +//Структура входных параметров метода вызова +const P8P_CA_INPUT_PARAM_SHAPE = PropTypes.shape({ + parameter: PropTypes.string, + inputParameter: PropTypes.string, + resultValue: P8P_CA_OBJECT_VALUE_SHAPE +}); + +//Структура параметров действия "Открыть раздел" +const P8P_CA_OPEN_UNIT_PARAMS_SHAPE = PropTypes.shape({ + unitCode: PropTypes.string, + unitName: PropTypes.string, + showMethod: PropTypes.string, + showMethodName: PropTypes.string, + inputParams: PropTypes.arrayOf(P8P_CA_INPUT_PARAM_SHAPE), + modal: PropTypes.bool +}); + +//Структура параметров действия "Открыть панель" +const P8P_CA_OPEN_PANEL_PARAMS_SHAPE = PropTypes.shape({ + tabValue: P8P_CA_OBJECT_VALUE_SHAPE, + panelValue: P8P_CA_OBJECT_VALUE_SHAPE +}); + +//Структура связи переменной действия "Установить переменную" +const P8P_CA_VARIABLE_LINK_SHAPE = PropTypes.shape({ + variableSource: PropTypes.string, + resultValue: P8P_CA_OBJECT_VALUE_SHAPE +}); + +//Структура действия +const P8P_CA_SHAPE = PropTypes.shape({ + type: PropTypes.oneOf(Object.keys(P8P_CA_TYPE)).isRequired, + area: PropTypes.string, + element: PropTypes.string, + params: PropTypes.oneOfType([P8P_CA_OPEN_UNIT_PARAMS_SHAPE, P8P_CA_OPEN_PANEL_PARAMS_SHAPE, PropTypes.arrayOf(P8P_CA_VARIABLE_LINK_SHAPE)]) +}); + +//Начальное состояние объекта значения с типом +const P8P_CA_OBJECT_VALUE_INITIAL = { + type: P8P_CA_DEF_TYPE_VALUE.TEXT_VALUE, + value: "" +}; + +//Начальное состояние входного параметра метода вызова +const P8P_CA_INPUT_PARAM_INITIAL = { + parameter: "", + inputParameter: "", + resultValue: { ...P8P_CA_OBJECT_VALUE_INITIAL } +}; + +//Начальное состояние параметров действия "Открыть раздел" +const P8P_CA_OPEN_UNIT_INITIAL = { + unitCode: "", + unitName: "", + showMethod: "", + showMethodName: "", + inputParams: [], + modal: true +}; + +//Начальное состояние параметров действия "Открыть панель" +const P8P_CA_OPEN_PANEL_INITIAL = { + tabValue: { ...P8P_CA_OBJECT_VALUE_INITIAL }, + panelValue: { ...P8P_CA_OBJECT_VALUE_INITIAL } +}; + +//Начальное состояние параметра связи действия "Установить переменную" +const P8P_CA_VARIABLE_LINK_INITIAL = { + variableSource: "", + resultValue: { ...P8P_CA_OBJECT_VALUE_INITIAL } +}; + +//Начальное состояние действия +const P8P_CA_INITIAL = { + type: P8P_CA_TYPE.openUnit.code, + area: "", + element: "", + params: { ...P8P_CA_OPEN_UNIT_INITIAL } +}; + +//Начальное состояние действий +const P8P_CAS_INITIAL = []; + +//---------------- +//Интерфейс модуля +//---------------- + +export { + P8P_CA_DEF_TYPE_VALUE, + P8P_CA_DEF_VALUE_TYPES, + P8P_CA_TYPE, + P8P_CA_SOURCE_FIELDS, + P8P_CA_OBJECT_SOURCE_FIELDS, + P8P_CA_OBJECT_VALUE_SHAPE, + P8P_CA_OPEN_UNIT_PARAMS_SHAPE, + P8P_CA_OPEN_PANEL_PARAMS_SHAPE, + P8P_CA_VARIABLE_LINK_SHAPE, + P8P_CA_SHAPE, + P8P_CA_INPUT_PARAM_INITIAL, + P8P_CA_OPEN_UNIT_INITIAL, + P8P_CA_OPEN_PANEL_INITIAL, + P8P_CA_VARIABLE_LINK_INITIAL, + P8P_CA_INITIAL, + P8P_CAS_INITIAL +}; diff --git a/app/components/editors/p8p_component_action/config_panel_open.js b/app/components/editors/p8p_component_action/config_panel_open.js new file mode 100644 index 0000000..ca3cdb6 --- /dev/null +++ b/app/components/editors/p8p_component_action/config_panel_open.js @@ -0,0 +1,159 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Компонент: Редактор действия "Открыть панель" +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React, { useState } from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { Box } from "@mui/material"; //Интерфейсные элементы +import { P8P_CA_DEF_TYPE_VALUE, P8P_CA_OPEN_PANEL_PARAMS_SHAPE } from "./common"; //Общие ресурсы действий +import { P8PCAFieldWithType } from "./field_with_type"; //Поле с выбором типа значения +import { PanelsManager } from "../../../panels/panels_editor/components/panels_manager/panels_manager"; //Менеджер панелей + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + CONTAINER_UNIT: { display: "flex", flexDirection: "column", gap: "10px" }, + BOX_SETTINGS: { display: "flex", flexDirection: "row", gap: "15px" } +}; + +//Ключ значений о странице +const SPROP_TAB = "tabValue"; +//Ключа значений о панели +const SPROP_PANEL = "panelValue"; + +//----------- +//Тело модуля +//----------- + +//Редактор действия "Открыть панель" +const P8PCAPanelOpenOptions = ({ panel, valueTypes, isValues, onStateChange, onValueSourceMenuClick }) => { + //Собственное состояние - Отображение менеджера панелей + const [openPanelsManager, setOpenPanelsManager] = useState(false); + + //При открытии менеджера панелей + const handlePanelsManagerOpen = () => setOpenPanelsManager(true); + + //При закрытии менеджера панелей + const handlePanelsManagerClose = () => setOpenPanelsManager(false); + + //При выборе панели в менеджере панелей + const handlePanelSelect = selectedPanel => { + //Устанавливаем мнемокод выбранной панели + onStateChange({ ...panel, panelValue: { ...panel.panelValue, value: selectedPanel.code } }); + //Закрываем менеджер панелей + handlePanelsManagerClose(); + }; + + //При изменении типа значения + const handleValueTypeChange = (value, keyProp) => + onStateChange({ + [keyProp]: { type: value, value: "" } + }); + + //При изменении значения + const handleValueChange = (value, keyProp) => + onStateChange({ + [keyProp]: { ...panel[keyProp], value: value } + }); + + //При очистке значения + const handleClearValueClick = keyProp => + onStateChange({ + [keyProp]: { ...panel[keyProp], value: "" } + }); + + //При изменении переменной + const handleValueSourceChange = (valueSource, keyProp) => + onStateChange({ + [keyProp]: { ...panel[keyProp], value: valueSource } + }); + + //При нажатии на выбор переменной + const handleValueSourceSelect = (e, keyProp) => + isValues + ? onValueSourceMenuClick && + onValueSourceMenuClick(e, valueSource => { + handleValueSourceChange(valueSource, keyProp); + }) + : null; + + //Формирование представления + return ( + + {openPanelsManager && ( + + )} + + handleValueTypeChange(e.target.value, SPROP_TAB)} + onValueChange={e => handleValueChange(e.target.value, SPROP_TAB)} + endAdornments={[ + { icon: "clear", onClick: () => handleClearValueClick(SPROP_TAB), isDisabled: false }, + { + icon: "settings_ethernet", + onClick: e => handleValueSourceSelect(e, SPROP_TAB), + isDisabled: panel.tabValue.type !== P8P_CA_DEF_TYPE_VALUE.VARIABLE + } + ]} + /> + handleValueTypeChange(e.target.value, SPROP_PANEL)} + onValueChange={e => handleValueChange(e.target.value, SPROP_PANEL)} + endAdornments={[ + { icon: "clear", onClick: () => handleClearValueClick(SPROP_PANEL), isDisabled: false }, + { + icon: "list", + onClick: handlePanelsManagerOpen, + isDisabled: panel.panelValue.type === P8P_CA_DEF_TYPE_VALUE.VARIABLE + }, + { + icon: "settings_ethernet", + onClick: e => handleValueSourceSelect(e, SPROP_PANEL), + isDisabled: panel.panelValue.type !== P8P_CA_DEF_TYPE_VALUE.VARIABLE + } + ]} + /> + + + ); +}; + +//Контроль свойств - редактор действия "Открыть панель" +P8PCAPanelOpenOptions.propTypes = { + panel: P8P_CA_OPEN_PANEL_PARAMS_SHAPE, + valueTypes: PropTypes.array, + isValues: PropTypes.bool.isRequired, + onStateChange: PropTypes.func.isRequired, + onValueSourceMenuClick: PropTypes.func.isRequired +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8PCAPanelOpenOptions }; diff --git a/app/components/editors/p8p_component_action/config_unit_open.js b/app/components/editors/p8p_component_action/config_unit_open.js new file mode 100644 index 0000000..25af29f --- /dev/null +++ b/app/components/editors/p8p_component_action/config_unit_open.js @@ -0,0 +1,285 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Компонент: Редактор действия "Открыть раздел" +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React, { useContext } from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { TextField, Box, InputAdornment, IconButton, Icon, FormControlLabel, Checkbox } from "@mui/material"; //Интерфейсные элементы +import { ApplicationСtx } from "../../../context/application"; //Контекст приложения +import { P8P_CA_OPEN_UNIT_PARAMS_SHAPE, P8P_CA_INPUT_PARAM_INITIAL, P8P_CA_DEF_TYPE_VALUE } from "./common"; //Общие ресурсы действий +import { P8PCAFieldWithType } from "./field_with_type"; //Поле с выбором типа значения +import { P8PCATablePropValues } from "./table_prop_values"; //Таблица значений свойств действия + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + CONTAINER_UNIT: { display: "flex", flexDirection: "column", gap: "10px" } +}; + +//----------- +//Тело модуля +//----------- + +//Редактор действия "Открыть раздел" +const P8PCAUnitOpenOptions = ({ unit, valueTypes, isValues, onStateChange, onValueSourceMenuClick }) => { + //Подключение к контексту приложения + const { pOnlineShowDictionary } = useContext(ApplicationСtx); + + //При изменении параметра открытия + const handleModalChange = () => onStateChange({ modal: !unit.modal }); + + //При нажатии на очистку раздела + const handleClearUnitClick = () => + onStateChange({ + unitCode: "", + unitName: "", + showMethod: "" + }); + + //При нажатии на выбор раздела + const handleSelectUnitClick = () => { + pOnlineShowDictionary({ + unitCode: "Units", + showMethod: "methods", + inputParameters: [ + { name: "pos_unit_name", value: unit.unitName }, + { name: "pos_method_name", value: unit.showMethodName } + ], + callBack: res => + res.success && + onStateChange({ + unitCode: res.outParameters.unit_code, + unitName: res.outParameters.unit_name, + showMethod: res.outParameters.method_code, + showMethodName: res.outParameters.method_name + }) + }); + }; + + //При добавлении связи входного параметра + const handleInputParameAdd = () => { + onStateChange({ + inputParams: [...unit.inputParams, { ...P8P_CA_INPUT_PARAM_INITIAL }] + }); + }; + + //При удалении связи входного параметра + const handleInputParamDelete = index => { + //Копируем связываемые переменные + let newInputParams = [...unit.inputParams]; + //Удаляем по индексу + newInputParams.splice(index, 1); + //Устанавливаем новый массив + onStateChange({ inputParams: [...newInputParams] }); + }; + + //При очистке входного параметра + const handleClearInputParameterClick = index => { + //Копируем связываемые переменные + let newInputParams = [...unit.inputParams]; + //Очищаем значение по индексу + newInputParams[index]["parameter"] = ""; + newInputParams[index]["inputParameter"] = ""; + //Устанавливаем новый массив + onStateChange({ inputParams: [...newInputParams] }); + }; + + //При нажатии на выбор параметра метода вызова + const handleSelectUnitParameterClick = index => { + unit.unitCode && + unit.showMethod && + pOnlineShowDictionary({ + unitCode: "UnitParams", + showMethod: "main", + inputParameters: [ + { name: "in_UNITCODE", value: unit.unitCode }, + { name: "in_PARENT_METHOD_CODE", value: unit.showMethod }, + { name: "in_PARAMNAME", value: unit.inputParams[index].parameter } + ], + callBack: res => { + if (res.success) { + //Копируем связываемые переменные + let newInputParams = [...unit.inputParams]; + //Очищаем значение по индексу + newInputParams[index]["parameter"] = res.outParameters.out_PARAMNAME; + newInputParams[index]["inputParameter"] = res.outParameters.out_IN_CODE; + //Устанавливаем новый массив + onStateChange({ inputParams: [...newInputParams] }); + } + } + }); + }; + + //При изменении типа значения входного параметра + const handleInputParamValueTypeChange = (value, index) => { + //Копируем связываемые переменные + let newInputParams = [...unit.inputParams]; + //Очищаем значение по индексу + newInputParams[index].resultValue = { ...newInputParams[index].resultValue, type: value, value: "" }; + //Устанавливаем новый массив + onStateChange({ inputParams: [...newInputParams] }); + }; + + //При очистке значения параметра + const handleClearInputParamValueClick = index => { + //Копируем связываемые переменные + let newInputParams = [...unit.inputParams]; + //Очищаем значение по индексу + newInputParams[index].resultValue = { ...newInputParams[index].resultValue, value: "" }; + //Устанавливаем новый массив + onStateChange({ inputParams: [...newInputParams] }); + }; + + //При изменении значения входного параметра + const handleInputParamValueChange = (value, index) => { + //Копируем связываемые переменные + let newInputParams = [...unit.inputParams]; + //Очищаем значение по индексу + newInputParams[index].resultValue = { ...newInputParams[index].resultValue, value: value }; + //Устанавливаем новый массив + onStateChange({ inputParams: [...newInputParams] }); + }; + + //При связывании значения входного параметра с переменной + const handleInputParamValueSourceChange = (valueSource, index) => { + //Копируем связываемые переменные + let newInputParams = [...unit.inputParams]; + //Очищаем значение по индексу + newInputParams[index].resultValue = { ...newInputParams[index].resultValue, value: valueSource }; + //Устанавливаем новый массив + onStateChange({ inputParams: [...newInputParams] }); + }; + + //При нажатии на выбор переменной + const handleValueSourceSelect = (e, index) => + isValues + ? onValueSourceMenuClick && + onValueSourceMenuClick(e, valueSource => { + handleInputParamValueSourceChange(valueSource, index); + }) + : null; + + //Рендер отображения колонки свойства + const handlePropertyCellRender = (item, index) => { + //Значение первой колонки + return ( + + handleClearInputParameterClick(index)}> + clear + + handleSelectUnitParameterClick(index)} disabled={!unit.showMethodName}> + list + + + ) + }} + /> + ); + }; + + //Рендер отображения колонки значения + const handleValueCellRender = (item, index) => { + //Формирование представления колонки значения + return ( + handleInputParamValueTypeChange(e.target.value, index)} + onValueChange={e => handleInputParamValueChange(e.target.value, index)} + endAdornments={[ + { icon: "clear", onClick: () => handleClearInputParamValueClick(index), isDisabled: false }, + { + icon: "settings_ethernet", + onClick: e => handleValueSourceSelect(e, index), + isDisabled: item.resultValue.type !== P8P_CA_DEF_TYPE_VALUE.VARIABLE + } + ]} + /> + ); + }; + + //Формирование представления + return ( + + + + clear + + + list + + + ) + }} + required={true} + /> + + handlePropertyCellRender(item, index)} + onValueCellRender={(item, index) => handleValueCellRender(item, index)} + onAddRow={handleInputParameAdd} + onRowDelete={index => handleInputParamDelete(index)} + /> + } label="Открывать модально" /> + + ); +}; + +//Контроль свойств - редактор действия "Открыть раздел" +P8PCAUnitOpenOptions.propTypes = { + unit: P8P_CA_OPEN_UNIT_PARAMS_SHAPE, + valueTypes: PropTypes.array, + isValues: PropTypes.bool.isRequired, + onStateChange: PropTypes.func.isRequired, + onValueSourceMenuClick: PropTypes.func.isRequired +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8PCAUnitOpenOptions }; diff --git a/app/components/editors/p8p_component_action/config_var_set.js b/app/components/editors/p8p_component_action/config_var_set.js new file mode 100644 index 0000000..3898eb6 --- /dev/null +++ b/app/components/editors/p8p_component_action/config_var_set.js @@ -0,0 +1,202 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Компонент: Редактор действия "Установить переменную" +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { TextField, InputAdornment, IconButton, Icon } from "@mui/material"; //Интерфейсные элементы +import { P8P_CA_VARIABLE_LINK_INITIAL, P8P_CA_VARIABLE_LINK_SHAPE, P8P_CA_DEF_TYPE_VALUE } from "./common"; //Общие ресурсы действий +import { P8PCAFieldWithType } from "./field_with_type"; //Поле с выбором типа значения +import { P8PCATablePropValues } from "./table_prop_values"; //Таблица значений свойств действия + +//--------- +//Константы +//--------- + +//----------- +//Тело модуля +//----------- + +//Редактор действия "Установить переменную" +const P8PCAVarSetOptions = ({ variables, valueTypes, isValues, onStateChange, onValueSourceMenuClick }) => { + //При добавлении связи переменной + const handleVariableAdd = () => { + onStateChange([...variables, { ...P8P_CA_VARIABLE_LINK_INITIAL }]); + }; + + //При удалении связи переменной + const handleVariableDelete = index => { + //Копируем связываемые переменные + let newVariables = [...variables]; + //Удаляем по индексу + newVariables.splice(index, 1); + //Устанавливаем новый массив + onStateChange([...newVariables]); + }; + + //При очистке значения переменной + const handleClearVariableClick = index => { + //Копируем текущее состояние связываемых переменных + let newParams = [...variables]; + //Очищаем значения у элемента по индексу + newParams[index].variableSource = ""; + //Устанавливаем новый массив + onStateChange([...newParams]); + }; + + //При связывании переменной + const handleVariablesSourceChange = (valueSource, index) => { + //Копируем связываемые переменные + let newVariables = [...variables]; + //Очищаем значение по индексу + newVariables[index].variableSource = valueSource; + //Устанавливаем новый массив + onStateChange([...newVariables]); + }; + + //При изменении типа значения + const handleResultValueTypeChange = (value, index) => { + //Копируем связываемые переменные + let newVariables = [...variables]; + //Очищаем значение по индексу + newVariables[index].resultValue = { ...newVariables[index].resultValue, type: value, value: "" }; + //Устанавливаем новый массив + onStateChange([...newVariables]); + }; + + //При очистке значения + const handleClearResultValueClick = index => { + //Копируем текущее состояние связываемых переменных + let newVariables = [...variables]; + //Очищаем значения у элемента по индексу + newVariables[index].resultValue = { ...newVariables[index].resultValue, value: "" }; + //Устанавливаем новый массив + onStateChange([...newVariables]); + }; + + //При ручном изменении значения + const handleResultValueChange = (value, index) => { + //Копируем связываемые переменные + let newVariables = [...variables]; + //Очищаем значение по индексу + newVariables[index].resultValue = { ...newVariables[index].resultValue, value: value }; + //Устанавливаем новый массив + onStateChange([...newVariables]); + }; + + //При связывании значения + const handleResultValueSourceChange = (valueSource, index) => { + //Копируем связываемые переменные + let newVariables = [...variables]; + //Очищаем значение по индексу + newVariables[index].resultValue = { ...newVariables[index].resultValue, value: valueSource }; + //Устанавливаем новый массив + onStateChange([...newVariables]); + }; + + //При нажатии на выбор переменной (значение) + const handleValueSourceSelect = (e, index) => + isValues + ? onValueSourceMenuClick && + onValueSourceMenuClick(e, valueSource => { + handleResultValueSourceChange(valueSource, index); + }) + : null; + + //При нажатии на выбор переменной (переменная) + const handleVariableSourceSelect = (e, index) => + isValues + ? onValueSourceMenuClick && + onValueSourceMenuClick(e, valueSource => { + handleVariablesSourceChange(valueSource, index); + }) + : null; + + //Рендер отображения колонки свойства + const handlePropertyCellRender = (item, index) => { + //Формирование представления колонки свойства + return ( + + handleClearVariableClick(index)}> + clear + + {isValues && ( + handleVariableSourceSelect(e, index)}> + settings_ethernet + + )} + + ) + }} + /> + ); + }; + + //Рендер отображения колонки значения + const handleValueCellRender = (item, index) => { + //Формирование представления колонки значения + return ( + handleResultValueTypeChange(e.target.value, index)} + onValueChange={e => handleResultValueChange(e.target.value, index)} + endAdornments={[ + { icon: "clear", onClick: () => handleClearResultValueClick(index), isDisabled: false }, + { + icon: "settings_ethernet", + onClick: e => handleValueSourceSelect(e, index), + isDisabled: item.resultValue.type !== P8P_CA_DEF_TYPE_VALUE.VARIABLE + } + ]} + /> + ); + }; + + //Формирование представления + return ( + handlePropertyCellRender(item, index)} + onValueCellRender={(item, index) => handleValueCellRender(item, index)} + onAddRow={handleVariableAdd} + onRowDelete={index => handleVariableDelete(index)} + /> + ); +}; + +//Контроль свойств - редактор действия "Установить переменную" +P8PCAVarSetOptions.propTypes = { + variables: PropTypes.arrayOf(P8P_CA_VARIABLE_LINK_SHAPE), + valueTypes: PropTypes.array, + isValues: PropTypes.bool.isRequired, + onStateChange: PropTypes.func.isRequired, + onValueSourceMenuClick: PropTypes.func.isRequired +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8PCAVarSetOptions }; diff --git a/app/components/editors/p8p_component_action/editor.js b/app/components/editors/p8p_component_action/editor.js new file mode 100644 index 0000000..594e4da --- /dev/null +++ b/app/components/editors/p8p_component_action/editor.js @@ -0,0 +1,250 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Компонент: Редактор действия +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React, { useState, useEffect } from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { TextField, Select, Menu, MenuItem, FormControl, InputLabel, Stack, Box } from "@mui/material"; //Интерфейсные элементы +import { TITLES } from "../../../../app.text"; //Общие текстовые ресурсы +import { P8PConfigDialog } from "../p8p_config_dialog"; //Диалог настройки +import { deepCopyObject } from "../../../core/utils"; //Вспомогательные функции +import { P8P_CA_DEF_VALUE_TYPES, P8P_CA_TYPE, P8P_CA_SHAPE, P8P_CA_INPUT_PARAM_INITIAL, P8P_CA_OPEN_UNIT_INITIAL, P8P_CA_INITIAL } from "./common"; //Общие ресурсы действий +import { P8PCAUnitOpenOptions } from "./config_unit_open"; //Компонент редактора действия "Открыть раздел" +import { P8PCAPanelOpenOptions } from "./config_panel_open"; //Компонент редактора действия "Открыть панель" +import { P8PCAVarSetOptions } from "./config_var_set"; //Компонент редактора действия "Установить переменную" +import { isActionOkDisabled, getActionTypeParams } from "./util"; //Вспомогательные ресурсы действий + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + CONTAINER: { display: "flex", flexDirection: "column", gap: "10px", minWidth: "300px" } +}; + +//----------- +//Тело модуля +//----------- + +//Редактор действия +const P8PCAEditor = ({ areas = [], valueTypes = [], action = null, onOk = null, onCancel = null, valueProviders = {} }) => { + //Собственное состояние - признак инициализиации + const [init, setInit] = useState(true); + + //Собственное состояние - параметры действия + const [state, setState] = useState({}); + + //Собственное состояние - доступные типы значений компонента + const [availableValueTypes, setAvailableValueTypes] = useState([]); + + //Собственное состояние - доступные области компонента + const [availableAreas, setAvailableAreas] = useState([]); + + //Собственное состояние - доступность поля "Элемент" + const [hasElement, setHasElement] = useState(false); + + //Собственное состояние - элемент привязки меню выбора источника + const [valueProvidersMenuAnchorEl, setValueProvidersMenuAnchorEl] = useState({ + target: null, + onChange: null + }); + + //Открытие/сокрытие меню выбора источника + const toggleValueProvidersMenu = (target, onChange) => + setValueProvidersMenuAnchorEl(target instanceof Element ? { target, onChange } : { target: null, onChange: null }); + + //При отображении меню связывания значения с поставщиком данных + const handleValueSourceLinkMenuClick = (e, onChange) => setValueProvidersMenuAnchorEl({ target: e.currentTarget, onChange }); + + //При закрытии редактора с сохранением + const handleOk = () => onOk && onOk({ ...state }); + + //При закрытии редактора с отменой + const handleCancel = () => onCancel && onCancel(); + + //При изменении типа действия + const handleTypeChange = e => { + //Иницилизируем параметры типа + let newParams = getActionTypeParams(e.target.value); + //Изменяем тип действия с изменение структуры параметров + setState(pv => ({ + ...pv, + type: e.target.value, + params: Array.isArray(newParams) ? [...newParams] : { ...newParams } + })); + }; + + //При ручном изменении общего параметра действия + const handleChange = e => setState(pv => ({ ...pv, [e.target.name]: e.target.value })); + + //При изменении области действия + const handleAreaChange = e => { + //Устанавливаем значение + setState(pv => ({ ...pv, [e.target.name]: e.target.value, name: "" })); + //Устанавливаем доступность "Элемент" + setHasElement(availableAreas.find(item => item.area === e.target.value).hasElement); + }; + + //При изменении полей параметров действия + const handleParamsChange = params => { + setState(pv => ({ ...pv, params: { ...pv.params, ...params } })); + }; + + //При изменении переменных параметров действия "Установить переменную" + const handleVariablesParamsChange = variables => { + setState(pv => ({ ...pv, params: [...variables] })); + }; + + //Список значений + const values = Object.keys(valueProviders); + + //Наличие значений + const isValues = values && values.length > 0 ? true : false; + + //Меню привязки к поставщикам значений + const valueProvidersMenu = isValues && ( + + {values.map((value, i) => ( + { + //Выполняем выбор параметра + valueProvidersMenuAnchorEl.onChange(value); + //Закрываем меню выбора переменной + toggleValueProvidersMenu(); + }} + > + {value} + + ))} + + ); + + //При инициализации действия + useEffect(() => { + //Если это иницализация + if (init) { + //Если это открытие действия - берем его параметры, иначе - инициализируем изначальными + const initAction = action + ? action + : { + ...P8P_CA_INITIAL, + area: areas[0].area, + params: { ...P8P_CA_OPEN_UNIT_INITIAL, inputParams: [{ ...P8P_CA_INPUT_PARAM_INITIAL }] } + }; + //Устанавливаем параметры действия + setState(deepCopyObject(initAction)); + //Заполняем доступные области + setAvailableAreas([...areas]); + //Определяем доступность "Элемент" + setHasElement(areas.find(item => item.area === initAction.area)?.hasElement || false); + //Заполняем доступные типы значений + setAvailableValueTypes([...P8P_CA_DEF_VALUE_TYPES, ...valueTypes]); + //Сбрасываем признак инициализации + setInit(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [init]); + + //Формирование представления + return ( + <> + {!init ? ( + + + {valueProvidersMenu} + + {availableAreas.length !== 0 ? ( + + Область + + + ) : null} + {hasElement && ( + + )} + + Тип + + + + {state.type === P8P_CA_TYPE.openUnit.code && ( + + )} + {state.type === P8P_CA_TYPE.openPanel.code && ( + + )} + {state.type === P8P_CA_TYPE.setVariable.code && ( + + )} + + + ) : null} + + ); +}; + +//Контроль свойств - Редактор условия +P8PCAEditor.propTypes = { + areas: PropTypes.array, + valueTypes: PropTypes.array, + action: P8P_CA_SHAPE, + onOk: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + valueProviders: PropTypes.object +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8PCAEditor }; diff --git a/app/components/editors/p8p_component_action/field_with_type.js b/app/components/editors/p8p_component_action/field_with_type.js new file mode 100644 index 0000000..956505a --- /dev/null +++ b/app/components/editors/p8p_component_action/field_with_type.js @@ -0,0 +1,105 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Компонент: Поле с выбором типа значения +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { TextField, Select, MenuItem, FormControl, Box, InputAdornment, IconButton, Icon, FormLabel } from "@mui/material"; //Интерфейсные элементы +import { P8P_CA_OBJECT_VALUE_SHAPE } from "./common"; //Общие ресурсы действий + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + CONTAINER: { display: "flex", flexDirection: "column", gap: "10px", alignItems: "center" }, + LEGEND_SETTINGS: { paddingTop: "10px", marginBottom: "-5px" } +}; + +//----------- +//Тело модуля +//----------- + +//Поле с выбором типа значения +const P8PCAFieldWithType = ({ + valueTypes, + item, + name, + valueLabel = null, + groupLabel = null, + isValueReadOnly = false, + onTypeChange, + onValueChange = null, + endAdornments = [] +}) => { + //Формирование представления + return ( + + {groupLabel ? ( + + {groupLabel} + + ) : null} + + + + { + onValueChange && onValueChange(e); + }} + fullWidth + InputProps={{ + readOnly: isValueReadOnly, + endAdornment: + endAdornments.length !== 0 ? ( + + {endAdornments.map((item, index) => ( + item.onClick(e)} key={index} disabled={item.isDisabled}> + {item.icon} + + ))} + + ) : null + }} + /> + + ); +}; + +//Контроль свойств - поле с выбором типа значения +P8PCAFieldWithType.propTypes = { + valueTypes: PropTypes.array.isRequired, + item: P8P_CA_OBJECT_VALUE_SHAPE.isRequired, + name: PropTypes.string.isRequired, + valueLabel: PropTypes.string, + groupLabel: PropTypes.string, + isValueReadOnly: PropTypes.bool, + onTypeChange: PropTypes.func.isRequired, + onValueChange: PropTypes.func, + endAdornments: PropTypes.arrayOf(PropTypes.object) +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8PCAFieldWithType }; diff --git a/app/components/editors/p8p_component_action/table_prop_values.js b/app/components/editors/p8p_component_action/table_prop_values.js new file mode 100644 index 0000000..c5b0939 --- /dev/null +++ b/app/components/editors/p8p_component_action/table_prop_values.js @@ -0,0 +1,99 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Компонент: Таблица значений свойств действия +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { Box, IconButton, Icon, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Typography } from "@mui/material"; //Интерфейсные элементы +import { APP_STYLES } from "../../../../app.styles"; //Типовые стили + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + CONTAINER_VARIABLES: { maxHeight: "240px", overflow: "auto", ...APP_STYLES.SCROLL }, + BOX_VARIABLES_INFO: { display: "flex", justifyContent: "space-between", alignItems: "center", paddingTop: "10px" }, + TABLE_HEAD_CELL_PROPERTY: { width: "45%", padding: "8px" }, + TABLE_HEAD_CELL_VALUE: { width: "45%", padding: "8px" }, + TABLE_HEAD_CELL_DELETE: { width: "10%", padding: "8px" }, + TABLE_VARIABLES: { maxWidth: "700px", minWidth: "700px" }, + TABLE_ROW_VARIABLES: { "&:last-child td, &:last-child th": { border: 0 } }, + TABLE_CELL_VARIABLES: { padding: "8px 16px" } +}; + +//----------- +//Тело модуля +//----------- + +//Таблица значений свойств действия +const P8PCATablePropValues = ({ name, items, propertyCellName, valueCellName, onPropertyCellRender, onValueCellRender, onAddRow, onRowDelete }) => { + //Формирование представления + return ( + <> + + {name} + + add + + + + + + + + {propertyCellName} + + + {valueCellName} + + + + + + {Array.isArray(items) && + items.map((item, i) => ( + + + {onPropertyCellRender ? onPropertyCellRender(item, i) : null} + + + {onValueCellRender ? onValueCellRender(item, i) : null} + + + onRowDelete(i)}> + clear + + + + ))} + +
+
+ + ); +}; + +//Контроль свойств - таблица значений свойств действия +P8PCATablePropValues.propTypes = { + name: PropTypes.string, + items: PropTypes.array, + propertyCellName: PropTypes.string, + valueCellName: PropTypes.string, + onPropertyCellRender: PropTypes.func, + onValueCellRender: PropTypes.func, + onAddRow: PropTypes.func, + onRowDelete: PropTypes.func +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8PCATablePropValues }; diff --git a/app/components/editors/p8p_component_action/util.js b/app/components/editors/p8p_component_action/util.js new file mode 100644 index 0000000..fa0198d --- /dev/null +++ b/app/components/editors/p8p_component_action/util.js @@ -0,0 +1,212 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Вспомогательные ресурсы компонента "Редактор действия" +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import { genUID } from "../../../core/utils"; //Вспомогательные функции +import { + P8P_CA_DEF_TYPE_VALUE, + P8P_CA_TYPE, + P8P_CA_SOURCE_FIELDS, + P8P_CA_OBJECT_SOURCE_FIELDS, + P8P_CA_OPEN_UNIT_INITIAL, + P8P_CA_INPUT_PARAM_INITIAL, + P8P_CA_OPEN_PANEL_INITIAL, + P8P_CA_VARIABLE_LINK_INITIAL +} from "./common"; //Общие ресурсы действий + +//--------- +//Константы +//--------- + +//------------------------------------ +//Вспомогательные функции и компоненты +//------------------------------------ + +//Доступность сохранения настроек действия +const isActionOkDisabled = action => { + //Если область или тип не указаны + if (!action.area || !action.type) return true; + //Исходим от типа + switch (action.type) { + //Открыть раздел + case P8P_CA_TYPE.openUnit.code: + return !action.params.unitCode || !action.params.showMethod; + //Открыть панель + case P8P_CA_TYPE.openPanel.code: + return !action.params.tabValue.type || !action.params.tabValue.value || !action.params.panelValue.type || !action.params.panelValue.value; + //Установить переменную + case P8P_CA_TYPE.setVariable.code: + return action.params.length === 0 || action.params.some(item => !item.variableSource); + //Для всех остальных + default: + return false; + } +}; + +//Считывание параметров типа действия +const getActionTypeParams = type => { + //Определяем от типа + switch (type) { + //Открыть раздел + case P8P_CA_TYPE.openUnit.code: + return { ...P8P_CA_OPEN_UNIT_INITIAL, inputParams: [{ ...P8P_CA_INPUT_PARAM_INITIAL }] }; + //Открыть панель + case P8P_CA_TYPE.openPanel.code: + return { ...P8P_CA_OPEN_PANEL_INITIAL }; + //Установить переменную + case P8P_CA_TYPE.setVariable.code: + return [{ ...P8P_CA_VARIABLE_LINK_INITIAL }]; + //Для всех остальных + default: + return {}; + } +}; + +//Определение значения действия по типу +const getActionValueByType = (type, value, values, prms = {}, getCustomTypeValue = null) => { + //Исходим от типа действия + switch (type) { + //Значение (стандартное) + case P8P_CA_DEF_TYPE_VALUE.TEXT_VALUE: + return value; + //Переменная (стандартное) + case P8P_CA_DEF_TYPE_VALUE.VARIABLE: + return values[value]; + //Кастомный тип + default: + return getCustomTypeValue ? getCustomTypeValue({ type, value, values, prms }) : null; + } +}; + +//Определения функции действия +const getActionFunction = (item, onOpenUnit, configUrlBase, onOpenPanel, onProviderChange, getCustomTypeValue = null) => { + //Выполняем исходя из типа действия + switch (item.type) { + //Открыть раздел + case P8P_CA_TYPE.openUnit.code: { + return ({ event, values, prms }) => { + onOpenUnit({ + unitCode: item.params.unitCode, + showMethod: item.params.showMethod, + inputParameters: item.params.inputParams.reduce( + (prev, cur) => + cur.inputParameter + ? [ + ...prev, + { + name: cur.inputParameter, + value: getActionValueByType(cur.resultValue.type, cur.resultValue.value, values, prms, getCustomTypeValue) + } + ] + : [...prev], + [] + ), + modal: item.params.modal + }); + event?.stopPropagation && event.stopPropagation(); + }; + } + //Открыть панель + case P8P_CA_TYPE.openPanel.code: { + return ({ event, values, prms }) => { + onOpenPanel({ + id: genUID(), + url: `${configUrlBase}panels_editor?SCODE=${getActionValueByType( + item.params.panelValue.type, + item.params.panelValue.value, + values, + prms, + getCustomTypeValue + )}`, + caption: + getActionValueByType(item.params.tabValue.type, item.params.tabValue.value, values, prms, getCustomTypeValue) || + "Редактор панелей" + }); + event?.stopPropagation && event.stopPropagation(); + }; + } + //Установить переменную + case P8P_CA_TYPE.setVariable.code: { + //Устанавливаем новые значения проводников + return ({ event, values, prms }) => { + //Собираем объект новых значений проводников + let changedValues = item.params.reduce( + (prev, cur) => ({ + ...prev, + ...{ + [cur.variableSource]: getActionValueByType(cur.resultValue.type, cur.resultValue.value, values, prms, getCustomTypeValue) + } + }), + {} + ); + onProviderChange({ ...changedValues }); + event?.stopPropagation && event.stopPropagation(); + }; + } + } +}; + +//Выполнение действий компонента +const getHandlersByActions = (actions, onOpenUnit, configUrlBase, onOpenPanel, onProviderChange, getCustomTypeValue = null) => { + //Инициализируем обработчики + const handlers = actions.reduce((prev, cur) => { + //Ключ обработчика + let key = `${cur.area}.${cur.element}`; + //Если уже был добавлен - не добавляем, иначе добавляем + return !prev[key] ? { ...prev, [key]: { area: cur.area, element: cur.element, fn: null } } : { ...prev }; + }, {}); + //Обходим уникальные обработчики + for (const handler in handlers) { + //Считываем действия области + let areaActions = actions.filter(item => item.area === handlers[handler].area && item.element === handlers[handler].element); + //Собираем массив обработчиков области + let areaFunctions = areaActions.reduce( + (fns, action) => [...fns, getActionFunction(action, onOpenUnit, configUrlBase, onOpenPanel, onProviderChange, getCustomTypeValue)], + [] + ); + //Устанавливаем области последовательный вызов обработчиков + handlers[handler].fn = ({ event, values, prms }) => { + areaFunctions.map(fn => fn({ event, values, prms })); + }; + } + //Возвращаем обработчики + return handlers; +}; + +//Считывание всех связанных переменных рекурсивно +const getActionsVariablesRecursive = (objValue, propName = null) => { + //Если это значение + if (!(typeof objValue === "object") && !Array.isArray(objValue)) { + //Если поле из списка хранящих ссылку - берем его + return P8P_CA_SOURCE_FIELDS.includes(propName) && objValue ? [objValue] : []; + } + //Если это объект хранящий тип и значение - может хранить ссылку на переменную + if (P8P_CA_OBJECT_SOURCE_FIELDS.includes(propName)) { + //Если тип "Переменная" - указываем значение + return objValue?.type === P8P_CA_DEF_TYPE_VALUE.VARIABLE && objValue?.value ? [objValue.value] : []; + } + //Если это массив + if (Array.isArray(objValue)) { + return objValue.reduce((prev, cur) => [...prev, ...getActionsVariablesRecursive(cur)], []); + } else { + //Если это объект + return Object.keys(objValue).reduce((prev, cur) => [...prev, ...getActionsVariablesRecursive(objValue[cur], cur)], []); + } +}; + +//Считывание всех связанных переменных действий +const getActionsVariables = actions => { + //Считываем связанные переменные действий рекурсивно + return getActionsVariablesRecursive(actions); +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { isActionOkDisabled, getActionTypeParams, getActionsVariables, getHandlersByActions }; diff --git a/app/components/editors/p8p_component_condition/common.js b/app/components/editors/p8p_component_condition/common.js new file mode 100644 index 0000000..d96f01d --- /dev/null +++ b/app/components/editors/p8p_component_condition/common.js @@ -0,0 +1,82 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Общие ресурсы компонента "Редактор условия" +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import PropTypes from "prop-types"; //Контроль свойств компонента + +//--------- +//Константы +//--------- + +//Доступные операторы условия +const P8P_CC_OPERATORS = [ + { name: "==", value: "equal" }, + { name: "!=", value: "notEqual" }, + { name: "<=", value: "lessEqual" }, + { name: "<", value: "less" }, + { name: ">=", value: "greaterEqual" }, + { name: ">", value: "greater" }, + { name: "in", value: "in" } +]; + +//Структура параметра поля условия +const P8P_CC_FIELD_PRM_SHAPE = PropTypes.shape({ + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + hasElement: PropTypes.bool, + icon: PropTypes.string +}); + +//Структура поля условия +const P8P_CC_FIELD_SHAPE = PropTypes.shape({ + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired +}); + +//Структура оператора условия +const P8P_CC_OPERATOR_SHAPE = PropTypes.shape({ + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired +}); + +//Структура условия +const P8P_CC_SHAPE = PropTypes.shape({ + condField: P8P_CC_FIELD_SHAPE.isRequired, + condOperator: P8P_CC_OPERATOR_SHAPE.isRequired, + condElement: PropTypes.string, //Пока + condValue: PropTypes.string.isRequired, + resField: P8P_CC_FIELD_SHAPE.isRequired, + resElement: PropTypes.string, //Пока + resValue: PropTypes.string.isRequired +}); + +//Начальное состояние поля условия +const P8P_CC_FIELD_INITIAL = { + name: "", + value: "" +}; + +//Начальное состояние условия +const P8P_CC_INITIAL = { + condField: { ...P8P_CC_FIELD_INITIAL }, + condOperator: { ...P8P_CC_OPERATORS[0] }, + condElement: "", + condValue: "", + resField: { ...P8P_CC_FIELD_INITIAL }, + resElement: "", + resValue: "" +}; + +//Начальное состояние условий +const P8P_CCS_INITIAL = []; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8P_CC_OPERATORS, P8P_CC_FIELD_PRM_SHAPE, P8P_CC_FIELD_SHAPE, P8P_CC_SHAPE, P8P_CC_INITIAL, P8P_CCS_INITIAL }; diff --git a/app/components/editors/p8p_component_condition/editor.js b/app/components/editors/p8p_component_condition/editor.js new file mode 100644 index 0000000..2fea9db --- /dev/null +++ b/app/components/editors/p8p_component_condition/editor.js @@ -0,0 +1,239 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Компонент: Редактор условия +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React, { useState, useEffect } from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { TextField, Select, MenuItem, FormControl, InputLabel, Stack, Box } from "@mui/material"; //Интерфейсные элементы +import { TITLES } from "../../../../app.text"; //Общие текстовые ресурсы +import { P8PConfigDialog } from "../p8p_config_dialog"; //Диалог настройки +import { P8P_CC_INITIAL, P8P_CC_SHAPE, P8P_CC_OPERATORS, P8P_CC_FIELD_PRM_SHAPE } from "./common"; //Общие ресурсы условий +import { deepCopyObject } from "../../../core/utils"; //Вспомогательные функции +import { isConditionOkDisabled } from "./util"; //Вспомогательные ресурсы условий + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + BOX_COND_GROUP: { display: "flex", flexDirection: "row", gap: "10px", paddingBottom: "10px" }, + BOX_COND_ELEMENT: width => ({ minWidth: width }) +}; + +//----------- +//Тело модуля +//----------- + +//Редактор условия +const P8PCCEditor = ({ condition = null, onOk = null, onCancel = null, condFields = [], resFields = [] }) => { + //Собственное состояние - признак инициализиации + const [init, setInit] = useState(true); + + //Собственное состояние - параметры элемента формы + const [state, setState] = useState({}); + + //При закрытии редактора с сохранением + const handleOk = () => onOk && onOk({ ...state }); + + //Собственное состояние - доступность поля "Элемент условия" + const [hasCondElement, setHasCondElement] = useState(false); + + //Собственное состояние - доступность поля "Элемент результата" + const [hasResElement, setHasResElement] = useState(false); + + //При закрытии редактора с отменой + const handleCancel = () => onCancel && onCancel(); + + //При изменении параметра элемента + const handleChange = e => setState(pv => ({ ...pv, [e.target.name]: e.target.value })); + + //При изменении поля условия + const handleCondFieldChange = e => { + //Считываем нужный элемент + const newCondField = condFields.find(item => item.name === e.target.value); + //Обновляем поле (объект) + setState(pv => ({ ...pv, [e.target.name]: { name: newCondField.name, value: newCondField.value } })); + //Определяем доступность поля "Элемент условия" + setHasCondElement(newCondField?.hasElement || false); + }; + + //При изменении поля результата + const handleResFieldChange = e => { + //Считываем нужный элемент + const newResField = resFields.find(item => item.name === e.target.value); + //Обновляем поле результата + setState(pv => ({ ...pv, [e.target.name]: { name: newResField.name, value: newResField.value } })); + //Определяем доступность поля "Элемент результата" + setHasResElement(newResField?.hasElement || false); + }; + + //При изменении оператора условия + const handleOperatorChange = e => { + //Считываем нужный элемент + const newOperator = P8P_CC_OPERATORS.find(item => item.name === e.target.value); + //Обновляем оператор + setState(pv => ({ ...pv, [e.target.name]: { ...newOperator } })); + }; + + //При инициализации условия + useEffect(() => { + //Если это иницализация + if (init) { + //Если это открытие условия - берем его параметры, иначе - инициализируем изначальными + const initCondition = condition + ? condition + : { + ...P8P_CC_INITIAL, + condField: { name: condFields[0].name, value: condFields[0].value }, + resField: { name: resFields[0].name, value: resFields[0].value } + }; + //Устанавливаем параметры действия + setState(deepCopyObject(initCondition)); + //Определяем доступность "Элемент условия" + setHasCondElement(condFields.find(item => item.name === initCondition.condField.name)?.hasElement || false); + //Определяем доступность "Элемент результата" + setHasResElement(resFields.find(item => item.name === initCondition.resField.name)?.hasElement || false); + //Сбрасываем признак инициализации + setInit(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [init]); + + //Формирование представления + return ( + <> + {!init ? ( + + + + + + Поле + + + + Оператор + + + + + {hasCondElement ? ( + + ) : null} + + + Поле результата + + + + + {hasResElement ? ( + + ) : null} + + + + ) : null} + + ); +}; + +//Контроль свойств - Редактор условия +P8PCCEditor.propTypes = { + condition: P8P_CC_SHAPE, + onOk: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + condFields: PropTypes.arrayOf(P8P_CC_FIELD_PRM_SHAPE).isRequired, + resFields: PropTypes.arrayOf(P8P_CC_FIELD_PRM_SHAPE).isRequired +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8PCCEditor }; diff --git a/app/components/editors/p8p_component_condition/util.js b/app/components/editors/p8p_component_condition/util.js new file mode 100644 index 0000000..bf0a946 --- /dev/null +++ b/app/components/editors/p8p_component_condition/util.js @@ -0,0 +1,89 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Вспомогательные ресурсы компонента "Редактор условия" +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +//--------- +//Константы +//--------- + +//Функции операторов условий +const P8P_CC_OPERATOR_FUNC = { + equal: (objValue, value) => objValue == value, + notEqual: (objValue, value) => objValue != value, + lessEqual: (objValue, value) => objValue <= value, + less: (objValue, value) => objValue < value, + greaterEqual: (objValue, value) => objValue >= value, + greater: (objValue, value) => objValue > value, + in: (objValue, value) => + value + .split(",") + .map(item => item.trim()) + .includes(objValue + "") +}; + +//------------------------------------ +//Вспомогательные функции и компоненты +//------------------------------------ + +//Загрузка условий в объект +const loadConditionsToObject = (obj, conditions) => { + //Инициализируем новый объект + let newObj = { ...obj }; + //Инициализируем функцию оператора + let operatorFunc; + //Если изначальный индикатор загружен + if (Object.keys(newObj).length !== 0) { + //Обходим условия + conditions.map(item => { + //Функция оператора + operatorFunc = P8P_CC_OPERATOR_FUNC[item.condOperator.value]; + //Проверяем условие + if (operatorFunc && operatorFunc(newObj[item.condField.value], item.condValue)) { + //Устанавливаем поле в новое значение + newObj[item.resField.value] = item.resValue; + } + }); + } + //Возвращаем новый объект + return newObj; +}; + +//Считывание результатов условия +const getConditionsValues = (obj, conditions, element = "") => { + //Инициализируем функцию оператора + let operatorFunc; + //Инициализируем значение поля + let condFieldValue = ""; + //Инициализируем результат + let resObject = {}; + //Обходим условия + conditions.map(item => { + //Определяем значение поля условия + condFieldValue = item.condElement ? obj[item.condElement] : obj[item.condField.value]; + //Функция оператора + operatorFunc = P8P_CC_OPERATOR_FUNC[item.condOperator.value]; + //Проверяем условие + if (operatorFunc && operatorFunc(condFieldValue, item.condValue)) { + //Если в условии нет элемента результата или он равен текущему элементу + if (!item.resElement || (element && item.resElement === element)) { + resObject[item.resField.value] = item.resValue; + } + } + }); + //Возвращаем новый объект + return resObject; +}; + +//Доступность сохранения настроек условия +const isConditionOkDisabled = condition => (!condition.condField.value || !condition.condOperator.value || !condition.resField.value ? true : false); + +//---------------- +//Интерфейс модуля +//---------------- + +export { loadConditionsToObject, getConditionsValues, isConditionOkDisabled }; diff --git a/app/components/editors/p8p_component_settings.js b/app/components/editors/p8p_component_settings.js new file mode 100644 index 0000000..7ac3cfc --- /dev/null +++ b/app/components/editors/p8p_component_settings.js @@ -0,0 +1,41 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Компонент: Дополнительные настройки источников +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +//--------- +//Константы +//--------- + +//Наименование параметра данных компонента, полученных с сервера +const P8P_COMPONENT_SETTINGS_RESP_ARGS = { + INDICATOR: "XINDICATOR", + CHART: "XCHART", + TABLE: "XDATA_GRID" +}; + +//Наименование путей компонента +const P8P_COMPONENT_SETTINGS_PATHS = { + INDICATOR: "indicator", + CHART: "chart", + TABLE: "table", + FORM: "form" +}; + +//------------------------------------ +//Вспомогательные функции и компоненты +//------------------------------------ + +//----------- +//Тело модуля +//----------- + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8P_COMPONENT_SETTINGS_RESP_ARGS, P8P_COMPONENT_SETTINGS_PATHS }; diff --git a/app/components/editors/p8p_conditions.js b/app/components/editors/p8p_conditions.js new file mode 100644 index 0000000..5157f67 --- /dev/null +++ b/app/components/editors/p8p_conditions.js @@ -0,0 +1,99 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Компонент: Условия +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React, { useState } from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { Icon, Button } from "@mui/material"; //Интерфейсные элементы +import { P8PCCEditor } from "./p8p_component_condition/editor"; //Редактор условия +import { P8PChipList } from "./p8p_chip_list"; //Дополнительные настройки редактора +import { P8P_CCS_INITIAL, P8P_CC_SHAPE, P8P_CC_FIELD_PRM_SHAPE } from "./p8p_component_condition/common"; //Общие ресурсы компонента "Редактор условия" + +//--------- +//Константы +//--------- + +//----------- +//Тело модуля +//----------- + +//Условия +const P8PConditions = ({ conditions = P8P_CCS_INITIAL, condFields, resFields, onChange = null } = {}) => { + //Собственное состояние - редактор условий + const [conditionEditor, setConditionEditor] = useState({ display: false, index: null }); + + //При изменении условий + const handleConditionsChange = conditions => onChange && onChange(conditions); + + //При добавлении нового условия + const handleConditionAdd = () => setConditionEditor({ display: true, index: null }); + + //При нажатии на условие + const handleConditionClick = i => setConditionEditor({ display: true, index: i }); + + //При удалении условия + const handleConditionDelete = i => { + const newConditions = [...conditions]; + newConditions.splice(i, 1); + handleConditionsChange(newConditions); + }; + + //При отмене сохранения изменений условия + const handleConditionCancel = () => setConditionEditor({ display: false, index: null }); + + //При сохранении изменений условия + const handleConditionSave = condition => { + const newConditions = [...conditions]; + conditionEditor.index == null ? newConditions.push({ ...condition }) : (newConditions[conditionEditor.index] = { ...condition }); + handleConditionsChange(newConditions); + setConditionEditor({ display: false, index: null }); + }; + + //Определяем структуру условий для отображения + const conditionChips = conditions.map(item => { + //Собираем текст условия + let text = `${item.condField.name} ${item.condOperator.name} ${item.condValue}`; + //Считываем поле результата + let resField = resFields.find(field => field.name === item.resField.name); + //Формируем структуру для отображения карточки действия + return { text: text, title: text, icon: resField?.icon, iconTitle: resField?.name }; + }); + + //Формирование представления + return ( + <> + {conditionEditor.display && ( + + )} + + + + ); +}; + +//Контроль свойств компонента - условия +P8PConditions.propTypes = { + conditions: PropTypes.arrayOf(P8P_CC_SHAPE), + condFields: PropTypes.arrayOf(P8P_CC_FIELD_PRM_SHAPE).isRequired, + resFields: PropTypes.arrayOf(P8P_CC_FIELD_PRM_SHAPE).isRequired, + onChange: PropTypes.func +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8PConditions }; diff --git a/app/components/editors/p8p_config_dialog.js b/app/components/editors/p8p_config_dialog.js index 133dd5b..a21b32c 100644 --- a/app/components/editors/p8p_config_dialog.js +++ b/app/components/editors/p8p_config_dialog.js @@ -9,17 +9,17 @@ import React from "react"; //Классы React import PropTypes from "prop-types"; //Контроль свойств компонента -import { P8PDialog } from "../p8p_dialog"; //Типовой диалог +import { P8PDialog, P8P_DIALOG_WIDTH } from "../p8p_dialog"; //Типовой диалог //----------- //Тело модуля //----------- //Диалог настройки -const P8PConfigDialog = ({ title, children, onOk, onCancel }) => { +const P8PConfigDialog = ({ title, children, width, onOk, onCancel, okDisabled = false }) => { //Формирование представления return ( - + {children} ); @@ -29,8 +29,10 @@ const P8PConfigDialog = ({ title, children, onOk, onCancel }) => { P8PConfigDialog.propTypes = { title: PropTypes.string.isRequired, children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]), + width: PropTypes.oneOf(Object.values(P8P_DIALOG_WIDTH)), onOk: PropTypes.func, - onCancel: PropTypes.func + onCancel: PropTypes.func, + okDisabled: PropTypes.bool }; //---------------- diff --git a/app/components/editors/p8p_data_source.js b/app/components/editors/p8p_data_source.js index 965e7a6..3a6dac2 100644 --- a/app/components/editors/p8p_data_source.js +++ b/app/components/editors/p8p_data_source.js @@ -9,11 +9,11 @@ import React, { useState } from "react"; //Классы React import PropTypes from "prop-types"; //Контроль свойств компонента -import { Stack, IconButton, Icon, Typography, Chip, Button, Card, CardContent, CardActions, CardActionArea } from "@mui/material"; //Интерфейсные элементы +import { Stack, IconButton, Icon, Typography, Button, Card, CardContent, CardActions, CardActionArea } from "@mui/material"; //Интерфейсные элементы import { BUTTONS, TEXTS } from "../../../app.text"; //Общие текстовые ресурсы -import { STYLES as COMMON_STYLES } from "./p8p_editors_common"; //Общие ресурсы редаторов import { P8P_DATA_SOURCE_SHAPE, P8P_DATA_SOURCE_TYPE, P8P_DATA_SOURCE_TYPE_NAME, P8P_DATA_SOURCE_INITIAL } from "./p8p_data_source_common"; //Общие ресурсы компонента "Источник данных" import { P8PDataSourceConfigDialog } from "./p8p_data_source_config_dialog"; //Диалог настройки источника данных +import { P8PChipList } from "./p8p_chip_list"; //Дополнительные настройки редактора //----------- //Тело модуля @@ -45,17 +45,9 @@ const P8PDataSource = ({ dataSource = null, valueProviders = {}, onChange = null //Расчет флага "настроенности" const configured = dataSource?.type ? true : false; - //Список аргументов - const args = - configured && - dataSource.arguments.map((argument, i) => ( - - )); + //Структура параметров для отображения + const argChips = + configured && dataSource.arguments.map(argument => ({ text: `:${argument.name} = ${argument.valueSource || argument.value || "NULL"}` })); //Формирование представления return ( @@ -79,7 +71,7 @@ const P8PDataSource = ({ dataSource = null, valueProviders = {}, onChange = null {P8P_DATA_SOURCE_TYPE_NAME[dataSource.type] || TEXTS.UNKNOWN_SOURCE_TYPE} - {args} + diff --git a/app/components/editors/p8p_data_source_common.js b/app/components/editors/p8p_data_source_common.js index 722e76d..c460ca2 100644 --- a/app/components/editors/p8p_data_source_common.js +++ b/app/components/editors/p8p_data_source_common.js @@ -67,6 +67,7 @@ const P8P_DATA_SOURCE_SHAPE = PropTypes.shape({ const P8P_DATA_SOURCE_INITIAL = { type: "", userProc: "", + query: "", stored: "", respArg: "", arguments: [] diff --git a/app/components/editors/p8p_data_source_config_dialog.js b/app/components/editors/p8p_data_source_config_dialog.js index 864a96f..abbc851 100644 --- a/app/components/editors/p8p_data_source_config_dialog.js +++ b/app/components/editors/p8p_data_source_config_dialog.js @@ -15,6 +15,8 @@ import { TITLES, CAPTIONS } from "../../../app.text"; //Общие тексто import { P8PConfigDialog } from "./p8p_config_dialog"; //Типовой диалог настройки import { P8P_DATA_SOURCE_TYPE, P8P_DATA_SOURCE_SHAPE, P8P_DATA_SOURCE_ARGUMENT_INITIAL, P8P_DATA_SOURCE_INITIAL } from "./p8p_data_source_common"; //Общие ресурсы компонента "Источник данных" import { useUserProcDesc } from "./p8p_data_source_hooks"; //Хуки источников данных +import { P8PDataSourceQuerySelector } from "./p8p_data_source_query_selector"; //Диалог выбора записи редактора запросов +import { hasValue } from "../../core/utils"; //Вспомогательные функции //----------- //Тело модуля @@ -34,6 +36,12 @@ const P8PDataSourceConfigDialog = ({ dataSource = null, valueProviders = {}, onO //Описание выбранной пользовательской процедуры const [userProcDesc] = useUserProcDesc({ code: state.userProc, refresh: refresh.userProcDesc }); + //Собственное состояние - отображение диалога выбора запроса + const [openQuerySelector, setOpenQuerySelector] = useState(false); + + //Собственное состояние - доступность полей выбора источника + const [disabledFields, setDisabledFields] = useState({ query: false, userProc: false }); + //Подключение к контексту приложения const { pOnlineShowDictionary } = useContext(ApplicationСtx); @@ -86,6 +94,21 @@ const P8PDataSourceConfigDialog = ({ dataSource = null, valueProviders = {}, onO //При вводе значения аргумента const handleArgumentChange = (index, value) => setArgumentValueSource(index, value, ""); + //Открытие диалога выбора запроса + const handleOpenQuerySelector = () => setOpenQuerySelector(true); + + //Закрытие диалога выбора запроса + const handleCancelQuerySelector = () => setOpenQuerySelector(false); + + //При нажатии на очистку мнемокода запроса + const handleQueryClearClick = () => setState({ ...P8P_DATA_SOURCE_INITIAL }); + + //При нажатии на выбор запроса в качестве источника данных + const handleQuerySelectClick = query => { + setState(pv => ({ ...pv, type: P8P_DATA_SOURCE_TYPE.QUERY, query: query.code })); + handleCancelQuerySelector(); + }; + //При изменении описания пользовательской процедуры useEffect(() => { if (userProcDesc) @@ -97,8 +120,22 @@ const P8PDataSourceConfigDialog = ({ dataSource = null, valueProviders = {}, onO })); }, [userProcDesc]); + //При изменении источника + useEffect(() => { + /* + Если выбран запрос - блокируем выбор процедуры + Если выбрана процедура - блокируем выбор запроса + Ничего не выбрано - доступны все варианты + */ + hasValue(state.query) + ? setDisabledFields({ query: false, userProc: true }) + : hasValue(state.userProc) + ? setDisabledFields({ query: true, userProc: false }) + : setDisabledFields({ query: false, userProc: false }); + }, [state.query, state.userProc]); + //Список значений - const values = Object.keys(valueProviders).reduce((res, key) => [...res, ...Object.keys(valueProviders[key])], []); + const values = Object.keys(valueProviders); //Наличие значений const isValues = values && values.length > 0 ? true : false; @@ -114,59 +151,90 @@ const P8PDataSourceConfigDialog = ({ dataSource = null, valueProviders = {}, onO ); + //Доступность сохранения настройки источника данных + const okDisabled = !state.query && !state.userProc ? true : false; + //Формирование представления return ( - - - {valueProvidersMenu} - - - clear - - - list - - - ) - }} - /> - {Array.isArray(state?.arguments) && - state.arguments.map((argument, i) => ( - handleArgumentChange(i, e.target.value)} - InputLabelProps={{ shrink: true }} - InputProps={{ - endAdornment: ( - - handleArgumentClearClick(i)}> - clear - - {isValues && ( - - settings_ethernet + <> + {openQuerySelector ? ( + + ) : null} + + + {valueProvidersMenu} + {/* ДОРАБАТЫВАТЬ ПОСЛЕ РЕАЛИЗАЦИИ ЗАПРОСОВ */} + + + clear + + + list + + + ) + }} + disabled={disabledFields.query} + /> + + + clear + + + list + + + ) + }} + disabled={disabledFields.userProc} + /> + {Array.isArray(state?.arguments) && + state.arguments.map((argument, i) => ( + handleArgumentChange(i, e.target.value)} + InputLabelProps={{ shrink: true }} + InputProps={{ + endAdornment: ( + + handleArgumentClearClick(i)}> + clear - )} - - ) - }} - /> - ))} - - + {isValues && ( + + settings_ethernet + + )} + + ) + }} + /> + ))} + + + ); }; diff --git a/app/components/editors/p8p_data_source_hooks.js b/app/components/editors/p8p_data_source_hooks.js index 60c9cf5..6841953 100644 --- a/app/components/editors/p8p_data_source_hooks.js +++ b/app/components/editors/p8p_data_source_hooks.js @@ -13,6 +13,9 @@ import { ERRORS } from "../../../app.text"; //Общие текстовые ре import { formatErrorMessage } from "../../core/utils"; //Общие вспомогательные функции import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером import { P8P_DATA_SOURCE_TYPE, P8P_DATA_SOURCE_ARGUMENT_DATA_TYPE } from "./p8p_data_source_common"; //Общие ресурсы источника данных +import { getConditionsValues } from "./p8p_component_condition/util"; //Вспомогательные ресурсы условий +import { getHandlersByActions } from "./p8p_component_action/util"; //Вспомогательные ресурсы действий +import { ApplicationСtx } from "../../context/application"; //Контекст приложения //----------- //Тело модуля @@ -58,7 +61,7 @@ const useUserProcDesc = ({ code, refresh }) => { }; //Получение данных из источника -const useDataSource = ({ dataSource, values }) => { +const useDataSource = ({ dataSource, values, componentRespArg = "" }) => { //Контроллер для прерывания запросов const abortController = useRef(null); @@ -69,14 +72,25 @@ const useDataSource = ({ dataSource, values }) => { const [isLoading, setLoading] = useState(false); //Собственное состояние - данные - const [data, setData] = useState({ init: false }); + const [data, setData] = useState({ componentData: {}, init: false }); //Собственное состояние - ошибка получения данных const [error, setError] = useState(null); + //Собственное состояние - наличие настроек + const [haveConfing, setHaveConfig] = useState(false); + + //Собственное состояние - наличие данных + const [haveData, setHaveData] = useState(false); + //Подключение к контексту взаимодействия с сервером const { executeStored } = useContext(BackEndСtx); + //При необходимости обновление информации о наличии данных + useEffect(() => { + setHaveData(data.init === true && !error ? true : false); + }, [data.init, error]); + //При необходимости обновить данные useEffect(() => { //Загрузка данных с сервера @@ -94,11 +108,11 @@ const useDataSource = ({ dataSource, values }) => { showErrorMessage: false }); setError(null); - setData({ ...data, init: true }); + setData({ componentData: { ...data[componentRespArg] }, init: true }); } catch (e) { if (e.message !== client.ERR_ABORTED) { setError(formatErrorMessage(e.message).text); - setData({ init: false }); + setData({ componentData: {}, init: false }); } } finally { setLoading(false); @@ -106,12 +120,15 @@ const useDataSource = ({ dataSource, values }) => { }; if (state.reqSet) { if (state.stored) loadData(); - } else setData({ init: false }); + } else setData({ componentData: {}, init: false }); return () => abortController.current?.abort?.(); - }, [state.stored, state.storedArgs, state.respArg, state.reqSet, executeStored]); + }, [state.stored, state.storedArgs, state.respArg, state.reqSet, executeStored, componentRespArg]); //При изменении свойств useEffect(() => { + //Устанавливаем признак наличия настроек + setHaveConfig(dataSource?.stored ? true : false); + //Устанавливаем параметры исполнения setState(pv => { if (dataSource?.type == P8P_DATA_SOURCE_TYPE.USER_PROC) { const { stored, respArg } = dataSource; @@ -132,7 +149,7 @@ const useDataSource = ({ dataSource, values }) => { if (pv.stored != stored || pv.respArg != respArg || JSON.stringify(pv.storedArgs) != JSON.stringify(storedArgs)) { if (!reqSet) { setError(ERRORS.DATA_SOURCE_NO_REQ_ARGS); - setData({ init: false }); + setData({ componentData: {}, init: false }); } return { stored, respArg, storedArgs, reqSet }; } else return pv; @@ -141,11 +158,72 @@ const useDataSource = ({ dataSource, values }) => { }, [dataSource, values]); //Возвращаем интерфейс хука - return [data, error, isLoading]; + return [data.componentData, error, haveConfing, haveData, isLoading]; +}; + +//Изменение данных компонента с учетом условий +const useConditions = ({ componentData, conditions }) => { + //Собственное состояние - текущие условия компонента + const [currentConditions, setCurrentConditions] = useState([]); + + //Собственное состояние - данные + const [data, setData] = useState(); + + //При обновлении условий компонента + useEffect(() => { + //Если условия изменились + if (JSON.stringify(currentConditions) != JSON.stringify(conditions)) { + //Устанавливаем новые условия + setCurrentConditions(conditions); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [conditions]); + + //При обновлении данных или условий компонента + useEffect(() => { + //Если есть текущие условия + if (currentConditions.length !== 0) { + //Устанавливаем данные с учетом условий + setData({ ...componentData, ...getConditionsValues(componentData, currentConditions) }); + } else { + //Оставляем данные компонента + setData({ ...componentData }); + } + }, [currentConditions, componentData]); + + //Возвращаем интерфейс хука + return [data]; +}; + +//Получение обработчиков компонента +const useComponentHandlers = ({ actions = [], onValuesChange = null, getCustomTypeValue = null }) => { + //Контроллер для текущего состояния действий + const currentActions = useRef([]); + + //Собственное состояние - обработчики компонента + const [handlers, setHandlers] = useState({}); + + //Подключение к контексту приложения + const { configUrlBase, pOnlineShowTab, pOnlineShowUnit } = useContext(ApplicationСtx); + + //При необходимости обновления информации об обработчиках + useEffect(() => { + //Если изменились действия или параметры + if (JSON.stringify(currentActions.current) != JSON.stringify(actions)) { + //Считываем обработчики компонента + setHandlers(getHandlersByActions(actions, pOnlineShowUnit, configUrlBase, pOnlineShowTab, onValuesChange, getCustomTypeValue)); + //Устанавливаем контроллер текущих действий + currentActions.current = actions; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actions]); + + //Возвращаем интерфейс хука + return [handlers]; }; //---------------- //Интерфейс модуля //---------------- -export { useUserProcDesc, useDataSource }; +export { useUserProcDesc, useDataSource, useConditions, useComponentHandlers }; diff --git a/app/components/editors/p8p_data_source_query_selector.js b/app/components/editors/p8p_data_source_query_selector.js new file mode 100644 index 0000000..6bb45d3 --- /dev/null +++ b/app/components/editors/p8p_data_source_query_selector.js @@ -0,0 +1,75 @@ +/* + Парус 8 - Панели мониторинга - Редакторы панелей + Компонент: Диалог выбора записи редактора запросов +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { Stack, List, ListItem, ListItemButton, ListItemText, Typography } from "@mui/material"; //Интерфейсные элементы +import { P8PConfigDialog } from "./p8p_config_dialog"; //Типовой диалог настройки + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + LIST: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto" } +}; + +//----------- +//Тело модуля +//----------- + +//Диалог выбора записи редактора запросов +const P8PDataSourceQuerySelector = ({ current = null, onSelect, onCancel }) => { + //!!! НА РЕАЛИЗАЦИИ + const test = [{ code: "TestCode", name: "TestName" }]; + //При выборе элемента списка + const handleSelectClick = query => { + onSelect && onSelect(query); + }; + + //Формирование представления + return ( + + + {test.map((query, i) => { + const selected = query.code === current; + return ( + + handleSelectClick(query)} selected={selected}> + + {`${query.code}, ${query.name}`} + + } + /> + + + ); + })} + + + ); +}; + +//Контроль свойств компонента - Диалог выбора записи редактора запросов +P8PDataSourceQuerySelector.propTypes = { + current: PropTypes.string, + onSelect: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8PDataSourceQuerySelector }; diff --git a/app/components/editors/p8p_editor_box.js b/app/components/editors/p8p_editor_box.js index 9f01da4..dcbf877 100644 --- a/app/components/editors/p8p_editor_box.js +++ b/app/components/editors/p8p_editor_box.js @@ -17,7 +17,7 @@ import { BUTTONS } from "../../../app.text"; //Общие текстовые р //----------- //Контейнер редактора -const P8PEditorBox = ({ title, children, onSave }) => { +const P8PEditorBox = ({ title, children, onSave, allowClose = true }) => { //При нажатии на "Сохранить" const handleSaveClick = (closeEditor = false) => onSave && onSave(closeEditor); @@ -36,9 +36,11 @@ const P8PEditorBox = ({ title, children, onSave }) => { handleSaveClick(false)} title={BUTTONS.APPLY}> done - handleSaveClick(true)} title={BUTTONS.SAVE}> - done_all - + {allowClose ? ( + handleSaveClick(true)} title={BUTTONS.SAVE}> + done_all + + ) : null} )} @@ -49,7 +51,8 @@ const P8PEditorBox = ({ title, children, onSave }) => { P8PEditorBox.propTypes = { title: PropTypes.string.isRequired, children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]), - onSave: PropTypes.func + onSave: PropTypes.func, + allowClose: PropTypes.bool }; //---------------- diff --git a/app/components/editors/p8p_editor_toolbar.js b/app/components/editors/p8p_editor_toolbar.js index bc01d08..78ee933 100644 --- a/app/components/editors/p8p_editor_toolbar.js +++ b/app/components/editors/p8p_editor_toolbar.js @@ -9,7 +9,7 @@ import React from "react"; //Классы React import PropTypes from "prop-types"; //Контроль свойств компонента -import { IconButton, Icon, Stack } from "@mui/material"; //Интерфейсные компоненты MUI +import { IconButton, Icon, Stack, Grid } from "@mui/material"; //Интерфейсные компоненты MUI //--------- //Константы @@ -20,7 +20,8 @@ const P8P_EDITOR_TOOL_BAR_ITEM_SHAPE = PropTypes.shape({ icon: PropTypes.string.isRequired, title: PropTypes.string.isRequired, disabled: PropTypes.bool, - onClick: PropTypes.func.isRequired + onClick: PropTypes.func.isRequired, + customRenderer: PropTypes.func }); //----------- @@ -32,11 +33,19 @@ const P8PEditorToolBar = ({ items = [] }) => { //Формирование представления return ( - {items.map((item, i) => ( - - {item.icon} - - ))} + + {items.map((item, i) => ( + + {item.customRenderer ? ( + item.customRenderer({ icon: item.icon, title: item.title, disabled: item?.disabled === true, onClick: item.onClick }) + ) : ( + + {item.icon} + + )} + + ))} + ); }; diff --git a/app/components/editors/p8p_editors_common.js b/app/components/editors/p8p_editors_common.js index 8911cc2..1eb4a73 100644 --- a/app/components/editors/p8p_editors_common.js +++ b/app/components/editors/p8p_editors_common.js @@ -23,8 +23,17 @@ const STYLES = { }) }; +//------------------------------------ +//Вспомогательные функции и компоненты +//------------------------------------ + +//Проверка корректности наименования элемента формы +const isElementNameCorrect = elementName => { + return new RegExp(/^[\w\d_]*$/).test(elementName); +}; + //---------------- //Интерфейс модуля //---------------- -export { STYLES }; +export { STYLES, isElementNameCorrect }; diff --git a/app/components/p8p_app_message.js b/app/components/p8p_app_message.js index 08a6520..c00fdb5 100644 --- a/app/components/p8p_app_message.js +++ b/app/components/p8p_app_message.js @@ -253,13 +253,13 @@ const P8PAppInlineInfo = props => buildVariantInlineMessage(props, P8P_APP_MESSA //Диалог подсказки const P8PHintDialog = ({ title, hint, onOk }) => { return ( - (onOk ? onOk() : null)}> + (onOk ? onOk(e) : null)}> {title}
- +
); diff --git a/app/components/p8p_indicator.js b/app/components/p8p_indicator.js index 0f19f9f..7877e34 100644 --- a/app/components/p8p_indicator.js +++ b/app/components/p8p_indicator.js @@ -61,7 +61,7 @@ const STYLES = { ...(clickable ? { cursor: "pointer", - "&:hover": { backgroundColor: APP_COLORS.HOVER.color }, + "&:hover": { filter: "brightness(0.92) !important" }, "&:active": { backgroundColor: APP_COLORS.ACTIVE.color } } : {}) @@ -69,7 +69,21 @@ const STYLES = { ICON: (state, userColor) => ({ fontSize: "50px", ...getColor(state, userColor) }), HINT_ICON: (state, userColor) => ({ fontSize: "1rem", ...getColor(state, userColor) }), VALUE_CAPTION_STACK: { containerType: "inline-size", width: "100%", overflow: "hidden" }, - CAPTION_TYPOGRAPHY: { width: "99cqw" } + CAPTION_TYPOGRAPHY: clickable => ({ + width: "99cqw", + ...(clickable + ? { + cursor: "pointer" + } + : {}) + }), + VALUE_TYPOGRAPHY: clickable => ({ + ...(clickable + ? { + cursor: "pointer" + } + : {}) + }) }; //----------------------- @@ -98,6 +112,8 @@ const P8PIndicator = ({ variant = P8P_INDICATOR_VARIANT.ELEVATION, hint = null, onClick = null, + onValueClick = null, + onCaptionClick = null, backgroundColor = null, color = null } = {}) => { @@ -114,14 +130,39 @@ const P8PIndicator = ({ }; //При нажатии на кнопку закрытия подсказки - const handleHintClose = () => setShowHint(false); + const handleHintClose = e => { + setShowHint(false); + e.stopPropagation(); + }; + + //При нажатии на заголовок + const handleCaptionClick = event => { + //Если есть функция нажатия на заголовок + onCaptionClick && onCaptionClick(event); + }; + + //При нажатии на значение + const handleValueClick = event => { + //Если есть функция нажатия на заголовок + onValueClick && onValueClick(event); + }; //Представление текста значения индикатора - const valueTextView = {[undefined, null, ""].includes(value) ? TEXTS.NO_DATA_FOUND_SHORT : value}; + const valueTextView = ( + + {[undefined, null, ""].includes(value) ? TEXTS.NO_DATA_FOUND_SHORT : value} + + ); //Представление текста подписи индикатора const captionView = ( - + {caption} ); @@ -175,6 +216,8 @@ P8PIndicator.propTypes = { variant: PropTypes.oneOf(Object.values(P8P_INDICATOR_VARIANT)), hint: PropTypes.string, onClick: PropTypes.func, + onValueClick: PropTypes.func, + onCaptionClick: PropTypes.func, backgroundColor: PropTypes.string, color: PropTypes.string }; diff --git a/app/core/utils.js b/app/core/utils.js index 56f02f1..ab6fe97 100644 --- a/app/core/utils.js +++ b/app/core/utils.js @@ -195,6 +195,9 @@ const genGUID = () => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16) ); +//Формироваеие уникального идентификатора по текущему времени +const genUID = prefix => (prefix ? `${prefix}_${Date.now()}` : `${Date.now()}`); + //---------------- //Интерфейс модуля //---------------- @@ -211,5 +214,6 @@ export { formatDateJSONDateOnly, formatNumberRFCurrency, formatErrorMessage, - genGUID + genGUID, + genUID }; diff --git a/app/panels/panels_editor/component_view.js b/app/panels/panels_editor/component_view.js index 2cac128..14b25b7 100644 --- a/app/panels/panels_editor/component_view.js +++ b/app/panels/panels_editor/component_view.js @@ -12,24 +12,35 @@ import PropTypes from "prop-types"; //Контроль свойств компо import { Box, Typography } from "@mui/material"; //Интерфейсные элементы import { useComponentModule } from "./components/components_hooks"; //Хуки компонентов +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + COMPONENT_BOX: isDragging => { + return isDragging ? { pointerEvents: "none" } : {}; + } +}; + //----------- //Тело модуля //----------- //Представление компонента панели -const ComponentView = ({ id, path, settings = {}, values = {}, onValuesChange = null } = {}) => { +const ComponentView = ({ id, isDragging, path, settings = {}, values = {}, onValuesChange = null } = {}) => { //Подгрузка модуля представления компонента (lazy здесь постоянно обновлялся при смене props, поэтому на хуке, от props независимого) const [ComponentView, init] = useComponentModule({ path, module: "view" }); //При смене значений - const handleValuesChange = values => onValuesChange && onValuesChange(id, values); + const handleValuesChange = values => onValuesChange && onValuesChange(values); //Расчёт флага наличия компонента const haveComponent = path ? true : false; //Формирование представления return ( - + {haveComponent && init && } {!haveComponent && Компонент не определён} @@ -39,6 +50,7 @@ const ComponentView = ({ id, path, settings = {}, values = {}, onValuesChange = //Контроль свойств компонента - компонент панели ComponentView.propTypes = { id: PropTypes.string.isRequired, + isDragging: PropTypes.bool.isRequired, path: PropTypes.string.isRequired, settings: PropTypes.object, values: PropTypes.object, diff --git a/app/panels/panels_editor/components/chart/action.js b/app/panels/panels_editor/components/chart/action.js new file mode 100644 index 0000000..dd4e334 --- /dev/null +++ b/app/panels/panels_editor/components/chart/action.js @@ -0,0 +1,51 @@ +/* + Парус 8 - Панели мониторинга - Редактор панелей + Компоненты: График (общие ресурсы действия) +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +//--------- +//Константы +//--------- + +//Собственные значения типа действия +const P8P_CA_CHART_TYPE_VALUE = { + GRPH_ELEMENT: "Элемент графика" +}; + +//Типы значений действий графика +const P8P_CA_CHART_VALUE_TYPES = [...Object.values(P8P_CA_CHART_TYPE_VALUE)]; + +//Доступные области действий графика +const P8P_CA_CHART_ACTION_AREAS = [ + { name: "Компонент", area: "component", hasElement: false }, + { name: "Элемент графика", area: "chart_item", hasElement: false } +]; + +//------------------------------------ +//Вспомогательные функции и компоненты +//------------------------------------ + +//Получение значения собственного типа действия +const getChartCustomTypeValue = ({ type, value, prms }) => { + //Если это элемента графика - возвращаем нужное значение, иначе - null + return type === P8P_CA_CHART_TYPE_VALUE.GRPH_ELEMENT ? prms.item[value] : null; +}; + +//Считывание обработчиков графика +const getChartHandlers = handlers => { + //Возвращаем объект хэндлеров (имеют формат "[Область].[Сегмент]", где [Сегмент] - необязательный) + return { + onComponentClick: handlers["component."]?.fn, + onChartItemClick: handlers["chart_item."]?.fn + }; +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8P_CA_CHART_VALUE_TYPES, P8P_CA_CHART_ACTION_AREAS, getChartCustomTypeValue, getChartHandlers }; diff --git a/app/panels/panels_editor/components/chart/editor.js b/app/panels/panels_editor/components/chart/editor.js index 5cf3379..0ab0317 100644 --- a/app/panels/panels_editor/components/chart/editor.js +++ b/app/panels/panels_editor/components/chart/editor.js @@ -13,32 +13,46 @@ import { P8PEditorBox } from "../../../../components/editors/p8p_editor_box"; // import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Заголовок раздела редактора import { P8P_DATA_SOURCE_SHAPE } from "../../../../components/editors/p8p_data_source_common"; //Общие ресурсы источника данных import { P8PDataSource } from "../../../../components/editors/p8p_data_source"; //Источник данных +import { P8P_CA_SHAPE, P8P_CAS_INITIAL } from "../../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий +import { P8P_CA_CHART_VALUE_TYPES, P8P_CA_CHART_ACTION_AREAS } from "./action"; //Общие ресурсы действий графика +import { P8PActions } from "../../../../components/editors/p8p_actions"; //Действия //----------- //Тело модуля //----------- //График (редактор настроек) -const ChartEditor = ({ id, dataSource = null, valueProviders = {}, onSettingsChange = null } = {}) => { +const ChartEditor = ({ id, dataSource = null, valueProviders = {}, actions = P8P_CAS_INITIAL, onSettingsChange = null } = {}) => { //Собственное состояние - текущие настройки const [settings, setSettings] = useState(null); - //При изменении компонента - useEffect(() => { - settings?.id != id && setSettings({ id, dataSource }); - }, [settings, id, dataSource]); - //При сохранении изменений элемента const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } })); + //При изменении действий + const handleActionsChange = actions => setSettings(pv => ({ ...pv, actions })); + //При сохранении настроек const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor }); + //При изменении компонента + useEffect(() => { + settings?.id != id && setSettings({ id, dataSource, actions }); + }, [settings, id, dataSource, actions]); + //Формирование представления return ( + + ); }; @@ -48,6 +62,7 @@ ChartEditor.propTypes = { id: PropTypes.string.isRequired, dataSource: P8P_DATA_SOURCE_SHAPE, valueProviders: PropTypes.object, + actions: PropTypes.arrayOf(P8P_CA_SHAPE), onSettingsChange: PropTypes.func }; diff --git a/app/panels/panels_editor/components/chart/view.js b/app/panels/panels_editor/components/chart/view.js index 3a31c68..104a9e4 100644 --- a/app/panels/panels_editor/components/chart/view.js +++ b/app/panels/panels_editor/components/chart/view.js @@ -11,13 +11,16 @@ import React from "react"; //Классы React import PropTypes from "prop-types"; //Контроль свойств компонента import { Paper } from "@mui/material"; //Интерфейсные элементы import { P8PChart } from "../../../../components/p8p_chart"; //График -import { useDataSource } from "../../../../components/editors/p8p_data_source_hooks"; //Хуки для данных +import { useDataSource, useComponentHandlers } from "../../../../components/editors/p8p_data_source_hooks"; //Хуки для данных import { P8P_DATA_SOURCE_SHAPE } from "../../../../components/editors/p8p_data_source_common"; //Общие ресурсы источника данных import { P8P_COMPONENT_INLINE_MESSAGE_TYPE, P8P_COMPONENT_INLINE_MESSAGE, P8PComponentInlineMessage } from "../../../../components/editors/p8p_component_inline_message"; //Информационное сообщение внутри компонента +import { P8P_COMPONENT_SETTINGS_RESP_ARGS } from "../../../../components/editors/p8p_component_settings"; //Дополнительный функционал источников данных +import { P8P_CA_SHAPE } from "../../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий +import { getChartCustomTypeValue, getChartHandlers } from "./action"; //Общие ресурсы действий графика //--------- //Константы @@ -31,7 +34,14 @@ const COMPONENT_NAME = "График"; //Стили const STYLES = { - CHART: { width: "100%", height: "100%", alignItems: "center", justifyContent: "center", display: "flex" } + CHART: { width: "100%", height: "100%", alignItems: "center", justifyContent: "center", display: "flex" }, + CONTAINER: clickable => ({ + ...(clickable + ? { + cursor: "pointer" + } + : {}) + }) }; //----------- @@ -39,24 +49,31 @@ const STYLES = { //----------- //График (представление) -const Chart = ({ dataSource = null, values = {} } = {}) => { +const Chart = ({ dataSource = null, values = {}, actions = [], onValuesChange = null } = {}) => { //Собственное состояние - данные - const [data, error] = useDataSource({ dataSource, values }); + const [chart, error, haveConfing, haveData] = useDataSource({ dataSource, values, componentRespArg: P8P_COMPONENT_SETTINGS_RESP_ARGS.CHART }); - //Флаг настроенности графика - const haveConfing = dataSource?.stored ? true : false; + //Собственное состояние - обработчики компонента + const [handlers] = useComponentHandlers({ actions, onValuesChange, getCustomTypeValue: getChartCustomTypeValue }); - //Флаг наличия данных - const haveData = data?.init === true && !error ? true : false; - - //Данные графика - const chart = data?.XCHART || {}; + //Обработчики областей + const { onComponentClick, onChartItemClick } = getChartHandlers(handlers); //Формирование представления return ( - + onComponentClick && onComponentClick({ event, values })} + > {haveConfing && haveData ? ( - + onChartItemClick && onChartItemClick({ values, prms })} + /> ) : ( { //Контроль свойств компонента - График (представление) Chart.propTypes = { dataSource: P8P_DATA_SOURCE_SHAPE, - values: PropTypes.object + values: PropTypes.object, + actions: PropTypes.arrayOf(P8P_CA_SHAPE), + onValuesChange: PropTypes.func }; //---------------- diff --git a/app/panels/panels_editor/components/components.js b/app/panels/panels_editor/components/components.js index a17c301..cd0ee86 100644 --- a/app/panels/panels_editor/components/components.js +++ b/app/panels/panels_editor/components/components.js @@ -7,123 +7,51 @@ //Константы //--------- -const COMPONETNS = [ +const COMPONENTS = [ { name: "Форма", path: "form", - settings2: { - title: "Параметры формирования", - autoApply: true, - items: [ - { - name: "AGENT", - caption: "Контрагент", - unitCode2: "AGNLIST", - unitName: "Контрагенты", - showMethod: "main", - showMethodName: "main", - parameter: "Мнемокод", - inputParameter: "in_AGNABBR", - outputParameter: "out_AGNABBR" - }, - { - name: "DOC_TYPE", - caption: "Тип документа", - unitCode2: "DOCTYPES", - unitName: "Типы документов", - showMethod: "main", - showMethodName: "main", - parameter: "Мнемокод", - inputParameter: "in_DOCCODE", - outputParameter: "out_DOCCODE" - } - ] + settings: { + id: "", + title: "", + autoApply: false, + orientation: "v", + items: [] } }, { name: "График", path: "chart", - settings2: { - dataSource: { - type: "USER_PROC", - userProc: "ГрафТоп5ДогКонтрТип", - stored: "UDO_P_P8P_AGNCONTR_CHART", - respArg: "COUT", - arguments: [ - { name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" }, - { name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" } - ] - } + settings: { + id: "", + dataSource: {}, + actions: [] } }, { name: "Таблица", path: "table", - settings2: { - dataSource: { - type: "USER_PROC", - userProc: "ТаблицаДогКонтрТип", - stored: "UDO_P_P8P_AGNCONTR_TABLE", - respArg: "COUT", - arguments: [ - { name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" }, - { name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" } - ] - } + settings: { + id: "", + dataSource: {}, + actions: [], + conditions: [] } }, { name: "Индикатор", path: "indicator", - settings2: { - dataSource: { - type: "USER_PROC", - userProc: "ИндКолДогКонтрТип", - stored: "UDO_P_P8P_AGNCONTR_IND", - respArg: "COUT", - arguments: [ - { name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" }, - { name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" }, - { - name: "NIND_TYPE", - caption: "Тип индикатора (0 - все, 1 - неутвержденные)", - dataType: "NUMB", - req: true, - value: "0", - valueSource: "" - } - ] - } - } - } /*, - { - name: "Индикатор2", - path: "indicator", settings: { - dataSource: { - type: "USER_PROC", - userProc: "ИндКолДогКонтрТип", - stored: "UDO_P_P8P_AGNCONTR_IND", - respArg: "COUT", - arguments: [ - { name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" }, - { name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" }, - { - name: "NIND_TYPE", - caption: "Тип индикатора (0 - все, 1 - неутвержденные)", - dataType: "NUMB", - req: true, - value: "1", - valueSource: "" - } - ] - } + id: "", + dataSource: {}, + actions: [], + conditions: [] } - }*/ + } ]; //---------------- //Интерфейс модуля //---------------- -export { COMPONETNS }; +export { COMPONENTS }; diff --git a/app/panels/panels_editor/components/components_hooks.js b/app/panels/panels_editor/components/components_hooks.js index e539f52..3dc3537 100644 --- a/app/panels/panels_editor/components/components_hooks.js +++ b/app/panels/panels_editor/components/components_hooks.js @@ -7,7 +7,140 @@ //Подключение библиотек //--------------------- -import { useState, useEffect } from "react"; //Классы React +import { useState, useEffect, useContext, useCallback, useLayoutEffect } from "react"; //Классы React +import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером +import { MessagingСtx } from "../../../context/messaging"; //Контекст сообщений +import { object2Base64XML, genUID, xml2JSON } from "../../../core/utils"; //Вспомогательные функции +import { exportXMLFile } from "../layouts"; //Дополнительная разметка и вёрстка клиентских элементов +import { P8P_COMPONENT_SETTINGS_PATHS } from "../../../components/editors/p8p_component_settings"; //Дополнительные настройки источников +import { getActionsVariables } from "../../../components/editors/p8p_component_action/util"; //Вспомогательный функционал действий компонентов +import { P8P_CA_TYPE } from "../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий + +//--------- +//Константы +//--------- + +//Начальное состояние размера макета +const INITIAL_BREAKPOINT = "lg"; + +//Начальное состояние макета +const INITIAL_LAYOUTS = { + [INITIAL_BREAKPOINT]: [] +}; + +//--------- +//Константы расчета высоты строки ResponsiveGridLayout +//--------- + +//Структура параметров для расчета высоты строки ResponsiveGridLayout +const GRID_LAYOUT_PARAMS = { + //Высота главного меню (px) + APP_BAR_HEIGHT: 64, + //Дополнительный отступ (px) + SAFE_OFFSET: 20, + //Максимальное количество строк (число) + MAX_ROWS: 68, + //Высота отступа (px) + MARGIN: 10 +}; + +//------------------------------------ +//Вспомогательные функции и компоненты +//------------------------------------ + +//Проверка на массив загружаемых данных панели +const isArrayPanelDesc = (name, jPath) => + ["items", "arguments", "conditions", "actions", "dependencies", "inputParams"].includes(name) || /(.*)XLAYOUTS\.[^.]*$/.test(jPath); + +//Обработка значений тэгов загружаемых данных панели +const tagValueProcessorPanelDesc = (name, val, jPath) => + ["condValue", "resValue", "description"].includes(name) + ? undefined + : /(.*)dataSource.arguments.value$/.test(jPath) || /(.*)XVALUE_PROVIDERS(.*)$/.test(jPath) + ? undefined + : val; + +//Конвертация серверного описания компонентов в данные для редактора панелей +const convertServerData2Components = components => { + //Корректировка информации о действия (Для типа "setVariable" значение ключа "params" - обязательно массив, в иных случаях - объект) + const correctionActions = actions => { + return actions.reduce( + (prevActions, action) => [ + ...prevActions, + { + ...(action.type === P8P_CA_TYPE.setVariable.code && !Array.isArray(action.params) + ? { ...action, params: [{ ...action.params }] } + : { ...action }) + } + ], + [] + ); + }; + + //Форматируем устанавливая пустой объект для dataSource, если он пуст + return Object.keys(components).reduce( + (prev, cur) => ({ + ...prev, + [cur]: { + ...components[cur], + settings: { + ...components[cur].settings, + //dataSource - обязательно объект + ...(components[cur].settings?.dataSource ? { dataSource: components[cur].settings.dataSource || {} } : {}), + //actions - требуют корректировки + ...(components[cur].settings?.actions + ? { + actions: correctionActions(components[cur].settings.actions) + } + : {}) + } + } + }), + {} + ); +}; + +//Конвертация серверного описания проводников значений в данные для редактора панелей +const convertServerData2ValueProviders = valueProviders => { + //Форматируем инициализируя dependencies, если требуется + return Object.keys(valueProviders).reduce( + (prev, cur) => ({ ...prev, [cur]: { ...valueProviders[cur], dependencies: valueProviders[cur].dependencies || [] } }), + {} + ); +}; + +//Конвертация серверного описания панели в данные для редактора панелей +const serverPanelData2PanelDesc = (components, valueProviders, layouts, breakpoint) => { + //Возвращаем информацию о панеле с учетом конвертаций + return { + components: convertServerData2Components(components), + valueProviders: convertServerData2ValueProviders(valueProviders), + layouts: layouts[breakpoint] ? { [breakpoint]: [...layouts[breakpoint]] } : INITIAL_LAYOUTS, + breakpoint: breakpoint + }; +}; + +//Считывание общего списка зависимостей от проводника значений +const getValueProvidersLinks = (componentPath, settings) => { + //Если это индикатор/график/таблица + if ([P8P_COMPONENT_SETTINGS_PATHS.INDICATOR, P8P_COMPONENT_SETTINGS_PATHS.CHART, P8P_COMPONENT_SETTINGS_PATHS.TABLE].includes(componentPath)) { + //Собираем зависимости из настройки источника + let argumentsSources = Array.isArray(settings?.dataSource?.arguments) + ? settings.dataSource.arguments.reduce((prev, cur) => (cur.valueSource ? [...prev, cur.valueSource] : [...prev]), []) + : []; + //Собираем зависимости из параметров действий + let actionsSources = Array.isArray(settings?.actions) ? getActionsVariables(settings.actions) : []; + //Возвращаем зависимости компонента + return [...new Set([...argumentsSources, ...actionsSources])]; + } + //Если это форма + if (P8P_COMPONENT_SETTINGS_PATHS.FORM === componentPath) { + //Собираем зависимости из элементов формы + let items = Array.isArray(settings?.items) ? settings.items.map(item => item.name) : []; + //Возвращаем зависимости компонента + return [...new Set(items)]; + } +}; //----------- //Тело модуля @@ -37,8 +170,470 @@ const useComponentModule = ({ path = null, module = "view" } = {}) => { return [componentModule, init]; }; +//Работа с панелью +const usePanel = () => { + //Собственное состояние - рабочая панель + const [panel, setPanel] = useState(); + //Собственное состояние - имя рабочей панели + const [panelName, setPanelName] = useState(null); + //Собственное состояние - наличие изменений рабочей панели + const [isPanelChanged, setIsPanelChanged] = useState(false); + //Собственное состояние - возможность редактирования + const [isEditAvaliable, setIsEditAvaliable] = useState(true); + //Собственное состояние - режим редактирования + const [editMode, setEditMode] = useState(true); + + //Подключение к контексту взаимодействия с сервером + const { executeStored } = useContext(BackEndСtx); + + //При необходимости загрузки информации о панели + const loadPanel = async panel => { + //Считываем информацию с сервера + const res = await executeStored({ + stored: "PKG_P8PANELS_PE.PANEL_ATTRS_GET_BY_CODE", + args: { + SCODE: panel + }, + respArg: "COUT", + loader: true + }); + //Устанавливаем рабочую панель + setPanel(res.XPANEL_ATTRS.rn); + //Устанавливаем наименование рабочей панели + setPanelName(res.XPANEL_ATTRS.name); + //Сбрасываем наличие изменений рабочей панели + setIsPanelChanged(false); + //Загружаемую панель запрещено редактировать + setIsEditAvaliable(false); + setEditMode(false); + }; + + //При выборе панели + const selectPanel = (panel, panelName, isPanelEditAvaliable) => { + //Обновляем информацию о панели + setPanel(panel); + setPanelName(panelName); + setIsEditAvaliable(isPanelEditAvaliable); + setEditMode(isPanelEditAvaliable); + setIsPanelChanged(false); + }; + + //При закрытии панели + const closePanel = () => { + setPanel(null); + setPanelName(null); + setIsPanelChanged(false); + }; + + //При установке признака изменений панели + const setPanelChanged = isChanged => setIsPanelChanged(isChanged); + + //При установке признака режима редактирования + const changeEditMode = isEditMode => setEditMode(isEditMode); + + //Возвращаем интерфейс хука + return [panel, panelName, editMode, isEditAvaliable, isPanelChanged, loadPanel, selectPanel, closePanel, changeEditMode, setPanelChanged]; +}; + +//Работа с менеджером панелей +const usePanelManager = () => { + //Собственное состояние - флаг необходимости обновления + const [refresh, setRefresh] = useState(true); + + //Собственное состояние - данные + const [data, setData] = useState(null); + + //Подключение к контексту взаимодействия с сервером + const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx); + + //Добавление панели + const insertPanel = useCallback( + async (code, name) => { + await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_INSERT", args: { SCODE: code, SNAME: name }, loader: false }); + setRefresh(true); + }, + [executeStored] + ); + + //Изменение панели + const updatePanel = useCallback( + async (query, code, name) => { + await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_UPDATE", args: { NRN: query, SCODE: code, SNAME: name }, loader: false }); + setRefresh(true); + }, + [executeStored] + ); + + //Удаление панели + const deletePanel = useCallback( + async query => { + await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_DELETE", args: { NRN: query }, loader: false }); + setRefresh(true); + }, + [executeStored] + ); + + //Установка флага готовности панели + const setPanelReady = useCallback( + async (query, ready) => { + await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_READY_SET", args: { NRN: query, NREADY: ready }, loader: false }); + setRefresh(true); + }, + [executeStored] + ); + + //Установка флага публичности панели + const setPanelPbl = useCallback( + async (query, pbl) => { + await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_PBL_SET", args: { NRN: query, NPBL: pbl }, loader: false }); + setRefresh(true); + }, + [executeStored] + ); + + //Импорт новой панели + const importPanel = useCallback( + async fileData => { + await executeStored({ + stored: "PKG_P8PANELS_PE.PANEL_IMPORT", + args: { + CPANEL: { + //Форматируем данные документа в base64 + VALUE: btoa(unescape(encodeURIComponent(fileData))), + SDATA_TYPE: SERV_DATA_TYPE_CLOB + } + }, + loader: true + }); + setRefresh(true); + }, + [SERV_DATA_TYPE_CLOB, executeStored] + ); + + //При необходимости получить/обновить данные + useEffect(() => { + //Загрузка данных с сервера + const loadData = async () => { + try { + const data = await executeStored({ + stored: "PKG_P8PANELS_PE.PANEL_LIST", + respArg: "COUT", + isArray: name => ["XPANEL"].includes(name), + attributeValueProcessor: (name, val) => (["code", "name"].includes(name) ? undefined : val), + loader: true + }); + setData(data?.XPANELS?.XPANEL || []); + } finally { + setRefresh(false); + } + }; + //Если надо обновить + if (refresh) + //Получим данные + loadData(); + }, [refresh, executeStored]); + + //Возвращаем интерфейс хука + return [data, insertPanel, updatePanel, deletePanel, setPanelReady, setPanelPbl, importPanel]; +}; + +//Работа с содержимым панели +const usePanelDesc = panel => { + //Собственное состояние - флаг инициализированности + const [isInit, setInit] = useState(false); + + //Собственное состояние - флаг необходимости обновления + const [refresh, setRefresh] = useState(true); + + //Собственное состояние - данные + const [data, setData] = useState({ + components: {}, + valueProviders: {}, + layouts: INITIAL_LAYOUTS, + breakpoint: INITIAL_BREAKPOINT + }); + + //Подключение к контексту взаимодействия с сервером + const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx); + + //Подключение к контексту сообщений + const { showMsgErr } = useContext(MessagingСtx); + + //Считывание базовой информации о панели + const getPanelInfo = useCallback(async () => { + //Считываем информацию с сервера + const res = await executeStored({ + stored: "PKG_P8PANELS_PE.PANEL_ATTRS_GET", + args: { + NRN: panel + }, + respArg: "COUT", + loader: true + }); + //Забираем все без RN + // eslint-disable-next-line no-unused-vars + const { rn, ...panelAttrs } = res.XPANEL_ATTRS; + //Возвращаем результат + return panelAttrs; + }, [executeStored, panel]); + + //Формирования данных панели для выгрузки + const makeXMLPanelDesc = useCallback( + async (includePanelInfo = false, isBase64 = true) => { + //Считываем информацию о выгружаемой панели + const panelInfo = includePanelInfo ? await getPanelInfo() : {}; + //Сформируем данные в формат XML + const xmlData = object2Base64XML( + { + XPANEL: { + ...(includePanelInfo ? { XPANEL_INFO: panelInfo } : {}), + XCOMPONENTS: data.components, + XVALUE_PROVIDERS: data.valueProviders, + XLAYOUTS: data.layouts, + XOPTIONS: { + breakpoint: data.breakpoint + } + } + }, + { suppressEmptyNode: false } + ); + //Возвращаем данные в формате XML (формат исходит от признака) + return isBase64 ? xmlData : decodeURIComponent(escape(atob(xmlData))); + }, + [data.breakpoint, data.components, data.layouts, data.valueProviders, getPanelInfo] + ); + + //Добавление компонента в макет + const addComponent = component => { + //Генерируем ID + const id = genUID(component.path); + //Добавляем компонент и его макет + setData(pv => ({ + ...pv, + components: { ...pv.components, [id]: { ...component, settings: { ...component.settings, id: id } } }, + layouts: { ...pv.layouts, [data.breakpoint]: [...pv.layouts[data.breakpoint], { i: id, x: 0, y: 0, w: 4, h: 10 }] } + })); + }; + + //Удаление компонента из макета + const deleteComponent = id => { + //Удаляем все старые зависимости от компонента + const newValueProviders = Object.keys(data.valueProviders).reduce( + (prev, cur) => ({ + ...prev, + [cur]: { ...data.valueProviders[cur], dependencies: data.valueProviders[cur].dependencies.filter(el => el !== id) } + }), + {} + ); + //Обновляем данные + setData(pv => ({ + ...pv, + layouts: { ...pv.layouts, [data.breakpoint]: data.layouts[data.breakpoint].filter(item => item.i !== id) }, + components: { ...pv.components, [id]: { ...pv.components[id], deleted: true } }, + valueProviders: { ...newValueProviders } + })); + }; + + //Изменение размера холста + const breakpointChange = breakpoint => setData(pv => ({ ...pv, breakpoint: breakpoint })); + + //Изменение состояния макета + const layoutsChange = layouts => setData(pv => ({ ...pv, layouts: layouts })); + + //Изменение значений в компоненте + const changeValueProviders = values => { + //Считываем проводники + const newValueProviders = { ...data.valueProviders }; + //Переносим новые значения в проводники + Object.keys(values).map(el => (newValueProviders[el].value = values[el])); + //Обновляем проводники + setData(pv => ({ ...pv, valueProviders: { ...newValueProviders } })); + }; + + //Изменение настроек компонента + const changeComponentSettings = (id = null, settings = {}, onChanged = null) => { + //Считываем новые зависимости компонента + const providedValues = getValueProvidersLinks(data.components[id].path, settings); + //Удаляем все старые зависимости от компонента + const newValueProviders = Object.keys(data.valueProviders).reduce( + (prev, cur) => ({ + ...prev, + [cur]: { ...data.valueProviders[cur], dependencies: data.valueProviders[cur].dependencies.filter(el => el !== id) } + }), + {} + ); + //Добавляем новые зависимости + providedValues.map(providerName => newValueProviders[providerName].dependencies.push(id)); + //Обновляем данные + setData(pv => ({ + ...pv, + components: { ...pv.components, [id]: { ...pv.components[id], settings: { ...settings } } }, + valueProviders: { ...newValueProviders } + })); + //Выполняем действия после изменения + onChanged && onChanged(); + }; + + //Изменение настроек панели + const changePanelSettings = valueProviders => { + //Обновляем данные + setData(pv => ({ + ...pv, + valueProviders: { ...valueProviders } + })); + }; + + //Сохранение описания панели + const savePanelDesc = useCallback( + async (callBack = null) => { + try { + await executeStored({ + stored: "PKG_P8PANELS_PE.PANEL_DESC_SET", + args: { + NRN: panel, + CPANEL: { + VALUE: await makeXMLPanelDesc(), + SDATA_TYPE: SERV_DATA_TYPE_CLOB + } + }, + loader: true + }); + callBack && callBack(false); + } catch (e) { + callBack && callBack(true); + } + }, + [SERV_DATA_TYPE_CLOB, executeStored, makeXMLPanelDesc, panel] + ); + + //Загрузка описания панели из файла + const importPanelDesc = async (fileData, callBack = null) => { + //Формируем данные панели + const panelData = await xml2JSON({ + xmlDoc: fileData, + isArray: isArrayPanelDesc, + tagValueProcessor: tagValueProcessorPanelDesc + }); + //Если файл содержит тэг XPANEL + if (panelData.XPANEL) { + setData( + serverPanelData2PanelDesc( + panelData.XPANEL?.XCOMPONENTS || {}, + panelData.XPANEL?.XVALUE_PROVIDERS || {}, + panelData.XPANEL?.XLAYOUTS || {}, + panelData.XPANEL?.XOPTIONS?.breakpoint || INITIAL_BREAKPOINT + ) + ); + callBack && callBack(true); + } else { + showMsgErr("Загружаемые данные не соответствуют формату настройки панели."); + callBack && callBack(false); + } + }; + + //Выгрузка панели в файл + const exportPanelDesc = async panelName => { + //Формируем XML-представление панели + const xmlPanelDesc = await makeXMLPanelDesc(true, false); + //Выгружаем в файл + exportXMLFile(xmlPanelDesc, panelName); + }; + + //При необходимости получить/обновить данные + useEffect(() => { + //Загрузка данных с сервера + const loadData = async () => { + try { + const data = await executeStored({ + stored: "PKG_P8PANELS_PE.PANEL_DESC_GET", + args: { NRN: panel }, + respArg: "COUT", + isArray: isArrayPanelDesc, + tagValueProcessor: tagValueProcessorPanelDesc, + loader: true + }); + setData( + serverPanelData2PanelDesc( + data?.XCOMPONENTS || {}, + data?.XVALUE_PROVIDERS || {}, + data?.XLAYOUTS || {}, + data?.XOPTIONS?.breakpoint || INITIAL_BREAKPOINT + ) + ); + setInit(true); + } finally { + setRefresh(false); + } + }; + //Если надо обновить + if (refresh) + if (panel) + //Если есть для чего получать данные + loadData(); + //Нет идентификатора запроса - нет данных + else + setData({ + components: {}, + valueProviders: {}, + layouts: INITIAL_LAYOUTS, + breakpoint: INITIAL_BREAKPOINT + }); + }, [refresh, panel, executeStored]); + + //При изменении входных свойств - поднимаем флаг обновления + useEffect(() => { + setInit(false); + setRefresh(true); + }, [panel]); + + //Возвращаем интерфейс хука + return [ + data, + isInit, + addComponent, + deleteComponent, + breakpointChange, + layoutsChange, + changeValueProviders, + changeComponentSettings, + changePanelSettings, + savePanelDesc, + importPanelDesc, + exportPanelDesc + ]; +}; + +//Работа с соотношением размеров +const useWindowResize = () => { + //Состояние высоты строки ResponsiveGridLayout + const [rowHeight, setRowHeight] = useState(1); + //При изменении размера + useLayoutEffect(() => { + //Расчет высоты строки + const updateRowHeight = () => { + //Определяем доступную область + const availableHeight = window.innerHeight - GRID_LAYOUT_PARAMS.APP_BAR_HEIGHT - GRID_LAYOUT_PARAMS.SAFE_OFFSET; + //Промежутки между рядами + const totalMargins = (GRID_LAYOUT_PARAMS.MAX_ROWS - 1) * GRID_LAYOUT_PARAMS.MARGIN; + //Рассчитываем высоту строки + const result = (availableHeight - totalMargins) / GRID_LAYOUT_PARAMS.MAX_ROWS; + //Устанавливаем + setRowHeight(result); + }; + //Запускаем при открытии панели + updateRowHeight(); + //Устанавливаем обработчик на событие + window.addEventListener("resize", updateRowHeight); + //Удаляем обработчик на событие + return () => window.removeEventListener("resize", updateRowHeight); + }, []); + + //Возвращаем интерфейс хука + return [rowHeight]; +}; + //---------------- //Интерфейс модуля //---------------- -export { useComponentModule }; +export { useComponentModule, usePanel, usePanelManager, usePanelDesc, useWindowResize }; diff --git a/app/panels/panels_editor/components/form/common.js b/app/panels/panels_editor/components/form/common.js index c076bfb..3a0a598 100644 --- a/app/panels/panels_editor/components/form/common.js +++ b/app/panels/panels_editor/components/form/common.js @@ -47,3 +47,6 @@ export const ORIENTATION = { H: "H", V: "v" }; + +//Доступность сохранения настроек элемента +export const isItemOkDisabled = item => (!item.name ? true : false); diff --git a/app/panels/panels_editor/components/form/editor.js b/app/panels/panels_editor/components/form/editor.js index 4020b6c..46b4631 100644 --- a/app/panels/panels_editor/components/form/editor.js +++ b/app/panels/panels_editor/components/form/editor.js @@ -16,6 +16,7 @@ import { Button, Icon, Select, + Menu, MenuItem, FormControl, InputLabel, @@ -31,7 +32,8 @@ import { P8PEditorBox } from "../../../../components/editors/p8p_editor_box"; // import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Заголовок раздела редактора import { P8PConfigDialog } from "../../../../components/editors/p8p_config_dialog"; //Диалог настройки import { STYLES as COMMON_STYLES } from "../../../../components/editors/p8p_editors_common"; //Общие ресурсы редакторов -import { ITEM_SHAPE, ITEM_INITIAL, ITEMS_INITIAL, ORIENTATION } from "./common"; //Общие ресурсы и константы формы +import { TITLES } from "../../../../../app.text"; //Общие текстовые ресурсы +import { ITEM_SHAPE, ITEM_INITIAL, ITEMS_INITIAL, ORIENTATION, isItemOkDisabled } from "./common"; //Общие ресурсы и константы формы //--------- //Константы @@ -47,10 +49,13 @@ const STYLES = { //------------------------------------ //Редактор элемента -const ItemEditor = ({ item = null, onOk = null, onCancel = null } = {}) => { +const ItemEditor = ({ item = null, valueProviders = {}, onOk = null, onCancel = null } = {}) => { //Собственное состояние - параметры элемента формы const [state, setState] = useState({ ...ITEM_INITIAL, ...item }); + //Собственное состояние - элемент привязки меню выбора источника + const [valueProvidersMenuAnchorEl, setValueProvidersMenuAnchorEl] = useState(null); + //Подключение к контексту приложения const { pOnlineShowDictionary } = useContext(ApplicationСtx); @@ -123,11 +128,74 @@ const ItemEditor = ({ item = null, onOk = null, onCancel = null } = {}) => { }); }; + //Открытие/сокрытие меню выбора источника + const toggleValueProvidersMenu = target => setValueProvidersMenuAnchorEl(target instanceof Element ? target : null); + + //При очистке значения/связывания параметра + const handleParamClearClick = () => setState(pv => ({ ...pv, name: "" })); + + //При отображении меню связывания параметра с поставщиком данных + const handleParamLinkMenuClick = e => setValueProvidersMenuAnchorEl(e.currentTarget); + + //При выборе элемента меню связывания аргумента с поставщиком данных + const handleParamLinkClick = valueSource => { + //Устанавливаем выбранный параметр + setState(pv => ({ ...pv, name: valueSource })); + //Закрываем меню + toggleValueProvidersMenu(); + }; + + //Список значений + const values = Object.keys(valueProviders); + + //Наличие значений + const isValues = values && values.length > 0 ? true : false; + + //Меню привязки к поставщикам значений + const valueProvidersMenu = isValues && ( + + {values.map((value, i) => ( + handleParamLinkClick(value)}> + {value} + + ))} + + ); + //Формирование представления return ( - + - + {valueProvidersMenu} + + + clear + + {isValues && ( + + settings_ethernet + + )} + + ) + }} + /> { //Контроль свойств - редактор элемента ItemEditor.propTypes = { item: ITEM_SHAPE, + valueProviders: PropTypes.object, onOk: PropTypes.func, onCancel: PropTypes.func }; @@ -191,13 +260,18 @@ ItemEditor.propTypes = { //----------- //Форма (редактор настроек) -const FormEditor = ({ id, title = "", orientation = ORIENTATION.V, autoApply = false, items = ITEMS_INITIAL, onSettingsChange = null } = {}) => { +const FormEditor = ({ + id, + title = "", + orientation = ORIENTATION.V, + autoApply = false, + items = ITEMS_INITIAL, + valueProviders = {}, + onSettingsChange = null +} = {}) => { //Собственное состояние - текущие настройки const [settings, setSettings] = useState(null); - //Собственное состояние - предоставляемые в панель значения - const [providedValues, setProvidedValues] = useState([]); - //Собственное состояние - редактор элементов формы const [itemEditor, setItemEditor] = useState({ display: false, index: null }); @@ -229,18 +303,13 @@ const FormEditor = ({ id, title = "", orientation = ORIENTATION.V, autoApply = f const handleItemCancel = () => setItemEditor({ display: false, index: null }); //При сохранении настроек - const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, providedValues, closeEditor }); + const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor }); //При изменении компонента useEffect(() => { settings?.id != id && setSettings({ id, title, orientation, autoApply, items }); }, [settings, id, title, orientation, autoApply, items]); - //При изменении состава элементов формы - useEffect(() => { - Array.isArray(settings?.items) && setProvidedValues(settings.items.map(item => item.name)); - }, [settings?.items]); - //Формирование представления return ( settings && ( @@ -248,6 +317,7 @@ const FormEditor = ({ id, title = "", orientation = ORIENTATION.V, autoApply = f {itemEditor.display && ( @@ -299,6 +369,7 @@ FormEditor.propTypes = { orientation: PropTypes.oneOf(Object.values(ORIENTATION)), autoApply: PropTypes.bool, items: PropTypes.arrayOf(ITEM_SHAPE), + valueProviders: PropTypes.object, onSettingsChange: PropTypes.func }; diff --git a/app/panels/panels_editor/components/form/view.js b/app/panels/panels_editor/components/form/view.js index a24dd1f..4eb4c67 100644 --- a/app/panels/panels_editor/components/form/view.js +++ b/app/panels/panels_editor/components/form/view.js @@ -13,11 +13,17 @@ import { Paper, Stack, Typography, Icon, TextField, IconButton, InputAdornment } import { ApplicationСtx } from "../../../../context/application"; //Контекст приложения import { P8P_COMPONENT_INLINE_MESSAGE, P8PComponentInlineMessage } from "../../../../components/editors/p8p_component_inline_message"; //Информационное сообщение внутри компонента import { ITEM_SHAPE, ITEMS_INITIAL, ORIENTATION } from "./common"; //Общие ресурсы и константы формы +import { APP_STYLES } from "../../../../../app.styles"; //--------- //Константы //--------- +//Стили +const STYLES = { + CONTAINER: { overflow: "auto", ...APP_STYLES.SCROLL } +}; + //Иконка компонента const COMPONENT_ICON = "fact_check"; @@ -116,7 +122,7 @@ const Form = ({ title = null, orientation = ORIENTATION.V, autoApply = false, it //Формирование представления return ( - + {haveConfing ? ( diff --git a/app/panels/panels_editor/components/indicator/action.js b/app/panels/panels_editor/components/indicator/action.js new file mode 100644 index 0000000..f1ea8ca --- /dev/null +++ b/app/panels/panels_editor/components/indicator/action.js @@ -0,0 +1,39 @@ +/* + Парус 8 - Панели мониторинга - Редактор панелей + Компоненты: Индикатор (общие ресурсы действия) +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +//--------- +//Константы +//--------- + +//Доступные области действий индикатора +const P8P_CA_INDICATOR_ACTION_AREAS = [ + { name: "Компонент", area: "component", hasElement: false }, + { name: "Заголовок", area: "inner_caption", hasElement: false }, + { name: "Значение", area: "inner_value", hasElement: false } +]; + +//------------------------------------ +//Вспомогательные функции и компоненты +//------------------------------------ + +//Считывание обработчиков индикатора +const getIndicatorHandlers = handlers => { + //Возвращаем объект хэндлеров (имеют формат "[Область].[Сегмент]", где [Сегмент] - необязательный) + return { + onComponentClick: handlers["component."]?.fn, + onCaptionClick: handlers["inner_caption."]?.fn, + onValueClick: handlers["inner_value."]?.fn + }; +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8P_CA_INDICATOR_ACTION_AREAS, getIndicatorHandlers }; diff --git a/app/panels/panels_editor/components/indicator/conditions.js b/app/panels/panels_editor/components/indicator/conditions.js new file mode 100644 index 0000000..525587a --- /dev/null +++ b/app/panels/panels_editor/components/indicator/conditions.js @@ -0,0 +1,33 @@ +/* + Парус 8 - Панели мониторинга - Редактор панелей + Компоненты: Индикатор (общие ресурсы условия) +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +//--------- +//Константы +//--------- + +//Доступные поля условий индикатора +const P8P_CC_INDICATOR_COND_FIELDS = [ + { name: "Состояние", value: "state", hasElement: false }, + { name: "Значение", value: "value", hasElement: false }, + { name: "Подсказка", value: "hint", hasElement: false } +]; + +//Доступные поля результата условия индикатора +const P8P_CC_INDICATOR_RES_FIELDS = [ + { name: "Подпись", value: "caption", hasElement: false, icon: "rtt" }, + { name: "Значение", value: "value", hasElement: false, icon: "pin" }, + { name: "Цвет заливки", value: "backgroundColor", hasElement: false, icon: "color_lens" }, + { name: "Цвет шрифта", value: "color", hasElement: false, icon: "format_color_text" } +]; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8P_CC_INDICATOR_COND_FIELDS, P8P_CC_INDICATOR_RES_FIELDS }; diff --git a/app/panels/panels_editor/components/indicator/editor.js b/app/panels/panels_editor/components/indicator/editor.js index 81b4666..4f56395 100644 --- a/app/panels/panels_editor/components/indicator/editor.js +++ b/app/panels/panels_editor/components/indicator/editor.js @@ -13,32 +13,69 @@ import { P8PEditorBox } from "../../../../components/editors/p8p_editor_box"; // import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Заголовок раздела редактора import { P8P_DATA_SOURCE_SHAPE } from "../../../../components/editors/p8p_data_source_common"; //Общие ресурсы источника данных import { P8PDataSource } from "../../../../components/editors/p8p_data_source"; //Источник данных +import { P8PActions } from "../../../../components/editors/p8p_actions"; //Действия +import { P8PConditions } from "../../../../components/editors/p8p_conditions"; //Условия +import { P8P_CA_SHAPE, P8P_CAS_INITIAL } from "../../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий +import { P8P_CCS_INITIAL, P8P_CC_SHAPE } from "../../../../components/editors/p8p_component_condition/common"; //Общие ресурсы компонента "Редактор условия" +import { P8P_CA_INDICATOR_ACTION_AREAS } from "./action"; //Общие ресурсы действий индикатора +import { P8P_CC_INDICATOR_COND_FIELDS, P8P_CC_INDICATOR_RES_FIELDS } from "./conditions"; //Общие ресурсы условий индикатора + +//--------- +//Константы +//--------- //----------- //Тело модуля //----------- //Индикатор (редактор настроек) -const IndicatorEditor = ({ id, dataSource = null, valueProviders = {}, onSettingsChange = null } = {}) => { +const IndicatorEditor = ({ + id, + dataSource = null, + valueProviders = {}, + conditions = P8P_CCS_INITIAL, + actions = P8P_CAS_INITIAL, + onSettingsChange = null +} = {}) => { //Собственное состояние - текущие настройки const [settings, setSettings] = useState(null); - //При изменении компонента - useEffect(() => { - settings?.id != id && setSettings({ id, dataSource }); - }, [settings, id, dataSource]); - //При сохранении изменений элемента const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } })); + //При изменении действий + const handleActionsChange = actions => setSettings(pv => ({ ...pv, actions })); + + //При изменении условий + const handleConditionsChange = conditions => setSettings(pv => ({ ...pv, conditions })); + //При сохранении настроек const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor }); + //При изменении компонента + useEffect(() => { + settings?.id != id && setSettings({ id, dataSource, conditions, actions }); + }, [settings, id, dataSource, conditions, actions]); + //Формирование представления return ( + + + + ); }; @@ -48,6 +85,8 @@ IndicatorEditor.propTypes = { id: PropTypes.string.isRequired, dataSource: P8P_DATA_SOURCE_SHAPE, valueProviders: PropTypes.object, + conditions: PropTypes.arrayOf(P8P_CC_SHAPE), + actions: PropTypes.arrayOf(P8P_CA_SHAPE), onSettingsChange: PropTypes.func }; diff --git a/app/panels/panels_editor/components/indicator/view.js b/app/panels/panels_editor/components/indicator/view.js index 6526a5d..a306001 100644 --- a/app/panels/panels_editor/components/indicator/view.js +++ b/app/panels/panels_editor/components/indicator/view.js @@ -11,13 +11,17 @@ import React from "react"; //Классы React import PropTypes from "prop-types"; //Контроль свойств компонента import { Paper } from "@mui/material"; //Интерфейсные элементы import { P8PIndicator } from "../../../../components/p8p_indicator"; //Компонент индикатора -import { useDataSource } from "../../../../components/editors/p8p_data_source_hooks"; //Хуки для данных +import { useDataSource, useConditions, useComponentHandlers } from "../../../../components/editors/p8p_data_source_hooks"; //Хуки для данных import { P8P_DATA_SOURCE_SHAPE } from "../../../../components/editors/p8p_data_source_common"; //Общие ресурсы источника данных import { P8P_COMPONENT_INLINE_MESSAGE_TYPE, P8P_COMPONENT_INLINE_MESSAGE, P8PComponentInlineMessage } from "../../../../components/editors/p8p_component_inline_message"; //Информационное сообщение внутри компонента +import { P8P_CC_SHAPE } from "../../../../components/editors/p8p_component_condition/common"; //Общие ресурсы условий +import { P8P_CA_SHAPE } from "../../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий +import { P8P_COMPONENT_SETTINGS_RESP_ARGS } from "../../../../components/editors/p8p_component_settings"; //Дополнительный функционал источников данных +import { getIndicatorHandlers } from "./action"; //Общие ресурсы действий индикатора //--------- //Константы @@ -31,7 +35,14 @@ const COMPONENT_NAME = "Индикатор"; //Стили const STYLES = { - CONTAINER: { height: "100%" } + CONTAINER: clickable => ({ + height: "100%", + ...(clickable + ? { + cursor: "pointer" + } + : {}) + }) }; //----------- @@ -39,29 +50,39 @@ const STYLES = { //----------- //Индикатор (представление) -const Indicator = ({ dataSource = null, values = {} } = {}) => { +const Indicator = ({ dataSource = null, values = {}, conditions = [], actions = [], onValuesChange = null }) => { //Собственное состояние - данные - const [data, error] = useDataSource({ dataSource, values }); + const [indicator, error, haveConfing, haveData] = useDataSource({ + dataSource, + values, + componentRespArg: P8P_COMPONENT_SETTINGS_RESP_ARGS.INDICATOR + }); - //Флаг настроенности индикатора - const haveConfing = dataSource?.stored ? true : false; + //Собственное состояние - данные с учетом условий + const [indicatorOrigin] = useConditions({ componentData: indicator, conditions }); - //Флаг наличия данных - const haveData = data?.init === true && !error ? true : false; + //Собственное состояние - обработчики компонента + const [handlers] = useComponentHandlers({ actions, onValuesChange }); - //Данные индикатора - const indicator = data?.XINDICATOR || {}; + //Обработчики областей + const { onComponentClick, onCaptionClick, onValueClick } = getIndicatorHandlers(handlers); //Формирование представления return ( onComponentClick && onComponentClick({ event, values })} > {haveConfing && haveData ? ( - + onValueClick({ event, values }) : null} + onCaptionClick={onCaptionClick ? event => onCaptionClick({ event, values }) : null} + elevation={0} + /> ) : ( { //Контроль свойств компонента - Индикатор (представление) Indicator.propTypes = { dataSource: P8P_DATA_SOURCE_SHAPE, - values: PropTypes.object + values: PropTypes.object, + conditions: PropTypes.arrayOf(P8P_CC_SHAPE), + actions: PropTypes.arrayOf(P8P_CA_SHAPE), + onValuesChange: PropTypes.func }; //---------------- diff --git a/app/panels/panels_editor/components/panels_manager/panel_iu_dialog.js b/app/panels/panels_editor/components/panels_manager/panel_iu_dialog.js new file mode 100644 index 0000000..6fe982f --- /dev/null +++ b/app/panels/panels_editor/components/panels_manager/panel_iu_dialog.js @@ -0,0 +1,54 @@ +/* + Парус 8 - Панели мониторинга - Редактор панелей + Компонент: Диалог добавления/исправления панели +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог +import { TITLES } from "../../../../../app.text"; //Общие текстовые ресурсы приложения + +//----------- +//Тело модуля +//----------- + +//Диалог добавления/исправления панели +const PanelIUDialog = ({ code = "", name = "", insert = true, onOk, onCancel }) => { + //Нажатие на кнопку "Ok" + const handleOk = values => onOk && onOk({ ...values }); + + //Нажатие на кнопку "Отмена" + const handleCancel = () => onCancel && onCancel(); + + //Генерация содержимого + return ( + + ); +}; + +//Контроль свойств - Диалог добавления/исправления панели +PanelIUDialog.propTypes = { + code: PropTypes.string, + name: PropTypes.string, + insert: PropTypes.bool, + onOk: PropTypes.func, + onCancel: PropTypes.func +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { PanelIUDialog }; diff --git a/app/panels/panels_editor/components/panels_manager/panels_list.js b/app/panels/panels_editor/components/panels_manager/panels_list.js new file mode 100644 index 0000000..8021d1a --- /dev/null +++ b/app/panels/panels_editor/components/panels_manager/panels_list.js @@ -0,0 +1,158 @@ +/* + Парус 8 - Панели мониторинга - Редактор панелей + Компонент: Список панелей +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { Stack, List, ListItem, IconButton, Icon, ListItemButton, ListItemText, Typography } from "@mui/material"; //Интерфейсные компоненты MUI +import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения +import { APP_STYLES } from "../../../../../app.styles"; //Типовые стили + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + LIST_PANELS: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL }, + SMALL_TOOL_ICON: { + fontSize: 20 + } +}; + +//--------- +//Константы +//--------- + +//Структура данных о сущности панели +const PANELS_LIST_ITEM_SHAPE = PropTypes.shape({ + rn: PropTypes.number.isRequired, + code: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + author: PropTypes.string.isRequired, + chDate: PropTypes.string.isRequired, + modify: PropTypes.oneOf([0, 1]).isRequired, + pbl: PropTypes.oneOf([0, 1]).isRequired, + ready: PropTypes.oneOf([0, 1]).isRequired +}); + +//----------- +//Тело модуля +//----------- + +//Диалог открытия панели +const PanelsList = ({ + panels = [], + isEditable = true, + current = null, + onSelect = null, + onPbl = null, + onReady = null, + onEdit = null, + onDelete = null +} = {}) => { + //При выборе элемента списка + const handleSelectClick = query => { + onSelect && onSelect(query); + }; + + //При нажатии на общедоступность + const handlePblClick = (e, query) => { + e.stopPropagation(); + onPbl && onPbl(query); + }; + + //При нажатии на готовность + const handleReadyClick = (e, query) => { + e.stopPropagation(); + onReady && onReady(query); + }; + + //При нажатии на исправлении + const handleEditClick = (e, query) => { + e.stopPropagation(); + onEdit && onEdit(query); + }; + + //При нажатии на удаление + const handleDeleteClick = (e, query) => { + e.stopPropagation(); + onDelete && onDelete(query); + }; + + //Формирование представления + return ( + + {panels.map((panel, i) => { + const selected = panel.rn === current || panel.code === current; + const disabled = !panel.modify; + const pblTitle = `${panel.pbl === 1 ? "Общедоступный" : "Приватный"}${!disabled ? " - нажмите, чтобы изменить" : ""}`; + const pblIcon = panel.pbl === 1 ? "groups" : "lock_person"; + const readyTitle = `${panel.ready === 1 ? "Готов" : "Не готов"}${!disabled ? " - нажмите, чтобы изменить" : ""}`; + const readyIcon = panel.ready === 1 ? "touch_app" : "do_not_touch"; + return ( + + handleSelectClick(panel)} selected={selected}> + + {`${panel.code}, ${panel.author}, ${panel.chDate}`} + {isEditable ? ( + +
+ handlePblClick(e, panel)}> + {pblIcon} + +
+
+ handleReadyClick(e, panel)}> + {readyIcon} + +
+
+ ) : null} +
+ } + /> + {isEditable ? ( + + handleEditClick(e, panel)} disabled={disabled} title={BUTTONS.UPDATE}> + edit + + handleDeleteClick(e, panel)} disabled={disabled} title={BUTTONS.DELETE}> + delete + + + ) : null} + + + ); + })} + + ); +}; + +//Контроль свойств компонента - Список панелей +PanelsList.propTypes = { + panels: PropTypes.arrayOf(PANELS_LIST_ITEM_SHAPE), + current: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + isEditable: PropTypes.bool, + onSelect: PropTypes.func, + onPbl: PropTypes.func, + onReady: PropTypes.func, + onEdit: PropTypes.func, + onDelete: PropTypes.func +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { PanelsList }; diff --git a/app/panels/panels_editor/components/panels_manager/panels_manager.js b/app/panels/panels_editor/components/panels_manager/panels_manager.js new file mode 100644 index 0000000..ef1275b --- /dev/null +++ b/app/panels/panels_editor/components/panels_manager/panels_manager.js @@ -0,0 +1,118 @@ +/* + Парус 8 - Панели мониторинга - Редактор панелей + Компонент: Менеджер панелей +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React, { useState, useContext } from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { Box, Button, Icon } from "@mui/material"; //Интерфейсные компоненты MUI +import { MessagingСtx } from "../../../../context/messaging"; //Контекст сообщений +import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения +import { P8PConfigDialog } from "../../../../components/editors/p8p_config_dialog"; //Типовой диалог настройки +import { usePanelManager } from "../components_hooks"; //Пользовательские хуки +import { PanelsList } from "./panels_list"; //Список панелей +import { PanelIUDialog } from "./panel_iu_dialog"; //Диалог добавления/исправления панели +import { ImportPanelButton } from "../../layouts"; //Дополнительная разметка и вёрстка клиентских элементов + +//----------- +//Тело модуля +//----------- + +//Менеджер панелей +const PanelsManager = ({ current = null, isEditable = true, onPanelSelect = null, onCancel = null } = {}) => { + //Собственное состояние - изменяемая панель + const [modPanel, setModPanel] = useState(null); + + //Работа со списком панелей + const [panels, insertPanel, updatePanel, deletePanel, setPanelReady, setPanelPbl, importPanel] = usePanelManager(); + + //Подключение к контексту сообщений + const { showMsgWarn } = useContext(MessagingСtx); + + //При добавлении панели + const handlePanelAdd = () => setModPanel(true); + + //При загрузке панели из файла + const handlePanelImport = fileData => { + importPanel(fileData); + }; + + //При выборе панели + const handlePanelSelect = panel => onPanelSelect && onPanelSelect(panel); + + //При установке признака публичности + const handlePanelPblSet = panel => setPanelPbl(panel.rn, panel.pbl === 1 ? 0 : 1); + + //При установке признака готовности + const handlePanelReadySet = panel => setPanelReady(panel.rn, panel.ready === 1 ? 0 : 1); + + //При исправлении панели + const handlePanelEdit = panel => setModPanel({ ...panel }); + + //При удалении панели + const handlePanelDelete = panel => showMsgWarn("Удалить панель?", () => deletePanel(panel.rn)); + + //При закрытии диалога добавления/исправления по "ОК" + const handleIUDialogOk = async values => { + if (modPanel === true) await insertPanel(values.code, values.name); + else await updatePanel(modPanel.rn, values.code, values.name); + setModPanel(null); + }; + + //При закрытии диалога добавления/исправления по "Отмена" + const handleIUDialogCancel = () => setModPanel(null); + + //При закрытии менеджера отменой + const handleCancel = () => onCancel && onCancel(); + + //Формирование представления + return ( + + {modPanel && ( + + )} + {isEditable ? ( + + + + + ) : null} + + + ); +}; + +//Контроль свойств компонента - Менеджер панелей +PanelsManager.propTypes = { + current: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + isEditable: PropTypes.bool, + onPanelSelect: PropTypes.func, + onCancel: PropTypes.func +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { PanelsManager }; diff --git a/app/panels/panels_editor/components/table/action.js b/app/panels/panels_editor/components/table/action.js new file mode 100644 index 0000000..8a4743f --- /dev/null +++ b/app/panels/panels_editor/components/table/action.js @@ -0,0 +1,58 @@ +/* + Парус 8 - Панели мониторинга - Редактор панелей + Компоненты: Таблица (общие ресурсы действия) +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +//--------- +//Константы +//--------- + +//Собственные значения типа действия +const P8P_CA_TABLE_TYPE_VALUE = { + COLUMN_VALUE: "Значение графы" +}; + +//Типы значений действий таблицы +const P8P_CA_TABLE_VALUE_TYPES = [...Object.values(P8P_CA_TABLE_TYPE_VALUE)]; + +//Доступные области действий таблицы +const P8P_CA_TABLE_ACTION_AREAS = [ + { name: "Компонент", area: "component", hasElement: false }, + { name: "Графа таблицы", area: "column", hasElement: true }, + { name: "Строка таблицы", area: "row", hasElement: false } +]; + +//------------------------------------ +//Вспомогательные функции и компоненты +//------------------------------------ + +//Получение значения собственного типа действия +const getDataGridCustomTypeValue = ({ type, value, prms }) => { + //Если это значение графы - возвращаем нужное значение, иначе - null + return type === P8P_CA_TABLE_TYPE_VALUE.COLUMN_VALUE ? prms.row[value] : null; +}; + +//Считывание обработчиков таблицы +const getDataGridHandlers = (handlers, element = null) => { + //Возвращаем объект хэндлеров (имеют формат "[Область].[Сегмент]", где [Сегмент] - необязательный) + return { + onComponentClick: handlers["component."]?.fn, + onRowClick: handlers["row."]?.fn, + onColumnClick: element ? handlers[`column.${element}`]?.fn : null + }; +}; + +//Проверка наличия обработчиков ячеек +const isHasCellRender = (handlers, conditions) => { + return Object.keys(handlers).some(handler => handler.startsWith("column.") || handler === "row.") || conditions.length !== 0; +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8P_CA_TABLE_VALUE_TYPES, P8P_CA_TABLE_ACTION_AREAS, getDataGridCustomTypeValue, getDataGridHandlers, isHasCellRender }; diff --git a/app/panels/panels_editor/components/table/conditions.js b/app/panels/panels_editor/components/table/conditions.js new file mode 100644 index 0000000..8427deb --- /dev/null +++ b/app/panels/panels_editor/components/table/conditions.js @@ -0,0 +1,38 @@ +/* + Парус 8 - Панели мониторинга - Редактор панелей + Компоненты: Индикатор (общие ресурсы условия) +*/ + +//--------------------- +//Подключение библиотек +//--------------------- +import { getConditionsValues } from "../../../../components/editors/p8p_component_condition/util"; //Вспомогательные ресурсы условий + +//--------- +//Константы +//--------- + +//Доступные поля условий таблицы +const P8P_CC_TABLE_COND_FIELDS = [{ name: "Графа", value: "column", hasElement: true }]; + +//Доступные поля результата условия таблицы +const P8P_CC_TABLE_RES_FIELDS = [ + { name: "Цвет заливки", value: "backgroundColor", hasElement: true, icon: "color_lens" }, + { name: "Цвет шрифта", value: "color", hasElement: true, icon: "format_color_text" } +]; + +//------------------------------------ +//Вспомогательные функции и компоненты +//------------------------------------ + +//Считывание стилей колонки по условиям +const getColumnStylesByConditions = (conditions, row, columnName) => { + //Считываем стили строки + return getConditionsValues(row, conditions, columnName); +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { P8P_CC_TABLE_COND_FIELDS, P8P_CC_TABLE_RES_FIELDS, getColumnStylesByConditions }; diff --git a/app/panels/panels_editor/components/table/editor.js b/app/panels/panels_editor/components/table/editor.js index b61a220..1ee82bf 100644 --- a/app/panels/panels_editor/components/table/editor.js +++ b/app/panels/panels_editor/components/table/editor.js @@ -13,32 +13,66 @@ import { P8PEditorBox } from "../../../../components/editors/p8p_editor_box"; // import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Заголовок раздела редактора import { P8P_DATA_SOURCE_SHAPE } from "../../../../components/editors/p8p_data_source_common"; //Общие ресурсы источника данных import { P8PDataSource } from "../../../../components/editors/p8p_data_source"; //Источник данных +import { P8PActions } from "../../../../components/editors/p8p_actions"; //Действия +import { P8PConditions } from "../../../../components/editors/p8p_conditions"; //Условия +import { P8P_CA_TABLE_VALUE_TYPES, P8P_CA_TABLE_ACTION_AREAS } from "./action"; //Общие ресурсы действий таблицы +import { P8P_CC_TABLE_COND_FIELDS, P8P_CC_TABLE_RES_FIELDS } from "./conditions"; //Общие ресурсы условий таблицы +import { P8P_CA_SHAPE, P8P_CAS_INITIAL } from "../../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий +import { P8P_CCS_INITIAL, P8P_CC_SHAPE } from "../../../../components/editors/p8p_component_condition/common"; //Общие ресурсы компонента "Редактор условия" //----------- //Тело модуля //----------- //Таблица (редактор настроек) -const TableEditor = ({ id, dataSource = null, valueProviders = {}, onSettingsChange = null } = {}) => { +const TableEditor = ({ + id, + dataSource = null, + valueProviders = {}, + conditions = P8P_CCS_INITIAL, + actions = P8P_CAS_INITIAL, + onSettingsChange = null +} = {}) => { //Собственное состояние - текущие настройки const [settings, setSettings] = useState(null); - //При изменении компонента - useEffect(() => { - settings?.id != id && setSettings({ id, dataSource }); - }, [settings, id, dataSource]); - //При сохранении изменений элемента const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } })); + //При изменении действий + const handleActionsChange = actions => setSettings(pv => ({ ...pv, actions })); + + //При изменении условий + const handleConditionsChange = conditions => setSettings(pv => ({ ...pv, conditions })); + //При сохранении настроек const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor }); + //При изменении компонента + useEffect(() => { + settings?.id != id && setSettings({ id, dataSource, conditions, actions }); + }, [settings, id, dataSource, conditions, actions]); + //Формирование представления return ( + + + + ); }; @@ -48,6 +82,8 @@ TableEditor.propTypes = { id: PropTypes.string.isRequired, dataSource: P8P_DATA_SOURCE_SHAPE, valueProviders: PropTypes.object, + conditions: PropTypes.arrayOf(P8P_CC_SHAPE), + actions: PropTypes.arrayOf(P8P_CA_SHAPE), onSettingsChange: PropTypes.func }; diff --git a/app/panels/panels_editor/components/table/view.js b/app/panels/panels_editor/components/table/view.js index d9b976f..b1b0be9 100644 --- a/app/panels/panels_editor/components/table/view.js +++ b/app/panels/panels_editor/components/table/view.js @@ -9,17 +9,22 @@ import React from "react"; //Классы React import PropTypes from "prop-types"; //Контроль свойств компонента -import { Paper } from "@mui/material"; //Интерфейсные элементы +import { Paper, Link } from "@mui/material"; //Интерфейсные элементы import { APP_STYLES } from "../../../../../app.styles"; //Типовые стили import { P8PDataGrid } from "../../../../components/p8p_data_grid"; //Таблица данных import { P8P_DATA_GRID_CONFIG_PROPS } from "../../../../config_wrapper"; //Подключение компонентов к настройкам приложения -import { useDataSource } from "../../../../components/editors/p8p_data_source_hooks"; //Хуки для данных +import { useDataSource, useComponentHandlers } from "../../../../components/editors/p8p_data_source_hooks"; //Хуки для данных import { P8P_DATA_SOURCE_SHAPE } from "../../../../components/editors/p8p_data_source_common"; //Общие ресурсы источника данных import { P8P_COMPONENT_INLINE_MESSAGE_TYPE, P8P_COMPONENT_INLINE_MESSAGE, P8PComponentInlineMessage } from "../../../../components/editors/p8p_component_inline_message"; //Информационное сообщение внутри компонента +import { P8P_COMPONENT_SETTINGS_RESP_ARGS } from "../../../../components/editors/p8p_component_settings"; //Дополнительный функционал источников данных +import { P8P_CC_SHAPE, P8P_CCS_INITIAL } from "../../../../components/editors/p8p_component_condition/common"; //Общие ресурсы условий +import { P8P_CA_SHAPE, P8P_CAS_INITIAL } from "../../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий +import { getDataGridCustomTypeValue, getDataGridHandlers, isHasCellRender } from "./action"; //Общие ресурсы действий таблицы +import { getColumnStylesByConditions } from "./conditions"; //--------- //Константы @@ -33,12 +38,54 @@ const COMPONENT_NAME = "Таблица"; //Стили const STYLES = { - CONTAINER: { display: "flex", height: "100%", overflow: "hidden" }, + CONTAINER: clickable => ({ + display: "flex", + height: "100%", + overflow: "hidden", + ...(clickable + ? { + cursor: "pointer" + } + : {}) + }), DATA_GRID: { width: "100%" }, DATA_GRID_CONTAINER: { height: `calc(100%)`, ...APP_STYLES.SCROLL - } + }, + DATA_GRID_ROW_CLICABLE: { cursor: "pointer" } +}; + +//--------------------------------------------- +//Вспомогательные функции и компоненты +//--------------------------------------------- + +//Форматирование значения ячейки +const dataCellRender = ({ row, columnDef, values, handlers, conditions }) => { + //Инициализируем обработчики строки + const { onRowClick, onColumnClick } = getDataGridHandlers(handlers, columnDef.name); + //Инициализируем стили по условию + const condStyles = getColumnStylesByConditions(conditions, row, columnDef.name); + //Накладываем нужные обработчики + return { + cellStyle: { ...(onRowClick ? { ...STYLES.DATA_GRID_ROW_CLICABLE } : {}), ...condStyles }, + cellProps: { + ...(onRowClick + ? { + onClick: event => { + onRowClick({ event, values, prms: { row, columnDef } }); + } + } + : {}) + }, + data: onColumnClick ? ( + onColumnClick({ event, values, prms: { row, columnDef } })}> + {row[columnDef.name]} + + ) : ( + row[columnDef.name] + ) + }; }; //----------- @@ -46,26 +93,27 @@ const STYLES = { //----------- //Таблица (представление) -const Table = ({ dataSource = null, values = {} } = {}) => { +const Table = ({ dataSource = null, values = {}, conditions = P8P_CCS_INITIAL, actions = P8P_CAS_INITIAL, onValuesChange = null } = {}) => { //Собственное состояние - данные - const [data, error] = useDataSource({ dataSource, values }); + const [dataGrid, error, haveConfing, haveData] = useDataSource({ dataSource, values, componentRespArg: P8P_COMPONENT_SETTINGS_RESP_ARGS.TABLE }); - //Флаг настроенности таблицы - const haveConfing = dataSource?.stored ? true : false; + //Собственное состояние - обработчики компонента + const [handlers] = useComponentHandlers({ actions, onValuesChange, getCustomTypeValue: getDataGridCustomTypeValue }); - //Флаг наличия данных - const haveData = data?.init === true && !error ? true : false; + //Признак необходимости рендера ячеек + const hasCellRender = isHasCellRender(handlers, conditions); - //Данные таблицы - const dataGrid = data?.XDATA_GRID || {}; + //Обработчики областей + const { onComponentClick } = getDataGridHandlers(handlers); //Формирование представления return ( onComponentClick && onComponentClick({ event, values })} > {haveConfing && haveData ? ( { {...dataGrid} style={STYLES.DATA_GRID} containerComponentProps={{ sx: STYLES.DATA_GRID_CONTAINER, elevation: 0 }} + dataCellRender={ + hasCellRender + ? prms => + dataCellRender({ + ...prms, + values, + handlers, + conditions + }) + : null + } /> ) : ( { //Контроль свойств компонента - Таблица (представление) Table.propTypes = { dataSource: P8P_DATA_SOURCE_SHAPE, - values: PropTypes.object + values: PropTypes.object, + conditions: PropTypes.arrayOf(P8P_CC_SHAPE), + actions: PropTypes.arrayOf(P8P_CA_SHAPE), + onValuesChange: PropTypes.func }; //---------------- diff --git a/app/panels/panels_editor/layout_item.js b/app/panels/panels_editor/layout_item.js index f4754d4..19e47c4 100644 --- a/app/panels/panels_editor/layout_item.js +++ b/app/panels/panels_editor/layout_item.js @@ -76,7 +76,8 @@ LayoutItem.propTypes = { onDeleteClick: PropTypes.func, item: PropTypes.object.isRequired, editMode: PropTypes.bool, - selected: PropTypes.bool + selected: PropTypes.bool, + isDragging: PropTypes.bool }; //---------------- diff --git a/app/panels/panels_editor/layouts.js b/app/panels/panels_editor/layouts.js new file mode 100644 index 0000000..c1a7da3 --- /dev/null +++ b/app/panels/panels_editor/layouts.js @@ -0,0 +1,134 @@ +/* + Парус 8 - Панели мониторинга - Редактор панелей + Дополнительная разметка и вёрстка клиентских элементов +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React, { useState } from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { InputLabel, Input, IconButton, Icon, Button } from "@mui/material"; //Интерфейсные компоненты +import { TEXTS } from "../../../app.text"; //Общие текстовые ресурсы приложения +import { P8PAppProgress } from "../../components/p8p_app_progress"; //Индикатор процесса + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + INPUT_FILE: { display: "none" }, + INPUT_LABEL: { display: "contents" } +}; + +//----------- +//Тело модуля +//----------- + +//Компонент "Загрузка из файла" +const ImportPanel = ({ id, buttonView, onClick, disabled = false }) => { + //Состояние загрузки файла + const [loading, setLoading] = useState(false); + + //При выборе файла + const handleChange = event => { + //Если файл выбран + if (event.target.files[0]) { + //Инициализируем считыватель файла + const reader = new FileReader(); + //Обработчик начала считывания данных + reader.onloadstart = () => setLoading(true); + //Обработчик считывания данных + reader.onload = async e => { + //Обрабатываем текст файла + onClick(e.target.result); + setLoading(false); + }; + //Обработчик ошибки считывания данных + reader.onerror = () => setLoading(false); + //Загружаем данные из файла + reader.readAsText(event.target.files[0]); + } + //Очищаем значение + event.target.value = null; + }; + + //Генерация содержимого + return ( + <> + {loading ? : null} + + + {buttonView} + + + ); +}; + +//Контроль свойств - Компонент "Загрузка из файла" +ImportPanel.propTypes = { + id: PropTypes.string.isRequired, + buttonView: PropTypes.object.isRequired, + onClick: PropTypes.func.isRequired, + disabled: PropTypes.bool +}; + +//Генерация действия "Загрузить из файла" меню действий +export const toolbarImportRenderer = ({ icon, title, disabled, onClick }) => { + //Представление кнопки загрузки + let importButtonView = ( + + {icon} + + ); + //Генерация содержимого + return ; +}; + +//Компонент кнопки импорта панели из файла +export const ImportPanelButton = ({ id, startIcon, title, onClick }) => { + //Представление кнопки загрузки + let importButtonView = ( + + ); + //Генерация содержимого + return ; +}; + +//Контроль свойств - Компонент кнопки импорта панели из файла +ImportPanelButton.propTypes = { + id: PropTypes.string.isRequired, + startIcon: PropTypes.string, + title: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired +}; + +//Выгрузка XML файла +export const exportXMLFile = (data, fileName) => { + //Переводим в данные для файла + const blobData = new Blob([data], { type: "text/plain" }); + //Формируем URL + const url = URL.createObjectURL(blobData); + //Делаем линк и устанавливаем параметры, после добавляем в DOM + const a = document.createElement("a"); + a.href = url; + a.download = `${fileName.replace(/[/\\?%*:|"<>.,;= ]/g, "_")}.xml`; + document.body.appendChild(a); + //Выполняем открытие ссылки + a.click(); + //Очищаем от лишних данных + document.body.removeChild(a); + URL.revokeObjectURL(url); +}; diff --git a/app/panels/panels_editor/panel_editor.js b/app/panels/panels_editor/panel_editor.js new file mode 100644 index 0000000..f3bbc15 --- /dev/null +++ b/app/panels/panels_editor/panel_editor.js @@ -0,0 +1,358 @@ +/* + Парус 8 - Панели мониторинга - Редактор панелей + Редактор панели +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React, { useState, useContext, useEffect } from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { Responsive, WidthProvider } from "react-grid-layout"; //Адаптивный макет +import { Box, Grid, Menu, MenuItem, Popover } from "@mui/material"; //Интерфейсные элементы +import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений +import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Рабочая область приложения +import { P8PEditorToolBar } from "../../components/editors/p8p_editor_toolbar"; //Панель инструментов редактора +import { LayoutItem } from "./layout_item"; //Элемент макета +import { ComponentView } from "./component_view"; //Представление компонента панели +import { ComponentEditor } from "./component_editor"; //Редактор свойств компонента панели +import { COMPONENTS } from "./components/components"; //Описание доступных компонентов +import { usePanelDesc, useWindowResize } from "./components/components_hooks"; //Вспомогательные хуки +import { toolbarImportRenderer } from "./layouts"; //Дополнительная разметка и вёрстка клиентских элементов +import "react-grid-layout/css/styles.css"; //Стили для адаптивного макета +import "react-resizable/css/styles.css"; //Стили для адаптивного макета +import "./panels_editor.css"; //Стили редактора панелей +import { APP_STYLES } from "../../../app.styles"; //Типовые стили +import { PanelPropsEditor } from "./panel_props_editor"; //Редактор глобальных свойств панели +import { PanelVariableList } from "./panel_variable_list"; //Список переменных панели + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + CONTAINER: { display: "flex", overflow: "auto", ...APP_STYLES.SCROLL }, + GRID_CONTAINER: { height: `calc(100vh - ${APP_BAR_HEIGHT})` }, + GRID_ITEM_INSPECTOR: { backgroundColor: "#e9ecef", height: "100%", overflow: "auto", ...APP_STYLES.SCROLL }, + POPOVER_CONTAINER: { [`& .MuiPaper-root`]: { maxHeight: "60%", width: "300px", ...APP_STYLES.SCROLL } } +}; + +//----------- +//Тело модуля +//----------- + +//Обёрдка для динамического макета +const ResponsiveGridLayout = WidthProvider(Responsive); + +//Редактор панели +const PanelEditor = ({ + panel, + panelName, + editMode, + isPanelChanged, + isPanelVariablesOpened, + onPanelChanged, + onPanelClose, + onEditModeToggle, + onPanelsManagerOpen, + onPanelVariablesClose +}) => { + //Собственное состояние - ID перемещаемого макета + //Требуется для разделения onDrag и onClick на макете + const [draggingID, setDraggingID] = useState(""); + + //Состояние описания и функций панели + const [ + panelDesc, + isInit, + addComponent, + deleteComponent, + breakpointChange, + layoutsChange, + changeValueProviders, + changeComponentSettings, + changePanelSettings, + savePanelDesc, + importPanelDesc, + exportPanelDesc + ] = usePanelDesc(panel); + + //Собственное состояние - признак инициализации макетов + const [isLayoutsInit, setIsLayoutsInit] = useState(true); + //Собственное состояние - редактирование компонента + const [editComponent, setEditComponent] = useState(null); + //Собственное состояние - элемент открывающий список + const [addMenuAnchorEl, setAddMenuAnchorEl] = useState(null); + //Собственное состояние - высота строки ResponsiveGridLayout + const [rowHeight] = useWindowResize(); + + //Подключение к контексту сообщений + const { showMsgWarn } = useContext(MessagingСtx); + + //При перемещении макета + const handleOnDrag = layout => { + //Если ID перемещаемого макета не установлен - устанавливаем + if (!draggingID) setDraggingID(layout.i); + }; + + //При завершении перемещения макета + const handleOnDragStop = () => setDraggingID(""); + + //Открытие редактора настроек компонента + const handleComponentSettingsEditorOpen = id => setEditComponent(id); + + //Закрытие реактора настроек компонента + const handleComponentSettingsEditorClose = () => setEditComponent(null); + + //При нажатии на настройки компонента + const handleComponentSettingsClick = id => (editComponent === id ? handleComponentSettingsEditorClose() : handleComponentSettingsEditorOpen(id)); + + //Открытие/сокрытие меню добавления + const handleAddMenuToggle = target => setAddMenuAnchorEl(target instanceof Element ? target : null); + + //При изменении размера холста + const handleBreakpointChange = breakpoint => breakpointChange(breakpoint); + + //При изменении состояния макета + const handleLayoutChange = (currentLayout, layouts) => { + //Изменяем только в случае, если панель выбрана, проиницализирована и это режим редактирования + if (panel && isInit) { + //ResponsiveGridLayout реагирует на каждое изменение, при первой загрузке с сервера не требуется перезагрузка макетов + if (isLayoutsInit) { + setIsLayoutsInit(false); + } else { + //Изменяем состояние макета + layoutsChange(layouts); + //Указываем, что панель изменилась (только при редактировании) + onPanelChanged(editMode ? true : false); + } + } + }; + + //Добвление компонента в макет + const handlePanelComponentAdd = component => { + //Добавляем компонент и его макет + addComponent(component); + //Указываем, что панель изменилась + onPanelChanged(true); + }; + + //Удаление компонента из макета + const handlePanelComponentDelete = id => { + //Удаляем компонент + deleteComponent(id); + //Если удаляемый компонент редактируется - закрываем редактор + editComponent === id && handleComponentSettingsEditorClose(); + //Указываем, что панель изменилась + onPanelChanged(true); + }; + + //При нажатии на кнопку добавления + const handleAddClick = e => handleAddMenuToggle(e.currentTarget); + + //При выборе элемента меню добавления + const handleAddMenuItemClick = component => { + handleAddMenuToggle(); + handlePanelComponentAdd(component); + }; + + //При изменении значений в компоненте + const handleComponentValuesChange = values => { + //Изменяем значения проводника + changeValueProviders(values); + //Указываем, что панель изменилась (только при редактировании) + onPanelChanged(editMode ? true : false); + }; + + //При изменении настроек компонента + const handleComponentSettingsChange = ({ id = null, settings = {}, closeEditor = false } = {}) => { + if (id && panelDesc.components[id]) { + //Изменяем настройки компонента + changeComponentSettings(id, settings, () => { + if (closeEditor === true) handleComponentSettingsEditorClose(); + //Указываем, что панель изменилась + onPanelChanged(true); + }); + } + }; + + //При изменении настроек панели + const handlePanelSettingsChange = ({ valueProviders }) => { + //Изменяем настройки панели + changePanelSettings(valueProviders); + //Указываем, что панель изменилась + onPanelChanged(true); + }; + + //При удалении компоненета + const handleComponentDeleteClick = id => handlePanelComponentDelete(id); + + //Закрытие панели + const handlePanelClose = () => onPanelClose(); + + //При запуске панели + const handlePanelPlay = () => onEditModeToggle(); + + //При сохранении описания панели + const handlePanelDescSave = () => savePanelDesc(isChanged => onPanelChanged(isChanged)); + + //При импорте настройки панели из файла + const handleImportPanel = (fileData, callback) => importPanelDesc(fileData, callback); + + //При выгрузке настройки панели в файл + const handleExportPanel = () => { + //Если панель выбрана и есть изменения + if (panel && isPanelChanged) + showMsgWarn(`Панель содержит несохраненные изменения. Выгрузить панель в текущем состоянии?`, () => exportPanelDesc(panelName)); + else exportPanelDesc(panelName); + }; + + //Текущие значения панели + const values = Object.keys(panelDesc.valueProviders).reduce((res, key) => ({ ...res, ...{ [key]: panelDesc.valueProviders[key].value } }), {}); + + //Меню добавления + const addMenu = ( + + {COMPONENTS.map((comp, i) => ( + handleAddMenuItemClick(comp)}> + {comp.name} + + ))} + + ); + + //Панель инструментов + const toolBar = ( + { + handleImportPanel(fileData, isLoaded => onPanelChanged(isLoaded)); + }, + customRenderer: toolbarImportRenderer, + disabled: !panel + }, + { icon: "file_download", title: "Выгрузить в файл", onClick: handleExportPanel, disabled: !panel }, + { icon: "settings", title: "Параметры панели", onClick: handleComponentSettingsEditorClose, disabled: !panel } + ]} + /> + ); + + //При изменении панели + useEffect(() => { + //Первое изменение состояния макета - инициализация + setIsLayoutsInit(true); + //Очищаем редактируемый компонент + setEditComponent(null); + }, [panel]); + + //Генерация содержимого + return ( + + {addMenu} + + + handleOnDrag(oldItem)} + onDragStop={handleOnDragStop} + > + {panelDesc.layouts[panelDesc.breakpoint].map(item => ( + + + + ))} + + + {editMode && ( + + {toolBar} + {panel ? ( + editComponent ? ( + + ) : ( + + ) + ) : null} + + )} + + + + + + ); +}; + +//Контроль свойств компонента - Редактор панели +PanelEditor.propTypes = { + panel: PropTypes.number.isRequired, + panelName: PropTypes.string.isRequired, + editMode: PropTypes.bool.isRequired, + isPanelChanged: PropTypes.bool.isRequired, + isPanelVariablesOpened: PropTypes.bool.isRequired, + onPanelChanged: PropTypes.func.isRequired, + onPanelClose: PropTypes.func.isRequired, + onEditModeToggle: PropTypes.func.isRequired, + onPanelsManagerOpen: PropTypes.func.isRequired, + onPanelVariablesClose: PropTypes.func.isRequired +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { PanelEditor }; diff --git a/app/panels/panels_editor/panel_props_editor.js b/app/panels/panels_editor/panel_props_editor.js new file mode 100644 index 0000000..c925e13 --- /dev/null +++ b/app/panels/panels_editor/panel_props_editor.js @@ -0,0 +1,230 @@ +/* + Парус 8 - Панели мониторинга - Редактор панелей + Редактор глобальных свойств панели +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React, { useState, useContext } from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { Chip, Stack, TextField, Button, Icon } from "@mui/material"; //Интерфейсные элементы +import { P8PConfigDialog } from "../../components/editors/p8p_config_dialog"; //Диалог настройки +import { isElementNameCorrect } from "../../components/editors/p8p_editors_common"; //Общие ресурсы редакторов +import { P8PEditorSubHeader } from "../../components/editors/p8p_editor_sub_header"; //Заголовок раздела редактора +import { P8PEditorBox } from "../../components/editors/p8p_editor_box"; //Контейнер редактора +import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений +import { APP_STYLES } from "../../../app.styles"; //Типовые стили +import { STYLES as COMMON_STYLES } from "../../components/editors/p8p_editors_common"; //Общие ресурсы редакторов +import { P8PChipList } from "../../components/editors/p8p_chip_list"; //Дополнительные настройки редактора + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + STACK_DEPENENCIES: { gap: "5px", ...APP_STYLES.SCROLL, overflow: "auto", maxHeight: "160px", width: "260px" }, + CHIP_DEPENDENCY: { ...COMMON_STYLES.CHIP(true, false), minHeight: "32px" } +}; + +//Начальное состояние переменной +const VARIABLE_INITIAL = { + dependencies: [], + description: "", + value: "" +}; + +//Структура переменной +export const VARIABLE_SHAPE = PropTypes.shape({ + dependencies: PropTypes.arrayOf(PropTypes.string), + description: PropTypes.string.isRequired, + value: PropTypes.string +}); + +//------------------------------------ +//Вспомогательные функции и компоненты +//------------------------------------ + +//Редактор переменной +const VariableEditor = ({ variableName = "", variable = null, dependencies = [], onDependencyClick, onOk, onCancel } = {}) => { + //Собственное состояние - имя переменной + const [name, setName] = useState(variableName || ""); + + //Собственное состояние - параметры переменной + const [state, setState] = useState({ ...VARIABLE_INITIAL, ...variable }); + + //При закрытии редактора с сохранением + const handleOk = () => onOk(name, { ...state }); + + //При закрытии редактора с отменой + const handleCancel = () => onCancel(); + + //При изменении параметра переменной + const handleChange = e => { + //Если это поле наименования, то проверяем корректность + if (e.target.id === "name" && isElementNameCorrect(e.target.value)) setName(e.target.value); + //Устанавливаем значение + else setState(pv => ({ ...pv, [e.target.id]: e.target.value })); + }; + + //Доступность сохранения настроек элемента + const okDisabled = !name || !state.description ? true : false; + + //Формирование представления + return ( + + + + + + {dependencies.length !== 0 ? ( + <> + + + {dependencies.map((item, i) => ( + onDependencyClick && onDependencyClick(item)} + sx={STYLES.CHIP_DEPENDENCY} + /> + ))} + + + ) : null} + + ); +}; + +//Контроль свойств - редактор переменной +VariableEditor.propTypes = { + variableName: PropTypes.string, + variable: VARIABLE_SHAPE, + dependencies: PropTypes.array, + onDependencyClick: PropTypes.func.isRequired, + onOk: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired +}; + +//----------- +//Тело модуля +//----------- + +//Редактор глобальных свойств панели +const PanelPropsEditor = ({ valueProviders = {}, onSettingsChange, onDependencyClick } = {}) => { + //Собственное состояние - редактор переменных панели + const [variableEditor, setVariableEditor] = useState({ display: false, item: null }); + + //Подключение к контексту сообщений + const { showMsgErr } = useContext(MessagingСtx); + + //При добавлении новой переменной + const handleVariableAdd = () => setVariableEditor({ display: true, item: null }); + + //При нажатии на переменную + const handleVariableClick = index => setVariableEditor({ display: true, item: variables[index] }); + + //При отмене сохранения изменений переменной + const handleVariableCancel = () => setVariableEditor({ display: false, index: null }); + + //При сохранении изменений переменной + const handleVariableSave = (variableName, variable) => { + //Текст ошибки + let msgError = ""; + //Если это добавление или изменилось наименование - проверяем на дублирование + if ((!variableEditor.item || variableEditor.item !== variableName) && Object.prototype.hasOwnProperty.call(valueProviders, variableName)) { + msgError = `Дублирование наименования параметра "${variableName}".`; + } + //Если это исправление наименования параметра, но он используется + if (variableEditor.item && variableEditor.item !== variableName && valueProviders[variableEditor.item].dependencies.length !== 0) { + msgError = `Переменная имеет связи с компонентами. Изменение наименования запрещено.`; + } + //Если есть ошибка - выводим + if (msgError) { + showMsgErr(msgError); + } else { + //Копируем параметры + let newValueProviders = { ...valueProviders }; + //Удаляем старое значение, если требуется + variableEditor.item && variableEditor.item !== variableName ? delete newValueProviders[variableEditor.item] : null; + //Добавляем новый параметр + newValueProviders = { ...newValueProviders, [variableName]: { ...variable } }; + //Обновляем проводники панели + onSettingsChange({ valueProviders: { ...newValueProviders } }); + //Закрываем редактирование + setVariableEditor({ display: false, index: null }); + } + }; + + //При удалении переменной + const handleVariableDelete = index => { + const variable = variables[index]; + //Если переменная не используется в компонентах - удаляем + if (valueProviders[variable].dependencies.length === 0) { + const newValueProviders = { ...valueProviders }; + delete newValueProviders[variable]; + onSettingsChange({ valueProviders: { ...newValueProviders } }); + } else { + showMsgErr(`Переменная имеет связи с компонентами. Удаление запрещено.`); + } + }; + + //При нажатии на зависимый компонент + const handleDependencyClick = id => { + //Закрываем редактирование + setVariableEditor({ display: false, index: null }); + //Открываем настройку компонента + onDependencyClick(id); + }; + + //Текущие переменные панели + const variables = Object.keys(valueProviders).reduce((res, key) => [...res, key], []); + + //Определяем структуру переменных для отображения + const variableChips = variables.map(item => ({ text: item, title: item })); + + //Формирование представления + return ( + + {variableEditor.display && ( + + )} + + + + + ); +}; + +//Контроль свойств компонента - редактор глобальных свойств панели +PanelPropsEditor.propTypes = { + valueProviders: PropTypes.object, + onSettingsChange: PropTypes.func.isRequired, + onDependencyClick: PropTypes.func.isRequired +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { PanelPropsEditor }; diff --git a/app/panels/panels_editor/panel_variable_list.js b/app/panels/panels_editor/panel_variable_list.js new file mode 100644 index 0000000..df34c81 --- /dev/null +++ b/app/panels/panels_editor/panel_variable_list.js @@ -0,0 +1,90 @@ +/* + Парус 8 - Панели мониторинга - Редактор панелей + Список переменных панели +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { Typography, Stack } from "@mui/material"; //Интерфейсные элементы +import { P8PEditorBox } from "../../components/editors/p8p_editor_box"; //Контейнер редактора +import { P8PChipList } from "../../components/editors/p8p_chip_list"; //Дополнительные настройки редактора +import { hasValue } from "../../core/utils"; //Вспомогательные процедуры и функции + +//--------- +//Константы +//--------- + +//Стили +const STYLES = { + STACK_ITEM: { alignItems: "center" }, + TYPOGRAPHY_INFO: { overflow: "hidden" } +}; + +//------------------------------------ +//Вспомогательные функции и компоненты +//------------------------------------ + +const chipRender = item => { + //Определяем описание переменной + const description = item.description; + //Определяем значение переменной + const value = item.value ?? ""; + //Определяем заголовок + const title = `${description}${value ? " - " : ""}${value}`; + return ( + + + {description} + {`${value ? " - " : ""}${value}`} + + + ); +}; + +//----------- +//Тело модуля +//----------- + +//Список переменных панели +const PanelVariableList = ({ valueProviders = {}, onValuesChange = null }) => { + //При очистке значения переменной + const handleVariableClear = index => onValuesChange && onValuesChange({ [values[index].key]: "" }); + + //Определяем структуру переменных + const values = Object.keys(valueProviders).reduce( + (prev, key) => [ + ...prev, + { + description: valueProviders[key].description, + value: valueProviders[key].value, + deleteIcon: "filter_alt_off", + disableDelete: !hasValue(valueProviders[key].value), + key + } + ], + [] + ); + + //Формирование представления + return ( + + handleVariableClear(index) : null} /> + + ); +}; + +//Контроль свойств компонента - список переменных панели +PanelVariableList.propTypes = { + valueProviders: PropTypes.object, + onValuesChange: PropTypes.func +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { PanelVariableList }; diff --git a/app/panels/panels_editor/panels_editor.js b/app/panels/panels_editor/panels_editor.js index 0af2219..9577f48 100644 --- a/app/panels/panels_editor/panels_editor.js +++ b/app/panels/panels_editor/panels_editor.js @@ -7,242 +7,171 @@ //Подключение библиотек //--------------------- -import React, { useEffect, useState, useContext } from "react"; //Классы React -import { Responsive, WidthProvider } from "react-grid-layout"; //Адаптивный макет -import { Box, Grid, Menu, MenuItem, Icon, Fab } from "@mui/material"; //Интерфейсные элементы +import React, { useState, useContext, useEffect } from "react"; //Классы React +import { Box, Icon, Fab } from "@mui/material"; //Интерфейсные элементы import { ApplicationСtx } from "../../context/application"; //Контекст приложения -import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Рабочая область приложения -import { P8PEditorToolBar } from "../../components/editors/p8p_editor_toolbar"; //Панель инструментов редактора -import { genGUID } from "../../core/utils"; //Общие вспомогательные функции -import { LayoutItem } from "./layout_item"; //Элемент макета -import { ComponentView } from "./component_view"; //Представление компонента панели -import { ComponentEditor } from "./component_editor"; //Редактор свойств компонента панели -import { COMPONETNS } from "./components/components"; //Описание доступных компонентов -import "react-grid-layout/css/styles.css"; //Стили для адаптивного макета -import "react-resizable/css/styles.css"; //Стили для адаптивного макета -import "./panels_editor.css"; //Стили редактора панелей +import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений +import { PanelsManager } from "./components/panels_manager/panels_manager"; //Менеджер панелей +import { usePanel } from "./components/components_hooks"; //Вспомогательные хуки +import { NavigationCtx } from "../../context/navigation"; //Контекст навигации +import { PanelEditor } from "./panel_editor"; //Редактор панели //--------- //Константы //--------- +//Заголовок панели по умолчанию +const APP_BAR_TITLE_DEFAULT = "Редактор панелей"; + //Стили const STYLES = { - CONTAINER: { display: "flex" }, - GRID_CONTAINER: { height: `calc(100vh - ${APP_BAR_HEIGHT})` }, - GRID_ITEM_INSPECTOR: { backgroundColor: "#e9ecef" }, - FAB_EDIT: { position: "absolute", top: 12, right: 12, zIndex: 2000 } -}; - -//Заголовоки по умолчанию -const PANEL_CAPTION_EDIT_MODE = "Редактор панелей"; -const PANEL_CAPTION_EXECUTE_MODE = "Исполнение панели"; - -//Начальное состояние размера макета -const INITIAL_BREAKPOINT = "lg"; - -//Начальное состояние макета -const INITIAL_LAYOUTS = { - [INITIAL_BREAKPOINT]: [] + BOX_PLAY_MODE: { + position: "absolute", + top: 12, + right: 15, + zIndex: 2000, + display: "flex", + height: "42px", + width: "145px", + justifyContent: "flex-end", + gap: "10px" + } }; //----------- //Тело модуля //----------- -//Обёрдка для динамического макета -const ResponsiveGridLayout = WidthProvider(Responsive); - //Корневой компонент редактора панелей const PanelsEditor = () => { - //Собственное состояние - const [components, setComponents] = useState({}); - const [valueProviders, setValueProviders] = useState({}); - const [layouts, setLayouts] = useState(INITIAL_LAYOUTS); - const [breakpoint, setBreakpoint] = useState(INITIAL_BREAKPOINT); - const [editMode, setEditMode] = useState(true); - const [editComponent, setEditComponent] = useState(null); - const [addMenuAnchorEl, setAddMenuAnchorEl] = useState(null); + //Собственное состояние - иниализация панели + const [init, setInit] = useState(true); + //Собственное состояние - иниализируемая панель + const [panel, panelName, editMode, isEditAvaliable, isPanelChanged, loadPanel, selectPanel, closePanel, changeEditMode, setPanelChanged] = + usePanel(); + //Отображения менеджера панелей + const [openPanelsManager, setOpenPanelsManager] = useState(false); + //Отображение параметров панели + const [openPanelVariables, setOpenPanelVariables] = useState(false); + //Подключение к контексту навигации + const { getNavigationSearch } = useContext(NavigationCtx); + //Подключение к контексту сообщений + const { showMsgWarn } = useContext(MessagingСtx); //Подключение к контексту приложения const { setAppBarTitle } = useContext(ApplicationСtx); - //Добвление компонента в макет - const addComponent = component => { - const id = genGUID(); - setLayouts(pv => ({ ...pv, [breakpoint]: [...pv[breakpoint], { i: id, x: 0, y: 0, w: 4, h: 10 }] })); - setComponents(pv => ({ ...pv, [id]: { ...component } })); - }; - - //Удаление компонента из макета - const deleteComponent = id => { - setLayouts(pv => ({ ...pv, [breakpoint]: layouts[breakpoint].filter(item => item.i !== id) })); - setComponents(pv => ({ ...pv, [id]: { ...pv[id], deleted: true } })); - if (valueProviders[id]) { - const vPTmp = { ...valueProviders }; - delete vPTmp[id]; - setValueProviders(vPTmp); - } - editComponent === id && closeComponentSettingsEditor(); - }; - - //Включение/выключение режима редиктирования - const toggleEditMode = () => { - if (!editMode) setAppBarTitle(PANEL_CAPTION_EDIT_MODE); - else setAppBarTitle(PANEL_CAPTION_EXECUTE_MODE); - setEditMode(!editMode); - }; - - //Открытие редактора настроек компонента - const openComponentSettingsEditor = id => setEditComponent(id); - - //Закрытие реактора настроек компонента - const closeComponentSettingsEditor = () => setEditComponent(null); - - //Открытие/сокрытие меню добавления - const toggleAddMenu = target => setAddMenuAnchorEl(target instanceof Element ? target : null); - - //При изменении размера холста - const handleBreakpointChange = breakpoint => setBreakpoint(breakpoint); - - //При изменении состояния макета - const handleLayoutChange = (currentLayout, layouts) => setLayouts(layouts); - - //При нажатии на кнопку добалвения - const handleAddClick = e => toggleAddMenu(e.currentTarget); - - //При выборе элемента меню добавления - const handleAddMenuItemClick = component => { - toggleAddMenu(); - addComponent(component); - }; - - //При изменении значений в компоненте - const handleComponentValuesChange = (id, values) => setValueProviders(pv => ({ ...pv, [id]: { ...values } })); - - //При нажатии на настройки компонента - const handleComponentSettingsClick = id => (editComponent === id ? closeComponentSettingsEditor() : openComponentSettingsEditor(id)); - - //При изменении настроек компонента - const handleComponentSettingsChange = ({ id = null, settings = {}, providedValues = [], closeEditor = false } = {}) => { - if (id && components[id]) { - const providedValuesInit = providedValues.reduce((res, providedValue) => ({ ...res, [providedValue]: undefined }), {}); - if (valueProviders[id]) { - const vPTmp = { ...valueProviders[id] }; - Object.keys(valueProviders[id]).forEach(key => !providedValues.includes(key) && delete vPTmp[key]); - setValueProviders(pv => ({ ...pv, [id]: { ...providedValuesInit, ...vPTmp } })); - } else setValueProviders(pv => ({ ...pv, [id]: providedValuesInit })); - setComponents(pv => ({ ...pv, [editComponent]: { ...pv[editComponent], settings: { ...settings } } })); - if (closeEditor === true) closeComponentSettingsEditor(); + //При выборе панели + const handlePanelSelect = panel => { + //Если панель выбрана и есть изменения + if (panel && isPanelChanged) + showMsgWarn( + `Панель содержит несохраненные изменения. Закрыть панель без изменений?`, + () => { + selectPanel(panel.rn, panel.name, panel.modify === 1 ? true : false); + setOpenPanelsManager(false); + }, + () => setOpenPanelsManager(false) + ); + else { + selectPanel(panel.rn, panel.name, panel.modify === 1 ? true : false); + setOpenPanelsManager(false); } }; - //При удалении компоненета - const handleComponentDeleteClick = id => deleteComponent(id); + //Закрытие панели + const handlePanelClose = () => { + //Если панель выбрана и есть изменения + if (panel && isPanelChanged) + showMsgWarn(`Панель содержит несохраненные изменения. Закрыть панель без изменений?`, () => { + closePanel(); + setOpenPanelsManager(true); + }); + else { + closePanel(); + setOpenPanelsManager(true); + } + }; - //При подключении к странице + //Открытие менеджера панелей + const handleOpenPanelsManager = () => setOpenPanelsManager(true); + + //Закрытие менеджера панелей + const handleCancelPanelsManager = () => setOpenPanelsManager(false); + + //Открытие/закрытие параметров панели + const handleToggleOpenPanelVariables = () => setOpenPanelVariables(!openPanelVariables); + + //Включение/выключение режима редактирования + const handleToggleEditMode = () => { + //Включаем/отключаем панель + changeEditMode(!editMode); + //Изначально отключаем отображение параметров панели + setOpenPanelVariables(false); + }; + + //Дополнительные кнопки режима использования панели + const runTimeButtons = !editMode && ( + + {isEditAvaliable ? ( + + edit + + ) : null} + + file_open + + + mediation + + + ); + + //При открытии панели useEffect(() => { - //addComponent(COMPONETNS[0]); - //addComponent(COMPONETNS[3]); - //addComponent(COMPONETNS[4]); - //addComponent(COMPONETNS[1]); - //addComponent(COMPONETNS[2]); + //Считаем параметры открытия панели + const panelPrms = getNavigationSearch(); + //Если указан рег. номер панели - устанавливаем изначальный рег. номер панели + if (panelPrms.SCODE) loadPanel(panelPrms.SCODE); + //Если рег. номера панели нет - открываем менеджер панелей + else handleOpenPanelsManager(); + //Указываем, что инициализация пройдена + setInit(false); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - //Текущие значения панели - const values = Object.keys(valueProviders).reduce((res, key) => ({ ...res, ...valueProviders[key] }), {}); - - //Меню добавления - const addMenu = ( - - {COMPONETNS.map((comp, i) => ( - handleAddMenuItemClick(comp)}> - {comp.name} - - ))} - - ); - - //Кнопка редактирования - const editButton = !editMode && ( - - edit - - ); - - //Панель инструмментов - const toolBar = ( - - ); + //При изменении мнемокода панели + useEffect(() => { + //Устанавливаем заголовок в шапке приложения + setAppBarTitle(panelName ? `Панель [${panelName}]` : APP_BAR_TITLE_DEFAULT); + }, [panelName, setAppBarTitle]); //Генерация содержимого return ( - - {editButton} - {addMenu} - - - - {layouts[breakpoint].map(item => ( - - - - ))} - - - {editMode && ( - - {toolBar} - {editComponent && ( - <> - - - )} - - )} - - + <> + {!init ? ( + <> + {runTimeButtons} + {openPanelsManager && ( + + )} + {panel && ( + + )} + + ) : null} + ); }; diff --git a/db/P8PNL_PE_PANEL.sql b/db/P8PNL_PE_PANEL.sql new file mode 100644 index 0000000..725e510 --- /dev/null +++ b/db/P8PNL_PE_PANEL.sql @@ -0,0 +1,22 @@ +/* + Парус 8 - Панели мониторинга - Общие - Редактор панелей + Панели +*/ +create table P8PNL_PE_PANEL +( + RN number(17) not null, -- Рег. номер записи + CODE varchar2(100) not null, -- Мнемокод + NAME varchar2(2000) not null, -- Наименование + AUTHOR varchar2(30) not null, -- Автор + CH_DATE date not null, -- Дата последнего изменения + READY number(1) default 0 not null, -- Флаг готовности к использованию (0 - нет, 1 - да) + PBL number(1) default 0 not null, -- Флаг публичности (0 - нет, 1 - да) + PANEL clob, -- Описание панели + constraint C_P8PNL_PE_PANEL_RN_PK primary key (RN), + constraint C_P8PNL_PE_PANEL_CODE_NB check (rtrim(CODE) is not null), + constraint C_P8PNL_PE_PANEL_NAME_NB check (rtrim(NAME) is not null), + constraint C_P8PNL_PE_PANEL_AUTHOR_FK foreign key (AUTHOR) references USERLIST (AUTHID) on delete cascade, + constraint C_P8PNL_PE_PANEL_READY_VAL check (READY in (0, 1)), + constraint C_P8PNL_PE_PANEL_PBL_VAL check (PBL in (0, 1)), + constraint C_P8PNL_PE_PANEL_UN unique (CODE) +); diff --git a/db/PKG_P8PANELS_PE.pck b/db/PKG_P8PANELS_PE.pck index 4f70e50..33bb6cb 100644 --- a/db/PKG_P8PANELS_PE.pck +++ b/db/PKG_P8PANELS_PE.pck @@ -6,6 +6,85 @@ create or replace package PKG_P8PANELS_PE as SCODE in varchar2, -- Мнемокод пользовательской процедуры COUT out clob -- Сериализованный список аргументов ); + + /* Получение списка панелей */ + procedure PANEL_LIST + ( + COUT out clob -- Сериализованный список панелей + ); + + /* Считывание базовой информации о панели */ + procedure PANEL_ATTRS_GET + ( + NRN in number, -- Рег. номер панели + NCHECK_ACCESS in number := 1, -- Признак проверки прав доступа (0 - нет, 1 - да) + COUT out clob -- Базовой информация о панели + ); + + /* Считывание базовой информации о панели (по мнемокоду) */ + procedure PANEL_ATTRS_GET_BY_CODE + ( + SCODE in varchar2, -- Мнемокод панели + NCHECK_ACCESS in number := 1, -- Признак проверки прав доступа (0 - нет, 1 - да) + COUT out clob -- Базовой информация о панели + ); + + /* Добавление панели */ + procedure PANEL_INSERT + ( + SCODE in varchar2, -- Мнемокод + SNAME in varchar2, -- Наименование + NRN out number -- Рег. номер добавленной панели + ); + + /* Исправление панели */ + procedure PANEL_UPDATE + ( + NRN in number, -- Рег. номер панели + SCODE in varchar2, -- Мнемокод + SNAME in varchar2 -- Наименование + ); + + /* Удаление панели */ + procedure PANEL_DELETE + ( + NRN in number -- Рег. номер панели + ); + + /* Установка признака "готовности" панели */ + procedure PANEL_READY_SET + ( + NRN in number, -- Рег. номер панели + NREADY in number -- Флаг готовности к использованию (0 - нет, 1 - да) + ); + + /* Установка признака "публичности" панели */ + procedure PANEL_PBL_SET + ( + NRN in number, -- Рег. номер панели + NPBL in number -- Флаг публичности (0 - приватный, 1 - публичный) + ); + + /* Импорт панели из файла */ + procedure PANEL_IMPORT + ( + CPANEL in clob, -- Описание импортируемой панели + NRN out number -- Рег. номер импортированной панели + ); + + /* Получение описания панели */ + procedure PANEL_DESC_GET + ( + NRN in number, -- Рег. номер панели + COUT out clob -- Сериализованное описание панели + ); + + /* Сохранение описания панели */ + procedure PANEL_DESC_SET + ( + NRN in number, -- Рег. номер панели + CPANEL in clob -- Описание панели + ); end PKG_P8PANELS_PE; / @@ -18,110 +97,169 @@ create or replace package body PKG_P8PANELS_PE as COUT out clob -- Сериализованный список аргументов ) is - SRESP_ARG PKG_STD.TSTRING; -- Имя выходного визуализируемого параметра begin - /* Обращаемся к процедуре */ - for C in (select T.RN NRN, - T.PROCNAME SPROC_NAME, - T.PROCTYPE NPROC_TYPE - from USERPROCS T - where T.CODE = SCODE) - loop - /* Проверим возможность использования ПП в качестве источника данных */ - if (C.NPROC_TYPE <> 0) then - P_EXCEPTION(0, - 'Пользовательская процедура "%s" не может быть использована в качестве источника данных: должна иметь тип "Хранимая процедура".', - SCODE); - end if; - if (C.SPROC_NAME is null) then - P_EXCEPTION(0, - 'Пользовательская процедура "%s" не может быть использована в качестве источника данных: не указана хранимая процедура.', - SCODE); - end if; - /* Начинаем формирование XML */ - PKG_XFAST.PROLOGUE(ITYPE => PKG_XFAST.CONTENT_); - /* Открываем корень */ - PKG_XFAST.DOWN_NODE(SNAME => 'XDATA'); - /* Открываем описание процедуры */ - PKG_XFAST.DOWN_NODE(SNAME => 'XUSERPROC'); - /* Обходим параметры */ - for P in (select T.PARAMTYPE NTYPE, - T.PARAMNAME SNAME, - T.NAME SCAPTION, - T.DATATYPE NDATA_TYPE, - case T.DATATYPE - when 0 then - 'STR' - when 1 then - 'NUMB' - when 2 then - 'DATE' - else - null - end SDATA_TYPE, - T.MANDATORY NREQ, - T.VISUALIZE NVISUALIZE - from USERPROCSPARAMS T - where T.PRN = C.NRN - order by T.POSITION) - loop - /* В результирующий список забираем только входные поддерживаемого типа */ - if ((P.NTYPE = 0) and (P.SDATA_TYPE is not null)) then - /* Открываем описание аргумента */ - PKG_XFAST.DOWN_NODE(SNAME => 'arguments'); - /* Описываем аргумент */ - PKG_XFAST.ATTR(SNAME => 'name', SVALUE => P.SNAME); - PKG_XFAST.ATTR(SNAME => 'caption', SVALUE => P.SCAPTION); - PKG_XFAST.ATTR(SNAME => 'dataType', SVALUE => P.SDATA_TYPE); - PKG_XFAST.ATTR(SNAME => 'req', - BVALUE => case P.NREQ - when 1 then - true - else - false - end); - /* Закрываем описание аргумента */ - PKG_XFAST.UP(); - end if; - /* Если встретился визуализируемый параметр типа CLOB */ - if ((P.NVISUALIZE = 1) and (P.NDATA_TYPE = 4)) then - if (SRESP_ARG is null) then - SRESP_ARG := P.SNAME; - else - /* Это уже второй такой - ошибка */ - P_EXCEPTION(0, - 'Пользовательская процедура "%s" не может быть использована в качестве источника данных: имеет более одного выходного параметра типа "Текстовые данные" с признаком "Визуализировать после выполнения".', - SCODE); - end if; - end if; - end loop; - /* Сформируем описание хранимой процедуры */ - PKG_XFAST.DOWN_NODE(SNAME => 'stored'); - PKG_XFAST.ATTR(SNAME => 'name', SVALUE => C.SPROC_NAME); - PKG_XFAST.ATTR(SNAME => 'respArg', SVALUE => SRESP_ARG); - PKG_XFAST.UP(); - /* Закрываем описание процедуры */ - PKG_XFAST.UP(); - /* Закрываем описание корня */ - PKG_XFAST.UP(); - /* Сериализуем */ - COUT := PKG_XFAST.SERIALIZE_TO_CLOB(); - /* Завершаем формирование XML */ - PKG_XFAST.EPILOGUE(); - end loop; - /* Если ничего не нашли */ - if (COUT is null) then - P_EXCEPTION(0, - 'Пользовательская процедура "%s" не определена.', - COALESCE(SCODE, '<НЕ УКАЗАНА>')); - end if; - /* Проверим возможность использования ПП в качестве источника данных - должен быть выходной визуализируемый параметр типа CLOB */ - if (SRESP_ARG is null) then - P_EXCEPTION(0, - 'Пользовательская процедура "%s" не может быть использована в качестве источника данных: должна иметь выходной параметр типа "Текстовые данные" с признаком "Визуализировать после выполнения".', - SCODE); - end if; + /* Базовое описание пользовательской процедуры */ + PKG_P8PANELS_PE_BASE.USERPROCS_DESC(SCODE => SCODE, COUT => COUT); end USERPROCS_DESC; + + /* Получение списка панелей */ + procedure PANEL_LIST + ( + COUT out clob -- Сериализованный список панелей + ) + is + begin + /* Базово сформируем список */ + COUT := PKG_P8PANELS_PE_BASE.PANEL_LIST_GET(SUSER => UTILIZER()); + end PANEL_LIST; + + /* Считывание базовой информации о панели */ + procedure PANEL_ATTRS_GET + ( + NRN in number, -- Рег. номер панели + NCHECK_ACCESS in number := 1, -- Признак проверки прав доступа (0 - нет, 1 - да) + COUT out clob -- Базовой информация о панели + ) + is + begin + /* Если права необходимо проверить */ + if (NCHECK_ACCESS = 1) then + /* Проверим права доступа */ + PKG_P8PANELS_PE_BASE.PANEL_ACCESS_VIEW(NRN => NRN, SUSER => UTILIZER()); + end if; + /* Считываем базовую информацию о панели */ + PKG_P8PANELS_PE_BASE.PANEL_ATTRS_GET(NRN => NRN, COUT => COUT); + end PANEL_ATTRS_GET; + + /* Считывание базовой информации о панели (по мнемокоду) */ + procedure PANEL_ATTRS_GET_BY_CODE + ( + SCODE in varchar2, -- Мнемокод панели + NCHECK_ACCESS in number := 1, -- Признак проверки прав доступа (0 - нет, 1 - да) + COUT out clob -- Базовой информация о панели + ) + is + NRN PKG_STD.TREF; -- Рег. номер панели + begin + /* Считываем рег. номер панели */ + PKG_P8PANELS_PE_BASE.PANEL_FIND_BY_CODE(NFLAG_SMART => 0, NFLAG_OPTION => 0, SCODE => SCODE, NRN => NRN); + /* Если права необходимо проверить */ + if (NCHECK_ACCESS = 1) then + /* Проверим права доступа */ + PKG_P8PANELS_PE_BASE.PANEL_ACCESS_VIEW(NRN => NRN, SUSER => UTILIZER()); + end if; + /* Считываем базовую информацию о панели */ + PKG_P8PANELS_PE_BASE.PANEL_ATTRS_GET(NRN => NRN, COUT => COUT); + end PANEL_ATTRS_GET_BY_CODE; + + /* Добавление панели */ + procedure PANEL_INSERT + ( + SCODE in varchar2, -- Мнемокод + SNAME in varchar2, -- Наименование + NRN out number -- Рег. номер добавленной панели + ) + is + begin + /* Базовое добавление */ + PKG_P8PANELS_PE_BASE.PANEL_INSERT(SCODE => SCODE, SNAME => SNAME, NRN => NRN); + end PANEL_INSERT; + + /* Исправление панели */ + procedure PANEL_UPDATE + ( + NRN in number, -- Рег. номер панели + SCODE in varchar2, -- Мнемокод + SNAME in varchar2 -- Наименование + ) + is + begin + /* Проверим права доступа */ + PKG_P8PANELS_PE_BASE.PANEL_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER()); + /* Базовое исправление */ + PKG_P8PANELS_PE_BASE.PANEL_UPDATE(NRN => NRN, SCODE => SCODE, SNAME => SNAME); + end PANEL_UPDATE; + + /* Удаление панели */ + procedure PANEL_DELETE + ( + NRN in number -- Рег. номер панели + ) + is + begin + /* Проверим права доступа */ + PKG_P8PANELS_PE_BASE.PANEL_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER()); + /* Базовое удаление */ + PKG_P8PANELS_PE_BASE.PANEL_DELETE(NRN => NRN); + end PANEL_DELETE; + + /* Установка признака "готовности" панели */ + procedure PANEL_READY_SET + ( + NRN in number, -- Рег. номер панели + NREADY in number -- Флаг готовности к использованию (0 - нет, 1 - да) + ) + is + begin + /* Проверим права доступа */ + PKG_P8PANELS_PE_BASE.PANEL_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER()); + /* Базовая установка признака готовности */ + PKG_P8PANELS_PE_BASE.PANEL_READY_SET(NRN => NRN, NREADY => NREADY); + end PANEL_READY_SET; + + /* Установка признака "публичности" панели */ + procedure PANEL_PBL_SET + ( + NRN in number, -- Рег. номер панели + NPBL in number -- Флаг публичности (0 - приватный, 1 - публичный) + ) + is + begin + /* Проверим права доступа */ + PKG_P8PANELS_PE_BASE.PANEL_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER()); + /* Базовая установка признака публичности */ + PKG_P8PANELS_PE_BASE.PANEL_PBL_SET(NRN => NRN, NPBL => NPBL); + end PANEL_PBL_SET; + + /* Импорт панели из файла */ + procedure PANEL_IMPORT + ( + CPANEL in clob, -- Описание импортируемой панели + NRN out number -- Рег. номер импортированной панели + ) + is + begin + /* Базовое импортирование панели */ + PKG_P8PANELS_PE_BASE.PANEL_IMPORT(CPANEL => CPANEL, NRN => NRN); + end PANEL_IMPORT; + + /* Получение описания панели */ + procedure PANEL_DESC_GET + ( + NRN in number, -- Рег. номер панели + COUT out clob -- Сериализованное описание панели + ) + is + begin + /* Проверим права доступа */ + PKG_P8PANELS_PE_BASE.PANEL_ACCESS_VIEW(NRN => NRN, SUSER => UTILIZER()); + /* Получим описание панели */ + COUT := PKG_P8PANELS_PE_BASE.PANEL_DESC_GET(NRN => NRN); + end PANEL_DESC_GET; + + /* Сохранение описания панели */ + procedure PANEL_DESC_SET + ( + NRN in number, -- Рег. номер панели + CPANEL in clob -- Описание панели + ) + is + begin + /* Проверим права доступа */ + PKG_P8PANELS_PE_BASE.PANEL_ACCESS_MODIFY(NRN => NRN, SUSER => UTILIZER()); + /* Сохраняем описание панели */ + PKG_P8PANELS_PE_BASE.PANEL_DESC_SET(NRN => NRN, CPANEL => CPANEL); + end PANEL_DESC_SET; end PKG_P8PANELS_PE; / diff --git a/db/PKG_P8PANELS_PE_BASE.pck b/db/PKG_P8PANELS_PE_BASE.pck new file mode 100644 index 0000000..a6b5ae0 --- /dev/null +++ b/db/PKG_P8PANELS_PE_BASE.pck @@ -0,0 +1,702 @@ +create or replace package PKG_P8PANELS_PE_BASE as + + /* Описание пользовательской процедуры */ + procedure USERPROCS_DESC + ( + SCODE in varchar2, -- Мнемокод пользовательской процедуры + COUT out clob -- Сериализованный список аргументов + ); + + /* Поиск рег. номера панели по мнемокоду */ + procedure PANEL_FIND_BY_CODE + ( + NFLAG_SMART in number, -- Признак генерации исключения (0 - да, 1 - нет) + NFLAG_OPTION in number, -- Признак генерации исключения для пустого sCODE (0 - да, 1 - нет) + SCODE in varchar2, -- Мнемокод панели + NRN out number -- Рег. номер панели + ); + + /* Получение признака возможности изменения панели */ + function PANEL_ACCESS_SIGN_MODIFY + ( + NRN in number, -- Рег. номер панели + SUSER in varchar2 -- Имя пользователя + ) return number; -- Признак возможности изменения панели (0 - нет, 1 - да) + + + /* Проверка возможности изменения панели */ + procedure PANEL_ACCESS_MODIFY + ( + NRN in number, -- Рег. номер панели + SUSER in varchar2 -- Имя пользователя + ); + + /* Получение признака возможности просмотра панели */ + function PANEL_ACCESS_SIGN_VIEW + ( + NRN in number, -- Рег. номер панели + SUSER in varchar2 -- Имя пользователя + ) return number; -- Признак возможности просмотра панели (0 - нет, 1 - да) + + /* Проверка возможности просмотра панели */ + procedure PANEL_ACCESS_VIEW + ( + NRN in number, -- Рег. номер панели + SUSER in varchar2 -- Имя пользователя + ); + + /* Считывание базовой информации о панели */ + procedure PANEL_ATTRS_GET + ( + NRN in number, -- Рег. номер панели + COUT out clob -- Базовой информация о панели + ); + + /* Добавление панели */ + procedure PANEL_INSERT + ( + SCODE in varchar2, -- Мнемокод + SNAME in varchar2, -- Наименование + NREADY in number := 0, -- Признак готовности к использованию + NPBL in number := 0, -- Публичность + CPANEL in clob := null, -- Описание панели + NRN out number -- Рег. номер добавленного запроса + ); + + /* Исправление панели */ + procedure PANEL_UPDATE + ( + NRN in number, -- Рег. номер панели + SCODE in varchar2, -- Мнемокод + SNAME in varchar2 -- Наименование + ); + + /* Удаление панели */ + procedure PANEL_DELETE + ( + NRN in number -- Рег. номер панели + ); + + /* Установка признака "готовности" панели */ + procedure PANEL_READY_SET + ( + NRN in number, -- Рег. номер панели + NREADY in number -- Флаг готовности к использованию (0 - нет, 1 - да) + ); + + /* Установка признака "публичности" панели */ + procedure PANEL_PBL_SET + ( + NRN in number, -- Рег. номер панели + NPBL in number -- Флаг публичности (0 - приватный, 1 - публичный) + ); + + /* Загрузка описания панели */ + function PANEL_DESC_GET + ( + NRN in number -- Рег. номер панели + ) return clob; -- XML-описание панели + + /* Сохранение описания панели */ + procedure PANEL_DESC_SET + ( + NRN in number, -- Рег. номер панели + CPANEL in clob -- Описание панели + ); + + /* Импорт панели из файла */ + procedure PANEL_IMPORT + ( + CPANEL in clob, -- Описание импортируемой панели + NRN out number -- Рег. номер импортированной панели + ); + + /* Формирование списка панелей */ + function PANEL_LIST_GET + ( + SUSER in varchar2 -- Имя пользователя + ) return clob; -- Список панелей + +end PKG_P8PANELS_PE_BASE; +/ +create or replace package body PKG_P8PANELS_PE_BASE as + + /* Константы - Теги для сериализации */ + STAG_DATA constant PKG_STD.TSTRING := 'XDATA'; -- Данные + STAG_PANELS constant PKG_STD.TSTRING := 'XPANELS'; -- Панели + STAG_PANEL constant PKG_STD.TSTRING := 'XPANEL'; -- Панель + STAG_PANEL_INFO constant PKG_STD.TSTRING := 'XPANEL_INFO'; -- Информация о панели + STAG_PANEL_ATTRS constant PKG_STD.TSTRING := 'XPANEL_ATTRS'; -- Атрибуты панели + + /* Константы - Атрибуты для сериализации */ + SATTR_RN constant PKG_STD.TSTRING := 'rn'; -- Регистрационный номер + SATTR_CODE constant PKG_STD.TSTRING := 'code'; -- Код + SATTR_NAME constant PKG_STD.TSTRING := 'name'; -- Имя + SATTR_AUTHOR constant PKG_STD.TSTRING := 'author'; -- Автор + SATTR_CH_DATE constant PKG_STD.TSTRING := 'chDate'; -- Дата изменения + SATTR_READY constant PKG_STD.TSTRING := 'ready'; -- Готовность к использованию + SATTR_PBL constant PKG_STD.TSTRING := 'pbl'; -- Публичность + SATTR_MODIFY constant PKG_STD.TSTRING := 'modify'; -- Изменяемость + + /* Описание пользовательской процедуры */ + procedure USERPROCS_DESC + ( + SCODE in varchar2, -- Мнемокод пользовательской процедуры + COUT out clob -- Сериализованный список аргументов + ) + is + SRESP_ARG PKG_STD.TSTRING; -- Имя выходного визуализируемого параметра + begin + /* Обращаемся к процедуре */ + for C in (select T.RN NRN, + T.PROCNAME SPROC_NAME, + T.PROCTYPE NPROC_TYPE + from USERPROCS T + where T.CODE = SCODE) + loop + /* Проверим возможность использования ПП в качестве источника данных */ + if (C.NPROC_TYPE <> 0) then + P_EXCEPTION(0, + 'Пользовательская процедура "%s" не может быть использована в качестве источника данных: должна иметь тип "Хранимая процедура".', + SCODE); + end if; + if (C.SPROC_NAME is null) then + P_EXCEPTION(0, + 'Пользовательская процедура "%s" не может быть использована в качестве источника данных: не указана хранимая процедура.', + SCODE); + end if; + /* Начинаем формирование XML */ + PKG_XFAST.PROLOGUE(ITYPE => PKG_XFAST.CONTENT_); + /* Открываем корень */ + PKG_XFAST.DOWN_NODE(SNAME => 'XDATA'); + /* Открываем описание процедуры */ + PKG_XFAST.DOWN_NODE(SNAME => 'XUSERPROC'); + /* Обходим параметры */ + for P in (select T.PARAMTYPE NTYPE, + T.PARAMNAME SNAME, + T.NAME SCAPTION, + T.DATATYPE NDATA_TYPE, + case T.DATATYPE + when 0 then + 'STR' + when 1 then + 'NUMB' + when 2 then + 'DATE' + else + null + end SDATA_TYPE, + T.MANDATORY NREQ, + T.VISUALIZE NVISUALIZE + from USERPROCSPARAMS T + where T.PRN = C.NRN + order by T.POSITION) + loop + /* В результирующий список забираем только входные поддерживаемого типа */ + if ((P.NTYPE = 0) and (P.SDATA_TYPE is not null)) then + /* Открываем описание аргумента */ + PKG_XFAST.DOWN_NODE(SNAME => 'arguments'); + /* Описываем аргумент */ + PKG_XFAST.ATTR(SNAME => 'name', SVALUE => P.SNAME); + PKG_XFAST.ATTR(SNAME => 'caption', SVALUE => P.SCAPTION); + PKG_XFAST.ATTR(SNAME => 'dataType', SVALUE => P.SDATA_TYPE); + PKG_XFAST.ATTR(SNAME => 'req', + BVALUE => case P.NREQ + when 1 then + true + else + false + end); + /* Закрываем описание аргумента */ + PKG_XFAST.UP(); + end if; + /* Если встретился визуализируемый параметр типа CLOB */ + if ((P.NVISUALIZE = 1) and (P.NDATA_TYPE = 4)) then + if (SRESP_ARG is null) then + SRESP_ARG := P.SNAME; + else + /* Это уже второй такой - ошибка */ + P_EXCEPTION(0, + 'Пользовательская процедура "%s" не может быть использована в качестве источника данных: имеет более одного выходного параметра типа "Текстовые данные" с признаком "Визуализировать после выполнения".', + SCODE); + end if; + end if; + end loop; + /* Сформируем описание хранимой процедуры */ + PKG_XFAST.DOWN_NODE(SNAME => 'stored'); + PKG_XFAST.ATTR(SNAME => 'name', SVALUE => C.SPROC_NAME); + PKG_XFAST.ATTR(SNAME => 'respArg', SVALUE => SRESP_ARG); + PKG_XFAST.UP(); + /* Закрываем описание процедуры */ + PKG_XFAST.UP(); + /* Закрываем описание корня */ + PKG_XFAST.UP(); + /* Сериализуем */ + COUT := PKG_XFAST.SERIALIZE_TO_CLOB(); + /* Завершаем формирование XML */ + PKG_XFAST.EPILOGUE(); + end loop; + /* Если ничего не нашли */ + if (COUT is null) then + P_EXCEPTION(0, + 'Пользовательская процедура "%s" не определена.', + COALESCE(SCODE, '<НЕ УКАЗАНА>')); + end if; + /* Проверим возможность использования ПП в качестве источника данных - должен быть выходной визуализируемый параметр типа CLOB */ + if (SRESP_ARG is null) then + P_EXCEPTION(0, + 'Пользовательская процедура "%s" не может быть использована в качестве источника данных: должна иметь выходной параметр типа "Текстовые данные" с признаком "Визуализировать после выполнения".', + SCODE); + end if; + end USERPROCS_DESC; + + /* Поиск рег. номера панели по мнемокоду */ + procedure PANEL_FIND_BY_CODE + ( + NFLAG_SMART in number, -- Признак генерации исключения (0 - да, 1 - нет) + NFLAG_OPTION in number, -- Признак генерации исключения для пустого sCODE (0 - да, 1 - нет) + SCODE in varchar2, -- Мнемокод панели + NRN out number -- Рег. номер панели + ) + is + begin + /* Если мнемокод не задан */ + if (RTRIM(SCODE) is null) then + /* Генерация исключения */ + if (NFLAG_OPTION = 0) then + P_EXCEPTION(NFLAG_SMART, 'Не задан мнемокод панели.'); + end if; + /* Мнемокод задан */ + else + /* Поиск записи */ + begin + select RN into NRN from P8PNL_PE_PANEL where CODE = SCODE; + exception + when NO_DATA_FOUND then + P_EXCEPTION(NFLAG_SMART, 'Панель "%s" не определена.', SCODE); + end; + end if; + end PANEL_FIND_BY_CODE; + + /* Считывание записи панели */ + function PANEL_GET + ( + NRN in number -- Рег. номер панели + ) return P8PNL_PE_PANEL%rowtype -- Запись панели + is + RRES P8PNL_PE_PANEL%rowtype; -- Буфер для результата + begin + /* Считываем запись */ + select T.* into RRES from P8PNL_PE_PANEL T where T.RN = NRN; + /* Возвращаем запись */ + return RRES; + exception + when NO_DATA_FOUND then + PKG_MSG.RECORD_NOT_FOUND(NFLAG_SMART => 0, NDOCUMENT => NRN, SUNIT_TABLE => 'P8PNL_PE_PANEL'); + end PANEL_GET; + + /* Получение признака возможности изменения панели */ + function PANEL_ACCESS_SIGN_MODIFY + ( + NRN in number, -- Рег. номер панели + SUSER in varchar2 -- Имя пользователя + ) return number -- Признак возможности изменения панели (0 - нет, 1 - да) + is + RP P8PNL_PE_PANEL%rowtype; -- Проверяемая запись панели + begin + /* Читаем панель */ + RP := PANEL_GET(NRN => NRN); + /* Менять можно только свою панель */ + if (RP.AUTHOR = SUSER) then + return 1; + end if; + /* Проверки не пройдены - менять нельзя */ + return 0; + end PANEL_ACCESS_SIGN_MODIFY; + + /* Проверка возможности изменения панели */ + procedure PANEL_ACCESS_MODIFY + ( + NRN in number, -- Рег. номер панели + SUSER in varchar2 -- Имя пользователя + ) + is + begin + /* Получим признак возможности изменения */ + if (PANEL_ACCESS_SIGN_MODIFY(NRN => NRN, SUSER => SUSER) = 0) then + /* Менять нельзя */ + P_EXCEPTION(0, 'У Вас нет прав доступа для изменения панели.'); + end if; + end PANEL_ACCESS_MODIFY; + + /* Получение признака возможности просмотра панели */ + function PANEL_ACCESS_SIGN_VIEW + ( + NRN in number, -- Рег. номер панели + SUSER in varchar2 -- Имя пользователя + ) return number -- Признак возможности просмотра панели (0 - нет, 1 - да) + is + RP P8PNL_PE_PANEL%rowtype; -- Проверяемая запись панели + begin + /* Читаем панель */ + RP := PANEL_GET(NRN => NRN); + /* Смотреть можно только свою или публичную и готовую панель */ + if (((RP.PBL = 1) and (RP.READY = 1)) or (RP.AUTHOR = SUSER)) then + return 1; + end if; + /* Проверки не пройдены - нельзя смотреть */ + return 0; + end PANEL_ACCESS_SIGN_VIEW; + + /* Проверка возможности просмотра панели */ + procedure PANEL_ACCESS_VIEW + ( + NRN in number, -- Рег. номер панели + SUSER in varchar2 -- Имя пользователя + ) + is + begin + /* Получим признак возможности просмотра */ + if (PANEL_ACCESS_SIGN_VIEW(NRN => NRN, SUSER => SUSER) = 0) then + /* Смотреть нельзя */ + P_EXCEPTION(0, 'У Вас нет прав доступа для просмотра панели.'); + end if; + end PANEL_ACCESS_VIEW; + + /* Проверка атрибутов панели */ + procedure PANEL_CHECK + ( + SCODE in varchar2, -- Мнемокод + SNAME in varchar2 -- Наименование + ) + is + begin + /* Мнемокод должен быть задан */ + if (SCODE is null) then + P_EXCEPTION(0, 'Не задан мнемокод панели.'); + end if; + /* Наименование должно быть задано */ + if (SNAME is null) then + P_EXCEPTION(0, 'Не задано наименование панели.'); + end if; + end PANEL_CHECK; + + /* Синхронизация даты изменения панели с текущим временем */ + procedure PANEL_CH_DATE_SYNC + ( + NRN in number -- Рег. номер панели + ) + is + begin + /* Установим текущую дату изменения */ + update P8PNL_PE_PANEL T set T.CH_DATE = sysdate where T.RN = NRN; + /* Контроль изменения данных */ + if (sql%notfound) then + PKG_MSG.RECORD_NOT_FOUND(NDOCUMENT => NRN, SUNIT_TABLE => 'P8PNL_PE_PANEL'); + end if; + end PANEL_CH_DATE_SYNC; + + /* Считывание базовой информации о панели */ + procedure PANEL_ATTRS_GET + ( + NRN in number, -- Рег. номер панели + COUT out clob -- Базовой информация о панели + ) + is + RP P8PNL_PE_PANEL%rowtype; -- Считываемая запись панели + begin + /* Читаем панель */ + RP := PANEL_GET(NRN => NRN); + /* Начинаем формирование XML */ + PKG_XFAST.PROLOGUE(ITYPE => PKG_XFAST.CONTENT_); + /* Открываем корень */ + PKG_XFAST.DOWN_NODE(SNAME => STAG_DATA); + /* Открываем информацию о панели */ + PKG_XFAST.DOWN_NODE(SNAME => STAG_PANEL_ATTRS); + /* Добавляем описание панели */ + PKG_XFAST.ATTR(SNAME => SATTR_RN, NVALUE => RP.RN); + PKG_XFAST.ATTR(SNAME => SATTR_CODE, SVALUE => RP.CODE); + PKG_XFAST.ATTR(SNAME => SATTR_NAME, SVALUE => RP.NAME); + PKG_XFAST.ATTR(SNAME => SATTR_READY, NVALUE => RP.READY); + PKG_XFAST.ATTR(SNAME => SATTR_PBL, NVALUE => RP.PBL); + /* Закрываем информацию о панели */ + PKG_XFAST.UP(); + /* Закрываем корень */ + PKG_XFAST.UP(); + /* Сериализуем */ + COUT := PKG_XFAST.SERIALIZE_TO_CLOB(); + /* Завершаем формирование XML */ + PKG_XFAST.EPILOGUE(); + exception + when others then + /* Завершаем формирование XML */ + PKG_XFAST.EPILOGUE(); + /* Вернем ошибку */ + PKG_STATE.DIAGNOSTICS_STACKED(); + P_EXCEPTION(0, PKG_STATE.SQL_ERRM()); + end PANEL_ATTRS_GET; + + /* Добавление панели */ + procedure PANEL_INSERT + ( + SCODE in varchar2, -- Мнемокод + SNAME in varchar2, -- Наименование + NREADY in number := 0, -- Признак готовности к использованию + NPBL in number := 0, -- Публичность + CPANEL in clob := null, -- Описание панели + NRN out number -- Рег. номер добавленного запроса + ) + is + begin + /* Проверим параметры */ + PANEL_CHECK(SCODE => SCODE, SNAME => SNAME); + /* Формируем рег. номер */ + NRN := GEN_ID(); + /* Добавляем данные */ + insert into P8PNL_PE_PANEL + (RN, CODE, name, AUTHOR, CH_DATE, READY, PBL, PANEL) + values + (NRN, SCODE, SNAME, UTILIZER(), sysdate, NREADY, NPBL, CPANEL); + end PANEL_INSERT; + + /* Исправление панели */ + procedure PANEL_UPDATE + ( + NRN in number, -- Рег. номер панели + SCODE in varchar2, -- Мнемокод + SNAME in varchar2 -- Наименование + ) + is + begin + /* Проверим параметры */ + PANEL_CHECK(SCODE => SCODE, SNAME => SNAME); + /* Изменяем данные */ + update P8PNL_PE_PANEL T + set T.CODE = SCODE, + T.NAME = SNAME + where T.RN = NRN; + /* Контроль изменения данных */ + if (sql%notfound) then + PKG_MSG.RECORD_NOT_FOUND(NDOCUMENT => NRN, SUNIT_TABLE => 'P8PNL_PE_PANEL'); + end if; + /* Обновим дату изменения панели */ + PANEL_CH_DATE_SYNC(NRN => NRN); + end PANEL_UPDATE; + + /* Удаление панели */ + procedure PANEL_DELETE + ( + NRN in number -- Рег. номер панели + ) + is + begin + /* Удаляем запись */ + delete from P8PNL_PE_PANEL T where T.RN = NRN; + /* Контроль изменения данных */ + if (sql%notfound) then + PKG_MSG.RECORD_NOT_FOUND(NDOCUMENT => NRN, SUNIT_TABLE => 'P8PNL_PE_PANEL'); + end if; + end PANEL_DELETE; + + /* Установка признака "готовности" панели */ + procedure PANEL_READY_SET + ( + NRN in number, -- Рег. номер панели + NREADY in number -- Флаг готовности к использованию (0 - нет, 1 - да) + ) + is + begin + /* Проверим параметры */ + if (NREADY is null) then + P_EXCEPTION(0, + 'Не задано значение признака готовности панели к использованию.'); + end if; + if (NREADY not in (0, 1)) then + P_EXCEPTION(0, + 'Значение признака готовности панели к использованию задано некорректно (ожидалось 0 или 1).'); + end if; + /* Установим флаг готовности к использованию */ + update P8PNL_PE_PANEL T set T.READY = NREADY where T.RN = NRN; + /* Контроль изменения данных */ + if (sql%notfound) then + PKG_MSG.RECORD_NOT_FOUND(NDOCUMENT => NRN, SUNIT_TABLE => 'P8PNL_PE_PANEL'); + end if; + /* Обновим дату изменения панели */ + PANEL_CH_DATE_SYNC(NRN => NRN); + end PANEL_READY_SET; + + /* Установка признака "публичности" панели */ + procedure PANEL_PBL_SET + ( + NRN in number, -- Рег. номер панели + NPBL in number -- Флаг публичности (0 - приватный, 1 - публичный) + ) + is + begin + /* Проверим параметры */ + if (NPBL is null) then + P_EXCEPTION(0, 'Не задано значение признака публичности запроса.'); + end if; + if (NPBL not in (0, 1)) then + P_EXCEPTION(0, + 'Значение признака публичноти запроса задано некорректно (ожидалось 0 или 1).'); + end if; + /* Установим флаг публичности */ + update P8PNL_PE_PANEL T set T.PBL = NPBL where T.RN = NRN; + /* Контроль изменения данных */ + if (sql%notfound) then + PKG_MSG.RECORD_NOT_FOUND(NDOCUMENT => NRN, SUNIT_TABLE => 'P8PNL_PE_PANEL'); + end if; + /* Обновим дату изменения панели */ + PANEL_CH_DATE_SYNC(NRN => NRN); + end PANEL_PBL_SET; + + /* Загрузка описания панели */ + function PANEL_DESC_GET + ( + NRN in number -- Рег. номер панели + ) return clob -- XML-описание панели + is + RP P8PNL_PE_PANEL%rowtype; -- Запись панели + begin + /* Читаем панель */ + RP := PANEL_GET(NRN => NRN); + /* Возвращаем результат */ + return RP.PANEL; + end PANEL_DESC_GET; + + /* Сохранение описания панели */ + procedure PANEL_DESC_SET + ( + NRN in number, -- Рег. номер панели + CPANEL in clob -- Описание панели + ) + is + CPANEL_DATA clob; -- Данные описания панели + begin + /* Считываем данные описания панели */ + CPANEL_DATA := BLOB2CLOB(LBDATA => BASE64_DECODE(LCSRCE => CPANEL), SCHARSET => PKG_CHARSET.CHARSET_UTF_()); + /* Установим данные панели */ + update P8PNL_PE_PANEL T set T.PANEL = CPANEL_DATA where T.RN = NRN; + /* Контроль изменения данных */ + if (sql%notfound) then + PKG_MSG.RECORD_NOT_FOUND(NDOCUMENT => NRN, SUNIT_TABLE => 'P8PNL_PE_PANEL'); + end if; + /* Обновим дату изменения панели */ + PANEL_CH_DATE_SYNC(NRN => NRN); + exception + when others then + /* Завершаем формирование XML */ + PKG_XFAST.EPILOGUE(); + /* Вернем ошибку */ + PKG_STATE.DIAGNOSTICS_STACKED(); + P_EXCEPTION(0, PKG_STATE.SQL_ERRM()); + end PANEL_DESC_SET; + + /* Импорт панели */ + procedure PANEL_IMPORT + ( + CPANEL in clob, -- Описание импортируемой панели + NRN out number -- Рег. номер импортированной панели + ) + is + RPARSEDOC PKG_XPATH.TDOCUMENT; -- Документ XML + RROOT PKG_XPATH.TNODE; -- Буфер узла документа + RP P8PNL_PE_PANEL%rowtype; -- Запись панели + CPANEL_DATA clob; -- Данные описания панели + begin + /* Считываем данные описания панели */ + CPANEL_DATA := BLOB2CLOB(LBDATA => BASE64_DECODE(LCSRCE => CPANEL), SCHARSET => PKG_CHARSET.CHARSET_UTF_()); + /* Преобразовываем CLOB */ + RPARSEDOC := PKG_XPATH.PARSE_FROM_CLOB(LCXML => CPANEL_DATA); + /* Считываем корневой узел */ + RROOT := PKG_XPATH.ROOT_NODE(RDOCUMENT => RPARSEDOC); + /* Считываем мнемокод */ + RP.CODE := PKG_XPATH.VALUE(RNODE => RROOT, SPATTERN => STAG_PANEL || '/' || STAG_PANEL_INFO || '/' || SATTR_CODE); + /* Считываем наименование */ + RP.NAME := PKG_XPATH.VALUE(RNODE => RROOT, SPATTERN => STAG_PANEL || '/' || STAG_PANEL_INFO || '/' || SATTR_NAME); + /* Если в описании панели не содержится основной информации о панели */ + if ((RP.CODE is null) or (RP.NAME is null)) then + P_EXCEPTION(0, + 'В описании импортируемой панели отсутствует информация о мнемокоде или наименовании.'); + end if; + /* Считываем готовность к использованию */ + RP.READY := PKG_XPATH.VALUE_NUM(RNODE => RROOT, + SPATTERN => STAG_PANEL || '/' || STAG_PANEL_INFO || '/' || SATTR_READY); + /* Считываем публичность */ + RP.PBL := PKG_XPATH.VALUE_NUM(RNODE => RROOT, SPATTERN => STAG_PANEL || '/' || STAG_PANEL_INFO || '/' || SATTR_PBL); + /* Удаляем лишнюю информацию из описания панели и устанавливаем его */ + RP.PANEL := REGEXP_REPLACE(CPANEL_DATA, '<' || STAG_PANEL_INFO || '>.*?', ''); + /* Добавляем панель */ + PANEL_INSERT(SCODE => RP.CODE, + SNAME => RP.NAME, + NREADY => COALESCE(RP.READY, 0), + NPBL => COALESCE(RP.PBL, 0), + CPANEL => RP.PANEL, + NRN => NRN); + end PANEL_IMPORT; + + /* Формирование списка панелей */ + function PANEL_LIST_GET + ( + SUSER in varchar2 -- Имя пользователя + ) return clob -- Список панелей + is + CRES clob; -- Буфер для сериализации + begin + /* Начинаем формирование XML */ + PKG_XFAST.PROLOGUE(ITYPE => PKG_XFAST.CONTENT_, BALINE => true, BINDENT => true); + /* Открываем корень */ + PKG_XFAST.DOWN_NODE(SNAME => STAG_DATA); + /* Открываем список запросов */ + PKG_XFAST.DOWN_NODE(SNAME => STAG_PANELS); + /* Обходим запросы - данного пользователя и публичные */ + for C in (select T.RN NRN, + T.CODE SCODE, + T.NAME SNAME, + UL.NAME SAUTHOR, + TO_CHAR(T.CH_DATE, 'dd.mm.yyyy hh24:mi:ss') SCH_DATE, + T.READY NREADY, + T.PBL NPBL, + PANEL_ACCESS_SIGN_MODIFY(T.RN, SUSER) NMODIFY + from P8PNL_PE_PANEL T, + USERLIST UL + where T.AUTHOR = UL.AUTHID + and PANEL_ACCESS_SIGN_VIEW(T.RN, SUSER) = 1 + order by T.CODE) + loop + /* Открываем описание панели */ + PKG_XFAST.DOWN_NODE(SNAME => STAG_PANEL); + /* Панель */ + PKG_XFAST.ATTR(SNAME => SATTR_RN, NVALUE => C.NRN); + PKG_XFAST.ATTR(SNAME => SATTR_CODE, SVALUE => C.SCODE); + PKG_XFAST.ATTR(SNAME => SATTR_NAME, SVALUE => C.SNAME); + PKG_XFAST.ATTR(SNAME => SATTR_AUTHOR, SVALUE => C.SAUTHOR); + PKG_XFAST.ATTR(SNAME => SATTR_CH_DATE, SVALUE => C.SCH_DATE); + PKG_XFAST.ATTR(SNAME => SATTR_READY, NVALUE => C.NREADY); + PKG_XFAST.ATTR(SNAME => SATTR_PBL, NVALUE => C.NPBL); + PKG_XFAST.ATTR(SNAME => SATTR_MODIFY, NVALUE => C.NMODIFY); + /* Закрываем описание панели */ + PKG_XFAST.UP(); + end loop; + /* Закрываем список панелей */ + PKG_XFAST.UP(); + /* Закрываем корень */ + PKG_XFAST.UP(); + /* Сериализуем */ + CRES := PKG_XFAST.SERIALIZE_TO_CLOB(); + /* Завершаем формирование XML */ + PKG_XFAST.EPILOGUE(); + /* Возвращаем результат */ + return CRES; + exception + when others then + /* Завершаем формирование XML */ + PKG_XFAST.EPILOGUE(); + /* Вернем ошибку */ + PKG_STATE.DIAGNOSTICS_STACKED(); + P_EXCEPTION(0, PKG_STATE.SQL_ERRM()); + end PANEL_LIST_GET; + +end PKG_P8PANELS_PE_BASE; +/