Возможность применять несколько JS-скриптов к одному полю

Здравствуйте!

Сейчас в Nocobase к полю в таблице можно подключить только один JS-обработчик рендера (ctx.element / ctx.render). Из-за этого приходится складывать всю логику отображения ячейки в один монолитный скрипт: форматирование значений, цветовая заливка (зебра, RGB, статусные цвета, подсветка расхождений), кнопка копирования, тултипы, inline-редактирование, открытие просмотра записи и т. д. У нас такой скрипт уже разросся до ~400 строк и переключается флагами в шапке.

Это создаёт несколько проблем:

  1. Переиспользование: нельзя взять «только кнопку Copy» или «только зебру» в другом поле — приходится копировать весь файл и отключать ненужное флагами.
  2. Поддержка: правка одного эффекта требует понимания всего скрипта, велик риск сломать соседнюю логику.
  3. Композиция: невозможно собрать поведение поля «из кубиков» — например, «зебра + тултип + редактирование» без копипасты.
  4. Производительность: один большой скрипт с общим ctx.observe срабатывает на любое изменение, даже если активна лишь часть фич.

Предложение: дать возможность подключать к одному полю несколько независимых JS-скриптов (цепочка / стек рендереров), которые применяются последовательно к одному и тому же ctx.element. Каждый скрипт включается/отключается отдельно в настройках поля. Можно представить это как «плагины ячейки»: один отвечает за фон, другой за кнопку Copy, третий за тултип и т. д.

Вопросы:

  • Рассматривается ли такая возможность в роадмапе?
  • Если да, можно ли заложить порядок применения (приоритет) и общий доступ к ctx между скриптами?
  • Если нет — какой подход вы рекомендуете для модульной композиции поведения ячейки в текущей архитектуре?

Спасибо!

Сейчас у меня код выглядит так:

// Версия кода 0115 (Добавлены переменные enableRgbBackground и rgbBackgroundColor для кастомной заливки цвета ячеек. Добавлена специальная обработка Position_units: извлекает value.Position_units ?? value.id; createdBy/updatedBy показывают displayName)

const utils = {
    isValidUrl(string) {
        try { new URL(string); return true; } catch (_) { return false; }
    },
    normalizeUrl(url) {
        if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ftp://')) return url;
        return 'https://' + url;
    },
    formatDate(value, ctx) {
        if (!value) return '';

        // Обработка belongsTo объектов
        if (typeof value === 'object' && !Array.isArray(value)) {
            const fieldName = ctx.collectionField?.name ?? '';
            const baseName = fieldName.replace(/_current$|_requests$/, '');

            // Специальная обработка для Position_units
            if (baseName === 'Position_units') {
                value = value.Position_units ?? value.id ?? '';
            }
            // Специальная обработка для пользовательских полей (createdBy, updatedBy)
            else if (fieldName.includes('createdBy') || fieldName.includes('updatedBy')) {
                if (value.nickname_short) {
                    value = value.nickname_short;
                } else if (value.nickname) {
                    value = value.nickname;
                } else if (value.GIS_name_two && value.GIS_name_one && value.GIS_name_three) {
                    value = `${value.GIS_name_two} ${value.GIS_name_one} ${value.GIS_name_three}`;
                } else {
                    value = value.id ?? '';
                }
            }
            // Для остальных объектов - преобразуем в строку как есть
        }

        const fieldType = ctx.collectionField?.type;
        const fieldInterface = ctx.collectionField?.interface;
        if (fieldType === 'date' || fieldInterface === 'createdAt' || fieldInterface === 'updatedAt') {
            try { return ctx.libs.dayjs(value).format('DD.MM.YYYY в HH:mm'); } catch (e) {}
        }
        return String(value ?? '');
    },
    getDisplayValue(ctx) {
        const fieldName = ctx.collectionField?.name ?? '';
        const record = ctx.record ?? {};
        let value = record[fieldName];

        if (!value) return '';

        // Если это объект (belongsTo связь), извлекаем нужное поле
        if (typeof value === 'object' && !Array.isArray(value)) {
            const baseName = fieldName.replace(/_current$|_requests$/, '');
            if (baseName === 'Position_units') {
                value = value.Position_units ?? value.id ?? '';
            } else {
                // Приоритет: nickname_short → nickname → ФИО → ID
                if (value.nickname_short) {
                    value = value.nickname_short;
                } else if (value.nickname) {
                    value = value.nickname;
                } else if (value.GIS_name_two && value.GIS_name_one && value.GIS_name_three) {
                    value = `${value.GIS_name_two} ${value.GIS_name_one} ${value.GIS_name_three}`;
                } else {
                    value = value.id ?? '';
                }
            }
        }

        // Если это дата, форматируем её
        const fieldType = ctx.collectionField?.type;
        const fieldInterface = ctx.collectionField?.interface;
        if (fieldType === 'date' || fieldInterface === 'createdAt' || fieldInterface === 'updatedAt') {
            try {
                const formatted = ctx.libs.dayjs(value).format('DD.MM.YYYY в HH:mm');
                if (!formatted.includes('Invalid')) {
                    return formatted;
                }
            } catch (e) {}
        }

        return String(value ?? '');
    },
    getBackgroundColor({ enableComparison, enableGrayBackground, enableFieldBackground, fieldBackgroundColor, enableRgbBackground, rgbBackgroundColor, Requests, Current }) {
        if (enableFieldBackground && fieldBackgroundColor) return fieldBackgroundColor;
        if (enableComparison && Requests !== null && Requests !== Current) return '#ffef88';
        if (enableRgbBackground && rgbBackgroundColor) return rgbBackgroundColor;
        if (enableGrayBackground) return '#F7F7F7';
        return 'transparent';
    },
    applyTextHighlight(element, { shouldHighlightRed, shouldHighlightGreen }) {
        if (shouldHighlightRed) {
            element.style.color = '#d32f2f';
            element.style.fontWeight = '600';
        } else if (shouldHighlightGreen) {
            element.style.color = '#7BD960';
            element.style.fontWeight = '600';
        }
    },
    resolveComparisonFields(ctx) {
        const fieldName = ctx.collectionField?.name ?? '';
        const record = ctx.record ?? {};
        let currentValue = record[fieldName];
        let requestsValue = null;
        if (fieldName.endsWith('_current')) {
            requestsValue = record[fieldName.replace(/_current$/, '_requests')];
        } else if (fieldName.endsWith('_requests')) {
            requestsValue = record[fieldName];
            currentValue = record[fieldName.replace(/_requests$/, '_current')];
        }

        // Логика извлечения значения из объектов связей
        const extractValue = (val) => {
            if (!val) return null;
            if (typeof val === 'object' && !Array.isArray(val)) {
                const baseName = fieldName.replace(/_current$|_requests$/, '');
                if (baseName === 'Position_units') {
                    return val.Position_units ?? val.id ?? val;
                }
                // Приоритет: nickname_short → nickname → ФИО
                if (val.nickname_short) {
                    return val.nickname_short;
                } else if (val.nickname) {
                    return val.nickname;
                } else if (val.GIS_name_two && val.GIS_name_one && val.GIS_name_three) {
                    return `${val.GIS_name_two} ${val.GIS_name_one} ${val.GIS_name_three}`;
                }
                return val.id ?? val;
            }
            return val;
        };

        return {
            Current: extractValue(currentValue) ?? null,
            Requests: extractValue(requestsValue) ?? null
        };
    },
    resolveFieldStatusBackground(ctx) {
        const fieldName = ctx.collectionField?.name ?? '';
        const record = ctx.record ?? {};
        const codeIUS = record['Code_IUS']; const codeES = record['Code_ES'];
        const requestIUS = record['Request_IUS']; const requestES = record['Request_ES'];
        const hasCodeIUS = !!String(codeIUS || '').trim();
        const hasCodeES = !!String(codeES || '').trim();
        const hasRequestIUS = !!String(requestIUS || '').trim();
        const hasRequestES = !!String(requestES || '').trim();
        if (fieldName === 'Request_IUS') return (!hasCodeIUS && hasRequestIUS) ? '#EDF9E8' : null;
        if (fieldName === 'Code_IUS') return hasCodeIUS ? '#EDF9E8' : null;
        if (fieldName === 'Request_ES') return (!hasCodeES && hasRequestES) ? '#E7F1FF' : null;
        if (fieldName === 'Code_ES') return hasCodeES ? '#E7F1FF' : null;
        return null;
    },
    async saveFieldValue({ ctx, recordId, fieldName, newValue, onSuccess }) {
        try {
            await ctx.request?.({
                url: `${ctx.collection?.name}:update`,
                method: 'patch',
                params: { filterByTk: String(recordId) },
                data: { [fieldName]: newValue },
            });
            onSuccess?.();
        } catch (error) { ctx.message.error('Ошибка сохранения'); }
    }
};

// ==========================================
// 1. КОНФИГУРАЦИЯ
// ==========================================
const isTargetField = false;         // Открывать просмотр записи по клику? (true/false)
const enableComparison = false;      // Подсвечивать желтым расхождения current/requests?
const enableRedHighlight = false;    // (Зарезервировано)
const enableGreenHighlight = false;  // (Зарезервировано)
const enableGrayBackground = true;   // Чередование серого фона (зебра)?
const enableRgbBackground = false;   // Включить заливку кастомным цветом?
const rgbBackgroundColor = '#19A8E1'; // Цвет заливки для enableRgbBackground
const enableWordWrap = true;        // Перенос строк (true) или обрезка многоточием (false)?
const copyButtonPosition = 'inline';  // Позиция кнопки копирования: 'right' (справа) или 'inline' (рядом с текстом)
const urlDisplayMode = 'label';      // Вид ссылок: 'label' (текст "Ссылка на сайт") или 'full' (полный URL)
const enableFieldStatus = false;      // Включить логику статусных цветов для полей IUS/ES?
const enableInlineEdit = false;       // Редактирование текста по двойному клику?
const enableTooltip = false;         // Показывать тултип с полным текстом при наведении?
const tooltipMaxWidth = 300;         // Макс. ширина тултипа
const tooltipBgColor = '';           // Цвет фона тултипа: '' = по умолчанию, или '#000000', '#fff' и т.д.
const tooltipTextColor = '';         // Цвет текста тултипа: '' = по умолчанию, или '#fff', '#333' и т.д.

// ==========================================
// 2. ФУНКЦИЯ РЕНДЕРА
// ==========================================

// Инициализация кэша для оптимизации
if (!ctx.element.__cacheData) {
    ctx.element.__cacheData = {
        lastFieldValue: undefined,
        lastComparisonState: null,
        lastStatusBg: null
    };
}

const render = () => {
    // Проверка изменений перед рендером
    const fieldName = ctx.collectionField?.name;
    const currentValue = ctx.record?.[fieldName];
    const cache = ctx.element.__cacheData;

    let needsRender = cache.lastFieldValue !== currentValue;

    if ((enableComparison || enableFieldStatus) && !needsRender) {
        const { Current, Requests } = utils.resolveComparisonFields(ctx);
        const currentComparison = JSON.stringify({ Current, Requests });
        if (cache.lastComparisonState !== currentComparison) needsRender = true;

        if (!needsRender && enableFieldStatus) {
            const statusBg = utils.resolveFieldStatusBackground(ctx);
            if (cache.lastStatusBg !== statusBg) needsRender = true;
        }
    }

    // При первом рендере всегда пересоздаём (cache.lastFieldValue === undefined)
    if (cache.lastFieldValue === undefined) needsRender = true;

    // Если ничего не изменилось, не пересоздаём компонент
    if (!needsRender) return;

    const { Current, Requests } = utils.resolveComparisonFields(ctx);
    const fieldStatusColor = enableFieldStatus ? utils.resolveFieldStatusBackground(ctx) : null;
    const bgColor = utils.getBackgroundColor({
        enableComparison, enableGrayBackground,
        enableFieldBackground: !!fieldStatusColor, fieldBackgroundColor: fieldStatusColor,
        enableRgbBackground, rgbBackgroundColor,
        Requests, Current
    });
    const shouldHighlightRed   = enableRedHighlight   && Requests !== null && Requests !== Current;
    const shouldHighlightGreen = enableGreenHighlight && Requests !== null && Requests !== Current;

    ctx.element.innerHTML = '';

    Object.assign(ctx.element.style, {
        backgroundColor: bgColor, width: '100%', display: 'flex', position: 'relative',
        minHeight: '28px', height: enableWordWrap ? 'auto' : '28px',
        borderRadius: '8px', overflow: 'hidden', alignItems: 'flex-start'
    });

    const text = utils.formatDate(ctx.value, ctx);
    const hasValue = text.trim().length > 0;
    const isUrl = utils.isValidUrl(text);

    const openRecordView = async () => {
        try {
            const popupUid = ctx.model.uid + '-1';
            await ctx.openView(popupUid, { mode: 'dialog', title: ctx.t('View Details'), size: 'medium' });
        } catch (error) { ctx.setAction?.('view'); }
    };

    const { Tooltip } = ctx.libs.antd;
    const React = ctx.libs.React;

    // ─── CopyBtn ───────────────────────────────────────────────────────────────
    // Всегда height: 20px — одинаковая высота во всех столбцах.
    // Позиционирование (absolute / inline) решается снаружи через обёртку.
    const CopyBtn = ({ visible }) => {
        const [hovered, setHovered] = React.useState(false);
        const isRight = copyButtonPosition === 'right';

        return React.createElement('div', {
            style: {
                display: 'inline-flex',
                alignItems: 'center',
                justifyContent: 'center',
                cursor: 'pointer',
                height: isRight ? '100%' : '20px', // right — на всю высоту ячейки, inline — фиксированные 20px
                flexShrink: 0,
                whiteSpace: 'nowrap',
                boxSizing: 'border-box',
                verticalAlign: 'middle',
                borderRadius: isRight ? '0 8px 8px 0' : '4px',
                // right-режим: только левая граница — остальные обрезаются overflow:hidden ячейки
                border: isRight ? 'none' : '1px solid #1677ff',
                borderLeft: '1px solid #1677ff',
                padding: hovered ? '2px 8px' : '2px 4px',
                backgroundColor: hovered ? '#1677ff' : '#f0f5ff',
                color: hovered ? '#fff' : '#1677ff',
                fontSize: '11px',
                transition: 'all 0.15s ease-in-out',
                opacity: visible ? 1 : 0,
                visibility: visible ? 'visible' : 'hidden',
            },
            onMouseEnter: () => setHovered(true),
            onMouseLeave: () => setHovered(false),
            onClick: (e) => {
                e.stopPropagation();
                navigator.clipboard.writeText(text);
                ctx.message.success('Скопировано');
            }
        }, [
            React.createElement('span', {
                key: 'icon',
                dangerouslySetInnerHTML: {
                    __html: '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>'
                }
            }),
            hovered && React.createElement('span', { key: 'label', style: { marginLeft: '4px' } }, 'Copy')
        ]);
    };

    // ─── MainContent ───────────────────────────────────────────────────────────
    const MainContent = () => {
        const [showBtn, setShowBtn] = React.useState(false);
        const [isEdit, setIsEdit] = React.useState(false);
        const [isHovered, setIsHovered] = React.useState(false);
        const [tempVal, setTempVal] = React.useState(text);
        const [isTextOverflowing, setIsTextOverflowing] = React.useState(false);
        const textRef = React.useRef(null);

        // Проверяем переполнение при первом рендере и при изменении текста
        React.useEffect(() => {
            const checkOverflow = () => {
                if (!textRef.current) return;
                try {
                    const element = textRef.current;
                    const isOverflowing = element.scrollWidth > element.clientWidth ||
                                            element.scrollHeight > element.clientHeight;
                    setIsTextOverflowing(isOverflowing);
                } catch (e) {
                    setIsTextOverflowing(false);
                }
            };

            // Проверяем с небольшой задержкой, чтобы DOM обновился
            const timer = setTimeout(checkOverflow, 0);
            return () => clearTimeout(timer);
        }, [text, enableWordWrap]);

        // Режим редактирования
        if (isEdit) {
            return React.createElement(enableWordWrap ? 'textarea' : 'input', {
                autoFocus: true, value: tempVal,
                style: {
                    width: '100%', height: '100%', minHeight: '28px',
                    padding: '4px 8px', margin: 0,
                    border: 'none', outline: 'none', backgroundColor: '#fff',
                    boxShadow: 'inset 0 0 0 2px #1677ff', borderRadius: '8px',
                    boxSizing: 'border-box', display: 'block',
                    resize: 'none', fontFamily: 'inherit', fontSize: 'inherit'
                },
                onChange: (e) => setTempVal(e.target.value),
                onBlur: () => {
                    if (tempVal !== text) {
                        const fieldName = ctx.collectionField?.name;
                        utils.saveFieldValue({
                            ctx, recordId: ctx.record?.id,
                            fieldName, newValue: tempVal,
                            onSuccess: () => {
                                // Патчим ctx.record и ctx.value сразу, не ждём обновления таблицы.
                                // Это позволяет render() увидеть новое значение и пересчитать
                                // цвет заливки (enableFieldStatus) без перезагрузки страницы.
                                if (ctx.record && fieldName) ctx.record[fieldName] = tempVal;
                                if ('value' in ctx) ctx.value = tempVal;
                                render(); // перерисовываем ячейку с новыми данными
                                ctx.emit?.('tableBlock:refreshByUid', { uid: ctx.blockModel?.uid });
                            }
                        });
                    } else { setIsEdit(false); }
                },
                onKeyDown: (e) => {
                    if (!enableWordWrap && e.key === 'Enter') e.target.blur();
                    if (e.key === 'Escape') setIsEdit(false);
                },
                onClick: (e) => e.stopPropagation()
            });
        }

        const isInline = copyButtonPosition === 'inline';
        const isRight  = copyButtonPosition === 'right';

        // Текстовый/ссылочный узел
        const textNode = isUrl
            ? React.createElement('a', {
                href: utils.normalizeUrl(text), target: '_blank',
                style: { color: '#1677ff', textDecoration: 'underline' },
                onClick: (e) => e.stopPropagation()
            }, urlDisplayMode === 'label' ? 'Ссылка на сайт' : text)
            : React.createElement('span', {
                style: {
                    color: shouldHighlightRed ? '#d32f2f' : shouldHighlightGreen ? '#7BD960' : (isTargetField && hasValue && isHovered && !isUrl) ? '#1677ff' : 'inherit',
                    textDecoration: (isTargetField && hasValue && isHovered && !isUrl) ? 'underline' : 'none',
                    fontWeight: (shouldHighlightRed || shouldHighlightGreen) ? '600' : 'inherit',
                }
            }, text || '\u00A0');

        // ── Сборка содержимого ячейки по режиму ──────────────────────────────
        //
        // RIGHT (оба варианта wordWrap):
        //   Текст занимает всю ширину. Кнопка — абсолютная, на всю высоту ячейки справа.
        //
        // INLINE + wordWrap:
        //   Текст переносится. Кнопка вставляется inline-flex сразу после текста.
        //
        // INLINE + !wordWrap:
        //   Текст в одну строку, обрезается ellipsis. Кнопка — flex-сосед справа.

        let cellChildren;

        if (isRight) {
            // Кнопка: position absolute, top/bottom=0 → на всю высоту ячейки, прижата к правому краю
            cellChildren = [
                React.createElement('div', {
                    key: 'text',
                    ref: textRef,
                    style: {
                        flex: 1, minWidth: 0,
                        overflow: 'hidden',
                        whiteSpace: enableWordWrap ? 'pre-wrap' : 'nowrap',
                        wordBreak: enableWordWrap ? 'break-word' : 'normal',
                        textOverflow: enableWordWrap ? 'clip' : 'ellipsis',
                    }
                }, textNode),

                hasValue && React.createElement('div', {
                    key: 'btn-wrap',
                    style: {
                        position: 'absolute',
                        right: 0,
                        top: 0,
                        bottom: 0,          // ← растягивается на всю высоту ячейки
                        display: 'flex',
                        alignItems: 'stretch',
                    }
                }, React.createElement(CopyBtn, { visible: showBtn }))
            ];

        } else if (enableWordWrap) {
            // INLINE + wordWrap: кнопка встроена в текстовый поток
            cellChildren = [
                React.createElement('div', {
                    key: 'text',
                    ref: textRef,
                    style: {
                        flex: 1, minWidth: 0,
                        whiteSpace: 'pre-wrap',
                        wordBreak: 'break-word',
                    }
                }, [
                    textNode,
                    // Кнопка сразу после текста, внутри строки
                    hasValue && React.createElement('span', {
                        key: 'btn-wrap',
                        style: { display: 'inline-flex', verticalAlign: 'middle', marginLeft: '6px' }
                    }, React.createElement(CopyBtn, { visible: showBtn }))
                ])
            ];

        } else {
            // INLINE + !wordWrap: текст обрезается, кнопка — flex-сосед
            cellChildren = [
                React.createElement('div', {
                    key: 'text',
                    ref: textRef,
                    style: {
                        flex: 1, minWidth: 0,
                        overflow: 'hidden',
                        whiteSpace: 'nowrap',
                        textOverflow: 'ellipsis',
                    }
                }, textNode),

                hasValue && React.createElement('div', {
                    key: 'btn-wrap',
                    style: { flexShrink: 0, marginLeft: '6px', display: 'flex', alignItems: 'center' }
                }, React.createElement(CopyBtn, { visible: showBtn }))
            ];
        }

        const cellBody = React.createElement('div', {
            style: {
                width: '100%',
                padding: '4px 8px',
                minWidth: 0,
                boxSizing: 'border-box',
                position: 'relative',           // нужно для absolute-кнопки в right-режиме
                display: 'flex',
                flexDirection: 'row',
                alignItems: enableWordWrap ? 'flex-start' : 'center',
                minHeight: '28px',
                cursor: (isTargetField && hasValue) ? 'pointer' : 'default',
            },
            onMouseEnter: () => { setShowBtn(true);  setIsHovered(true);  },
            onMouseLeave: () => { setShowBtn(false); setIsHovered(false); },
            onDoubleClick: (e) => {
                if (!isTargetField && enableInlineEdit) {
                    e.stopPropagation();
                    setIsEdit(true);
                }
            },
            onClick: () => { if (isTargetField && hasValue && !isUrl) openRecordView(); }
        }, cellChildren);

        // ИЗМЕНЕННАЯ ЛОГИКА: показываем тултип ТОЛЬКО если текст обрезан
        const tooltipProps = {
            title: text,
            placement: 'top',
            overlayStyle: { maxWidth: tooltipMaxWidth },
        };
        if (tooltipBgColor) {
            tooltipProps.color = tooltipBgColor;
        }
        if (tooltipTextColor) {
            tooltipProps.overlayInnerStyle = { color: tooltipTextColor };
        }

        return (enableTooltip && hasValue && isTextOverflowing)
            ? React.createElement(Tooltip, tooltipProps, cellBody)
            : cellBody;
    };

    const root = document.createElement('div');
    root.style.width = '100%';
    root.style.height = '100%';

    ctx.render(React.createElement(MainContent), root);
    ctx.element.appendChild(root);

    // Обновляем кэш после успешного рендера
    cache.lastFieldValue = ctx.record?.[fieldName];
    if (enableComparison || enableFieldStatus) {
        const { Current, Requests } = utils.resolveComparisonFields(ctx);
        cache.lastComparisonState = JSON.stringify({ Current, Requests });
        if (enableFieldStatus) {
            cache.lastStatusBg = utils.resolveFieldStatusBackground(ctx);
        }
    }
};

render();

if (ctx.record && !ctx.element.__observeSet) {
    ctx.element.__observeSet = true;
    const fieldName = ctx.collectionField?.name;

    // Вместо { deep: true } наблюдаем только за конкретными полями
    // Это существенно снижает количество срабатываний наблюдателя
    ctx.observe(() => {
        // Собираем только нужные значения для наблюдения
        return {
            fieldValue: ctx.record?.[fieldName],
            // Если есть сравнение/статус - следим за релевантными полями
            ...(enableComparison || enableFieldStatus ? {
                codeIUS: ctx.record?.Code_IUS,
                codeES: ctx.record?.Code_ES,
                requestIUS: ctx.record?.Request_IUS,
                requestES: ctx.record?.Request_ES,
                compareField: fieldName?.endsWith('_current')
                    ? ctx.record?.[fieldName.replace(/_current$/, '_requests')]
                    : fieldName?.endsWith('_requests')
                        ? ctx.record?.[fieldName.replace(/_requests$/, '_current')]
                        : null
            } : {})
        };
    }, render, { deep: false });
}

Thanks for your feedback.
This feature is not currently supported. I have documented this requirement and will evaluate it in the future.