Карточка записи

В карточку записи могут быть добавлены следующие дополнительные точки:

Новый пункт в выпадающем меню

Выпадающее меню

Рисунок 1 - Выпадающее меню

UEDataCardMenuItem предназначен для добавления пунктов меню на карточку записи. Принимает AbstractCardStore в резолвере, так что определенные элементы могут быть добавлены / исключены с помощью реализации определенного класса хранилища.

Пример UEDataCardMenuItem

import {INamespaceDataRecord, UeModuleBase} from '@unidata/core-app';
import {ComponentType} from 'react';
import {AbstractCardStore} from '../../store';

type DataCardMenuProps<T extends INamespaceDataRecord> = {
    dataCardStore: AbstractCardStore<T>;
}

export type UEDataCardMenuItem<T extends INamespaceDataRecord> = UeModuleBase & {
    default: {
        component: ComponentType<DataCardMenuProps<T>>; // the component that will be rendered in place of the button.
        meta: {
            menuGroupId: string; // specifying a group to separate menu items
        };
        resolver: (dataCardStore: AbstractCardStore<T>) => boolean; // a function that determines in which case to show the button (depending on the state of the card)
    };
}

Пример реализации: "История записей", которая отображается только в опубликованных записях.

interface IProps {
    dataCardStore: DataCardStore;
}

export class HistoryMenuItem extends React.Component<IProps> {
    openHistoryPage = () => {
        const dataCardStore = this.props.dataCardStore;
        const etalonId = dataCardStore.etalonId;

        if (etalonId) {
            dataCardStore.routerStore.setRoute(RouteKeys.RecordHistoryPage, {
                namespace: dataCardStore.namespace,
                entityName: dataCardStore.typeName,
                etalonId: etalonId
            });
        }
    }

    override render () {
        return (
            <DropDown.Item onClick={this.openHistoryPage}>
                {i18n.t('module.data>history>recordHistory')}
            </DropDown.Item>
        );
    }
}

export const historyButtonUE = {
        'default': {
            type: UEList.DataCardMenuItem,
            moduleId: 'history',
            active: true,
            system: false,
            component: HistoryMenuItem,
            resolver: (dataCardStore: AbstractCardStore<any>) => {
                return dataCardStore instanceof DataCardStore &&
                    dataCardStore.draftStore?.draftId === undefined;
            },
            meta: {
                menuGroupId: DataCardMenuGroupName.history
            }
        }
    };

Новая вкладка в карточке записи

Новая вкладка в карточке

Рисунок 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>>; // the component that renders the contents of the tab
        meta: {
            tab: TabItem; // tab description (display name, key, etc.)
            position: 'left' | 'right'; // the ability to add tabs both to the general list and on the right side of the card
        };
        resolver: (dataCardStore: AbstractCardStore<T>) => boolean; // a function that determines in which case to show the button (depending on the state of the card)
    };
}

Пример реализации:

export const relationGraphUE: UEDataCardTabItem<DataRecord> = {
    'default': {
        type: UEList.DataCardTabItem,
        moduleId: 'relationGraph',
        active: true,
        system: false,

        component: RelationGraphTabItem, // component that renders relations graph
        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>>; // The component that renders the contents of the panel
        meta: {
            order: number; // panel sequence
        };
        resolver: (dataCardStore: AbstractCardStore<T>) => boolean; // a function that determines in which case to show the button (depending on the state of the card)
    };
}

Пример реализации:

export const clusterWidgetUE: UEDataCardSidePanelItem<any> = {
    'default': {
        type: UEList.DataCardSidePanelItem,
        moduleId: 'dataRecordClusters',
        active: true,
        system: false,
        component: ClustersWidget, // panel content component
        resolver: (dataCardStore: AbstractCardStore<any>) => {
            return Boolean(dataCardStore.etalonId); // display only if there is an EtalonId of the record
        },
        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>; // component for render Attribute
        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; // indicates whether it is allowed to change view to table
    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
        }
    }
};

Метод, вызываемый после публикации черновика записи

UEAfterPublishRecord позволяет вызывать функции после публикации черновика записи.

Описание UEAfterPublishRecord

export type UEAfterPublishRecord<T extends IRecordEntity> = UeModuleBase & {
    default: {
        fn: (dataCardStore: AbstractCardStore<T>) => void;
        resolver: (dataCardStore: AbstractCardStore<T>) => boolean;
    };
}

Пример UEAfterPublishRecord - дополнительное сообщение, если настроен процесс согласования при публикации записи

function afterPublish () {
    Dialog.showMessage(i18n.t('module.workflow>dataCardAfterPublish>message'));
}

export const afterPublishRecordUe: UEAfterPublishRecord<any> = {
    'default': {
        type: UEList.AfterPublishRecord,
        moduleId: 'afterPublishWorkflow',
        active: true,
        system: false,
        fn: afterPublish,
        resolver: (store: AbstractCardStore<any>) => {
            return store.draftStore?.selectedDraft?.state.getValue() === 'DELAYED_BY_WORKFLOW';
        },
        meta: {}
    }
};

Метод, вызываемый после публикации c ошибкой

UEAfterPublishRecordFailure позволяет вызывать функции после публикации черновика записи с ошибкой.

Описание UEAfterPublishRecordFailure

export type UEAfterPublishRecordFailure<T extends IRecordEntity> = UeModuleBase & {
    default: {
        fn: (dataCardStore: AbstractCardStore<T>, errors: ServerDetailsError[]) => void;
        resolver: (dataCardStore: AbstractCardStore<T>, errors: ServerDetailsError[]) => boolean;
    };
}

Добавление дополнительного стора на карточку записи

Точка расширения UERecordCardInnerStore используется для добавления доп.логики в карточку записи.

Созданный стор вызывается на все глобальные действия с карточкой - получение данных (процессинг) перед сохранением (для добавления данных в запрос).

Описание UERecordCardInnerStore

type Resolver = (dataCardStore: AbstractCardStore<IRecordEntity>) => boolean;

export type UERecordCardInnerStore = UeModuleBase<Resolver, {}> & {
    fn: (dataCardStore: AbstractCardStore<IRecordEntity>) => IInnerRecordCardStore;
};

Пример использования UERecordCardInnerStore

ueModuleManager.addModule('RecordCardInnerStore', {
    moduleId: 'relationDataInnerStore',
    active: true,
    system: false,
    resolver: (cardStore: AbstractCardStore<IRecordEntity>) => cardStore instanceof  DataCardStore,
    meta: {},
    fn: (cardStore: DataCardStore) => {
        return new DataCardRelationStore(cardStore);
    }
});

Клонирование записи

UECloneRecord предназначен для добавления фрагмента к запросу на клонирование записи и вывода в окне настроек клонирования интерфейса для изменения передаваемых во фрагменте параметров.

Используемые типы

interface ICloneInnerStore<T extends keyof UniverseUE.ICloneAtomicPayload> {
    /*
    * Возвращает фрагмент запроса на клонирование и ключ, под которым он будет добавлен в запрос, должны быть
    * глобально объявлены в пространстве имен UniverseUE
    */
    payloadContent: {
        key: T;
        content: UniverseUE.ICloneAtomicPayload[T];
    };

    /*
    * Возвращает true, если нужно запретить подтверждение клонирования записи
    */
    readonly hasErrors: boolean;
}

/*
* Передаваемые в компонент параметров пропсы
*/
interface ICloneRecordParametersProps {
    store: CloneStore;
}

type UECloneRecordResolver = (cloneStore: CloneStore) => boolean;

type UECloneRecordMeta = {
    /*
    * @param key - передается meta.key текущего User Exit
    */
    getStore: (cloneStore: CloneStore, key: string) => ICloneInnerStore;
    key: string;
};

export type UECloneRecord = UeModuleBase<UECloneRecordResolver, UECloneRecordMeta> & {
    component: ComponentType<ICloneRecordParametersProps>;
};

Пример UECloneRecord

const CLONE_RECORD_ADDITIONAL_PAYLOAD_KEY = 'clone_record_additional_fragment_v1' as const;

interface ICloneRecordAdditionalPayload {
    cleanUnique: boolean;
}

declare global {
    namespace UniverseUE {
        export interface ICloneAtomicPayload {
            [CLONE_RECORD_ADDITIONAL_PAYLOAD_KEY]: ICloneRecordAdditionalPayload;
        }
    }
}

class CloneRecordAdditionalFragmentStore implements ICloneInnerStore {
    @observable
    cleanUnique: boolean;

    @action
    setCleanUnique (cleanUnique: boolean) {
        this.cleanUnique = cleanUnique;
    }

    get payloadContent () {
        return {
            key: CLONE_RECORD_ADDITIONAL_PAYLOAD_KEY,
            content: {
                cleanUnique: this.cleanUnique
            }
        }
    }

    get hasErrors () {
        return false;
    }

}

@observer
class CloneRecordUserExit extends React.Component<ICloneRecordParametersProps> {
    get store () {
        return this.props.store.getStore(CLONE_RECORD_ADDITIONAL_PAYLOAD_KEY) as CloneRecordAdditionalFragmentStore;
    }

    override render () {
        const {store} = this;

        return (
            <CardPanel
                internal={true}
                title={'Дополнительные параметры клонирования'}>
                <Field.Checkbox
                    label={'Очистить уникальные значения?'}
                    defaultChecked={store.cleanUnique}
                    onChange={(name, value) => store.setCleanUnique(value)}
                />
            </CardPanel>
        );
    }
}

ueModuleManager.addModule('CloneRecord', {
    active: true,
    component: CloneRecordUserExit,
    meta: {
        key: CLONE_RECORD_ADDITIONAL_PAYLOAD_KEY,
        getStore: () => {
            return new CloneRecordAdditionalFragmentStore();
        }
    },
    moduleId: CLONE_RECORD_ADDITIONAL_PAYLOAD_KEY,
    resolver: (cloneStore: CloneStore) => {
        const dataRecordInnerStore = cloneStore.dataCardStore.getInnerStore('data-record-additional-store');

        return dataRecordInnerStore !== undefined;
    },
    system: false
});
Пример отображения параметров клонирования записи реестра, имеющего классификацию

Рисунок 5 - Пример отображения параметров клонирования записи реестра, имеющего классификацию