Commit 6f87c633 authored by Jaden DIEFENBAUGH's avatar Jaden DIEFENBAUGH
Browse files

updated tc block editor to have save/cancel func, fixes #129 and #134, maybe #110

parent 8cc308f8
Pipeline #24813 passed with stages
in 42 minutes and 52 seconds
......@@ -529,7 +529,7 @@ export default class GraphicalEditor extends React.PureComponent<Props, State> {
// check in endDrag() if the distance travelled is small
// enough to be considered a click or not
function startDrag(d) {
console.log('startDrag');
//console.log('startDrag');
startX = d3.event.x;
startY = d3.event.y;
// find the block that the user started from
......@@ -553,7 +553,7 @@ export default class GraphicalEditor extends React.PureComponent<Props, State> {
const processEndBlock = (endInput: HTMLElement) => {
if(!startBlock)
return;
console.log(endInput);
//console.log(endInput);
const endId = endInput.id.replace('-input-', '.');
// create a new connection assuming theres a given func that does it
if(createConnection)
......
......@@ -315,7 +315,7 @@ describe('<ToolchainEditor />', function() {
wrapper.update();
wrapper.find('.modal CacheInput[value="output0"]').prop('onChange')( { target: { value: 'species' }});
wrapper.update();
wrapper.find('button.close').simulate('click');
wrapper.find('.modal button.btn-success').simulate('click');
wrapper.update();
expect(wrapper.find('ToolchainModal').props().active).to.equal(false);
......@@ -331,6 +331,7 @@ describe('<ToolchainEditor />', function() {
wrapper.find('rect#block_dataset0').simulate('click');
wrapper.update();
expect(wrapper.find('ToolchainModal').props().active).to.equal(true);
wrapper.update();
expect(wrapper.find('.modal').find('CacheInput#tcModalInitFocus').props().value).to.equal('dataset0');
wrapper.find('.modal').find('CacheInput#tcModalInitFocus').prop('onChange')( { target: { value: 'testing_data' }});
wrapper.update();
......@@ -344,7 +345,7 @@ describe('<ToolchainEditor />', function() {
wrapper.update();
wrapper.find('.modal CacheInput[value="output0"]').prop('onChange')( { target: { value: 'species' }});
wrapper.update();
wrapper.find('button.close').simulate('click');
wrapper.find('.modal button.btn-success').simulate('click');
wrapper.update();
expect(wrapper.props().data.contents.datasets[1]).to.deep.equal({
......@@ -376,7 +377,7 @@ describe('<ToolchainEditor />', function() {
wrapper.update();
wrapper.find('.modal CacheInput[value="output"]').prop('onChange')( { target: { value: 'lda_machine' }});
wrapper.update();
wrapper.find('button.close').simulate('click');
wrapper.find('.modal button.btn-success').simulate('click');
wrapper.update();
expect(wrapper.props().data.contents.blocks[0]).to.deep.equal({
......@@ -412,7 +413,7 @@ describe('<ToolchainEditor />', function() {
wrapper.update();
wrapper.find('.modal CacheInput[value="output"]').prop('onChange')( { target: { value: 'scores' }});
wrapper.update();
wrapper.find('button.close').simulate('click');
wrapper.find('.modal button.btn-success').simulate('click');
wrapper.update();
expect(wrapper.props().data.contents.blocks[1]).to.deep.equal({
......@@ -442,7 +443,7 @@ describe('<ToolchainEditor />', function() {
wrapper.update();
wrapper.find('.modal CacheInput[value="input0"]').prop('onChange')( { target: { value: 'species' }});
wrapper.update();
wrapper.find('button.close').simulate('click');
wrapper.find('.modal button.btn-success').simulate('click');
wrapper.update();
expect(wrapper.props().data.contents.analyzers[0]).to.deep.equal({
......@@ -964,12 +965,13 @@ describe('<ToolchainEditor />', function() {
wrapper.update();
expect(wrapper.find('ToolchainModal').props().active).to.equal(true);
wrapper.find('.modal CacheInput[value="lda_machine"]').prop('onChange')( { target: { value: 'lda' }});
wrapper.find('.modal button.btn-success').simulate('click');
wrapper.update();
expect(wrapper.find('.modal CacheInput[value="lda"]').props().value).to.equal('lda');
const data = wrapper.props().data;
expect(data.contents.representation.connections).to.have.property('training_alg.lda_machine/testing_alg.lda');
expect(data.contents.representation.connections).to.not.have.property('training_alg.lda_machine/testing_alg.lda_machine');
expect(data.contents.representation.connections).to.have.property('training_alg.lda_machine/testing_alg.lda');
});
it('Properly changes connection names when renaming an output', () => {
......@@ -993,12 +995,13 @@ describe('<ToolchainEditor />', function() {
wrapper.update();
expect(wrapper.find('ToolchainModal').props().active).to.equal(true);
wrapper.find('.modal CacheInput[value="lda_machine"]').prop('onChange')( { target: { value: 'lda' }});
wrapper.find('.modal button.btn-success').simulate('click');
wrapper.update();
expect(wrapper.find('.modal CacheInput[value="lda"]').props().value).to.equal('lda');
const data = wrapper.props().data;
expect(data.contents.representation.connections).to.have.property('training_alg.lda/testing_alg.lda_machine');
expect(data.contents.representation.connections).to.not.have.property('training_alg.lda_machine/testing_alg.lda_machine');
expect(data.contents.representation.connections).to.have.property('training_alg.lda/testing_alg.lda_machine');
});
it('Properly deletes the training_data.species/training_alg.species connection', async () => {
......
......@@ -29,7 +29,7 @@ import {
import CacheInput from '../CacheInput.jsx';
import DeleteInputBtn from '../DeleteInputBtn.jsx';
import { generateNewKey } from '@helpers';
import { generateNewKey, copyObj } from '@helpers';
import type { BlockType } from '@helpers/toolchainTypes';
type Props = {
......@@ -43,41 +43,136 @@ type Props = {
possibleChannels: { [string]: string },
// func to close modal
toggle: () => any,
// change the block name
updateBlockName: (newName: string) => any,
// change the name of an input or output
updateBlockIOName: (ioType: 'input' | 'output', oldName: string, newName: string) => any,
// create an input or output
addBlockIO: (ioType: 'input' | 'output', newName: string) => any,
// delete an input or output
deleteBlockIO: (ioType: 'input' | 'output', ioName: string) => any,
// delete the block
deleteBlock: () => any,
// update sync'd channel
updateBlockChannel: (channel: string) => any,
// updates the block info
updateBlockData: (data: ModalCache) => any,
};
export type ModalAction = 'delete' | 'add' | 'change';
export type ModalCache = {
name: string,
synchronized_channel: ?string,
inputs: ?{ name: string, action: ?ModalAction, original: string }[],
outputs: ?{ name: string, action: ?ModalAction, original: string }[],
};
type State = {
// name, synchronized_channel, inputs, outputs
cache: ModalCache,
};
const newCacheFromData = (data: BlockType): ModalCache => {
return {
synchronized_channel: data.synchronized_channel || undefined,
name: data.name,
inputs: data.inputs ? data.inputs.map(i => ({ name: i, action: 'change', original: i })) : undefined,
outputs: data.outputs ? data.outputs.map(i => ({ name: i, action: 'change', original: i })) : undefined,
};
};
const getCurrentObjs = (arr: any) => arr
.filter((o, i, os) => o.action !== 'delete' && !os.slice(i).find(p => p.action === 'delete' && p.name === o.name));
const getCurrentNames = (arr: any) => getCurrentObjs(arr).map(o => o.name);
class ToolchainModal extends React.Component<Props, State> {
constructor(props: Props){
super(props);
}
state = {
cache: newCacheFromData(this.props.data),
}
componentDidUpdate = (prevProps: Props, prevState: State) => {
if(this.props.data && prevProps.data !== this.props.data){
this.setState({ cache: newCacheFromData(this.props.data) });
}
}
shouldComponentUpdate(nextProps: Props){
if(nextProps.active !== this.props.active)
return true;
if(nextProps.data !== undefined && nextProps.data !== this.props.data)
return true;
return false;
addIO = (io: boolean) => {
const arr = io ? 'inputs' : 'outputs';
const newKey = generateNewKey(io ? 'input' : 'output', getCurrentNames(this.state.cache[arr]));
this.setState({
cache: {
...this.state.cache,
[arr]: [
...this.state.cache[arr],
{
name: newKey,
action: 'add',
original: newKey,
}
]
}
});
}
deleteIO = (io: boolean, name: string) => {
const arr = io ? 'inputs' : 'outputs';
this.setState({
cache: {
...this.state.cache,
[arr]: [
...this.state.cache[arr],
{
name: name,
action: 'delete',
original: name,
}
]
}
});
}
changeIO = (io: boolean, oldName: string, newName: string) => {
const arr = io ? 'inputs' : 'outputs';
const obj = this.state.cache[arr].slice().reverse().find(obj => obj.action !== 'delete' && obj.name === oldName);
if(!obj){
console.error(`cant find ${ oldName } in:`);
console.table(this.state.cache[arr]);
return;
//throw new Error();
}
const newObj = {...obj, name: newName};
this.setState({
cache: {
...this.state.cache,
[arr]: this.state.cache[arr].map(o => o === obj ? newObj : o),
}
});
}
changeName = (newName: string) => {
this.setState({
cache: {
...this.state.cache,
name: newName,
}
});
}
changeChannel = (channel: string) => {
//console.log(`changing channel from ${ this.state.cache.synchronized_channel } to ${ channel }`);
this.setState({
cache: {
...this.state.cache,
synchronized_channel: channel,
}
});
}
saveBlock = () => {
this.props.updateBlockData(this.state.cache);
//this.setState({ cache: newCacheFromData(this.props.data) });
}
render = () => {
const data = this.props.data;
const data = this.state.cache;
if(!data)
return null;
......@@ -100,6 +195,11 @@ class ToolchainModal extends React.Component<Props, State> {
);
};
const inputNames = hasInputs ? getCurrentNames(data.inputs) : [];
const outputNames = hasOutputs ? getCurrentNames(data.outputs) : [];
const hasInputDups = (new Set(inputNames)).size !== inputNames.length;
const hasOutputDups = (new Set(outputNames)).size !== outputNames.length;
return (
<Modal
isOpen={this.props.active}
......@@ -109,6 +209,7 @@ class ToolchainModal extends React.Component<Props, State> {
const f = document.querySelector('#tcModalInitFocus');
if(f && f.focus)
f.focus();
this.setState({ cache: newCacheFromData(this.props.data) });
}}
>
<ModalHeader toggle={this.props.toggle}>
......@@ -124,7 +225,7 @@ class ToolchainModal extends React.Component<Props, State> {
value={data.name || ''}
fieldTest={true}
onChange={(e) => {
this.props.updateBlockName(e.target.value);
this.changeName(e.target.value);
}}
validateFunc={(str) => {
if(str === data.name){
......@@ -140,7 +241,7 @@ class ToolchainModal extends React.Component<Props, State> {
}}
/>
</FormGroup>
{ data.hasOwnProperty('synchronized_channel')
{ data.synchronized_channel !== undefined
&&
<FormGroup>
<Label>
......@@ -161,7 +262,7 @@ class ToolchainModal extends React.Component<Props, State> {
<DropdownItem
id={`channel-${ c }`}
key={i}
onClick={e => this.props.updateBlockChannel(c)}
onClick={e => this.changeChannel(c)}
>
{ formatChannel(c) }
</DropdownItem>
......@@ -172,43 +273,49 @@ class ToolchainModal extends React.Component<Props, State> {
</Label>
</FormGroup>
}
<Row>
{ (hasInputDups || hasOutputDups) &&
<Alert color='danger'>
Two inputs or outputs cannot share the same name!
</Alert>
}
</Row>
<Row>
{
hasInputs &&
<Col sm={hasOutputs ? 6 : 12}>
Inputs
{
data.inputs.map((input, i, inputs) =>
<FormGroup
key={i}
>
<CacheInput
value={input}
fieldTest={true}
onChange={(e) => {
this.props.updateBlockIOName('input', input, e.target.value);
}}
validateFunc={(str) => {
return (
str === input ||
!inputs.includes(str) ||
<span>This input name is already in use in this block</span>
);
}}
getCurrentObjs(data.inputs)
.map((obj, i, inputs) => {
const input = obj.name;
return (
<FormGroup
key={i}
>
<DeleteInputBtn
deleteFunc={e => {
this.props.deleteBlockIO('input', input);
<CacheInput
value={input}
fieldTest={true}
onChange={(e) => {
this.changeIO(true, input, e.target.value);
}}
validateFunc={(str) => {
return true;
}}
/>
</CacheInput>
</FormGroup>
)
>
<DeleteInputBtn
deleteFunc={e => {
this.deleteIO(true, input);
}}
/>
</CacheInput>
</FormGroup>
);
})
}
<Button
onClick={e => {
const newKey = generateNewKey('input', data.inputs);
this.props.addBlockIO('input', newKey);
this.addIO(true);
}}
>
Add Input
......@@ -220,38 +327,36 @@ class ToolchainModal extends React.Component<Props, State> {
<Col sm={hasInputs ? 6 : 12}>
Outputs
{
data.outputs.map((output, i, outputs) =>
<FormGroup
key={i}
>
<CacheInput
value={output}
fieldTest
onChange={(e) => {
this.props.updateBlockIOName('output', output, e.target.value);
}}
validateFunc={(str) => {
return (
str === output ||
!outputs.includes(str) ||
<span>This output name is already in use in this block</span>
);
}}
getCurrentObjs(data.outputs)
.map((obj, i, outputs) => {
const output = obj.name;
return (
<FormGroup
key={i}
>
<DeleteInputBtn
deleteFunc={e => {
this.props.deleteBlockIO('output', output);
<CacheInput
value={output}
fieldTest={true}
onChange={(e) => {
this.changeIO(false, output, e.target.value);
}}
/>
</CacheInput>
</FormGroup>
)
validateFunc={(str) => {
return true;
}}
>
<DeleteInputBtn
deleteFunc={e => {
this.deleteIO(false, output);
}}
/>
</CacheInput>
</FormGroup>
);
})
}
<Button
onClick={e => {
const newKey = generateNewKey('output', data.outputs);
this.props.addBlockIO('output', newKey);
this.addIO(false);
}}
>
Add Output
......@@ -262,7 +367,7 @@ class ToolchainModal extends React.Component<Props, State> {
</Form>
</ModalBody>
<ModalFooter>
<Button block color='danger'
<Button color='danger'
onClick={(e) => {
this.props.deleteBlock();
this.props.toggle();
......@@ -270,6 +375,15 @@ class ToolchainModal extends React.Component<Props, State> {
>
Delete Block
</Button>
<Button color='success'
disabled={hasInputDups || hasOutputDups}
onClick={(e) => {
this.saveBlock();
this.props.toggle();
}}
>
Save Changes
</Button>
</ModalFooter>
</Modal>
);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment