Карточка записи
В карточку записи могут быть добавлены следующие дополнительные точки:
Новая вкладка в карточке записи
 
Рисунок 2 - Новая вкладка в карточке
Точка расширения UEDataCardTabItem предназначена для добавления вкладок в карточке записи.
Текущие вкладки атрибутов, связей и т.д. привязаны к DataCardStore в резолвере. Если вы наследуете от AbstractCardStore, вам необходимо самостоятельно подготовить содержимое вкладки.
Пример UEDataCardTabItem:
type DataCardTabItemProps<T extends INamespaceDataRecord> = {
    dataCardStore: AbstractCardStore<T>;
}
export type UEDataCardTabItem<T extends INamespaceDataRecord> = UeModuleBase & {
    default: {
        component: ComponentType<DataCardTabItemProps<T>>; // Компонент, отображающий содержимое вкладки
        meta: {
            tab: TabItem; // Описание вкладки (отображаемое имя, ключ и т.д.)
            position: 'left' | 'right'; // Возможность добавления вкладок как в общий список, так и в правую часть карточки
        };
        resolver: (dataCardStore: AbstractCardStore<T>) => boolean; // Функция, определяющая, в каком случае показывать кнопку (в зависимости от состояния карточки)
    };
}
Пример реализации:
export const relationGraphUE: UEDataCardTabItem<DataRecord> = {
    'default': {
        type: UEList.DataCardTabItem,
        moduleId: 'relationGraph',
        active: true,
        system: false,
        component: RelationGraphTabItem, // Компонент, отображающий граф связей
        meta: {
            tab: {
                key: 'relationGraph',
                tab: i18n.t('module.data-ee>ue>relationGraph>tabLabel'),
                order: 30
            },
            position: 'left'
        },
        resolver: (dataCardStore: AbstractCardStore<DataRecord>) => {
            return dataCardStore instanceof DataCardStore &&
                MetaTypeGuards.isEntity(dataCardStore.metaRecordStore.getMetaEntity()) && Boolean(dataCardStore.etalonId) &&
                dataCardStore.draftStore?.draftId === undefined;
        }
    }
};
Новый контент на правой боковой панели
 
Рисунок 3 - Правая боковая панель
Точка расширения UEDataCardSidePanelItem предназначена для отображения элементов управления в правой части карточки записи.
Периоды действия, кластеры, задачи - это пользовательские точки расширения типа UEDataCardSidePanelItem.
В текущей реализации они не привязаны к типу хранилища и будут доступны для всех возможных записей. Это поведение может быть изменено в соответствии с требованиями проекта.
Пример UEDataCardSidePanelItem:
type DataCardSidePanelItemProps<T extends INamespaceDataRecord> = {
    dataCardStore: AbstractCardStore<T>;
}
export type UEDataCardSidePanelItem<T extends INamespaceDataRecord> = UeModuleBase & {
    default: {
        component: ComponentType<DataCardSidePanelItemProps<T>>; // Компонент, отображающий содержимое панели
        meta: {
            order: number; // Последовательность панелей
        };
        resolver: (dataCardStore: AbstractCardStore<T>) => boolean; // Функция, определяющая, в каком случае показывать кнопку (в зависимости от состояния карточки)
    };
}
Пример реализации:
export const clusterWidgetUE: UEDataCardSidePanelItem<any> = {
    'default': {
        type: UEList.DataCardSidePanelItem,
        moduleId: 'dataRecordClusters',
        active: true,
        system: false,
        component: ClustersWidget, // Компонент панели
        resolver: (dataCardStore: AbstractCardStore<any>) => {
            return Boolean(dataCardStore.etalonId); // Отображать только при наличии EtalonId записи
        },
        meta: {
            order: 20
        }
    }
};
Отображение атрибута в карточке записи
 
Рисунок 4 - Отображение простых атрибутов и атрибутов массива
Точка расширения UEAttributePreview отвечает за отображение атрибутов в карточке записи (Рисунок 4).
Описание UEAttributePreview:
import {INamespaceMetaModel, UeModuleBase} from '@unidata/core-app';
import {ComponentType} from 'react';
import {AbstractAttribute, UPathMetaStore} from '@unidata/meta';
import {AbstractModel} from '@unidata/core';
import {ArrayAttributeStore} from '../../page/dataview_light/dataviewer/card/attribute/store/ArrayAttributeStore';
import {SimpleAttributeStore} from '../../page/dataview_light/dataviewer/card/attribute/store/SimpleAttributeStore';
import {AbstractDataEntityStore} from '../../page/dataview_light/store/dataEntity/AbstractDataEntityStore';
type AttributePreviewProps = {
    attributeStore: SimpleAttributeStore | ArrayAttributeStore;
    dataEntityStore: AbstractDataEntityStore;
    metaEntityStore: UPathMetaStore<INamespaceMetaModel>;
}
export type UEAttributePreview = UeModuleBase & {
    default: {
        component: ComponentType<AttributePreviewProps>; // компонент для рендеринга атрибута
        meta: {
            name: string;
            displayName: () => string;
        };
        resolver: (attribute: AbstractAttribute, model: AbstractModel) => boolean;
    };
}
Отображение кастомного атрибута в карточке записи
Точка расширения UERenderAttributeOnDataCard предназначена для кастомного отображения атрибута в карточке записи.
Описание UERenderAttributeOnDataCard:
type RenderDataAttributeProps = {
    attributeStore: SimpleAttributeStore | ArrayAttributeStore;
    dataEntityStore: AbstractRecordEntityStore;
    metaEntityStore: UPathMetaStore<IMetaModel>;
}
type Resolver = (attribute: IMetaAbstractAttribute, model: AbstractModel) => boolean;
type Meta = {
    name: string; // Ключ, который используется для настройки вида атрибута в метамодели.
    previewOptions?: string [];
    displayName: () => string;
    additionalProps?: {type?: string};
};
export type UERenderAttributeOnDataCard = UeModuleBase<Resolver, Meta> & {
    component: ComponentType<RenderDataAttributeProps>;
}
Пример реализации:
type UERenderAttributeOnDataCard = UniverseUE.IUeMeta['RenderAttributeOnDataCard'];
// Описание UE
const simpleType: UERenderAttributeOnDataCard = {
    moduleId: 'customStringAttribute',
    active: true,
    system: false,
    component: MyCustomAttribute, // Компонент логики
    resolver: (attribute: AbstractAttribute, model: AbstractModel) => {
        return MetaTypeGuards.isAbstractSimpleAttribute(attribute) &&
            attribute.typeCategory === AttributeTypeCategory.simpleDataType &&
            attribute.simpleDataType.getValue() === SIMPLE_DATA_TYPE.STRING;
    },
    meta: {
        name: 'default',
        displayName: () => i18n.t('module.record>dataview>defaultView'),
        additionalProps: {type: 'string'}
    }
};
Пример файла с компонентом MyCustomAttribute:
import * as React from 'react';
import {observer} from 'mobx-react';
import {computed} from 'mobx';
import {Input} from '@universe-platform/uikit';
import {i18n} from '@universe-platform/sdk';
import {IMetaModel, UPathMetaStore, AbstractRecordEntityStore, SimpleAttributeStore} from '@universe-platform/sdk-mdm
   interface IProps {
       attributeStore: SimpleAttributeStore;
       dataEntityStore: AbstractRecordEntityStore;
       metaEntityStore: UPathMetaStore<IMetaModel>;
       type: 'string' | 'number' | 'integer';
   }
   @observer
   export class MyCustomAttribute extends React.Component<IProps> {
       get store () {
           return this.props.attributeStore;
       }
       get inputMode () {
           if (this.props.type === 'integer') {
               return 'numeric';
           } else if (this.props.type === 'number') {
               return 'decimal';
           }
           return 'text';
       }
       @computed
       get attribute () {
           return this.props.attributeStore.getDataAttribute();
       }
       onTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
           let value = e.target.value;
           this.setAttributeValue(value);
       };
       onNumberChange = (valueArg: number | undefined) => {
           const value = valueArg === undefined ? null : valueArg;
           this.setAttributeValue(value);
       };
       setAttributeValue (value: string | number | null) {
           this.props.attributeStore.setAttributeValue(value);
       }
       getPlaceholder (readOnly: boolean, type: string = 'text') {
           if (readOnly) {
               return i18n.t('module.record>dataview>valueUnset');
           }
           if (type === 'number') {
               return i18n.t('module.record>dataview>enterNumber');
           } else if (type === 'integer') {
               return i18n.t('module.record>dataview>enterInteger');
           } else {
               return i18n.t('module.record>dataview>enterText');
           }
       }
       private renderNumberInput () {
           const value = Number.parseFloat(this.attribute.value.getValue()) || undefined;
           const errorMessage = i18n.t(this.attribute.getErrorMessage('value'));
           const readOnly = this.store.getReadOnly() || !this.store.getIsEditMode();
           const placeholder = this.getPlaceholder(readOnly, this.props.type);
           return (
               <Input.Number
                   value={value}
                   onChange={this.onNumberChange}
                   hasError={Boolean(errorMessage)}
                   errorMessage={errorMessage || undefined}
                   isInteger={this.inputMode === 'numeric'}
                   inputMode={this.inputMode}
                   autoFocus={true}
                   allowClear={true}
                   placeholder={placeholder}
                   onBlur={this.store.setEditModeOff}
               />
           );
       }
       private renderTextInput () {
           const errorMessage = i18n.t(this.attribute.getErrorMessage('value'));
           const readOnly = this.store.getReadOnly() || !this.store.getIsEditMode();
           const placeholder = this.getPlaceholder(readOnly, this.props.type);
           return (
               <Input
                   value={this.attribute.value.getValue()}
                   onChange={this.onTextChange}
                   hasError={Boolean(errorMessage)}
                   allowClear={true}
                   errorMessage={errorMessage || undefined}
                   inputMode={this.inputMode}
                   autoFocus={true}
                   placeholder={placeholder}
                   onBlur={this.store.setEditModeOff}
               />
           );
       }
       override render () {
           if (this.props.type === 'number' || this.props.type === 'integer') {
               return this.renderNumberInput();
           }
           return this.renderTextInput();
       }
   }
Пример настройки UE для кастомного представления атрибута определенного типа (например String):
type UERenderAttributeOnDataCard = UniverseUE.IUeMeta['RenderAttributeOnDataCard'];
// Описание UE
const simpleType: UERenderAttributeOnDataCard = {
    moduleId: 'customStringAttribute',
    active: true,
    system: false,
    component: MyCustomAttribute, // Компонент с логикой
    resolver: (attribute: AbstractAttribute, model: AbstractModel) => {
        return MetaTypeGuards.isAbstractSimpleAttribute(attribute) &&
            attribute.typeCategory === AttributeTypeCategory.simpleDataType &&
            attribute.simpleDataType.getValue() === SIMPLE_DATA_TYPE.STRING;
    },
    meta: {
        name: 'some_custom_view', // Произвольный ключ, который будет использоваться в поле `вид` в метамодели в настройке атрибута наряду с displayName
        displayName: () => 'Название представления этого атрибута',
        additionalProps: {type: 'string'}
    }
};
Принцип выбора атрибутов из UE для отрисовки в карточке
Если в модели данных выбрать вид атрибута, то в его custom_properties в поле DATACARD_ATTRIBUTE_TYPE запишется значение из поля name раздела meta настроек UE атрибута. Таким образом, это значение участвует при фильтрации нужного представления (см. ниже).
// const metaAttribute: AbstractAttribute;
// const store: SimpleAttributeStore | ArrayAttributeStore;
// ...
const typeCategory = metaAttribute.typeCategory;
const customProperty = metaAttribute.getCustomProperty(AttributeCustomPropertyEnum.DATACARD_ATTRIBUTE_TYPE);
let view = 'default';
if (customProperty) {
    view = customProperty.value.getValue();
}
if (!typeCategory) {
    return null;
}
const ueAttributes = ueModuleManager.getResolvedModulesByType('RenderAttributeOnDataCard', [
    this.metaAttribute,
    this.metaEntityStore.getMetaEntity()
]);
let ueAttribute = ueAttributes.find((module) => {
        return module.meta.name === view;
    }) ||
    ueAttributes.find((module) => {
        return module.meta.name === 'default';
    });
Отображение секции комплексного атрибута
Точка расширения UEComplexAttributeSection отвечает за отображение секции с комплексными атрибутами.
Описание UEComplexAttributeSection:
type Props = {
    allowChangeView: boolean; // Указывает, разрешено ли изменять вид на таблицу
    allowModalForNested?: boolean;
    dataPath: string;
    metaPath: string;
    isNavigable: boolean;
    title: string;
    attrLabelWidth?: number;
    attrLabelPosition?: LabelPosition | string;
    dataEntityStore: AbstractRecordEntityStore;
    metaEntityStore: UPathMetaStore<IMetaModel>;
    navigableItemsStore?: NavigableItemsStore;
    dataCardStore: AbstractCardStore<IRecordEntity>;
}
type Resolver = (group: RecordCardGroupLayoutItem, dataCardStore: AbstractCardStore<IRecordEntity>) => boolean;
export type UEComplexAttributeSection = UeModuleBase<Resolver, {}> & {
    component: ComponentType<Props>;
}
Пример использования UEComplexAttributeSection:
ueModuleManager.addModule('ComplexAttributeSection', {
    moduleId: 'mdmComplexAttributes',
    active: true,
    system: false,
    component: ComplexAttributeSection, // Компонент, отображающий комплексные атрибуты в карточке
    resolver: (group: RecordCardGroupLayoutItem, dataCardStore: AbstractCardStore<IRecordEntity>) => {
        return group.isComplex;
    },
    meta: {
        order: 10
    }
});
Отображение элемента перед атрибутом
Точка расширения UEDataViewElementPrefix отвечает за отображение доп. информации перед атрибутами в карточке атрибутами. Используется для подсветки атрибутов с ошибками.
Описание UEDataViewElementPrefix:
type DataAttributePrefixProps = {
    metaEntity: IMetaModel;
    path: string;
    parentPath?: string;
    type: RecordViewElementType;
    cmpRef: React.RefObject<React.Component>;
    navigableItemsStore?: NavigableItemsStore;
}
type Meta = {
    order: number;
};
export type UEDataViewElementPrefix = UeModuleBase<DefaultUeResolver, Meta> & {
    component: ComponentType<DataAttributePrefixProps>;
}
Пример использования UEDataViewElementPrefix:
ueModuleManager.addModule('DataViewElementPrefix', {
    moduleId: 'dqErrorIndicator',
    active: true,
    system: false,
    resolver: () => true,
    meta: {
        order: 1
    },
    component: DqErrorIndicatorContainer // Компонент, отображающий индикацию ошибок
});
Отображение тега в шапке карточки записи
Точка расширения UEDataCardTag предназначена для добавления тега в шапку карточки записи.
Описание UEDataCardTag:
type DataCardTagProps<T extends IRecordEntity> = {
    dataCardStore: AbstractCardStore<T>;
}
export type UEDataCardTag<T extends IRecordEntity> = UeModuleBase & {
    default: {
        component: ComponentType<DataCardTagProps<T>>;
        meta: {
            order: number;
        };
        resolver: (dataCardStore: AbstractCardStore<T>) => boolean;
    };
}
Пример UEDataCardTag: отображение тега черновика записи в процессе согласования.
interface IProps {
    dataCardStore: AbstractCardStore<any>;
}
@observer
class TagDraftIsDelayed extends React.PureComponent<IProps> {
    override render () {
        const draft = this.props.dataCardStore.draftStore?.selectedDraft;
        const state = draft?.state.selectedItem;
        return (
            <Tag intent={INTENT.INFO} key={'draftTag'}>
                <Tooltip overlay={state?.description.getValue()}>
                    <span>
                        {draft?.state.displayValue}
                    </span>
                </Tooltip>
            </Tag>
        );
    }
}
export const dataCardTagUe: UEDataCardTag<IRecordEntity> = {
    'default': {
        type: UEList.DataCardTag,
        moduleId: 'TagDraftDelayedByWorkflow',
        active: true,
        system: false,
        component: TagDraftIsDelayed,
        resolver: (dataCardStore: AbstractCardStore<IRecordEntity>) => {
            return dataCardStore.draftStore?.selectedDraft?.state.getValue() === 'DELAYED_BY_WORKFLOW';
        },
        meta: {
            order: 0
        }
    }
};
Добавление дополнительной логики в карточку записи
Точка расширения UERecordCardInnerStore используется для добавления дополнительной логики в карточку записи.
Созданный стор вызывается на все глобальные действия с карточкой: получение данных (процессинг) перед сохранением (для добавления данных в запрос).
Описание UERecordCardInnerStore:
type Resolver = (dataCardStore: AbstractCardStore<IRecordEntity>) => boolean;
export type UERecordCardInnerStore = UeModuleBase<Resolver, {}> & {
    fn: (dataCardStore: AbstractCardStore<IRecordEntity>) => IInnerRecordCardStore;
};
Описание интерфейса IInnerRecordCardStore:
export interface IInnerRecordCardStore {
    /**
     * Функция инициализации внутреннего стора. Вызывается один раз при создании стора карточки записи.
     *
     * Здесь могут быть любые методы инициализации стора. Например, загрузка данных.
     * Если ничего инициализировать не нужно, можно вернуть пустое обещание.
     */
    init (): Promise<void>;
    /**
     * Флаг указывающий, что внутренний стор имеет измененные данные
     *
     * У родительского стора (DataCardStore) есть аналогичный флаг isDirty, который собирает это состояние со
     * всех своих внутренних сторов. Если хотя бы у одного из внутренних сторов isDirty равно true, то значит
     * isDirty всей карточки равно true.
     *
     * Этот флаг нужен, например, для блокировки кнопки "Сохранить". Если нет необходимости использовать эту
     * функциональность, то флаг можно выставить в false
     */
    isDirty: boolean;
    /**
     * Ключ таба карточки записи, к которому относится данный стор.
     * Этот таб будет подсвечен в случае ошибок валидации.
     *
     * Свойство необходимо для подсветки валидационных ошибок. tabKey это тот таб в котором
     * должны располагаться поля с валидационными ошибками. Свойство отвечает только за подсветку таба.
     * В случае если в сторе не предполагаются валидационные ошибки, то можно указать пустую строку
     */
    tabKey: string;
    /**
     * Ключ для помещения payload в запросе
     *
     * По умолчанию для получения/изменения данных записи используется atomic-запрос
     * Это составной запрос в котором может быть указано несколько различных ключей с полезной нагрузкой
     *
     * Если нет необходимости дополнять запрос дополнительными аргументами, то в качестве значения можно
     * указать пустую строку.
     */
    payloadKey: string;
    /**
     * Payload который передается в запрос получения данных
     *
     * Подробнее см. описание payloadKey
     */
    payloadForGet?: Object;
    /**
     * Payload который передается в запрос сохранения
     *
     * Подробнее см. описание payloadKey
     */
    payloadForSave?: Object;
    /**
     * Payload который передается в запрос восстановления данных
     *
     * Подробнее см. описание payloadKey
     */
    payloadForRestore?: Object;
    /**
     * Метод обработки данных из ответа на запрос получения данных
     *
     * Если дополнительная обработка полученных данных из atomic-запроса не требуется,
     * то тело метода можно оставить пустым
     */
    processAtomic (data: object, operationType: AtomicOperationType): void;
    /**
     * Метод валидации данных во внутреннем сторе. Вызывается перед публикацией записи
     *
     * Если будут возвращены ошибки валидации, то публикация прервется
     */
    validate (): IValidationResult;
    /**
     * Метод обогащения / изменения данных сущностей во внутреннем сторе.
     */
    enrich?(enrichedEntities: IEnrichedRecordEntity[]): void;
}
Пример использования UERecordCardInnerStore:
class CustomCardInnerStore implements IInnerRecordCardStore {
    // Если нет необходимости дополнять запрос дополнительными аргументами,
    // можно указать пустой объект
    payloadForGet = {};
    // Если нет необходимости дополнять запрос дополнительными аргументами,
    // можно указать пустой объект
    payloadForRestore = {};
    // Если нет необходимости дополнять запрос дополнительными аргументами,
    // можно указать пустой объект
    payloadForSave = {};
    /**
     * Пример данных хранящихся в сторе. В данном случе это простой счетчик
     * и функция increase для его увеличения
     */
    @observable
    counter = 0;
    @action
    increase () {
        this.counter = this.counter + 1;
    }
    init () {
        return Promise.resolve();
    }
    get isDirty () {
        return false;
    }
    get payloadKey () {
        // Если нет необходимости дополнять запрос дополнительными аргументами,
        // то в качестве значения можно указать пустую строку.
        return '';
    }
    processAtomic (data: object, operationType: AtomicOperationType) {
        // Если дополнительная обработка полученных данных из запроса /data/atomic/ не требуется,
        // то тело метода можно оставить пустым
    }
    get tabKey () {
        // В случае если в сторе не предполагаются валидационные ошибки, то можно указать пустую строку
        return '';
    }
    validate () {
        return new Map();
    }
}
/**
 * Инициализация innerStore
 */
ueModuleManager.addModule('RecordCardInnerStore', {
    moduleId: 'customCardInnerStore',
    active: true,
    system: false,
    resolver: () => true,
    meta: {},
    fn: (cardStore: DataCardStore) => new CustomCardInnerStore()
});
Пример получения UERecordCardInnerStore внутри другого UE:
Стор AbstractCardStore предоставляет доступ к innerStore с помощью метода getInnerStore(moduleId).
Получение innerStore показано на примере  UE DataCardMenuItem:
ueModuleManager.addModule('DataCardMenuItem', {
    moduleId: 'customCardMenuItem',
    active: true,
    system: false,
    component: observer(class CustomCardMenuItem extends React.Component<DataCardMenuProps> {
        override render () {
            const innerStore = this.props.dataCardStore.getInnerStore('customCardInnerStore') as CustomCardInnerStore;
            return (
                <DropDown.Item
                    onClick={() => {
                        innerStore.increase();
                    }}
                >
                    {`increment (${innerStore.counter})`}
                </DropDown.Item>
            );
        }
    }),
    resolver: () => true,
    meta: {
        menuGroupId: 'delete'
    }
});
