Editing of triggers seems to work.

Some further fixes.
This commit is contained in:
Tomas Bures 2018-08-04 15:00:37 +05:30
parent ffc26a4836
commit 965f30cea7
23 changed files with 855 additions and 377 deletions

View file

@ -217,34 +217,28 @@ export default class CUD extends Component {
const t = this.props.t;
const isEdit = !!this.props.entity;
for (const key of state.keys()) {
state.setIn([key, 'error'], null);
}
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
} else {
state.setIn(['name', 'error'], null);
}
if (!state.getIn(['list', 'value'])) {
state.setIn(['list', 'error'], t('List must be selected'));
} else {
state.setIn(['list', 'error'], null);
}
if (state.getIn(['useSegmentation', 'value']) && !state.getIn(['segment', 'value'])) {
state.setIn(['segment', 'error'], t('Segment must be selected'));
} else {
state.setIn(['segment', 'error'], null);
}
if (!state.getIn(['send_configuration', 'value'])) {
state.setIn(['send_configuration', 'error'], t('Send configuration must be selected'));
} else {
state.setIn(['send_configuration', 'error'], null);
}
if (state.getIn(['from_email_overriden', 'value']) && !state.getIn(['from_email_override', 'value'])) {
state.setIn(['from_email_override', 'error'], t('"From" email must not be empty'));
} else {
state.setIn(['from_email_override', 'error'], null);
}
@ -252,12 +246,6 @@ export default class CUD extends Component {
const sourceTypeKey = Number.parseInt(state.getIn(['source', 'value']));
for (const key of state.keys()) {
if (key.startsWith('data_')) {
state.setIn([key, 'error'], null);
}
}
if (sourceTypeKey === CampaignSource.TEMPLATE || (!isEdit && sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE)) {
if (!state.getIn(['data_sourceTemplate', 'value'])) {
state.setIn(['data_sourceTemplate', 'error'], t('Template must be selected'));
@ -292,7 +280,6 @@ export default class CUD extends Component {
}
validateNamespace(t, state);
}
async submitHandler() {
@ -361,8 +348,11 @@ export default class CUD extends Component {
});
if (submitResponse) {
const sourceTypeKey = Number.parseInt(this.getFormValue('source'));
if (this.props.entity) {
this.navigateToWithFlashMessage('/campaigns', 'success', t('Campaign saved'));
} else if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/content`, 'success', t('Campaign saved'));
} else {
this.navigateToWithFlashMessage(`/campaigns/${submitResponse}/edit`, 'success', t('Campaign saved'));
}
@ -494,6 +484,14 @@ export default class CUD extends Component {
templateEdit = <InputField id="data_sourceUrl" label={t('Render URL')} help={t('If a message is sent then this URL will be POSTed to using Merge Tags as POST body. Use this if you want to generate the HTML message yourself.')}/>
}
let saveButtonLabel;
if (isEdit) {
saveButtonLabel = t('Save');
} else if (sourceTypeKey === CampaignSource.CUSTOM || sourceTypeKey === CampaignSource.CUSTOM_FROM_TEMPLATE || sourceTypeKey === CampaignSource.CUSTOM_FROM_CAMPAIGN) {
saveButtonLabel = t('Save and edit content');
} else {
saveButtonLabel = t('Save and edit campaign');
}
return (
<div>
@ -545,7 +543,7 @@ export default class CUD extends Component {
{templateEdit}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={isEdit ? t('Save') : t('Save and edit campaign')}/>
<Button type="submit" className="btn-primary" icon="ok" label={saveButtonLabel}/>
{canDelete && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/campaigns/${this.props.entity.id}/delete`}/> }
</ButtonRow>
</Form>

View file

@ -97,6 +97,7 @@ export default class List extends Component {
actions: data => {
const actions = [];
const perms = data[9];
const campaignType = data[3];
const campaignSource = data[6];
if (perms.includes('edit')) {
@ -127,6 +128,13 @@ export default class List extends Component {
});
}
if (campaignType === CampaignType.TRIGGERED && perms.includes('viewTriggers')) {
actions.push({
label: <Icon icon="flash" title={t('Triggers')}/>,
link: `/campaigns/${data[0]}/triggers`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,

View file

@ -7,7 +7,12 @@ import Content from './Content';
import CampaignsList from './List';
import Share from '../shares/Share';
import Files from "../lib/files";
import {CampaignSource, CampaignType} from "../../../shared/campaigns";
import {
CampaignSource,
CampaignType
} from "../../../shared/campaigns";
import TriggersCUD from './triggers/CUD';
import TriggersList from './triggers/List';
function getMenus(t) {
@ -51,6 +56,32 @@ function getMenus(t) {
visible: resolved => resolved.campaign.permissions.includes('viewAttachments'),
panelRender: props => <Files title={t('Attachments')} help={t('These files will be attached to the campaign emails as proper attachments. This means they count towards to resulting eventual size of the email.')} entity={props.resolved.campaign} entityTypeId="campaign" entitySubTypeId="attachment" managePermission="manageAttachments"/>
},
triggers: {
title: t('Triggers'),
link: params => `/campaigns/${params.campaignId}/triggers/`,
visible: resolved => resolved.campaign.type === CampaignType.TRIGGERED && resolved.campaign.permissions.includes('viewTriggers'),
panelRender: props => <TriggersList campaign={props.resolved.campaign} />,
children: {
':triggerId([0-9]+)': {
title: resolved => t('Trigger "{{name}}"', {name: resolved.trigger.name}),
resolve: {
trigger: params => `rest/triggers/${params.campaignId}/${params.triggerId}`,
},
link: params => `/campaigns/${params.campaignId}/triggers/${params.triggerId}/edit`,
navs: {
':action(edit|delete)': {
title: t('Edit'),
link: params => `/campaigns/${params.campaignId}/triggers/${params.triggerId}/edit`,
panelRender: props => <TriggersCUD action={props.match.params.action} entity={props.resolved.trigger} campaign={props.resolved.campaign} />
}
}
},
create: {
title: t('Create'),
panelRender: props => <TriggersCUD action="create" campaign={props.resolved.campaign} />
}
}
},
share: {
title: t('Share'),
link: params => `/campaigns/${params.campaignId}/share`,

View file

@ -0,0 +1,237 @@
'use strict';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import {
NavButton,
requiresAuthenticatedUser,
Title,
withPageHelpers
} from '../../lib/page';
import {
AlignedRow,
Button,
ButtonRow,
CheckBox,
Dropdown,
Form,
FormSendMethod,
InputField,
TableSelect,
TextArea,
withForm
} from '../../lib/form';
import {withErrorHandling} from '../../lib/error-handling';
import {DeleteModalDialog} from "../../lib/modals";
import {getTriggerTypes} from './helpers';
import {
Entity,
Event
} from '../../../../shared/triggers';
import moment from 'moment';
import {getCampaignTypeLabels} from "../helpers";
@translate()
@withForm
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class CUD extends Component {
constructor(props) {
super(props);
this.state = {};
this.campaignTypes = getCampaignTypeLabels(props.t);
const {entityLabels, eventLabels} = getTriggerTypes(props.t);
this.entityLabels = entityLabels;
this.entityOptions = [
{key: Entity.SUBSCRIPTION, label: entityLabels[Entity.SUBSCRIPTION]},
{key: Entity.CAMPAIGN, label: entityLabels[Entity.CAMPAIGN]}
];
const SubscriptionEvent = Event[Entity.SUBSCRIPTION];
const CampaignEvent = Event[Entity.CAMPAIGN];
this.eventOptions = {
[Entity.SUBSCRIPTION]: [
{key: SubscriptionEvent.CREATED, label: eventLabels[Entity.SUBSCRIPTION][SubscriptionEvent.CREATED]},
{key: SubscriptionEvent.LATEST_OPEN, label: eventLabels[Entity.SUBSCRIPTION][SubscriptionEvent.LATEST_OPEN]},
{key: SubscriptionEvent.LATEST_CLICK, label: eventLabels[Entity.SUBSCRIPTION][SubscriptionEvent.LATEST_CLICK]}
],
[Entity.CAMPAIGN]: [
{key: CampaignEvent.DELIVERED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.DELIVERED]},
{key: CampaignEvent.OPENED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.OPENED]},
{key: CampaignEvent.CLICKED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.CLICKED]},
{key: CampaignEvent.NOT_OPENED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.NOT_OPENED]},
{key: CampaignEvent.NOT_CLICKED, label: eventLabels[Entity.CAMPAIGN][CampaignEvent.NOT_CLICKED]}
]
};
this.initForm();
}
static propTypes = {
action: PropTypes.string.isRequired,
campaign: PropTypes.object,
entity: PropTypes.object
}
componentDidMount() {
if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => {
data.daysAfter = (Math.round(data.seconds_after / (3600 * 24))).toString();
if (data.entity === Entity.SUBSCRIPTION) {
data.subscriptionEvent = data.event;
} else {
data.subscriptionEvent = Event[Entity.SUBSCRIPTION].CREATED;
}
if (data.entity === Entity.CAMPAIGN) {
data.campaignEvent = data.event;
} else {
data.campaignEvent = Event[Entity.CAMPAIGN].DELIVERED;
}
});
} else {
this.populateFormValues({
name: '',
description: '',
entity: Entity.SUBSCRIPTION,
subscriptionEvent: Event[Entity.SUBSCRIPTION].CREATED,
campaignEvent: Event[Entity.CAMPAIGN].DELIVERED,
daysAfter: '',
enabled: true,
source_campaign: null
});
}
}
localValidateFormValues(state) {
const t = this.props.t;
const entityKey = state.getIn(['entity', 'value']);
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
} else {
state.setIn(['name', 'error'], null);
}
const daysAfter = state.getIn(['daysAfter', 'value']).trim();
if (daysAfter === '') {
state.setIn(['daysAfter', 'error'], t('Values must not be empty'));
} else if (isNaN(daysAfter) || Number.parseInt(daysAfter) < 0) {
state.setIn(['daysAfter', 'error'], t('Value must be a non-negative number'));
} else {
state.setIn(['daysAfter', 'error'], null);
}
if (entityKey === Entity.CAMPAIGN && !state.getIn(['source_campaign', 'value'])) {
state.setIn(['source_campaign', 'error'], t('Source campaign must not be empty'));
} else {
state.setIn(['source_campaign', 'error'], null);
}
}
async submitHandler() {
const t = this.props.t;
let sendMethod, url;
if (this.props.entity) {
sendMethod = FormSendMethod.PUT;
url = `rest/triggers/${this.props.campaign.id}/${this.props.entity.id}`
} else {
sendMethod = FormSendMethod.POST;
url = `rest/triggers/${this.props.campaign.id}`
}
try {
this.disableForm();
this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
data.seconds_after = Number.parseInt(data.daysAfter) * 3600 * 24;
if (data.entity === Entity.SUBSCRIPTION) {
data.event = data.subscriptionEvent;
} else if (data.entity === Entity.CAMPAIGN) {
data.event = data.campaignEvent;
}
});
if (submitSuccessful) {
this.navigateToWithFlashMessage(`/campaigns/${this.props.campaign.id}/triggers`, 'success', t('Trigger saved'));
} else {
this.enableForm();
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
}
} catch (error) {
throw error;
}
}
render() {
const t = this.props.t;
const isEdit = !!this.props.entity;
const entityKey = this.getFormValue('entity');
const campaignsColumns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Type'), render: data => this.campaignTypes[data] },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 5, title: t('Namespace') }
];
return (
<div>
{isEdit &&
<DeleteModalDialog
stateOwner={this}
visible={this.props.action === 'delete'}
deleteUrl={`rest/triggers/${this.props.campaign.id}/${this.props.entity.id}`}
cudUrl={`/campaigns/${this.props.campaign.id}/triggers/${this.props.entity.id}/edit`}
listUrl={`/campaigns/${this.props.campaign.id}/triggers`}
deletingMsg={t('Deleting trigger ...')}
deletedMsg={t('Trigger deleted')}/>
}
<Title>{isEdit ? t('Edit Trigger') : t('Create Trigger')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/>
<TextArea id="description" label={t('Description')}/>
<Dropdown id="entity" label={t('Entity')} options={this.entityOptions} help={t('Select the type of the trigger rule.')}/>
<InputField id="daysAfter" label={t('Trigger fires')}/>
<AlignedRow>days after:</AlignedRow>
{entityKey === Entity.SUBSCRIPTION && <Dropdown id="subscriptionEvent" label={t('Event')} options={this.eventOptions[Entity.SUBSCRIPTION]} help={t('Select the event that triggers sending the campaign.')}/>}
{entityKey === Entity.CAMPAIGN && <Dropdown id="campaignEvent" label={t('Event')} options={this.eventOptions[Entity.CAMPAIGN]} help={t('Select the event that triggers sending the campaign.')}/>}
{entityKey === Entity.CAMPAIGN &&
<TableSelect id="source_campaign" label={t('Campaign')} withHeader dropdown dataUrl={`rest/campaigns-others-by-list-table/${this.props.campaign.id}/${this.props.campaign.list}`} columns={campaignsColumns} selectionLabelIndex={1} />
}
<CheckBox id="enabled" text={t('Enabled')}/>
<ButtonRow>
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
{isEdit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/campaigns/${this.props.campaign.id}/triggers/${this.props.entity.id}/delete`}/>}
</ButtonRow>
</Form>
</div>
);
}
}

View file

@ -0,0 +1,81 @@
'use strict';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import {
NavButton,
requiresAuthenticatedUser,
Title,
Toolbar,
withPageHelpers
} from '../../lib/page';
import {withErrorHandling} from '../../lib/error-handling';
import {Table} from '../../lib/table';
import {getTriggerTypes} from './helpers';
import {Icon} from "../../lib/bootstrap-components";
@translate()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class List extends Component {
constructor(props) {
super(props);
const {entityLabels, eventLabels} = getTriggerTypes(props.t);
this.entityLabels = entityLabels;
this.eventLabels = eventLabels;
this.state = {};
}
static propTypes = {
campaign: PropTypes.object
}
componentDidMount() {
}
render() {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('List') },
{ data: 4, title: t('Entity'), render: data => this.entityLabels[data], searchable: false },
{ data: 5, title: t('Event'), render: (data, cmd, rowData) => this.eventLabels[rowData[4]][data], searchable: false },
{ data: 6, title: t('Days after'), render: data => Math.round(data / (3600 * 24)) },
{ data: 7, title: t('Enabled'), render: data => data ? t('Yes') : t('No'), searchable: false},
{
actions: data => {
const actions = [];
if (this.props.campaign.permissions.includes('manageTriggers')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
link: `/campaigns/${this.props.campaign.id}/triggers/${data[0]}/edit`
});
}
return actions;
}
}
];
return (
<div>
{this.props.campaign.permissions.includes('manageTriggers') &&
<Toolbar>
<NavButton linkTo={`/campaigns/${this.props.campaign.id}/triggers/create`} className="btn-primary" icon="plus" label={t('Create Trigger')}/>
</Toolbar>
}
<Title>{t('Triggers')}</Title>
<Table withHeader dataUrl={`rest/triggers-by-campaign-table/${this.props.campaign.id}`} columns={columns} />
</div>
);
}
}

View file

@ -0,0 +1,35 @@
'use strict';
import {Entity, Event} from '../../../../shared/triggers';
export function getTriggerTypes(t) {
const entityLabels = {
[Entity.SUBSCRIPTION]: t('Subscription'),
[Entity.CAMPAIGN]: t('Campaign')
};
const SubscriptionEvent = Event[Entity.SUBSCRIPTION];
const CampaignEvent = Event[Entity.CAMPAIGN];
const eventLabels = {
[Entity.SUBSCRIPTION]: {
[SubscriptionEvent.CREATED]: t('Created'),
[SubscriptionEvent.LATEST_OPEN]: t('Latest open'),
[SubscriptionEvent.LATEST_CLICK]: t('Latest click')
},
[Entity.CAMPAIGN]: {
[CampaignEvent.DELIVERED]: t('Delivered'),
[CampaignEvent.OPENED]: t('Opened'),
[CampaignEvent.CLICKED]: t('Clicked'),
[CampaignEvent.NOT_OPENED]: t('Not opened'),
[CampaignEvent.NOT_CLICKED]: t('Not clicked')
}
};
return {
entityLabels,
eventLabels
};
}

View file

@ -1,8 +1,9 @@
'use strict';
import {anonymousRestrictedAccessToken} from '../../../shared/urls';
import mailtrainConfig from "mailtrainConfig";
let restrictedAccessToken = 'ANONYMOUS';
let restrictedAccessToken = anonymousRestrictedAccessToken;
function setRestrictedAccessToken(token) {
restrictedAccessToken = token;
@ -28,7 +29,7 @@ function getBaseDir() {
if (mailtrainConfig.trusted) {
return mailtrainConfig.trustedUrlBaseDir;
} else {
return mailtrainConfig.sandboxUrlBaseDir + 'ANONYMOUS';
return mailtrainConfig.sandboxUrlBaseDir + anonymousRestrictedAccessToken;
}
}

View file

@ -47,7 +47,7 @@ export default class List extends Component {
data: 1,
title: t('Name'),
actions: data => {
const perms = data[6];
const perms = data[7];
if (perms.includes('viewSubscriptions')) {
return [{label: data[1], link: `/lists/${data[0]}/subscriptions`}];
} else {
@ -62,7 +62,9 @@ export default class List extends Component {
{
actions: data => {
const actions = [];
const perms = data[6];
const triggersCount = data[6];
const perms = data[7];
console.log(data);
if (perms.includes('viewSubscriptions')) {
actions.push({
@ -92,6 +94,13 @@ export default class List extends Component {
});
}
if (triggersCount > 0) {
actions.push({
label: <Icon icon="flash" title={t('Triggers')}/>,
link: `/lists/${data[0]}/triggers`
});
}
if (perms.includes('share')) {
actions.push({
label: <Icon icon="share-alt" title={t('Share')}/>,

View file

@ -0,0 +1,75 @@
'use strict';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {translate} from 'react-i18next';
import {
requiresAuthenticatedUser,
Title,
withPageHelpers
} from '../lib/page';
import {withErrorHandling} from '../lib/error-handling';
import {Table} from '../lib/table';
import {getTriggerTypes} from '../campaigns/triggers/helpers';
import {Icon} from "../lib/bootstrap-components";
@translate()
@withPageHelpers
@withErrorHandling
@requiresAuthenticatedUser
export default class List extends Component {
constructor(props) {
super(props);
const {entityLabels, eventLabels} = getTriggerTypes(props.t);
this.entityLabels = entityLabels;
this.eventLabels = eventLabels;
this.state = {};
}
static propTypes = {
list: PropTypes.object
}
componentDidMount() {
}
render() {
const t = this.props.t;
const columns = [
{ data: 1, title: t('Name') },
{ data: 2, title: t('Description') },
{ data: 3, title: t('Campaign') },
{ data: 4, title: t('Entity'), render: data => this.entityLabels[data], searchable: false },
{ data: 5, title: t('Event'), render: (data, cmd, rowData) => this.eventLabels[rowData[4]][data], searchable: false },
{ data: 6, title: t('Days after'), render: data => Math.round(data / (3600 * 24)) },
{ data: 7, title: t('Enabled'), render: data => data ? t('Yes') : t('No'), searchable: false},
{
actions: data => {
const actions = [];
const perms = data[9];
const campaignId = data[8];
if (perms.includes('manageTriggers')) {
actions.push({
label: <Icon icon="edit" title={t('Edit')}/>,
link: `/campaigns/${campaignId}/triggers/${data[0]}/edit`
});
}
return actions;
}
}
];
return (
<div>
<Title>{t('Triggers')}</Title>
<Table withHeader dataUrl={`rest/triggers-by-list-table/${this.props.list.id}`} columns={columns} />
</div>
);
}
}

View file

@ -13,7 +13,7 @@ import SubscriptionsCUD from './subscriptions/CUD';
import SegmentsList from './segments/List';
import SegmentsCUD from './segments/CUD';
import Share from '../shares/Share';
import TriggersList from './TriggersList';
function getMenus(t) {
return {
@ -127,6 +127,11 @@ function getMenus(t) {
}
}
},
triggers: {
title: t('Triggers'),
link: params => `/lists/${params.listId}/triggers`,
panelRender: props => <TriggersList list={props.resolved.list} />
},
share: {
title: t('Share'),
link: params => `/lists/${params.listId}/share`,

View file

@ -94,17 +94,17 @@ export default class CUD extends Component {
localValidateFormValues(state) {
const t = this.props.t;
for (const key of state.keys()) {
state.setIn([key, 'error'], null);
}
if (!state.getIn(['name', 'value'])) {
state.setIn(['name', 'error'], t('Name must not be empty'));
} else {
state.setIn(['name', 'error'], null);
}
const typeKey = state.getIn(['type', 'value']);
if (!typeKey) {
state.setIn(['type', 'error'], t('Type must be selected'));
} else {
state.setIn(['type', 'error'], null);
}
validateNamespace(t, state);

View file

@ -112,14 +112,12 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
const mosaicoTemplate = state.getIn([prefix + 'mosaicoTemplate', 'value']);
if (!mosaicoTemplate) {
state.setIn([prefix + 'mosaicoTemplate', 'error'], t('Mosaico template must be selected'));
} else {
state.setIn([prefix + 'mosaicoTemplate', 'error'], null);
}
}
};
const mosaicoFsTemplatesOptions = mailtrainConfig.mosaico.fsTemplates.map(([key, label]) => ({key, label}));
const mosaicoFsTemplatesLabels = new Map(mailtrainConfig.mosaico.fsTemplates);
const mosaicoFsTemplatesOptions = mailtrainConfig.mosaico.fsTemplates;
const mosaicoFsTemplatesLabels = new Map(mailtrainConfig.mosaico.fsTemplates.map(({key, label}) => ([key, label])));
templateTypes.mosaicoWithFsTemplate = {
typeName: t('Mosaico with predefined templates'),
@ -151,7 +149,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
});
},
initData: () => ({
[prefix + 'mosaicoFsTemplate']: mailtrainConfig.mosaico.fsTemplates[0][0],
[prefix + 'mosaicoFsTemplate']: mailtrainConfig.mosaico.fsTemplates[0].key,
[prefix + 'mosaicoData']: {}
}),
afterLoad: data => {