ЦИТК-1042 - API для работы с настройками панелей

This commit is contained in:
Dollerino 2026-06-09 16:04:51 +03:00
parent 35ff204c07
commit dd338e38a1
17 changed files with 2939 additions and 28 deletions

394
README.md
View File

@ -111,9 +111,10 @@ git clone https://git.citpb.ru/CITKParus/P8-Panels.git
При компиляции учитывайте следующее:
- объекты `PKG_P8PANELS`, `PKG_P8PANELS_BASE`, `PKG_P8PANELS_VISUAL` и `P8PNL_SELECTLIST` - обязательны к компиляции. **Это ядро серверной части расширения. Без этих объектов его работа невозможна. Все остальные, перечисленные ниже объекты серверной части - прикладные, не обязательны к компиляции и на функционирование непосредственно фреймворка влияния не оказывают.**
- объекты `PKG_P8PANELS`, `PKG_P8PANELS_BASE`, `PKG_P8PANELS_VISUAL`, `PKG_P8PANELS_SETTINGS`, `P8PNL_SETTINGS` и `P8PNL_SELECTLIST` и - обязательны к компиляции. **Это ядро серверной части расширения. Без этих объектов его работа невозможна. Все остальные, перечисленные ниже объекты серверной части - прикладные, не обязательны к компиляции и на функционирование непосредственно фреймворка влияния не оказывают.**
- объекты `PKG_P8PANELS_SAMPLES`, `P8PNL_SMPL_CYCLOGRAM` и `P8PNL_SMPL_GANTT` - обеспечивают работу панели "Samples" ("Примеры для разработчиков"). Это абстрактный набор примеров применения фреймворка, не привязанный жестко к комплектации Системы. Должен откомпилироваться практически в любой комплектации Системы (единственное требование - наличие словаря "Контрагенты").
- объекты `PKG_P8PANELS_PROJECTS`, `P8PNL_JB_JOBS`, `P8PNL_JB_JOBSPREV`, `P8PNL_JB_PERIODS`, `P8PNL_JB_PRJCTS` и `P8PNL_JB_PRMS` - могут быть откомпилированы в Системе, имеющей в комлектации приложения "Планирование и учёт в проектах" и "Управление закупками, складом и реализацией". Эти объекты отвечают за работу панелей группы "Планирование и учёт в проектах" ("PrjFin" - "Экономика проектов", "PrjJobs" - "Работы проектов", "PrjGraph" - "Графики проектов", "PrjInfo" - "Информация о проектах"). Компилируйте только если комлектация Системы включает указанные приложения.
- объекты `I_P8PNL_*` - содержат индексы для загружаемых таблиц, **перед созданием индексов необходимо заменить в тексте файла шаблон `<ИМЯ_ИНДЕКСНОГО_ТАБЛИЧНОГОРОСТРАНСТВА>` на имя индексного табличного пространства экземпляра БД**.
- объект `PKG_P8PANELS_CLNTTSKBRD` - может быть откомпилирован в Системе, имеющей в комлектации приложение "Управление деловыми процессами". Этот объект отвечает за работу панелей группы "Управление деловыми процессами" ("ClntTaskBoard" - "Доски задач"). Компилируйте только если комлектация Системы включает указанное приложение.
- объект `PKG_P8PANELS_MECHREC` - может быть откомпилирован в Системе, имеющей в комлектации приложение "Планирование и учёт в дискретном производстве". Этот объект отвечает за работу панелей группы "Планирование и учёт в дискретном производстве" ("MechRecCostProdPlans" - "Производственная программа", "MechRecDeptCostProdPlans" - "Производственный план цеха", "MechRecCostJobsManage" - "Выдача сменного задания", "MechRecCostJobsManageMP" - "Выдача сменного задания на участок", "MechRecDeptCostJobs" - "Загрузка цеха", "MechRecAssemblyMon" - "Мониторинг сборки изделий"). Компилируйте только если комлектация Системы включает указанное приложение.
- объект `PKG_P8PANELS_EQUIPSRV` - может быть откомпилирован в Системе, имеющей в комлектации приложение "Управление техническим обслуживанием и ремонтами". Этот объект отвечает за работу панелей группы "Управление техническим обслуживанием и ремонтами" ("EqsPrfrm" - "Выполнение работ по ТОиР"). Компилируйте только если комлектация Системы включает указанное приложение.
@ -986,6 +987,397 @@ const P8Online = ({ title }) => {
```
### API для взаимодействия с параметрами фреймворка
Для работы с параметрами панелей в составе расширения предусмотрен специальный API. Его подключение к компоненте панели осуществляется через контекст `SettingsCtx` ("app/context/settings.js"). Данный контекст обеспечивает инициализацию, считывание и обновление параметров различных видов (системные, панельные, пользовательские).
В состав API входят:
- `SETTINGS_KINDS` - объект с константами видов параметров
- `initSettings` - функция, инициализация параметров панелей
- `getSettings` - функция, считывание параметров
- `putSettings` - функция, добавление/обновление параметров
- `showSettingsDialog` - функция, отображение диалога настройки параметров
#### `SETTINGS_KINDS`
Объект, содержащий константы видов параметров:
```
{
SYSTEM: 0,
PANEL: 1,
USER: 2
}
```
- `SYSTEM` - Системные параметры, общие для всех панелей. Не требуют указания панели при работе.
- `PANEL` - Параметры конкретной панели. Требуют указания панели (`panel`).
- `USER` - Пользовательские параметры. Требуют указания панели (`panel`).
#### `async Object initSettings(Object)`
Инициализирует параметры панелей в БД Системы. Если параметры с указанными ключами уже существуют - будут перезаписаны.
**Входные параметры:**
```
{
kind,
settings,
panel = null,
full = false,
loader = true
}
```
`kind` - обязательный, число, вид параметров (`SETTINGS_KINDS.SYSTEM`, `SETTINGS_KINDS.PANEL` или `SETTINGS_KINDS.USER`)\
`settings` - обязательный, объект, параметры для инициализации вида `{ "МНЕМОКОД_ПАРАМЕТРА": { name: <НАИМЕНОВАНИЕ>, dataType: <ТИП_ДАННЫХ>, value: <ЗНАЧЕНИЕ>, desc: <ОПИСАНИЕ>, options: {...} }, ... }`\
`panel` - необязательный, строка, имя панели (требуется для `PANEL` и `USER`, игнорируется для `SYSTEM`)\
`full` - необязательный, логический, признак возврата полного ответа сервера (по умолчанию `false`)\
`loader` - необязательный, логический, признак отображения типового индикатора процесса
**Результат:** объект с инициализированными параметрами (если `full = false`) или полный типовой ответ сервера (если `full = true`).
**Особенности:**
- Для вида `SYSTEM` параметр `panel` не требуется и игнорируется
- Для видов `PANEL` и `USER` параметр `panel` обязателен, иначе будет выведена ошибка
- Параметр `options` является необязательным, но если указан, то должен быть объект вида `{<КЛЮЧ>:<ЗНАЧЕНИЕ>, ...}`. Параметры поддерживают привязку к разделам системы, для этого требуется указать следующие `options` `{unitcode: <МНЕМОКОД_РАЗДЕЛА>, showMethod: <МЕТОД_РАЗДЕЛА>, inParameter: <ИМЯ_ВХОДНОГОАРАМЕТРА>, outParameter: <ИМЯ_ВЫХОДНОГОАРАМЕТРА>}`, все параметры являются обязательными
- Функция выполняет обновление существующих параметров, обновляемые значения: `options`, `name`, `desc` и анализирует необходимость обновления значения, если `dataType` не соответствует инициализируемому - обновляет значение `value`, в ином случае значение не обновляется
**Пример:**
```
import React, { useContext } from "react";
import { SettingsCtx } from "../../context/settings";
import { P8P_DATA_TYPES } from "../../core/data_types";
//Инициализируемые параметры
const INIT_SETTINGS = {
INFO: {
name: "Дополнительная информация",
dataType: P8P_DATA_TYPES.STR,
value: "Необходима проверка документов.",
desc: "Дополнительная информация для контрагента"
},
COUNT_DOCS: {
name: "Количество отображаемых документов",
dataType: P8P_DATA_TYPES.NUMB,
value: 10,
desc: ""
},
START_DATE: {
name: "Дата начала",
dataType: P8P_DATA_TYPES.DATE,
value: "2026-02-15",
desc: "Дата начала по умолчанию"
},
AGENT: {
name: "Выбранный контрагент",
dataType: P8P_DATA_TYPES.STR,
value: "",
desc: "Мнемокод выбранного контрагента",
options: {
unitcode: "AGNLIST",
showMethod: "main",
inParameter: "in_AGNABBR",
outParameter: "out_AGNABBR"
}
}
};
const MyPanel = () => {
//Собственное состояние - параметры
const [state, setState] = useState({});
//Подключение к контексту параметров
const { initSettings, SETTINGS_KINDS } = useContext(SettingsCtx);
//Инициализация параметров
const initializeSettings = async () => {
// Инициализация пользовательских параметров
const res = await initSettings({
kind: SETTINGS_KINDS.USER,
panel: "PanelName",
settings: INIT_SETTINGS
});
//Устанавливаем параметры
setState({ ...res });
};
return <div>...</div>;
};
```
#### `async Object getSettings(Object)`
Считывает параметры панелей из БД Системы.
**Входные параметры:**
```
{
kind,
code = null,
panel = null,
full = false,
loader = true
}
```
`kind` - обязательный, число, вид параметров (`SETTINGS_KINDS.SYSTEM`, `SETTINGS_KINDS.PANEL` или `SETTINGS_KINDS.USER`)\
`code` - необязательный, строка, мнемокод параметра для отбора, при пустом значении считывает все доступные параметры\
`panel` - необязательный, строка, имя панели (требуется для `PANEL` и `USER`, игнорируется для `SYSTEM`)\
`full` - необязательный, логический, признак возврата полного ответа сервера с информацией о параметре (по умолчанию `false`)\
`loader` - необязательный, логический, признак отображения типового индикатора процесса
**Результат:** объект с считанными параметрами вида `{ <МНЕМОКОД_ПАРАМЕТРА>: <ЗНАЧЕНИЕ>, ... }` (если `full = false`) или полный ответ сервера (если `full = true`).
**Особенности:**
- Для вида `SYSTEM` параметр `panel` не требуется и игнорируется
- Для видов `PANEL` и `USER` параметр `panel` рекомендуется указать для получения параметров конкретной панели
- Возвращает только значения параметров, без метаданных (если `full = false`)
**Пример:**
```
import React, { useContext, useState, useEffect } from "react";
import { SettingsCtx } from "../../context/settings";
const MyPanel = () => {
//Собственное состояние - параметры
const [state, setState] = useState({});
//Подключение к контексту параметров
const { getSettings, SETTINGS_KINDS } = useContext(SettingsCtx);
//Загрузка параметров пользователя
const handleLoadSettings = async () => {
// Считывание параметров панели
const settings = await getSettings({
kind: SETTINGS_KINDS.USER,
panel: "PanelName"
});
//Устанавливаем параметры
setState({ ...settings });
};
return <div>...</div>;
};
```
#### `async Object putSettings(Object)`
Добавляет или обновляет параметры панелей в БД Системы.
**Входные параметры:**
```
{
kind,
settings,
panel = null,
loader = true
}
```
`kind` - обязательный, число, вид параметров (`SETTINGS_KINDS.SYSTEM`, `SETTINGS_KINDS.PANEL` или `SETTINGS_KINDS.USER`)\
`settings` - обязательный, объект, параметры для добавления/обновления вида `{ <МНЕМОКОД_ПАРАМЕТРА>: { name: <НАИМЕНОВАНИЕ>, dataType: <ТИП_ДАННЫХ>, value: <ЗНАЧЕНИЕ>, desc: <ОПИСАНИЕ>, options: {...} }, ... }`\
`panel` - необязательный, строка, имя панели (требуется для `PANEL` и `USER`, игнорируется для `SYSTEM`)\
`loader` - необязательный, логический, признак отображения типового индикатора процесса
**Результат:** функция не возвращает значимого результата.
**Особенности:**
- Для вида `SYSTEM` параметр `panel` не требуется и игнорируется
- Для видов `PANEL` и `USER` параметр `panel` обязателен, иначе будет выведена ошибка и функция вернётся без выполнения
- При обновлении параметра обновляются следующие значения: `options`, `name`, `desc`, `dataType`, `value`
**Пример:**
```
import React, { useContext } from "react";
import { SettingsCtx } from "../../context/settings";
import { formatDateJSONDateOnly } from "../../core/utils";
//Инициализируемые параметры
const PUT_SETTINGS = {
LAST_UPDATE: {
name: "Дата последнего обновления",
dataType: "DATE",
value: formatDateJSONDateOnly(new Date()),
desc: ""
},
COUNT_DOCS: {
name: "Количество отображаемых документов",
dataType: P8P_DATA_TYPES.NUMB,
value: 15,
desc: ""
}
};
const MyPanel = () => {
//Собственное состояние - параметры
const [state, setState] = useState({});
//Подключение к контексту параметров
const { putSettings, SETTINGS_KINDS } = useContext(SettingsCtx);
//Добавление/обновление параметров пользователя
const handlePutSettings = async () => {
//Обновление системных параметров
await putSettings({
kind: SETTINGS_KINDS.USER,
panel: "PanelName",
settings: PUT_SETTINGS
});
};
return <div>...</div>;
};
```
#### `async undefined showSettingsDialog(Object)`
Отображает диалог настройки параметров пользователя. Диалог позволяет пользователю изменить значения параметров.
**Входные параметры:**
```
{
panel = null,
onOk = null,
onCancel = null
}
```
`panel` - необязательный, строка, имя панели (если не указан - открываются все параметры с разделением по панелям)\
`onOk` - необязательный, функция вида `f({ settings })`, будет вызвана при нажатии "ОК" в диалоге. В параметр `settings` передаются изменённые параметры\
`onCancel` - необязательный, функция вида `f()`, будет вызвана при нажатии "Отмена" или закрытии диалога
**Результат:** функция возвращает следующие объекты:\
- `settings` - объект, содержащий информацию о всех параметрах с учетом изменений
- `isChanged` - признак изменений данных (`true` - параметры были изменены, `false` - изменений не было)
**Особенности:**
- Диалог реализован компонентом `P8PSettingsDialog` ("app/components/p8p_settings_dialog.js")
- При нажатии "ОК" параметры автоматически сохраняются через `putSettings`
- При нажатии "Отмена" параметры не сохраняются
- Диалог закрывается автоматически после обработки нажатий
**Пример:**
```
import React, { useContext } from "react";
import { SettingsCtx } from "../../context/settings";
import { Button } from "@mui/material";
const MyPanel = () => {
//Подключение к контексту параметров
const { showSettingsDialog, SETTINGS_KINDS } = useContext(SettingsCtx);
//При отображении параметров панели
const handleSettingsDialog = async () => {
//Отображаем параметры панели
showSettingsDialog({
panel: "PanelName",
onOk: ({ settings }) => {
console.log("Параметры сохранены:", { settings, isChanged });
},
onCancel: () => {
console.log("Изменения отменены");
}
});
};
return (
<div>
<Button variant="contained" onClick={handleSettingsDialog}>
Настроить параметры
</Button>
</div>
);
};
```
#### Формат параметров
Параметры имеют следующую структуру:
```
{
"МНЕМОКОД_ПАРАМЕТРА": {
name: <НАИМЕНОВАНИЕ>,
dataType: <ТИП_ДАННЫХ>,
value: <ЗНАЧЕНИЕ>,
desc: <ОПИСАНИЕ>,
options: { ... } // необязательный
}
}
```
**Поля параметра:**
- `name` - обязательный, строка, наименование параметра
- `dataType` - обязательный, строка, тип данных параметра (`STR` - строка, `NUMB` - число, `DATE` - дата)
- `value` - необязательный, значение параметра (тип должен соответствовать `dataType`)
- `desc` - необязательный, строка, описание параметра (может использоваться в диалоге настроек как подсказка)
- `options` - необязательный, объект, дополнительные настройки параметра
**Настройки привязки к разделу (`options`):**
Параметр может быть привязан к разделу Системы для автоматического заполнения значения через словарь. Для этого в `options` указываются:
- `unitcode` - строка, код раздела Системы (например, `"AGNLIST"` для словаря "Контрагенты")
- `showMethod` - строка, метод вызова раздела
- `inParameter` - строка, имя входного параметра метода вызова для отбора/позиционирования (например, `"in_AGNABBR"`)
- `outParameter` - строка, имя выходного параметра метода вызова для получения значения (например, `"out_AGNABBR"`)
**Пример параметра с привязкой к словарю:**
```javascript
{
AGENT: {
name: "Выбранный контрагент",
dataType: "STR",
value: "",
desc: "Мнемокод контрагента для отчёта",
options: {
unitcode: "AGNLIST",
showMethod: "main",
inParameter: "in_AGNABBR",
outParameter: "out_AGNABBR"
}
}
}
```
При использовании такого параметра в диалоге настроек (`showSettingsDialog`) будет автоматически предложен выбор из словаря "Контрагенты", и значение параметра будет заполнено после выбора записи.
**Пример:**
Полный актуальный исходный код примера можно увидеть в "app/panels/samples/settings.js" данного репозитория.
#### Примечания
1. **Наименование панели:** при использовании параметра `panel` в него необходимо передавать соответствующее наименование панели из `p8panels.config`.
2. **Формат параметров:** каждый параметр должен быть объектом со структурой `{ name, dataType, value, desc, options }`.
3. **Типы данных (`dataType`):** поддерживаются следующие типы:
- `STR` - строка
- `NUMB` - число
- `DATE` - дата (в формате ISO 8601, например `"2024-01-15"`)
- `BOOL` - логическое (`true`/`false`)
4. **Имена параметров:** мнемокоды параметров должны быть уникальными в рамках одной панели/системы.
5. **Привязка к разделу:** для параметров, которые должны заполняться через выбор из словаря/раздела Системы, обязательно заполняется `options` с полями `unitcode`, `showMethod`, `inParameter`, `outParameter`.
6. **Метаданные:** параметр `full = true` возвращает не только значения параметров, но и все метаданные (`name`, `dataType`, `desc`, `options`), что полезно для построения динамических форм редактирования.
### Компоненты пользовательского интерфейса
#### Типовые интерфейсные примитивы

View File

@ -33,6 +33,7 @@ export const TEXTS = {
export const BUTTONS = {
NAVIGATE_HOME: "Домой", //Переход к домашней странице
NAVIGATE_BACK: "Назад", //Возврат назад по навигации
NAVIGATE_SETTINGS: "Параметры", //Переход к параметрам панелей
NAVIGATE: "Перейти", //Переход к разделу/панели/адресу
OK: "ОК", //Ок
CANCEL: "Отмена", //Отмена

View File

@ -11,6 +11,7 @@ import React, { useState, useContext, useEffect } from "react"; //Классы R
import PropTypes from "prop-types"; //Контроль свойств компонента
import { createHashRouter, RouterProvider, useRouteError } from "react-router-dom"; //Роутер
import { ApplicationCtx } from "./context/application"; //Контекст приложения
import { SettingsCtx } from "./context/settings"; //Контекст параметров
import { NavigationContext, NavigationCtx, getRootLocation } from "./context/navigation"; //Контекст навигации
import { P8PAppErrorPage } from "./components/p8p_app_error_page"; //Страница с ошибкой
import { P8PAppWorkspace } from "./components/p8p_app_workspace"; //Рабочее пространство панели
@ -89,6 +90,12 @@ const Workspace = ({ panels = [], selectedPanel, children } = {}) => {
//Подключение к контексту приложения
const { appState } = useContext(ApplicationCtx);
//Подключение к контексту параметров
const { showSettingsDialog } = useContext(SettingsCtx);
//При открытии диалога параметров панели
const handleSettingsDialog = panel => showSettingsDialog({ panel });
//Отработка действия навигации домой
const handleHomeNavigate = () => navigateRoot();
@ -103,8 +110,10 @@ const Workspace = ({ panels = [], selectedPanel, children } = {}) => {
selectedPanel={selectedPanel}
caption={appState.appBarTitle}
showAppBar={appState.appBarShow}
showAppBarSettings={appState.appBarSettingsShow}
onHomeNavigate={handleHomeNavigate}
onItemNavigate={handleItemNavigate}
onSettingsDialog={handleSettingsDialog}
>
{children}
</P8PAppWorkspace>

View File

@ -21,7 +21,8 @@ import {
List,
ListItemButton,
ListItemIcon,
ListItemText
ListItemText,
Divider
} from "@mui/material"; //Интерфейсные компоненты
import { P8PPanelsMenuDrawer, P8P_PANELS_MENU_PANEL_SHAPE } from "./p8p_panels_menu"; //Меню
import { APP_STYLES } from "../../app.styles"; //Типовые стили
@ -38,6 +39,9 @@ const STYLES = {
DRAWER: { [`& .MuiDrawer-paper`]: { ...APP_STYLES.SCROLL } },
ROOT_BOX: { display: "flex" },
APP_BAR: { position: "fixed" },
APP_BAR_MAIN_BOX: { display: "flex", width: "100vw", alignItems: "center", justifyContent: "space-between" },
APP_BAR_LEFT_SIDE: { display: "flex", alignItems: "center", justifyContent: "flex-start" },
APP_BAR_RIGHT_SIDE: { display: "flex", alignItems: "center", justifyContent: "flex-end" },
APP_BAR_BUTTON: { mr: 2 },
MAIN: { flexGrow: 1 }
};
@ -53,10 +57,13 @@ const P8PAppWorkspace = ({
selectedPanel,
caption,
showAppBar = true,
showAppBarSettings = true,
closeCaption,
homeCaption,
settingsCaption,
onHomeNavigate,
onItemNavigate
onItemNavigate,
onSettingsDialog
} = {}) => {
//Собственное состояния
const [open, setOpen] = useState(false);
@ -80,6 +87,9 @@ const P8PAppWorkspace = ({
onItemNavigate ? onItemNavigate(panel) : null;
};
//Отработка нажатия на элемент открытия параметров
const handleSettingsDialog = (panel = null) => onSettingsDialog(panel);
//Генерация содержимого
return (
<Box sx={STYLES.ROOT_BOX}>
@ -88,6 +98,8 @@ const P8PAppWorkspace = ({
<CssBaseline />
<AppBar sx={STYLES.APP_BAR}>
<Toolbar>
<Box sx={STYLES.APP_BAR_MAIN_BOX}>
<Box sx={STYLES.APP_BAR_LEFT_SIDE}>
<IconButton
color="inherit"
aria-label="open drawer"
@ -100,6 +112,21 @@ const P8PAppWorkspace = ({
<Typography variant="h6" noWrap component="div">
{caption || selectedPanel?.caption}
</Typography>
</Box>
<Box sx={STYLES.APP_BAR_RIGHT_SIDE}>
{showAppBarSettings && selectedPanel.showUserSettings ? (
<IconButton
color="inherit"
aria-label="open drawer"
onClick={() => handleSettingsDialog(selectedPanel.name)}
edge="end"
sx={STYLES.APP_BAR_BUTTON}
>
<Icon>settings</Icon>
</IconButton>
) : null}
</Box>
</Box>
</Toolbar>
</AppBar>
<Drawer anchor="left" open={open} onClose={handleDrawerClose} sx={STYLES.DRAWER}>
@ -116,6 +143,13 @@ const P8PAppWorkspace = ({
</ListItemIcon>
<ListItemText primary={homeCaption} />
</ListItemButton>
<Divider component="li" />
<ListItemButton onClick={() => handleSettingsDialog()}>
<ListItemIcon>
<Icon>settings</Icon>
</ListItemIcon>
<ListItemText primary={settingsCaption} />
</ListItemButton>
</List>
<P8PPanelsMenuDrawer panels={panels} selectedPanel={selectedPanel} onItemNavigate={handleItemNavigate} />
</Drawer>
@ -136,10 +170,13 @@ P8PAppWorkspace.propTypes = {
selectedPanel: P8P_PANELS_MENU_PANEL_SHAPE,
caption: PropTypes.string,
showAppBar: PropTypes.bool,
showAppBarSettings: PropTypes.bool,
closeCaption: PropTypes.string.isRequired,
homeCaption: PropTypes.string.isRequired,
settingsCaption: PropTypes.string.isRequired,
onHomeNavigate: PropTypes.func,
onItemNavigate: PropTypes.func
onItemNavigate: PropTypes.func,
onSettingsDialog: PropTypes.func
};
//----------------

View File

@ -0,0 +1,153 @@
/*
Парус 8 - Панели мониторинга
Компонент: Диалог отображения параметров
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext, useMemo } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, List, ListItem, ListItemButton, ListItemText, Typography, Box, Divider } from "@mui/material"; //Интерфейсные элементы
import { P8PDialog, P8P_DIALOG_WIDTH } from "./p8p_dialog"; //Типовой диалог
import { APP_STYLES } from "../../app.styles"; //Типовые стили
import { P8PSettingsList } from "./p8p_settings_list"; //Список параметров
import { ApplicationCtx } from "../context/application"; //Контекст приложения
import { deepCopyObject, hasValue } from "../core/utils"; //Вспомогательные функции
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER: { display: "flex", flexDirection: "row", alignItems: "flex-start" },
BOX_PANELS: { width: "300px", height: "500px", overflow: "auto", ...APP_STYLES.SCROLL },
BOX_SETTINGS: { width: "520px", height: "500px", overflow: "auto", ...APP_STYLES.SCROLL }
};
//-----------
//Тело модуля
//-----------
//Диалог отображения параметров
const P8PSettingsDialog = ({ settings, panel = null, onOk, onClose }) => {
//Собственное состояние - параметры панелей
const [panelSettings, setPanelSettings] = useState(settings);
//Собственное состояние - мнемокод выбранной панели
const [selectedPanel, setSelectedPanel] = useState(panel);
//Собственное состояние - наличие отображаемых данных
const isSettingsExists = useMemo(() => {
return selectedPanel
? panelSettings && Object.keys(panelSettings).length > 0 && Object.keys(panelSettings[selectedPanel]).length > 0
: Object.keys(panelSettings).length > 0;
}, [selectedPanel, panelSettings]);
//Подключение к контексту приложения
const { appState } = useContext(ApplicationCtx);
//При нажатии на "ОК"
const handleOk = () => onOk && onOk(panelSettings);
//При нажатии на "Отменить"
const handleCancel = () => onClose && onClose();
//При изменении параметра
const handleSettingChange = (settingCode, newSettings) => {
//Определяем панели
const newPanelSettings = deepCopyObject(panelSettings);
//Изменяем настройку у панели
newPanelSettings[selectedPanel][settingCode] = deepCopyObject(newSettings);
//Обновляем панели
setPanelSettings(newPanelSettings);
};
//При нажатии на панель
const handlePanelSelect = panelName => setSelectedPanel(selectedPanel === panelName ? null : panelName);
//Генерация области выбора панели
const panelsListRender = () => {
//Генерация содержимого
return (
<>
<Box sx={STYLES.BOX_PANELS}>
<List>
{Object.keys(panelSettings).map((panel, i) => {
//Считываем информацию о панели
const panelInfo = appState.panels.find(appPanel => appPanel.name == panel);
//Элемент панели
return (
<ListItem key={i}>
<ListItemButton onClick={() => handlePanelSelect(panel)} selected={panel === selectedPanel}>
<ListItemText
primary={panelInfo.name}
secondaryTypographyProps={{ component: "div" }}
secondary={
<Stack direction={"row"} justifyContent={"space-between"} gap={2}>
<Typography
variant={"caption"}
noWrap={true}
title={panelInfo.desc ? panelInfo.desc : "Описание отсутствует"}
>{`${panelInfo.desc ? panelInfo.desc : "Описание отсутствует"}`}</Typography>
</Stack>
}
/>
</ListItemButton>
</ListItem>
);
})}
</List>
</Box>
<Divider orientation="vertical" flexItem />
</>
);
};
//Формирование представления
return (
<P8PDialog
title={`Параметры ${panel ? `"${appState.panels.find(appPanel => appPanel.name == panel).caption}"` : "панелей"}`}
width={P8P_DIALOG_WIDTH.LG}
onCancel={handleCancel}
onOk={handleOk}
scrollContent={false}
okDisabled={Object.keys(panelSettings).length === 0}
>
{isSettingsExists ? (
<Box sx={STYLES.CONTAINER}>
{!hasValue(panel) ? panelsListRender() : null}
<Box sx={STYLES.BOX_SETTINGS}>
{hasValue(selectedPanel) ? (
<P8PSettingsList settings={panelSettings[selectedPanel]} onSettingChange={handleSettingChange} />
) : (
<Typography align="center" variant="subtitle1">
Выберите панель для отображения параметров
</Typography>
)}
</Box>
</Box>
) : (
<Typography align="center" variant="subtitle1">
Отсутствуют доступные параметры
</Typography>
)}
</P8PDialog>
);
};
//Контроль свойств компонента - Диалог отображения параметров
P8PSettingsDialog.propTypes = {
settings: PropTypes.object.isRequired,
panel: PropTypes.string,
onOk: PropTypes.func,
onClose: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PSettingsDialog };

View File

@ -0,0 +1,178 @@
/*
Парус 8 - Панели мониторинга
Компонент: Список параметров
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext, useMemo } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, List, ListItem, ListItemButton, ListItemText, Typography } from "@mui/material"; //Интерфейсные элементы
import { P8PDialog } from "./p8p_dialog"; //Типовой диалог
import { deepCopyObject } from "../core/utils"; //Вспомогательные функции
import { ApplicationCtx } from "../context/application"; //Контекст приложения
import { P8P_DATA_TYPES } from "../core/data_types"; //Типы данных
//---------
//Константы
//---------
//Стили
const STYLES = {
LIST: { width: "510px", bgcolor: "background.paper", overflowY: "auto" },
TYPOGRAPHY_VALUE: { maxWidth: "200px" },
TEXT_FIELD_STR_VALUE: { minWidth: "400px" }
};
//--------------------------------
//Вспомогательные классы и функции
//--------------------------------
//Диалог изменения параметра
const P8PSettingChangeDialog = ({ initSetting, onSave, onClose }) => {
//Собственное состояние - параметр
const [setting, setSetting] = useState(deepCopyObject(initSetting));
//Собственное состояние - доступность выбора из словаря
const isDictionary = useMemo(() => {
//Проверяем наличие обязательных настроек
return (
setting.options &&
setting.options?.unitcode &&
setting.options?.showMethod &&
setting.options?.inParameter &&
setting.options?.outParameter
);
}, [setting.options]);
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationCtx);
//При закрытии диалога
const handleClose = () => onClose && onClose();
//При нажатии "ОК"
const handleOk = () => onSave && onSave(setting);
//При изменении значения параметра
const handleSettingChange = (name, value) => {
setSetting(pv => ({ ...pv, [name]: value }));
};
//При выборе значения из справочника
const handleDictionarySelect = (currentFormValues, setFormValues) => {
//Открываем справочник с учетом параметров
pOnlineShowDictionary({
unitCode: initSetting.options.unitcode,
showMethod: initSetting.options.showMethod,
inputParameters: [{ name: initSetting.options.inParameter, value: currentFormValues.value }],
callBack: res => {
res.success === true ? setFormValues([{ name: "value", value: res.outParameters[initSetting.options.outParameter] }]) : null;
}
});
};
//Формирование представления
return (
<P8PDialog
title={setting.name}
width="xs"
fullWidth
onCancel={handleClose}
onOk={handleOk}
inputs={[
{
name: "value",
value: setting.value,
label: "Значение",
dictionary: isDictionary ? handleDictionarySelect : null,
type: setting.dataType === P8P_DATA_TYPES.NUMB ? "number" : setting.dataType === P8P_DATA_TYPES.DATE ? "date" : "text"
}
]}
onInputChange={handleSettingChange}
/>
);
};
//Контроль свойств компонента - Диалог изменения параметра
P8PSettingChangeDialog.propTypes = {
initSetting: PropTypes.object,
onSave: PropTypes.func,
onClose: PropTypes.func
};
//-----------
//Тело модуля
//-----------
//Список параметров
const P8PSettingsList = ({ settings, onSettingChange }) => {
//Собственное состояние - изменяемый параметр
const [changingSetting, setChangingSetting] = useState(null);
//При нажатии на параметр
const handleSettingClick = settingCode => setChangingSetting(settingCode);
//При очистки изменяемого параметра
const handleChangingSettingClear = () => setChangingSetting(null);
//При изменении значения параметра
const handleSettingChange = newSetting => {
//Вызываем изменение параметра
onSettingChange && onSettingChange(changingSetting, newSetting);
//Очищаем изменяемый параметр
handleChangingSettingClear();
};
//Формирование представления
return (
<>
<List sx={STYLES.LIST}>
{Object.keys(settings).map((setting, i) => {
return (
<ListItem key={i}>
<ListItemButton onClick={() => handleSettingClick(setting)}>
<ListItemText
primary={settings[setting].name}
secondaryTypographyProps={{ component: "div" }}
secondary={
<Stack direction={"row"} justifyContent={"space-between"} gap={2}>
<Typography
variant={"caption"}
noWrap={true}
title={settings[setting].desc ? settings[setting].desc : "Описание отсутствует"}
>{`${settings[setting].desc ? settings[setting].desc : "Описание отсутствует"}`}</Typography>
<Typography
variant={"caption"}
sx={STYLES.TYPOGRAPHY_VALUE}
noWrap={true}
title={settings[setting].value}
>{`${settings[setting].value}`}</Typography>
</Stack>
}
/>
</ListItemButton>
</ListItem>
);
})}
</List>
{changingSetting ? (
<P8PSettingChangeDialog initSetting={settings[changingSetting]} onSave={handleSettingChange} onClose={handleChangingSettingClear} />
) : null}
</>
);
};
//Контроль свойств компонента - Список параметров
P8PSettingsList.propTypes = {
settings: PropTypes.object,
onSettingChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PSettingsList };

View File

@ -37,6 +37,7 @@ import {
import { useTheme } from "@mui/material/styles"; //Взаимодействие со стилями MUI
import { P8PAppInlineError, P8PHintDialog } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
import { P8P_TABLE_AT, HEADER_INITIAL_STATE, hasValue, p8pTableReducer } from "./p8p_table_reducer"; //Редьюсер состояния
import { P8P_DATA_TYPES } from "../core/data_types"; //Типы данных
//---------
//Константы
@ -50,9 +51,9 @@ const P8P_TABLE_SIZE = {
//Типы данных
const P8P_TABLE_DATA_TYPE = {
STR: "STR",
NUMB: "NUMB",
DATE: "DATE"
STR: P8P_DATA_TYPES.STR,
NUMB: P8P_DATA_TYPES.NUMB,
DATE: P8P_DATA_TYPES.DATE
};
//Направления сортировки

View File

@ -30,7 +30,8 @@ const P8P_PANELS_MENU_GRID_CONFIG_PROPS = {
//Конфигурируемые свойства "Рабочего пространства" (P8PAppWorkspace)
const P8P_APP_WORKSPACE_CONFIG_PROPS = {
closeCaption: BUTTONS.CLOSE,
homeCaption: BUTTONS.NAVIGATE_HOME
homeCaption: BUTTONS.NAVIGATE_HOME,
settingsCaption: BUTTONS.NAVIGATE_SETTINGS
};
//Конфигурируемые свойства "Таблицы" (P8PTable)

View File

@ -39,7 +39,7 @@ export const ApplicationContext = ({ errors, displaySizeGetter, guidGenerator, c
const [state, dispatch] = useReducer(applicationReducer, INITIAL_STATE(displaySizeGetter));
//Подключение к контексту взаимодействия с сервером
const { getConfig, getRespPayload } = useContext(BackEndCtx);
const { getConfig, getRespPayload, executeStored } = useContext(BackEndCtx);
//Подключение к контексту отображения сообщений
const { showMsgErr } = useContext(MessagingCtx);
@ -54,7 +54,7 @@ export const ApplicationContext = ({ errors, displaySizeGetter, guidGenerator, c
const setUrlBase = urlBase => dispatch({ type: APP_AT.SET_URL_BASE, payload: urlBase });
//Установка списка панелей
const setPanels = panels => dispatch({ type: APP_AT.LOAD_PANELS, payload: panels });
const setPanels = (panels, panelsSettings) => dispatch({ type: APP_AT.LOAD_PANELS, payload: { panels, panelsSettings } });
//Установка заголовка в шапке приложения
const setAppBarTitle = useCallback(appBarTitle => dispatch({ type: APP_AT.SET_APP_BAR_TITLE, payload: appBarTitle }), []);
@ -62,6 +62,12 @@ export const ApplicationContext = ({ errors, displaySizeGetter, guidGenerator, c
//Установка флага отображения шапки приложения
const setAppBarShow = useCallback(appBarShow => dispatch({ type: APP_AT.SET_APP_BAR_SHOW, payload: appBarShow }), []);
//Установка флага отображения настройки параметров в шапке приложения
const setAppBarSettingsShow = useCallback(
appBarSettingsShow => dispatch({ type: APP_AT.SET_APP_BAR_SETTINGS_SHOW, payload: appBarSettingsShow }),
[]
);
//Поиск раздела по имени
const findPanelByName = name => state.panels.find(panel => panel.name == name);
@ -147,17 +153,36 @@ export const ApplicationContext = ({ errors, displaySizeGetter, guidGenerator, c
//Получение базового URL приложения
const configUrlBase = useMemo(() => state.urlBase, [state.urlBase]);
//Инициализация информации о параметрах панелей
const initPanelsParams = useCallback(async () => {
//Инициализируем параметры панелей
try {
const res = await executeStored({
stored: "PKG_P8PANELS_SETTINGS.PANELS_INIT",
respArg: "COUT",
loader: false
});
return res;
} catch (e) {
showMsgErr(`Ошибка инициализации параметров панелей.`, null, e.message);
}
}, [executeStored, showMsgErr]);
//Инициализация приложения
const initApp = useCallback(async () => {
//Читаем конфигурацию с сервера
let res = await getConfig();
//Сохраняем базовый URL приложения
setUrlBase(getRespPayload(res)?.Panels?.urlBase);
//Инициализируем панели
const panels = getRespPayload(res)?.Panels?.Panel;
//Инициализируем информацию о параметрах панелей
const panelsSettings = await initPanelsParams();
//Сохраняем список панелей
setPanels(getRespPayload(res)?.Panels?.Panel);
setPanels(panels, panelsSettings);
//Установим флаг завершения инициализации
setInitialized();
}, [getConfig, getRespPayload]);
}, [getConfig, getRespPayload, initPanelsParams]);
//Обработка подключения контекста к странице
useEffect(() => {
@ -177,6 +202,7 @@ export const ApplicationContext = ({ errors, displaySizeGetter, guidGenerator, c
value={{
setAppBarTitle,
setAppBarShow,
setAppBarSettingsShow,
findPanelByName,
pOnlineShowTab,
pOnlineShowUnit,

View File

@ -14,7 +14,8 @@ const APP_AT = {
SET_INITIALIZED: "SET_INITIALIZED", //Установка флага инициализированности приложения
SET_DISPLAY_SIZE: "SET_DISPLAY_SIZE", //Установка текущего типового размера экрана
SET_APP_BAR_TITLE: "SET_APP_BAR_TITLE", //Установка заголовка в шапке приложения
SET_APP_BAR_SHOW: "SET_APP_BAR_SHOW" //Установка флага отображения шапки приложения
SET_APP_BAR_SHOW: "SET_APP_BAR_SHOW", //Установка флага отображения шапки приложения
SET_APP_BAR_SETTINGS_SHOW: "SET_APP_BAR_SETTINGS_SHOW" //Установка флага отображения настройки параметров в шапке приложения
};
//Состояние приложения по умолчанию
@ -22,6 +23,7 @@ const INITIAL_STATE = displaySizeGetter => ({
displaySize: displaySizeGetter(),
appBarTitle: "",
appBarShow: true,
appBarSettingsShow: true,
urlBase: "",
panels: [],
panelsLoaded: false,
@ -39,7 +41,8 @@ const handlers = {
//Загрузка списка панелей
[APP_AT.LOAD_PANELS]: (state, { payload }) => {
let panels = [];
if (payload && Array.isArray(payload)) for (let p of payload) panels.push({ ...p });
if (payload && Array.isArray(payload.panels))
for (let p of payload.panels) panels.push({ ...p, showUserSettings: payload?.panelsSettings?.[p.name]?.userSettings === 1 ? true : false });
return {
...state,
panels,
@ -54,6 +57,8 @@ const handlers = {
[APP_AT.SET_APP_BAR_TITLE]: (state, { payload }) => ({ ...state, appBarTitle: payload }),
//Установка флага отображения шапки приложения
[APP_AT.SET_APP_BAR_SHOW]: (state, { payload }) => ({ ...state, appBarShow: payload }),
//Установка флага отображения настройки параметров в шапке приложения
[APP_AT.SET_APP_BAR_SETTINGS_SHOW]: (state, { payload }) => ({ ...state, appBarSettingsShow: payload }),
//Обработчик по умолчанию
DEFAULT: state => state
};

172
app/context/settings.js Normal file
View File

@ -0,0 +1,172 @@
/*
Парус 8 - Панели мониторинга
Контекст: Взаимодействие с API параметров
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { createContext, useContext, useCallback, useState, useRef } from "react"; //ReactJS
import PropTypes from "prop-types"; //Контроль свойств компонента
import { MessagingCtx } from "./messaging"; //Контекст сообщений
import { P8PSettingsDialog } from "../components/p8p_settings_dialog"; //Диалог отображения параметров
import { useSettingsContext } from "./settings_hooks"; //Вспомогательные хуки параметров
//---------
//Константы
//---------
//Доступные виды параметров
const SETTINGS_KINDS = {
SYSTEM: 0,
PANEL: 1,
USER: 2
};
//Доступные действия параметров
const SETTINGS_ACTIONS = {
INIT: "INIT",
GET: "GET",
PUT: "PUT"
};
//Псевдокод панели для системных параметров
const SETTINGS_SYSTEM_PSEUDO = "__P8_SYSTEM";
//----------------
//Интерфейс модуля
//----------------
//Контекст взаимодействия с API параметров
export const SettingsCtx = createContext();
//Провайдер контекста взаимодействия с API параметров
export const SettingsContext = ({ children }) => {
//Подключение к контексту сообщений
const { showMsgErr } = useContext(MessagingCtx);
//Собственное состояние - действия параметров
const { putPanelSettings, getPanelSettings, initPanelSettings } = useSettingsContext(showMsgErr);
//Собственное состояние - отображаемые параметры
const [settings, setSettings] = useState({ isOpen: false, data: {}, panel: null });
//Собственное состояние - колбэки для обработки событий
const settingsCallbacks = useRef({ onOk: null, onCancel: null });
//Формирование структуры параметров панели
const buildPanelSettings = (kind, panel, settings) =>
kind === SETTINGS_KINDS.SYSTEM ? { [SETTINGS_SYSTEM_PSEUDO]: { ...settings } } : { [panel]: { ...settings } };
//Извлечение настроек из ответа сервера
const extractSettings = (res, kind, panel) =>
kind === SETTINGS_KINDS.SYSTEM ? { ...(res[SETTINGS_SYSTEM_PSEUDO] || {}) } : { ...(panel ? res[panel] : res) };
//Валидация параметров настройки
const validateSettingsParams = useCallback(
({ action, kind, panel }) => {
if ([SETTINGS_KINDS.PANEL, SETTINGS_KINDS.USER].includes(kind) && !panel) {
//Формируем текст ошибки
const errMsg = `Ошибка ${
action === SETTINGS_ACTIONS.INIT ? "инициализации" : action === SETTINGS_ACTIONS.PUT ? "добавления" : "действия"
} параметров. Для панельных/пользовательских параметров необходимо указать панель.`;
//Отображаем ошибку
showMsgErr(errMsg);
throw new Error(errMsg);
}
},
[showMsgErr]
);
//Добавление параметров панелей
const putSettings = useCallback(
async ({ kind, settings, panel = null, loader = true }) => {
//Проверка корректности параметров
validateSettingsParams({ action: SETTINGS_ACTIONS.PUT, kind, panel });
//Оборачиваем параметры в панель
const panelSettings = buildPanelSettings(kind, panel, settings);
//Добавляем параметры панели
const res = await putPanelSettings({ kind, panelSettings, loader });
//Возвращаем результат
return res;
},
[putPanelSettings, validateSettingsParams]
);
//Считывание параметров
const getSettings = useCallback(
async ({ kind, code = null, panel = null, full = false, loader = true }) => {
//Считываем параметры панели
const res = await getPanelSettings({ kind, code, panel, full, loader });
//Возвращаем результат
return extractSettings(res, kind, panel);
},
[getPanelSettings]
);
//Инициализация параметров
const initSettings = useCallback(
async ({ kind, settings, panel = null, full = false, loader = true }) => {
//Проверка корректности параметров
validateSettingsParams({ action: SETTINGS_ACTIONS.INIT, kind, panel });
//Оборачиваем параметры в панель
const panelSettings = buildPanelSettings(kind, panel, settings);
//Добавляем параметры панели
const res = await initPanelSettings({ kind, panelSettings, full, loader });
//Возвращаем результат
return extractSettings(res, kind, panel);
},
[initPanelSettings, validateSettingsParams]
);
//При нажатии "ОК" диалога параметров
const handleOk = async newSettings => {
//Признак наличия изменений параметров
const isChanged = JSON.stringify(newSettings) !== JSON.stringify(settings.data);
//Если параметры изменились - обновляем на сервере
if (isChanged) await putPanelSettings({ kind: SETTINGS_KINDS.USER, panelSettings: newSettings });
//Если есть колбэк на нажатие "ОК" - вызываем его
if (settingsCallbacks.current.onOk)
settingsCallbacks.current.onOk({ settings: settings.panel ? newSettings[settings.panel] : newSettings, isChanged });
//Сбрасываем отображаемые данные
setSettings({ isOpen: false, data: {}, panel: null });
};
//При нажатии "Отменить" диалога параметров
const handleClose = () => {
//Если есть колбэк на нажатие "Отменить" - вызываем его
if (settingsCallbacks.current.onCancel) settingsCallbacks.current.onCancel();
//Сбрасываем отображаемые данные
setSettings({ isOpen: false, data: {}, panel: null });
};
//Открытие диалога параметров
const showSettingsDialog = async ({ panel = null, onOk = null, onCancel = null } = {}) => {
//Загружаем информацию о параметрах
const panelSettings = await getPanelSettings({ kind: SETTINGS_KINDS.USER, panel, full: true });
//Устанавливаем отображаемые данные
setSettings({ isOpen: true, data: panelSettings, panel });
//Устанавливаем колбэки
settingsCallbacks.current = { onOk, onCancel };
};
//Вернём компонент провайдера
return (
<SettingsCtx.Provider
value={{
SETTINGS_KINDS,
initSettings,
getSettings,
putSettings,
showSettingsDialog
}}
>
{settings.isOpen ? <P8PSettingsDialog settings={settings.data} panel={settings.panel} onOk={handleOk} onClose={handleClose} /> : null}
{children}
</SettingsCtx.Provider>
);
};
//Контроль свойств - Провайдер контекста взаимодействия с API параметров
SettingsContext.propTypes = {
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node])
};

View File

@ -0,0 +1,120 @@
/*
Парус 8 - Панели мониторинга
Контекст: Параметры - вспомогательные хуки
*/
//---------------------
//Подключение библиотек
//---------------------
import { useCallback, useContext } from "react"; //Классы React
import { BackEndCtx } from "./backend"; //Контекст взаимодействия с сервером
import { object2Base64XML, formatErrorMessage } from "../core/utils"; //Вспомогательные функции
//-----------
//Тело модуля
//-----------
//Хук для SettingsContext
const useSettingsContext = showMsgErr => {
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndCtx);
//Добавление параметров панели
const putPanelSettings = useCallback(
async ({ kind, panelSettings, loader = true }) => {
//Добавляем параметры
try {
await executeStored({
stored: "PKG_P8PANELS_SETTINGS.PUT",
args: {
NKIND: kind,
CPANELS: {
VALUE: object2Base64XML({
XPANELS: panelSettings
}),
SDATA_TYPE: SERV_DATA_TYPE_CLOB
}
},
loader
});
} catch (e) {
//Разбираем текст ошибки
let errMsg = formatErrorMessage(e.message);
//Отображаем ошибку
showMsgErr(errMsg.text, null, errMsg.fullErrorText);
throw e;
}
},
[SERV_DATA_TYPE_CLOB, executeStored, showMsgErr]
);
//Считывание параметров
const getPanelSettings = useCallback(
async ({ kind, code = null, panel = null, full = false, loader = true }) => {
//Считываем параметры
try {
const res = await executeStored({
stored: "PKG_P8PANELS_SETTINGS.GET",
args: {
NKIND: kind,
SCODE: code,
SPANEL: panel,
NFULL: full ? 1 : 0
},
loader,
respArg: "COUT"
});
return res || {};
} catch (e) {
//Разбираем текст ошибки
let errMsg = formatErrorMessage(e.message);
//Отображаем ошибку
showMsgErr(errMsg.text, null, errMsg.fullErrorText);
throw e;
}
},
[executeStored, showMsgErr]
);
//Инициализация параметров панелей
const initPanelSettings = useCallback(
async ({ kind, panelSettings, full = false, loader = true }) => {
//Инициализируем параметры
try {
const res = await executeStored({
stored: "PKG_P8PANELS_SETTINGS.INIT",
args: {
NKIND: kind,
CPANELS: {
VALUE: object2Base64XML({
XPANELS: panelSettings
}),
SDATA_TYPE: SERV_DATA_TYPE_CLOB
},
NFULL: full ? 1 : 0
},
loader,
respArg: "COUT"
});
return res;
} catch (e) {
//Разбираем текст ошибки
let errMsg = formatErrorMessage(e.message);
//Отображаем ошибку
showMsgErr(errMsg.text, null, errMsg.fullErrorText);
throw e;
}
},
[SERV_DATA_TYPE_CLOB, executeStored, showMsgErr]
);
//Возвращаем результат
return { putPanelSettings, getPanelSettings, initPanelSettings };
};
//----------------
//Интерфейс модуля
//----------------
export { useSettingsContext };

15
app/core/data_types.js Normal file
View File

@ -0,0 +1,15 @@
/*
Парус 8 - Панели мониторинга
Ядро: Типы данных
*/
//---------
//Константы
//---------
//Типы данных
export const P8P_DATA_TYPES = {
STR: "STR",
NUMB: "NUMB",
DATE: "DATE"
};

View File

@ -7,7 +7,7 @@
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import React, { useState, useEffect, useContext } from "react"; //Классы React
import { Button, Fab, Icon } from "@mui/material"; //Интерфейсные элементы
import { BUTTONS } from "../../../app.text"; //Текстовые ресурсы и константы
import { P8Online } from "./p8online"; //Пример: API для взаимодействия с "ПАРУС 8 Онлайн"
@ -20,6 +20,8 @@ import { Gantt } from "./gantt"; //Пример: Диаграмма Ганта "
import { Svg } from "./svg"; //Пример: Интерактивные изображения "P8PSVG"
import { Cyclogram } from "./cyclogram"; //Пример: Циклограмма "P8PCyclogram"
import { Indicator } from "./indicator"; //Пример: Индикатор "P8PIndicator"
import { Settings } from "./settings"; //Пример: Параметры панелей
import { ApplicationCtx } from "../../context/application"; //Контекст приложения
//---------
//Константы
@ -36,7 +38,8 @@ const MODES = {
GANTT: { name: "GANTT", caption: 'Диаграмма Ганта "P8PGantt"', component: Gantt },
SVG: { name: "SVG", caption: 'Интерактивные изображения "P8PSVG"', component: Svg },
CYCLOGRAM: { name: "CYCLOGRAM", caption: 'Циклограмма "P8PCyclogram"', component: Cyclogram },
INDICATOR: { name: "INDICATOR", caption: 'Индикатор "P8PIndicator"', component: Indicator }
INDICATOR: { name: "INDICATOR", caption: 'Индикатор "P8PIndicator"', component: Indicator },
SETTINGS: { name: "SETTINGS", caption: "Параметры панелей", component: Settings }
};
//Стили
@ -55,6 +58,15 @@ const Samples = () => {
//Собственное состояние
const [mode, setMode] = useState("");
//Подключение к контексту приложения
const { setAppBarSettingsShow } = useContext(ApplicationCtx);
//При загрузке/открытии режима
useEffect(() => {
if (mode === MODES.SETTINGS.name) setAppBarSettingsShow(true);
else setAppBarSettingsShow(false);
}, [mode, setAppBarSettingsShow]);
//Генерация содержимого
return (
<div style={STYLES.ROOT}>

View File

@ -0,0 +1,235 @@
/*
Парус 8 - Панели мониторинга - Примеры для разработчиков
Пример: Сообщения
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useContext, useState, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Typography, Divider, Button, Box } from "@mui/material"; //Интерфейсные элементы
import { SettingsCtx } from "../../context/settings"; //Контекст параметров
import { P8P_DATA_TYPES } from "../../core/data_types"; //Типы данных
import { formatDateJSONDateOnly } from "../../core/utils"; //Вспомогательные функции
import { ApplicationCtx } from "../../context/application"; //Контекст приложения
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER: { textAlign: "center", paddingTop: "20px" },
TITLE: { paddingBottom: "15px" },
DIVIDER: { margin: "15px" },
BOX_SETTINGS_VALUES: { display: "flex", justifyContent: "space-around" },
SETTING_CARD: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "12px 16px",
borderRadius: 1,
border: "1px solid",
borderColor: "divider"
},
SETTING_NAME: {
fontWeight: 500,
color: "text.primary"
},
SETTING_VALUE: {
fontWeight: 600,
color: "primary.main",
marginLeft: 2
}
};
//Наименование панели
const PANEL_NAME = "Samples";
//Параметры панели
const PANEL_PARAMS = {
STR_PARAM: {
name: "Строковый параметр",
dataType: P8P_DATA_TYPES.STR,
//value: "Строковое значение",
desc: "Описание строкового параметра"
},
NUM_PARAM: {
name: "Числовой параметр",
dataType: P8P_DATA_TYPES.NUMB,
value: 10,
desc: "Описание числового параметра"
},
DATE_PARAM: {
name: "Параметр дата",
dataType: P8P_DATA_TYPES.DATE,
value: "2026-02-15",
desc: ""
},
AGENT: {
name: "Привязка к контрагенту",
dataType: P8P_DATA_TYPES.STR,
value: "",
desc: "Мнемокод выбранного контрагента",
options: {
unitcode: "AGNLIST",
showMethod: "main",
inParameter: "in_AGNABBR",
outParameter: "out_AGNABBR"
}
}
};
//Параметр "Дата последнего обновления"
const LAST_UPDATE_PARAM = {
LAST_UPDATE: {
name: "Дата последнего обновления",
dataType: "DATE",
value: formatDateJSONDateOnly(new Date()),
desc: ""
}
};
//-----------
//Тело модуля
//-----------
//Пример: Параметры
const Settings = ({ title }) => {
//Собственное состояние - параметры
const [state, setState] = useState({});
//Собственное состояние - отображение кнопки параметров панели
const [showPanelSettings, setShowPanelSettings] = useState(true);
//Подключение к контексту параметров
const { getSettings, initSettings, putSettings, showSettingsDialog, SETTINGS_KINDS } = useContext(SettingsCtx);
//Подключение к контексту приложения
const { setAppBarSettingsShow } = useContext(ApplicationCtx);
//Инициализация параметров пользователя
const handleInitSettings = async () => {
// Инициализация пользовательских параметров
const settings = await initSettings({
kind: SETTINGS_KINDS.USER,
panel: PANEL_NAME,
settings: PANEL_PARAMS
});
//Устанавливаем параметры
setState({ ...settings });
};
//Загрузка параметров пользователя
const handleLoadSettings = async () => {
// Считывание параметров панели
const settings = await getSettings({
kind: SETTINGS_KINDS.USER,
panel: PANEL_NAME
});
//Устанавливаем параметры
setState({ ...settings });
};
//Добавление/обновление параметров пользователя
const handlePutSettings = async () => {
//Добавляем параметр
await putSettings({
kind: SETTINGS_KINDS.USER,
panel: PANEL_NAME,
settings: LAST_UPDATE_PARAM
});
//Перечитываем параметры
handleLoadSettings();
};
//При отображении параметров панели
const handleSettingsDialog = async () => {
//Отображаем параметры панели
showSettingsDialog({
panel: PANEL_NAME,
onOk: ({ settings, isChanged }) => {
console.log("Параметры сохранены:", { settings, isChanged });
},
onCancel: () => {
console.log("Изменения отменены");
}
});
};
//Отображение/сокрытие кнопки открытия параметров панели
const handleShowPanelSettingsToggle = () => {
//Переключаем состояние
setShowPanelSettings(!showPanelSettings);
//Скрываем/отображаем кнопку параметров
setAppBarSettingsShow(!showPanelSettings);
};
//При открытии панели
useEffect(() => {
//Инициализируем параметры
handleInitSettings();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//Генерация содержимого
return (
<div style={STYLES.CONTAINER}>
<Typography sx={STYLES.TITLE} variant={"h6"}>
{title}
</Typography>
<Box sx={STYLES.BOX_BUTTON_GROUP}>
<Button variant="contained" onClick={handleInitSettings}>
Инициализация
</Button>
</Box>
<Divider sx={STYLES.DIVIDER} />
<Button variant="contained" onClick={handleLoadSettings}>
Считывание
</Button>
<Divider sx={STYLES.DIVIDER} />
<Button variant="contained" onClick={handlePutSettings}>
Добавление
</Button>
<Divider sx={STYLES.DIVIDER} />
<Button variant="contained" onClick={handleSettingsDialog}>
Диалог параметров панели
</Button>
<Divider sx={STYLES.DIVIDER} />
<Button variant="contained" onClick={handleShowPanelSettingsToggle}>
{`${showPanelSettings ? "Скрыть" : "Отобразить"} кнопку открытия параметров в шапке`}
</Button>
{Object.keys(state).length > 0 ? (
<>
<Divider sx={STYLES.DIVIDER} />
<Typography pt={2} sx={STYLES.TITLE} variant={"h6"}>
Значения параметров
</Typography>
<Box sx={STYLES.BOX_SETTINGS_VALUES}>
{Object.keys(state).map((item, index) => (
<Box key={index} sx={STYLES.SETTING_CARD}>
<Typography variant="body2" sx={STYLES.SETTING_NAME}>
{`${Object.keys(PANEL_PARAMS).includes(item) ? PANEL_PARAMS[item].name : LAST_UPDATE_PARAM[item].name}:`}
</Typography>
<Typography variant="body2" sx={STYLES.SETTING_VALUE}>
{state[item]}
</Typography>
</Box>
))}
</Box>
</>
) : null}
</div>
);
};
//Контроль свойств - Пример: параметры
Settings.propTypes = {
title: PropTypes.string.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { Settings };

30
db/P8PNL_SETTINGS.sql Normal file
View File

@ -0,0 +1,30 @@
/*
Парус 8 - Панели мониторинга
Параметры панелей
*/
create table P8PNL_SETTINGS
(
RN number(17) not null, -- Рег. номер записи
CODE varchar2(40) not null, -- Мнемокод параметра
NAME varchar2(240) not null, -- Наименование параметра
KIND number(1) not null -- Вид параметра
constraint C_P8PNL_SETTINGS_KIND_VAL check ( KIND in (0,1,2) ),
PANEL varchar2(100) default null, -- Код панели
COMPANY number(17) default null, -- Рег. номер организации
AUTHID varchar2(30) default null, -- Пользователь
OPTIONS clob, -- Дополнительные параметры
DATA_TYPE number(1) default 0 -- Тип данных
constraint C_P8PNL_SETTINGS_DT_VAL check ( DATA_TYPE in (0,1,2) ),
VALUE_STR varchar2(4000) default null, -- Значение (строка)
VALUE_NUM number default null, -- Значение (число)
VALUE_DATE date default null, -- Значение (дата)
DESCRIPTION varchar2(240) default null, -- Примечание
constraint C_P8PNL_SETTINGS_PK primary key (RN),
constraint C_P8PNL_SETTINGS_UK unique (CODE, PANEL, COMPANY, AUTHID),
constraint C_P8PNL_SETTINGS_DATA_TYPE_CH check (((DATA_TYPE = 0) and ((VALUE_NUM is null) and (VALUE_DATE is null)))
or ((DATA_TYPE = 1) and ((VALUE_STR is null) and (VALUE_DATE is null)))
or ((DATA_TYPE = 2) and ((VALUE_STR is null) and (VALUE_NUM is null)))),
constraint C_P8PNL_SETTINGS_KIND_CH check (((KIND = 0) and ((PANEL is null) and (COMPANY is null) and (AUTHID is null)))
or ((KIND = 1) and ((PANEL is not null) and (COMPANY is null) and (AUTHID is not null)))
or ((KIND = 2) and ((PANEL is not null) and (COMPANY is not null) and (AUTHID is not null))))
);

1524
db/PKG_P8PANELS_SETTINGS.pck Normal file

File diff suppressed because it is too large Load Diff