426 lines
No EOL
15 KiB
JavaScript
426 lines
No EOL
15 KiB
JavaScript
'use strict';
|
|
|
|
import React, {Component} from "react";
|
|
import PropTypes
|
|
from "prop-types";
|
|
import {withTranslation} from '../../lib/i18n';
|
|
import {
|
|
NavButton,
|
|
requiresAuthenticatedUser,
|
|
Title,
|
|
Toolbar,
|
|
withPageHelpers
|
|
} from "../../lib/page";
|
|
import {
|
|
Button as FormButton,
|
|
ButtonRow,
|
|
Dropdown,
|
|
Form,
|
|
FormSendMethod,
|
|
InputField,
|
|
withForm
|
|
} from "../../lib/form";
|
|
import {withErrorHandling} from "../../lib/error-handling";
|
|
import {DeleteModalDialog} from "../../lib/modals";
|
|
|
|
import styles
|
|
from "./CUD.scss";
|
|
import {DragDropContext} from "react-dnd";
|
|
import HTML5Backend
|
|
from "react-dnd-html5-backend";
|
|
import TouchBackend
|
|
from "react-dnd-touch-backend";
|
|
import SortableTree
|
|
from "react-sortable-tree";
|
|
import 'react-sortable-tree/style.css';
|
|
import {
|
|
ActionLink,
|
|
Button,
|
|
Icon
|
|
} from "../../lib/bootstrap-components";
|
|
import {getRuleHelpers} from "./helpers";
|
|
import RuleSettingsPane
|
|
from "./RuleSettingsPane";
|
|
import {withComponentMixins} from "../../lib/decorator-helpers";
|
|
|
|
// https://stackoverflow.com/a/4819886/1601953
|
|
const isTouchDevice = !!('ontouchstart' in window || navigator.maxTouchPoints);
|
|
|
|
@DragDropContext(isTouchDevice ? TouchBackend : HTML5Backend)
|
|
@withComponentMixins([
|
|
withTranslation,
|
|
withForm,
|
|
withErrorHandling,
|
|
withPageHelpers,
|
|
requiresAuthenticatedUser
|
|
])
|
|
export default class CUD extends Component {
|
|
// The code below keeps the segment settings in form value. However, it uses it as a mutable datastructure.
|
|
// After initilization, segment settings is never set using setState. This is OK we update the state.rulesTree
|
|
// from the segment settings on relevant events (changes in the tree and closing the rule settings pane).
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.ruleHelpers = getRuleHelpers(props.t, props.fields);
|
|
|
|
this.state = {
|
|
rulesTree: this.getTreeFromRules([])
|
|
// There is no ruleOptionsVisible here. We have 3 state logic for the visibility:
|
|
// Undef - not shown, True - shown with entry animation, False - hidden with exit animation
|
|
};
|
|
|
|
this.initForm();
|
|
|
|
this.onRuleSettingsPaneUpdatedHandler = ::this.onRuleSettingsPaneUpdated;
|
|
this.onRuleSettingsPaneCloseHandler = ::this.onRuleSettingsPaneClose;
|
|
this.onRuleSettingsPaneDeleteHandler = ::this.onRuleSettingsPaneDelete;
|
|
}
|
|
|
|
static propTypes = {
|
|
action: PropTypes.string.isRequired,
|
|
list: PropTypes.object,
|
|
fields: PropTypes.array,
|
|
entity: PropTypes.object
|
|
}
|
|
|
|
getRulesFromTree(tree) {
|
|
const rules = [];
|
|
|
|
for (const node of tree) {
|
|
const rule = node.rule;
|
|
|
|
if (this.ruleHelpers.isCompositeRuleType(rule.type)) {
|
|
rule.rules = this.getRulesFromTree(node.children);
|
|
}
|
|
|
|
rules.push(rule);
|
|
}
|
|
|
|
return rules;
|
|
}
|
|
|
|
getTreeFromRules(rules) {
|
|
const ruleHelpers = this.ruleHelpers;
|
|
|
|
const tree = [];
|
|
for (const rule of rules) {
|
|
const ruleTypeSettings = ruleHelpers.getRuleTypeSettings(rule);
|
|
const title = ruleTypeSettings ? ruleTypeSettings.treeLabel(rule) : this.props.t('newRule');
|
|
|
|
tree.push({
|
|
rule,
|
|
title,
|
|
expanded: true,
|
|
children: this.getTreeFromRules(rule.rules || [])
|
|
});
|
|
}
|
|
|
|
return tree;
|
|
}
|
|
|
|
componentDidMount() {
|
|
if (this.props.entity) {
|
|
this.setState({
|
|
rulesTree: this.getTreeFromRules(this.props.entity.settings.rootRule.rules)
|
|
});
|
|
|
|
this.getFormValuesFromEntity(this.props.entity, data => {
|
|
data.rootRuleType = data.settings.rootRule.type;
|
|
data.selectedRule = null; // Validation errors of the selected rule are attached to this which makes sure we don't submit the segment if the opened rule has errors
|
|
});
|
|
|
|
} else {
|
|
this.populateFormValues({
|
|
name: '',
|
|
settings: {
|
|
rootRule: {
|
|
type: 'all',
|
|
rules: []
|
|
}
|
|
},
|
|
rootRuleType: 'all',
|
|
selectedRule: null
|
|
});
|
|
}
|
|
}
|
|
|
|
localValidateFormValues(state) {
|
|
const t = this.props.t;
|
|
|
|
if (!state.getIn(['name', 'value'])) {
|
|
state.setIn(['name', 'error'], t('nameMustNotBeEmpty'));
|
|
} else {
|
|
state.setIn(['name', 'error'], null);
|
|
}
|
|
|
|
if (state.getIn(['selectedRule', 'value']) === null) {
|
|
state.setIn(['selectedRule', 'error'], null);
|
|
}
|
|
}
|
|
|
|
async doSubmit(stay) {
|
|
const t = this.props.t;
|
|
|
|
let sendMethod, url;
|
|
if (this.props.entity) {
|
|
sendMethod = FormSendMethod.PUT;
|
|
url = `rest/segments/${this.props.list.id}/${this.props.entity.id}`
|
|
} else {
|
|
sendMethod = FormSendMethod.POST;
|
|
url = `rest/segments/${this.props.list.id}`
|
|
}
|
|
|
|
try {
|
|
this.disableForm();
|
|
this.setFormStatusMessage('info', t('saving'));
|
|
|
|
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
|
const keep = ['name', 'settings', 'originalHash'];
|
|
|
|
data.settings.rootRule.type = data.rootRuleType;
|
|
|
|
delete data.rootRuleType;
|
|
delete data.selectedRule;
|
|
});
|
|
|
|
if (submitSuccessful) {
|
|
if (stay) {
|
|
await this.getFormValuesFromURL(`rest/segments/${this.props.list.id}/${this.props.entity.id}`, data => {
|
|
data.rootRuleType = data.settings.rootRule.type;
|
|
data.selectedRule = null; // Validation errors of the selected rule are attached to this which makes sure we don't submit the segment if the opened rule has errors
|
|
|
|
this.setState({
|
|
rulesTree: this.getTreeFromRules(data.settings.rootRule.rules)
|
|
});
|
|
});
|
|
this.enableForm();
|
|
this.setFormStatusMessage('success', t('segmentSaved'));
|
|
} else {
|
|
this.navigateToWithFlashMessage(`/lists/${this.props.list.id}/segments`, 'success', t('segmentSaved'));
|
|
}
|
|
} else {
|
|
this.enableForm();
|
|
this.setFormStatusMessage('warning', t('thereAreErrorsInTheFormPleaseFixThemAnd'));
|
|
}
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async submitAndStay() {
|
|
await this.formHandleChangedError(async () => await this.doSubmit(true));
|
|
}
|
|
|
|
async submitAndLeave() {
|
|
await this.formHandleChangedError(async () => await this.doSubmit(false));
|
|
}
|
|
|
|
onRulesChanged(rulesTree) {
|
|
// This assumes that !this.state.ruleOptionsVisible
|
|
this.getFormValue('settings').rootRule.rules = this.getRulesFromTree(rulesTree);
|
|
|
|
this.setState({
|
|
rulesTree
|
|
})
|
|
}
|
|
|
|
showRuleOptions(rule) {
|
|
this.updateFormValue('selectedRule', rule);
|
|
|
|
this.setState({
|
|
ruleOptionsVisible: true
|
|
});
|
|
}
|
|
|
|
onRuleSettingsPaneClose() {
|
|
this.updateFormValue('selectedRule', null);
|
|
|
|
this.setState({
|
|
ruleOptionsVisible: false,
|
|
rulesTree: this.getTreeFromRules(this.getFormValue('settings').rootRule.rules)
|
|
});
|
|
}
|
|
|
|
onRuleSettingsPaneDelete() {
|
|
const selectedRule = this.getFormValue('selectedRule');
|
|
this.updateFormValue('selectedRule', null);
|
|
|
|
this.setState({
|
|
ruleOptionsVisible: false,
|
|
});
|
|
|
|
this.deleteRule(selectedRule);
|
|
}
|
|
|
|
onRuleSettingsPaneUpdated(hasErrors) {
|
|
this.setState(previousState => ({
|
|
formState: previousState.formState.setIn(['data', 'selectedRule', 'error'], hasErrors)
|
|
}));
|
|
}
|
|
|
|
addRule(rule) {
|
|
if (!this.state.ruleOptionsVisible) {
|
|
const rules = this.getFormValue('settings').rootRule.rules;
|
|
rules.push(rule);
|
|
|
|
this.updateFormValue('selectedRule', rule);
|
|
|
|
this.setState({
|
|
ruleOptionsVisible: true,
|
|
rulesTree: this.getTreeFromRules(rules)
|
|
});
|
|
}
|
|
}
|
|
|
|
async addCompositeRule() {
|
|
this.addRule({
|
|
type: 'all',
|
|
rules: []
|
|
});
|
|
}
|
|
|
|
async addPrimitiveRule() {
|
|
this.addRule({
|
|
type: null // Null type means a primitive rule where the type has to be chosen based on the chosen column
|
|
});
|
|
}
|
|
|
|
deleteRule(ruleToDelete) {
|
|
let finishedSearching = false;
|
|
|
|
function childrenWithoutRule(rules) {
|
|
const newRules = [];
|
|
|
|
for (const rule of rules) {
|
|
if (finishedSearching) {
|
|
newRules.push(rule);
|
|
|
|
} else if (rule !== ruleToDelete) {
|
|
const newRule = Object.assign({}, rule);
|
|
|
|
if (rule.rules) {
|
|
newRule.rules = childrenWithoutRule(rule.rules);
|
|
}
|
|
|
|
newRules.push(newRule);
|
|
|
|
} else {
|
|
finishedSearching = true;
|
|
}
|
|
}
|
|
|
|
return newRules;
|
|
}
|
|
|
|
const rules = childrenWithoutRule(this.getFormValue('settings').rootRule.rules);
|
|
|
|
this.getFormValue('settings').rootRule.rules = rules;
|
|
|
|
this.setState({
|
|
rulesTree: this.getTreeFromRules(rules)
|
|
});
|
|
}
|
|
|
|
|
|
render() {
|
|
const t = this.props.t;
|
|
const isEdit = !!this.props.entity;
|
|
const selectedRule = this.getFormValue('selectedRule');
|
|
const ruleHelpers = this.ruleHelpers;
|
|
|
|
let ruleOptionsVisibilityClass = '';
|
|
if ('ruleOptionsVisible' in this.state) {
|
|
if (this.state.ruleOptionsVisible) {
|
|
ruleOptionsVisibilityClass = ' ' + styles.ruleOptionsVisible;
|
|
} else {
|
|
ruleOptionsVisibilityClass = ' ' + styles.ruleOptionsHidden;
|
|
}
|
|
}
|
|
|
|
return (
|
|
|
|
<div>
|
|
{isEdit &&
|
|
<DeleteModalDialog
|
|
stateOwner={this}
|
|
visible={this.props.action === 'delete'}
|
|
deleteUrl={`rest/segments/${this.props.list.id}/${this.props.entity.id}`}
|
|
backUrl={`/lists/${this.props.list.id}/segments/${this.props.entity.id}/edit`}
|
|
successUrl={`/lists/${this.props.list.id}/segments`}
|
|
deletingMsg={t('deletingSegment')}
|
|
deletedMsg={t('segmentDeleted')}/>
|
|
}
|
|
|
|
<Title>{isEdit ? t('editSegment') : t('createSegment')}</Title>
|
|
|
|
<Form stateOwner={this} onSubmitAsync={::this.submitAndLeave}>
|
|
<h3>{t('segmentOptions')}</h3>
|
|
|
|
<InputField id="name" label={t('name')} />
|
|
<Dropdown id="rootRuleType" label={t('toplevelMatchType')} options={ruleHelpers.getCompositeRuleTypeOptions()} />
|
|
</Form>
|
|
|
|
<hr />
|
|
|
|
<div className={styles.rulePane + ruleOptionsVisibilityClass}>
|
|
<div className={styles.leftPane}>
|
|
<div className={styles.leftPaneInner}>
|
|
<Toolbar>
|
|
<Button className="btn-secondary" label={t('addCompositeRule')} onClickAsync={::this.addCompositeRule}/>
|
|
<Button className="btn-secondary" label={t('addRule')} onClickAsync={::this.addPrimitiveRule}/>
|
|
</Toolbar>
|
|
|
|
<h3>{t('rules')}</h3>
|
|
|
|
<div className="clearfix"/>
|
|
|
|
<div className={styles.ruleTree}>
|
|
<SortableTree
|
|
treeData={this.state.rulesTree}
|
|
onChange={rulesTree => this.onRulesChanged(rulesTree)}
|
|
isVirtualized={false}
|
|
canDrop={ data => !data.nextParent || (ruleHelpers.isCompositeRuleType(data.nextParent.rule.type)) }
|
|
generateNodeProps={data => ({
|
|
buttons: [
|
|
<ActionLink onClickAsync={async () => !this.state.ruleOptionsVisible && this.showRuleOptions(data.node.rule)} className={styles.ruleActionLink}><Icon icon="edit" title={t('edit')}/></ActionLink>,
|
|
<ActionLink onClickAsync={async () => !this.state.ruleOptionsVisible && this.deleteRule(data.node.rule)} className={styles.ruleActionLink}><Icon icon="trash-alt" title={t('delete')}/></ActionLink>
|
|
]
|
|
})}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.leftPaneOverlay} />
|
|
|
|
<div className={styles.paneDivider}>
|
|
<div className={styles.paneDividerSolidBackground}/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.rightPane}>
|
|
<div className={styles.rightPaneInner}>
|
|
{selectedRule &&
|
|
<RuleSettingsPane rule={selectedRule} fields={this.props.fields} onChange={this.onRuleSettingsPaneUpdatedHandler} onClose={this.onRuleSettingsPaneCloseHandler} onDelete={this.onRuleSettingsPaneDeleteHandler} forceShowValidation={this.isFormValidationShown()}/>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr/>
|
|
{isEdit ?
|
|
<ButtonRow format="wide" className={`col-xs-12 ${styles.toolbar}`}>
|
|
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndStay')} onClickAsync={::this.submitAndStay}/>
|
|
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndLeave')} onClickAsync={::this.submitAndLeave}/>
|
|
|
|
<NavButton className="btn-danger" icon="trash-alt" label={t('delete')} linkTo={`/lists/${this.props.list.id}/segments/${this.props.entity.id}/delete`}/>
|
|
</ButtonRow>
|
|
:
|
|
<ButtonRow format="wide" className={`col-xs-12 ${styles.toolbar}`}>
|
|
<Button type="submit" className="btn-primary" icon="check" label={t('save')} onClickAsync={::this.submitAndLeave}/>
|
|
</ButtonRow>
|
|
}
|
|
|
|
</div>
|
|
);
|
|
}
|
|
} |