diff --git a/app.text.js b/app.text.js index 333f1ff..fe03322 100644 --- a/app.text.js +++ b/app.text.js @@ -15,7 +15,8 @@ export const TITLES = { DEFAULT_PANELS_GROUP: "Без привязки к группе", //Заголовок группы панелей по умолчанию DATA_SOURCE_CONFIG: "Настройка источника данных", //Заголовок для настройки источника данных INSERT: "Добавление", //Заголовок для диалогов/форм добавления - UPDATE: "Исправление" //Заголовок для диалогов/форм исправления + UPDATE: "Исправление", //Заголовок для диалогов/форм исправления + CONFIG: "Настройка" //Заголовок для диалога настройки }; //Текст diff --git a/app/panels/query_editor/components/inspector_query_ents/attr_setup_dialog.js b/app/panels/query_editor/components/inspector_query_ents/attr_setup_dialog.js new file mode 100644 index 0000000..ca613d7 --- /dev/null +++ b/app/panels/query_editor/components/inspector_query_ents/attr_setup_dialog.js @@ -0,0 +1,73 @@ +/* + Парус 8 - Панели мониторинга - Редактор запросов + Компонент: Диалог настройки атрибута сущности +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +import React from "react"; //Классы React +import PropTypes from "prop-types"; //Контроль свойств компонента +import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог +import { TITLES } from "../../../../../app.text"; //Общие текстовые ресурсы приложения +import { DATA_TYPE } from "../../common"; //Общие константы редактора +import { ATTRIBUTE_SHAPE } from "../attribute/attribute"; //Описание атрибута + +//----------- +//Тело модуля +//----------- + +//Диалог добавления/исправления аргумента запроса +const AttrSetupDialog = ({ attr, onOk, onCancel }) => { + //Достанем необходимое из атрибута + const { name, title, dataType, alias } = attr; + + //Нажатие на кнопку "Ok" + const handleOk = values => onOk && onOk({ ...values }); + + //Нажатие на кнопку "Отмена" + const handleCancel = () => onCancel && onCancel(); + + //Генерация содержимого + return ( + + ); +}; + +//Контроль свойств - Диалог настройки атрибута сущности +AttrSetupDialog.propTypes = { + attr: ATTRIBUTE_SHAPE.isRequired, + onOk: PropTypes.func, + onCancel: PropTypes.func +}; + +//---------------- +//Интерфейс модуля +//---------------- + +export { AttrSetupDialog }; diff --git a/app/panels/query_editor/components/inspector_query_ents/attrs_list.js b/app/panels/query_editor/components/inspector_query_ents/attrs_list.js index 72ddbfd..7942003 100644 --- a/app/panels/query_editor/components/inspector_query_ents/attrs_list.js +++ b/app/panels/query_editor/components/inspector_query_ents/attrs_list.js @@ -30,7 +30,7 @@ const STYLES = { //----------- //Список атрибутов сущности -const AttrsList = ({ attrs = [], onSelect = null, onShow = null } = {}) => { +const AttrsList = ({ attrs = [], onSelect = null, onShow = null, onSetup = null } = {}) => { //При выборе элемента списка const handleSelectClick = attr => { onSelect && onSelect(attr); @@ -42,6 +42,12 @@ const AttrsList = ({ attrs = [], onSelect = null, onShow = null } = {}) => { onShow && onShow(attr); }; + //При нажатии на кнопку настройки атрибута + const handleSetupClick = (e, attr) => { + e.stopPropagation(); + onSetup && onSetup(attr); + }; + //Формирование представления return ( @@ -60,7 +66,7 @@ const AttrsList = ({ attrs = [], onSelect = null, onShow = null } = {}) => { secondaryTypographyProps={{ component: "div" }} secondary={ - {`${attr.alias || attr.name}`} + {`${attr.name}${attr.alias ? ` (${attr.alias})` : ""}`} } /> @@ -68,6 +74,9 @@ const AttrsList = ({ attrs = [], onSelect = null, onShow = null } = {}) => { handleShowClick(e, attr)} title={showTitle}> {showIcon} + handleSetupClick(e, attr)} title={"Настроить"}> + settings + @@ -81,7 +90,8 @@ const AttrsList = ({ attrs = [], onSelect = null, onShow = null } = {}) => { AttrsList.propTypes = { attrs: PropTypes.arrayOf(ATTRIBUTE_SHAPE), onSelect: PropTypes.func, - onShow: PropTypes.func + onShow: PropTypes.func, + onSetup: PropTypes.func }; //---------------- diff --git a/app/panels/query_editor/components/inspector_query_ents/entity_attrs_dialog.js b/app/panels/query_editor/components/inspector_query_ents/entity_attrs_dialog.js index 11c0d14..a112047 100644 --- a/app/panels/query_editor/components/inspector_query_ents/entity_attrs_dialog.js +++ b/app/panels/query_editor/components/inspector_query_ents/entity_attrs_dialog.js @@ -13,6 +13,7 @@ import { Box, TextField, InputAdornment, Icon, IconButton } from "@mui/material" import { P8PDialog, P8P_DIALOG_WIDTH } from "../../../../components/p8p_dialog"; //Типовой диалог import { P8PComponentInlineMessage } from "../../../../components/editors/p8p_component_inline_message"; //Типовое встраиваемое сообщение import { AttrsList } from "./attrs_list"; //Список атрибутов сущности +import { AttrSetupDialog } from "./attr_setup_dialog"; //Диалог настройки атрибута import { useEntityAttrs } from "./hooks"; //Хуки диалога настройки атрибутов сущности //--------- @@ -36,6 +37,9 @@ const EntityAttrsDialog = ({ query, id, title, onOk, onCancel }) => { //Собственное состояние - список атрибутов const [attrs, setAttrs] = useState([]); + //Собственное состояние - настраиваемый атрибут + const [setupAttr, setSetupAttr] = useState(null); + //Хук для взаимодействия с сервером const [srvAttrs, saveAttrs] = useEntityAttrs(query, id); @@ -64,6 +68,21 @@ const EntityAttrsDialog = ({ query, id, title, onOk, onCancel }) => { })) ); + //Настройка атрибута + const handleAttrSetup = attr => setSetupAttr({ ...attr }); + + //При закрытии диалога настройки атрибута по "ОК" + const handleAttrSetupOk = values => { + setAttrs( + attrs.map(a => ({ + ...(a.id === setupAttr.id ? { ...a, use: 1, alias: values.alias } : a) + })) + ); + setSetupAttr(null); + }; + + const handleAttrSetupCancel = () => setSetupAttr(null); + //При изменении значения фильтра const handleFilterChange = e => setFilter(e.target.value); @@ -87,6 +106,7 @@ const EntityAttrsDialog = ({ query, id, title, onOk, onCancel }) => { //Генерация содержимого return ( + {setupAttr && } { }} /> - {displayAttrsList === true && } + {displayAttrsList === true && ( + + )} {displayAttrsList === false && ( RENTS(NENT_INDEX).RATTRS, SCHECK_MSG => SCHECK_MSG); + if (SCHECK_MSG is not null) then + P_EXCEPTION(0, SCHECK_MSG); + end if; /* Сохраняем обновленный набор сущностей */ PKG_P8PANELS_QE_BASE.QUERY_ENTS_SET(NRN => RQ.RN, RENTS => RENTS); /* Сохраняем обновленный набор связей */ diff --git a/db/PKG_P8PANELS_QE_BASE.pck b/db/PKG_P8PANELS_QE_BASE.pck index 42d1a69..56ba0f4 100644 --- a/db/PKG_P8PANELS_QE_BASE.pck +++ b/db/PKG_P8PANELS_QE_BASE.pck @@ -69,6 +69,13 @@ create or replace package PKG_P8PANELS_QE_BASE as SVIEW_NAME in varchar2 -- Имя представления ) return varchar2; -- Заголовок представления из метаданных + /* Поиск индекса атрибута по псевдониму */ + function TATTRS_INDEX_BY_ALIAS + ( + RATTRS in TATTRS, -- Коллекция атрибутов + SALIAS in varchar2 -- Искомый псевдоним + ) return number; -- Индекс найденного атрибута (null - если не найдено) + /* Десериализация коллекции атрибутов сущности (из BASE64) */ function TATTRS_FROM_XML_BASE64 ( @@ -77,6 +84,13 @@ create or replace package PKG_P8PANELS_QE_BASE as BADD_ROOT in boolean := false -- Флаг необходимости добавления корневого тэга (false - нет, true - да) ) return TATTRS; -- Коллекция атрибутов сущности + /* Проверка корректности атрибутов сущности */ + procedure TATTRS_CHECK + ( + RATTRS in TATTRS, -- Коллекция атрибутов сущности + SCHECK_MSG out varchar2 -- Сообщения проверки (null - ошибок нет) + ); + /* Поиск индекса аргумента по имени */ function TARGS_INDEX_BY_NAME ( @@ -809,6 +823,36 @@ create or replace package body PKG_P8PANELS_QE_BASE as return SENT_ID || '.' || SNAME; end TATTR_ID_MAKE; + /* Формирование псевдонима атрибута сущности */ + function TATTR_ALIAS_MAKE + ( + SENT_ID in varchar2, -- Уникальный идентификатор родительской сущности + RATTRS in TATTRS, -- Коллекция уже существующих атрибутов + NINDEX in number -- Индекс элемента, для которого формируется псевдоним + ) return varchar2 -- Сформированный идентификатор + is + SRES PKG_STD.TSTRING; -- Буфер для результата + NCNT PKG_STD.TNUMBER := 0; -- Текущая попытка + NMAX_CNT PKG_STD.TNUMBER := 1000; -- Предельное количество попыток + NATTR_INDEX PKG_STD.TNUMBER := 0; -- Индекс поля в коллекции атрибутов сущности, при проверке уникальности псевдонима + begin + /* Подбираем псевдоним */ + while ((NATTR_INDEX is not null) and (NCNT < NMAX_CNT)) + loop + SRES := SENT_ID || TO_CHAR(NINDEX + NCNT); + NATTR_INDEX := TATTRS_INDEX_BY_ALIAS(RATTRS => RATTRS, SALIAS => SRES); + NCNT := NCNT + 1; + end loop; + /* Если так и не смогли сформировать уникальный псевдоним */ + if ((NATTR_INDEX is not null) and (NCNT >= NMAX_CNT)) then + P_EXCEPTION(0, + 'Не удалось сформировать уникальный псевдоним для атрибута сущности "%s".', + SENT_ID); + end if; + /* Вернём полученный псевдоним */ + return SRES; + end TATTR_ALIAS_MAKE; + /* Сериализация атрибута сущности */ procedure TATTR_TO_XML ( @@ -929,6 +973,61 @@ create or replace package body PKG_P8PANELS_QE_BASE as /* Ничего не нашли */ return null; end TATTRS_INDEX_BY_NAME; + + /* Поиск индекса атрибута по псевдониму */ + function TATTRS_INDEX_BY_ALIAS + ( + RATTRS in TATTRS, -- Коллекция атрибутов + SALIAS in varchar2 -- Искомый псевдоним + ) return number -- Индекс найденного атрибута (null - если не найдено) + is + begin + /* Обходим коллекцию */ + if ((RATTRS is not null) and (RATTRS.COUNT > 0)) then + for I in RATTRS.FIRST .. RATTRS.LAST + loop + begin + /* Возвращаем найденный индекс */ + if (RATTRS(I).SALIAS = SALIAS) then + return I; + end if; + exception + when NO_DATA_FOUND then + null; + end; + end loop; + end if; + /* Ничего не нашли */ + return null; + end TATTRS_INDEX_BY_ALIAS; + + /* Подсчет количества атрибутов по псевдониму */ + function TATTRS_COUNT_BY_ALIAS + ( + RATTRS in TATTRS, -- Коллекция атрибутов + SALIAS in varchar2 -- Искомый псевдоним + ) return number -- Количество найденных атрибутов + is + NRES PKG_STD.TNUMBER := 0; --Буфер для расчета + begin + /* Обходим коллекцию */ + if ((RATTRS is not null) and (RATTRS.COUNT > 0)) then + for I in RATTRS.FIRST .. RATTRS.LAST + loop + begin + /* Увеличиваем счетчик, если наш клиент */ + if (RATTRS(I).SALIAS = SALIAS) then + NRES := NRES + 1; + end if; + exception + when NO_DATA_FOUND then + null; + end; + end loop; + end if; + /* Вернем количество найденных */ + return NRES; + end TATTRS_COUNT_BY_ALIAS; /* Формирование списка атрибутов сущности */ function TATTRS_MAKE @@ -995,6 +1094,7 @@ create or replace package body PKG_P8PANELS_QE_BASE as RRES(RRES.LAST).STITLE := DMSCLVIEWSATTRS_TITLE_GET(SVIEW_NAME => RENT.SNAME, SATTR_NAME => RRES(RRES.LAST).SNAME); RRES(RRES.LAST).NDATA_TYPE := RVIEW_FIELD.DATA_TYPE; + RRES(RRES.LAST).SALIAS := TATTR_ALIAS_MAKE(SENT_ID => RENT.SID, RATTRS => RRES, NINDEX => RRES.LAST); /* Если это инициализиция сущности - установим флаг применения в запросе (тогда атрибут будет отображен в диаграмме) */ if (BINIT) then RRES(RRES.LAST).NUSE := 1; @@ -1108,24 +1208,61 @@ create or replace package body PKG_P8PANELS_QE_BASE as return TATTRS_FROM_XML(CXML => CTMP); end TATTRS_FROM_XML_BASE64; + /* Проверка корректности атрибутов сущности */ + procedure TATTRS_CHECK + ( + RATTRS in TATTRS, -- Коллекция атрибутов сущности + SCHECK_MSG out varchar2 -- Сообщения проверки (null - ошибок нет) + ) + is + begin + /* Обходим коллекцию */ + if ((RATTRS is not null) and (RATTRS.COUNT > 0)) then + for I in RATTRS.FIRST .. RATTRS.LAST + loop + /* Псевдоним должен быть задан */ + if (RATTRS(I).SALIAS is null) then + SCHECK_MSG := SCHECK_MSG || 'Для атрибута "' || RATTRS(I).SNAME || '" не задан псевдоним; '; + else + /* А если задан - должен соответствовать по длине */ + if (LENGTH(RATTRS(I).SALIAS) > 30) then + SCHECK_MSG := SCHECK_MSG || 'Длина псевдонима атрибута "' || RATTRS(I).SNAME || + '" превышает максимально допустимую (30 символов); '; + end if; + /* Набору символов */ + if (INSTR(RATTRS(I).SALIAS, '"') <> 0) then + SCHECK_MSG := SCHECK_MSG || 'Псевдоним атрибута "' || RATTRS(I).SNAME || + '" содержит недопустимые символы; '; + end if; + /* Быть уникальным */ + if (TATTRS_COUNT_BY_ALIAS(RATTRS => RATTRS, SALIAS => RATTRS(I).SALIAS) > 1) then + SCHECK_MSG := SCHECK_MSG || 'Нарушение уникальности псевдонима атрибута "' || RATTRS(I).SNAME || '"; '; + end if; + end if; + end loop; + end if; + if (SCHECK_MSG is not null) then + SCHECK_MSG := RTRIM(SCHECK_MSG, '; '); + end if; + end TATTRS_CHECK; + /* Формирование идентификатора сущности */ function TENT_ID_MAKE ( - SNAME in varchar2, -- Имя сущности + STYPE in varchar2, -- Тип (см. константы SENT_TYPE_*) NNUMB in number -- Номер сущности в запросе ) return varchar2 -- Сформированный идентификатор is begin /* Проверим параметры */ - if (SNAME is null) then - P_EXCEPTION(0, 'Не указано имя сущности.'); + if (STYPE is null) then + P_EXCEPTION(0, 'Не указан тип сущности.'); + end if; + if (NNUMB is null) then + P_EXCEPTION(0, 'Не указан номер сущности в запросе.'); end if; /* Соберем идентификатор */ - if (NNUMB is null) then - return SNAME; - else - return SNAME || TO_CHAR(NNUMB); - end if; + return SUBSTR(STYPE, 1, 1) || TO_CHAR(NNUMB); end TENT_ID_MAKE; /* Формирование описания сущности */ @@ -1154,7 +1291,7 @@ create or replace package body PKG_P8PANELS_QE_BASE as /* Получим описание представления */ RVIEW := PKG_OBJECT_DESC.DESC_VIEW(SVIEW_NAME => SNAME, BRAISE_ERROR => true); /* Собираем заголовок сущности */ - RENT.SID := TENT_ID_MAKE(SNAME => RVIEW.VIEW_NAME, NNUMB => NNUMB); + RENT.SID := TENT_ID_MAKE(STYPE => STYPE, NNUMB => NNUMB); RENT.SNAME := RVIEW.VIEW_NAME; RENT.STITLE := DMSCLVIEWS_TITLE_GET(SVIEW_NAME => RENT.SNAME); RENT.STYPE := SENT_TYPE_VIEW; @@ -1306,13 +1443,13 @@ create or replace package body PKG_P8PANELS_QE_BASE as function TENTS_NEXT_NUMB ( RENTS in TENTS, -- Коллекция сущностей - SNAME in varchar2 -- Имя + STYPE in varchar2 -- Тип (см. константы SENT_TYPE_*) ) return number -- Номер сущности в коллекции is NNUMB PKG_STD.TNUMBER := 0; -- Буфер для результата begin /* Подбираем первый свободный номер */ - while (TENTS_INDEX_BY_ID(RENTS => RENTS, SID => TENT_ID_MAKE(SNAME => SNAME, NNUMB => NNUMB)) is not null) + while (TENTS_INDEX_BY_ID(RENTS => RENTS, SID => TENT_ID_MAKE(STYPE => STYPE, NNUMB => NNUMB)) is not null) loop NNUMB := NNUMB + 1; end loop; @@ -1338,7 +1475,7 @@ create or replace package body PKG_P8PANELS_QE_BASE as /* Формируем пописание сущности */ RENT := TENT_MAKE(SNAME => SNAME, STYPE => STYPE, - NNUMB => TENTS_NEXT_NUMB(RENTS => RENTS, SNAME => SNAME), + NNUMB => TENTS_NEXT_NUMB(RENTS => RENTS, STYPE => STYPE), NINIT_ATTRS => NINIT_ATTRS); /* Добавляем её в коллекцию */ RENTS.EXTEND(); @@ -2580,6 +2717,7 @@ create or replace package body PKG_P8PANELS_QE_BASE as /* Если сущность одна - то она и главная */ if (RENTS.COUNT = 1) then RENT := RENTS(RENTS.FIRST); + return; end if; /* Обходим все сущности */ for E in RENTS.FIRST .. RENTS.LAST @@ -2590,13 +2728,15 @@ create or replace package body PKG_P8PANELS_QE_BASE as for A in RENTS(E).RATTRS.FIRST .. RENTS(E).RATTRS.LAST loop /* Обойдем связи и поищем атрибут среди источников */ - for R in RRLS.FIRST .. RRLS.LAST - loop - if (RRLS(R).SSOURCE = RENTS(E).RATTRS(A).SID) then - BFOUND := true; - exit; - end if; - end loop; + if ((RRLS is not null) and (RRLS.COUNT > 0)) then + for R in RRLS.FIRST .. RRLS.LAST + loop + if (RRLS(R).SSOURCE = RENTS(E).RATTRS(A).SID) then + BFOUND := true; + exit; + end if; + end loop; + end if; /* Если хоть один атрибут сущности есть среди источников - дальше можно атрибуты этой сущности не рассматривать */ exit when BFOUND = true; end loop; @@ -2621,12 +2761,14 @@ create or replace package body PKG_P8PANELS_QE_BASE as /* Формирование части "select" */ procedure BUILD_SELECT ( - RENTS TENTS, -- Сущности запроса - SQRY in out varchar2 -- Буфер для запроса + RENTS TENTS, -- Сущности запроса + SQRY in out varchar2 -- Буфер для запроса ) is - SFIELD PKG_STD.TSTRING; -- Буфер для поля запроса + SFIELD PKG_STD.TSTRING; -- Буфер для поля запроса + BFOUND boolean := false; -- Флаг наличия визуализируемых атрибутов begin + /* Формируем */ PKG_SQL_BUILD.APPEND(SSQL => SQRY, SELEMENT1 => 'select '); for I in RENTS.FIRST .. RENTS.LAST loop @@ -2634,15 +2776,24 @@ create or replace package body PKG_P8PANELS_QE_BASE as for J in RENTS(I).RATTRS.FIRST .. RENTS(I).RATTRS.LAST loop if (RENTS(I).RATTRS(J).NSHOW = 1) then - SFIELD := RENTS(I).RATTRS(J).SID || ' ' || RENTS(I).RATTRS(J).SNAME || I || J; - if (not ((I = RENTS.LAST) and (J = RENTS(I).RATTRS.LAST))) then - SFIELD := SFIELD || ', '; + if (RENTS(I).RATTRS(J).SALIAS is null) then + P_EXCEPTION(0, + 'Для атрибута ("%s") не задан севдоним.', + RENTS(I).RATTRS(J).SID); end if; + SFIELD := RENTS(I).RATTRS(J).SID || ' "' || RENTS(I).RATTRS(J).SALIAS || '", '; PKG_SQL_BUILD.APPEND(SSQL => SQRY, SELEMENT1 => SFIELD); + BFOUND := true; end if; end loop; end if; end loop; + SQRY := RTRIM(SQRY, ', '); + /* Если не нашли ни одного атрибута */ + if (not BFOUND) then + P_EXCEPTION(0, + 'Для запроса не определено ни одного визуализируемого атрибута.'); + end if; end BUILD_SELECT; /* Формирование иерархии связей в секции "from" */ @@ -2701,7 +2852,7 @@ create or replace package body PKG_P8PANELS_QE_BASE as PKG_SQL_BUILD.APPEND(SSQL => SQRY, SELEMENT1 => 'from '); STABLE := RENT_ROOT.SNAME || ' ' || RENT_ROOT.SID; PKG_SQL_BUILD.APPEND(SSQL => SQRY, SELEMENT1 => STABLE); - if (RRLS.COUNT > 0) then + if ((RRLS is not null) and (RRLS.COUNT > 0)) then BUILD_FROM_HIER(RENT_START => RENT_ROOT, RENTS => RENTS, RRLS => RRLS, SQRY => SQRY); end if; else @@ -2723,10 +2874,6 @@ create or replace package body PKG_P8PANELS_QE_BASE as end if; end BUILD_WHERE; begin - /* - TODO: owner="root" created="09.09.2025" - text="Контроль длины псевдонима таблицы и поля (максимум 30 символов)" - */ /* TODO: owner="root" created="10.09.2025" text="Предусмотреть связям признак ""обязательность"" @@ -2746,14 +2893,14 @@ create or replace package body PKG_P8PANELS_QE_BASE as /* Читаем сущности запроса */ RENTS := QUERY_ENTS_GET(CENTS => RQ.ENTS); /* Нет сущностей - нет запроса */ - if ((RENTS is null) and (RENTS.COUNT = 0)) then + if ((RENTS is null) or (RENTS.COUNT = 0)) then SET_MSG(SMESSAGE => 'В запросе нет сущностей.'); return; end if; /* Читаем связи запроса */ RRLS := QUERY_RLS_GET(CRLS => RQ.RLS); /* Нельзя построить запрос, если есть несвязанные сущности */ - if ((RENTS.COUNT > 1) and (RENTS.COUNT - 1 > RRLS.COUNT)) then + if ((RENTS.COUNT > 1) and ((RRLS is null) or (RENTS.COUNT - 1 > RRLS.COUNT))) then SET_MSG(SMESSAGE => 'В запросе есть несвязанные сущности.'); return; end if; @@ -2766,7 +2913,7 @@ create or replace package body PKG_P8PANELS_QE_BASE as /* Собираем запрос - таблицы и связи */ BUILD_FROM(RENTS => RENTS, RRLS => RRLS, SQRY => SQRY); /* Собираем запрос - условия */ - BUILD_WHERE(ROPT => ROPT, SQRY => SQRY); + BUILD_WHERE(ROPT => ROPT, SQRY => SQRY); exception when others then PKG_STATE.DIAGNOSTICS_STACKED();