Skip to content

Commit

Permalink
feat(ui-contract-editor): update formulas when dependent variables ch…
Browse files Browse the repository at this point in the history
…ange (#171)

* feat(ui-contract-editor): update formulas when dependent variables change
Signed-off-by: Dan Selman <[email protected]>
  • Loading branch information
dselman authored Aug 19, 2020
1 parent 78801f7 commit cfb8c06
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 42 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 12 additions & 25 deletions packages/storybook/src/stories/4-ContractEditor.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { Provider, connect } from 'react-redux';

/* Accord Project */
import { SlateTransformer } from '@accordproject/markdown-slate';
import { TemplateMarkTransformer } from '@accordproject/markdown-template';
import { Template, Clause, TemplateLibrary, version } from '@accordproject/cicero-core';
import ContractEditor from '@accordproject/ui-contract-editor';
import { getChildren } from '@accordproject/ui-contract-editor';

/* Storybook */
import { action } from '@storybook/addon-actions';
Expand All @@ -16,7 +16,6 @@ import { storiesOf } from '@storybook/react'

/* Slate */
import { Editor, Node, Transforms } from 'slate';
import { ReactEditor } from 'slate-react';

/* Misc */
import { uuid } from 'uuidv4';
Expand Down Expand Up @@ -165,32 +164,20 @@ export const contractEditor = () => {
parseResult,
});

const hasFormulas = getChildren(clauseNode, (n) => n.type === 'formula');
let draftedSlateNode = null;

if(hasFormulas) {
const slateDom = await ciceroClause.draft({format:'slate'});
draftedSlateNode = JSON.parse(JSON.stringify(clauseNode));
draftedSlateNode.children = slateDom.document.children;
}

return Promise.resolve({
node: null,
operation: null,
node: hasFormulas ? draftedSlateNode : null,
operation: hasFormulas ? 'update_formulas' : null,
error: null,
});

/* XXX What do we do with this? - JS
const something = await ciceroClause.draft({format:'slate'});
const found = val.children[1].children.filter(element => element.type === 'formula' && element.data.name === 'formula');
action('Clause -> Parse: ')({
'Clause': ciceroClause,
'AST': ast,
'Draft': something
});
const path = ReactEditor.findPath(newReduxState.editor, found[0]);
const newConditional = {
object: 'inline',
type: 'formula',
data: { name: "formula", elementType: "Double" },
children: [{ object: "text", text: `${Math.round(Math.random() * 10)}` }]
};
Editor.withoutNormalizing(newReduxState.editor, () => {
Transforms.removeNodes(newReduxState.editor, { at: path });
Transforms.insertNodes(newReduxState.editor, newConditional, { at: path });
});
*/
} catch (err) {
action('Clause -> Parse Error: ')({
clause: TEMPLATE_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,23 @@ import { HistoryEditor } from 'slate-history';

import { SlateTransformer } from '@accordproject/markdown-slate';
import { HtmlTransformer } from '@accordproject/markdown-html';
import { CLAUSE, VARIABLE } from './withClauseSchema';
import { CLAUSE, VARIABLE, FORMULA } from './withClauseSchema';
import getChildren from '../../utilities/getChildren';

import '../../styles.css';

/**
* Returns a formula inline from clauseNode whose name
* matches the name of the search formula
* @param {*} clauseNode the input clause node
* @param {*} search the formula to look for in the clause node
*/
export const findFormula = (clauseNode, search) => {
const formulas = getChildren(clauseNode, (n) => n.type === FORMULA
&& n.data.name === search.data.name);
return formulas && formulas.length > 0 ? formulas[0] : null;
};

/**
* Check if UI valid (depth first traversal)
* @param {object} params - recursion params
Expand All @@ -23,10 +36,6 @@ function _recursive(params, children) {
default: {
// eslint-disable-next-line default-case
switch (childType) {
case 'formula':
throw new Error('Formula not supported');
case 'image':
throw new Error('Image not supported');
case 'ol_list':
case 'ul_list': {
if (child.data.kind === VARIABLE) {
Expand Down Expand Up @@ -82,14 +91,13 @@ const withClauses = (editor, withClausesProps) => {

editor.onChange = () => {
onChange();
if (onClauseUpdated && editor.isInsideClause()) {
if (editor.isInsideClause()) {
const [clauseNode, path] = editor.getClauseWithPath();
const [variable] = Editor.nodes(editor, { match: n => n.type === VARIABLE });

// if we have edited a variable, then we ensure that all
// occurences of the variable get the new value
if (variable && variable[0].type === VARIABLE
&& variable[0].data && variable[0].data.name) {
if (variable && variable[0].data && variable[0].data.name) {
const variableName = variable[0].data.name;
const variableIterator = Editor.nodes(editor, { match: n => n.type === VARIABLE
&& n.data.name === variableName,
Expand All @@ -114,17 +122,47 @@ const withClauses = (editor, withClausesProps) => {
}
}

onClauseUpdated(clauseNode).then(({ node, operation, error }) => {
if (operation === 'replace_node' && node) {
Transforms.removeNodes(editor, { at: path });
Transforms.insertNodes(editor, node, { at: path });
} else {
if (onClauseUpdated) {
onClauseUpdated(clauseNode).then(({ node, operation, error }) => {
if (operation === 'replace_node') {
Transforms.removeNodes(editor, { at: path });
Transforms.insertNodes(editor, node, { at: path });
} else if (operation === 'update_formulas') {
// if we have edited a variable, then we ensure that all
// formulas that depend on the variable are updated based on the values in 'node'
if (variable && variable[0].data && variable[0].data.name) {
const variableName = variable[0].data.name;
const formulasIterator = Editor.nodes(editor, { match: n => n.type === FORMULA
&& n.data.dependencies.includes(variableName),
at: path });
let result = formulasIterator.next();
while (!result.done) {
const formulaEntry = result.value;
const newFormula = findFormula(node, formulaEntry[0]);
if (newFormula) {
const oldFormulaValue = Node.string(formulaEntry[0]);
const newFormulaValue = Node.string(newFormula);
if (newFormulaValue !== oldFormulaValue) {
HistoryEditor.withoutSaving(editor, () => {
Editor.withoutNormalizing(editor, () => {
Transforms.removeNodes(editor, { at: formulaEntry[1] });
Transforms.insertNodes(editor, newFormula, { at: formulaEntry[1] });
});
});
}
}
result = formulasIterator.next();
}
}
}

// set or clear errors
HistoryEditor.withoutSaving(editor, () => {
Transforms.setNodes(editor, { error: !!error }, { at: path });
});
}
});
}
});
}
} // inside a clause
};

editor.insertData = (data) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-contract-editor/src/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import ContractEditor from './ContractEditor';
import getChildren from './utilities/getChildren';

export { getChildren };
export default ContractEditor;
28 changes: 28 additions & 0 deletions packages/ui-contract-editor/src/lib/utilities/getChildren.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Recursively match nodes in a slate dom using a matching function
* @param {*} node a slate node
* @param {*} matcher a matching function
* @returns {[*]} the array of matched nodes
*/
const getChildren = (node, matcher) => {
if (matcher(node)) {
return node;
}

if (node.children) {
let result = [];
node.children.forEach(n => {
const r = getChildren(n, matcher);
if (Array.isArray(r)) {
result = result.concat(r);
} else {
result.push(r);
}
});
return result;
}

return [];
};

export default getChildren;

0 comments on commit cfb8c06

Please sign in to comment.