diff --git a/conda/js/src/components/toolchain/GraphicalEditor.jsx b/conda/js/src/components/toolchain/GraphicalEditor.jsx
index cd6b596f83f3a536a2c251d2549223ffa548b55e..a78ced3420f98d62ec961e45ce89239a14fc86e9 100644
--- a/conda/js/src/components/toolchain/GraphicalEditor.jsx
+++ b/conda/js/src/components/toolchain/GraphicalEditor.jsx
@@ -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)
diff --git a/conda/js/src/components/toolchain/ToolchainEditor.jsx b/conda/js/src/components/toolchain/ToolchainEditor.jsx
index 1ee33705f0c4dd7401dfc19eb28b8fe92f65da2d..afb671cc3d9d9f552e83effca4439ed51e1a17c4 100644
--- a/conda/js/src/components/toolchain/ToolchainEditor.jsx
+++ b/conda/js/src/components/toolchain/ToolchainEditor.jsx
@@ -45,6 +45,7 @@ import { connectionToId } from './ToolchainConnection.jsx';
 import ValidSchemaBadge from '../ValidSchemaBadge.jsx';
 import CacheInput from '../CacheInput.jsx';
 import EditModal from './ToolchainModal.jsx';
+import type { ModalCache, ModalAction } from './ToolchainModal.jsx';
 import GraphicalEditor, {
 	blockWidth,
 	gridDistance,
@@ -123,6 +124,59 @@ const generateNewHistory = (state: State, data: any): History => {
 
 const generateConnectionRepresentations = (connections: ConnectionType[]) => Object.assign({}, ...connections.map(c => ({[`${ c.from }/${ c.to }`]: []})));
 
+// helper func for addBlocks()
+// generates all the new data necessary for a new block,
+// including the representation (location & dimensions),
+// the channel color,
+// and an initial block object
+const generateNewBlockData = (blockName: string, set: BlockSet, x: number, y: number, copyBlock?: ?BlockType) => {
+	let newBlock = copyBlock ? { ...copyBlock } : undefined;
+	if(newBlock === undefined){
+		newBlock = {};
+		if(set === 'blocks' || set === 'analyzers'){
+			newBlock.inputs = [
+				'input',
+			];
+			newBlock.synchronized_channel = '';
+		}
+		if(set === 'blocks' || set === 'datasets'){
+			newBlock.outputs = [
+				'output',
+			];
+		}
+	}
+	newBlock.name = blockName;
+
+	const repBlock = {
+		col: x,
+		row: y,
+		height: 3,
+		width: blockWidth / gridDistance,
+	};
+
+	let newChannelColor = set === 'datasets' ? getRandomBrightColor() : null;
+
+	return {
+		block: newBlock,
+		rep: repBlock,
+		channelColor: newChannelColor,
+	};
+};
+
+// finds a block by name from the three block sub-arrays:
+// blocks (normal blocks), datasets (dataset blocks), analyzers (analyzer blocks)
+const findBlock = (contents: any, name: string): BlockType => {
+	const b = contents.blocks.find(b => b.name === name) ||
+		contents.datasets.find(b => b.name === name) ||
+		contents.analyzers.find(b => b.name === name)
+	;
+	if(!b)
+		throw new Error(`invalid block name: ${ name }`);
+
+	return b;
+};
+
+
 export class ToolchainEditor extends React.PureComponent<Props, State> {
 	constructor(props: Props) {
 		super(props);
@@ -143,6 +197,8 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 		},
 	}
 
+	/* HELPERS */
+
 	// helper to set the contents (this.props.data.contents) object
 	setContents = (newContents: any) => {
 		this.setState((prevState, props) => ({
@@ -173,6 +229,21 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 		});
 	}
 
+	// finds a block by name from the three block sub-arrays:
+	// blocks (normal blocks), datasets (dataset blocks), analyzers (analyzer blocks)
+	findBlock = (name: string): BlockType => findBlock(this.props.data.contents, name);
+
+	// gets possible channels the block can sync to
+	getPossibleChannels = (blockName: string) => {
+		const relConns = this.props.data.contents.connections.filter(c => c.to.split('.')[0] === blockName) || [];
+		const possibleChannels = Array.from(new Set(relConns.map(c => c.channel)))
+		.reduce((o, channel) => ({...o, [channel]: this.props.data.contents.representation.channel_colors[channel]}), {});
+
+		return possibleChannels;
+	}
+
+	/* HISTORY */
+
 	// undoes history by saving the current state to the future
 	// and switching to the latest history state
 	undoHistory = () => {
@@ -223,6 +294,8 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 		this.props.updateFunc(newData);
 	};
 
+	/* MODALS */
+
 	// toggles whether the object insert modal is active or not
 	toggleInsertModal = () => {
 		this.setState((prevState) => ({ insertModalActive: !prevState.insertModalActive }));
@@ -233,24 +306,14 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 		this.setState((prevState) => ({ renameGroupModalInfo: val }));
 	}
 
+	/* GROUPS */
+
 	// renames a group from an old name to a new one
 	renameGroup = (oldName: string, newName: string) => {
 		this.setGroups(this.props.data.extraContents.groups.map(g => g.name === oldName ? {...g, name: newName} : g));
 	}
 
-	// finds a block by name from the three block sub-arrays:
-	// blocks (normal blocks), datasets (dataset blocks), analyzers (analyzer blocks)
-	findBlock = (name: string): BlockType => {
-		const b = this.props.data.contents.blocks.find(b => b.name === name) ||
-			this.props.data.contents.datasets.find(b => b.name === name) ||
-			this.props.data.contents.analyzers.find(b => b.name === name)
-		;
-		if(!b)
-			throw new Error(`invalid block name: ${ name }`);
-
-		return b;
-
-	}
+	/* CONNECTIONS */
 
 	// creates a connection between a block's output and a block's input with a specific channel
 	createConnections = (connectionData: ConnectionType[]) => {
@@ -295,6 +358,121 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 		});
 	}
 
+	// deletes a connection
+	// looks at the representation object and the connections array
+	deleteConnection = (cn: ConnectionType) => {
+		const rep = {...this.props.data.contents.representation};
+
+		rep.connections = Object.entries(rep.connections).filter(([name, rep]) => {
+			const [from, to] = name.split('/');
+			if(from === cn.from && to === cn.to)
+				return false;
+			return true;
+		})
+		.reduce((cs, [name, rep]) => ({...cs, [name]: rep}), {})
+		;
+
+		const newConns = this.props.data.contents.connections.filter(c => {
+			if(c.from === cn.from && c.to === cn.to)
+				return false;
+			return true;
+		});
+
+		// if the given block has no conns to it, the syncd channel should become ''
+		const removeInvalidChannels = (b: NormalBlock | AnalyzerBlock): NormalBlock | AnalyzerBlock => {
+			if(newConns.find(c => c.to.split('.')[0] == b.name))
+				return b;
+
+			return {
+				...b,
+				synchronized_channel: '',
+			};
+		};
+
+		const newContents = {
+			connections: newConns,
+			blocks: this.props.data.contents.blocks.map(b => removeInvalidChannels(b)),
+			analyzers: this.props.data.contents.analyzers.map(b => removeInvalidChannels(b)),
+			representation: rep,
+		};
+
+		this.setContents(newContents);
+	}
+
+	/* BLOCKS */
+
+	// adds new blocks at a certain location via an array of arrays of info about the new block(s)
+	// it will also copy connections via looking for connections between copied blocks and copying them to the related new blocks
+	// Parameters:
+	// blockData: each array is info about a new block:
+	// 	- the first string is the new name
+	// 	- the BlockSet is the set to put the block in
+	// 	- the numbers are the x & y coords of the new block
+	// 	- last entry is an optional block object to copy from
+	// connections:
+	// 	if you are inserting blocks from different toolchains, give the new connections as the second arg!
+	// 	these new connections should already be updated to have the new syncd channel names
+	addBlocks = (blockData: [string, BlockSet, number, number, ?BlockType][], connections: ConnectionType[]) => {
+		const newBlocks = blockData.map(d => generateNewBlockData(...d));
+		const sets = blockData.map(b => b[1]);
+
+		const rep = {
+			...this.props.data.contents.representation,
+			blocks: {
+				...this.props.data.contents.representation.blocks,
+				...newBlocks.reduce((newReps, b) => ({...newReps, [b.block.name]: b.rep}), {}),
+			},
+			channel_colors: {
+				...this.props.data.contents.representation.channel_colors,
+				...newBlocks.filter(b => b.channelColor !== null)
+				.reduce((newColors, b) => ({...newColors, [b.block.name]: b.channelColor}), {}),
+			}
+		};
+
+
+		const newContents = {
+			blocks: [...this.props.data.contents.blocks, ...newBlocks.filter((b, i) => sets[i] === 'blocks').map(b => b.block)],
+			datasets: [...this.props.data.contents.datasets, ...newBlocks.filter((b, i) => sets[i] === 'datasets').map(b => b.block)],
+			analyzers: [...this.props.data.contents.analyzers, ...newBlocks.filter((b, i) => sets[i] === 'analyzers').map(b => b.block)],
+			representation: rep,
+			connections: this.props.data.contents.connections,
+		};
+
+		// if the blocks werent copied from an external toolchain,
+		// check for connection info from the copied blocks and create new connections between the new blocks
+		if(!connections){
+			const copyBlocks = blockData.filter(bd => bd.length > 4);
+			const newConnections: ConnectionType[] = [...this.props.data.contents.connections].map(({ from, to, channel }) => {
+				const fromBlock = copyBlocks.find(bd => from.startsWith(`${ bd[4].name }.`));
+				const toBlock = copyBlocks.find(bd => to.startsWith(`${ bd[4].name }.`));
+				if(!fromBlock || !toBlock)
+					return false;
+				const newFromBlockData = newBlocks.find(b => b.block.name === fromBlock[0]);
+				if(!newFromBlockData)
+					return false;
+				else {
+					return {
+						from: from.replace(`${ fromBlock[4].name }.`, `${ fromBlock[0] }.`),
+						to: to.replace(`${ toBlock[4].name }.`, `${ toBlock[0] }.`),
+						channel: newFromBlockData.block.synchronized_channel || newFromBlockData.block.name,
+					};
+				}
+			})
+			.filter(c => c);
+			newContents.connections = [...this.props.data.contents.connections, ...newConnections];
+			const newRepConns = generateConnectionRepresentations(newConnections);
+			newContents.representation.connections = {...rep.connections, ...newRepConns};
+		} else {
+			// add the new connections from the copied toolchain
+			const newConnections = connections;
+			newContents.connections = [...this.props.data.contents.connections, ...newConnections];
+			const newRepConns = generateConnectionRepresentations(newConnections);
+			newContents.representation.connections = {...rep.connections, ...newRepConns};
+		}
+
+		this.setContents(newContents);
+	}
+
 	// updates a blocks location in the graphical editor by providing an x & y offset relative to the current location.
 	updateBlockLocations = (bLocs: {blockName: string, x: number, y: number}[]) => {
 		const rep = this.props.data.contents.representation;
@@ -312,8 +490,147 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 		});
 	}
 
+	// deletes a block from the toolchain,
+	// as well as any connections to/from it
+	// if it was the sole member of a group, deletes the group too
+	deleteBlocks = (names: string[]) => {
+		const rep = copyObj(this.props.data.contents.representation);
+
+		names.forEach(name => {
+			delete rep.blocks[name];
+			delete rep.channel_colors[name];
+		});
+
+		rep.connections = Object.entries(rep.connections).filter(([name, rep]) => {
+			const [from, to] = name.split('/');
+			if(names.find(blockName => to.startsWith(`${ blockName }.`) || from.startsWith(`${ blockName }.`)))
+				return false;
+			return true;
+		})
+		.reduce((cs, [name, r]) => ({...cs, [name]: r}), {})
+		;
+
+		const newContents = {
+			...this.props.data.contents,
+			blocks: this.props.data.contents.blocks.filter(s => !names.includes(s.name)),
+			datasets: this.props.data.contents.datasets.filter(s => !names.includes(s.name)),
+			analyzers: this.props.data.contents.analyzers.filter(s => !names.includes(s.name)),
+			connections: this.props.data.contents.connections.filter(c => {
+				if(names.find(blockName => c.to.startsWith(`${ blockName }.`) || c.from.startsWith(`${ blockName }.`)))
+					return false;
+				return true;
+			}),
+			representation: rep,
+		};
+
+		const newGroups = this.props.data.extraContents.groups
+		.map(({ name, blocks }) => ({ name, blocks: blocks.filter(n => !names.includes(n)) }))
+		.filter(g => g.blocks.length !== 0);
+
+		this.setState({
+			history: generateNewHistory(this.state, this.props.data),
+		});
+
+		this.props.updateFunc({
+			...this.props.data,
+			contents: newContents,
+			extraContents: {
+				...this.props.data.extraContents,
+				groups: newGroups,
+			},
+		});
+	}
+
+	/* BLOCK EDIT MODAL */
+
+	// updates the block data for a given block
+	// by sequentially applying all updates made from the ToolchainModal editor
+	// to the data of the toolchain.
+	updateBlockData = (data: ModalCache) => {
+		const set = this.state.modalBlockInfo.set;
+		const name = this.state.modalBlockInfo.name;
+		if(!set || !name){
+			console.error(`Modal not open for update for ${ data.name }`);
+			return;
+		}
+
+		const oldData = this.props.data;
+		const oldBlock = oldData.contents[set].find(b => b.name === name);
+		if(!oldBlock){
+			console.error(`cannot find block ${ name }`);
+			return;
+		}
+
+		let newData = copyObj(oldData);
+
+		// update name
+		if(name !== data.name) {
+			newData = this.updateBlockNameFunc(newData, set, name, data.name);
+		}
+
+		// update channel
+		if(data.synchronized_channel && oldBlock.synchronized_channel !== data.synchronized_channel){
+			newData = this.updateBlockChannelFunc(newData, data.name, set, data.synchronized_channel);
+		}
+
+		const findNewBlock = () => newData.contents[set].find(b => b.name === data.name);
+		let newBlock = findNewBlock();
+
+		// update inputs
+		if(Array.isArray(data.inputs)){
+			data.inputs
+			.forEach(o => {
+				switch(o.action){
+					case 'change':
+						// when the modal opens,
+						// a new "change" operation is added to the cache
+						// for each input/output already in the block, with a name === original
+						// to avoid extra processing, skip these if they exist
+						if(o.original === o.name)
+							return;
+						newData = this.updateBlockIONameFunc(newData, newBlock.name, set, 'input', o.original, o.name);
+						break;
+					case 'delete':
+						newData = this.deleteBlockIOFunc(newData, newBlock.name, set, 'input', o.original);
+						break;
+					case 'add':
+						newData = this.addBlockIOFunc(newData, newBlock.name, set, 'input', o.name);
+						break;
+				}
+				newBlock = findNewBlock();
+			});
+		}
+
+		// update outputs
+		if(Array.isArray(data.outputs)){
+			data.outputs
+			.forEach(o => {
+				switch(o.action){
+					case 'change':
+						if(o.original === o.name)
+							return;
+						newData = this.updateBlockIONameFunc(newData, newBlock.name, set, 'output', o.original, o.name);
+						break;
+					case 'delete':
+						newData = this.deleteBlockIOFunc(newData, newBlock.name, set, 'output', o.original);
+						break;
+					case 'add':
+						newData = this.addBlockIOFunc(newData, newBlock.name, set, 'output', o.name);
+						break;
+				}
+				newBlock = findNewBlock();
+			});
+		}
+
+		this.setState((prevState, props) => ({
+			history: generateNewHistory(prevState, props.data),
+		}));
+
+		this.props.updateFunc(newData);
+	}
+
 	// changes a block's name given the set it belongs to, the old name, and the new name
-	updateBlockName = (set: BlockSet, oldName: string, newName: string) => {
+	updateBlockNameFunc = (oldData: any, set: BlockSet, oldName: string, newName: string): any => {
 		// func to update from/to strings with old names to new names
 		const updateConn = (from, to): string[] => {
 			const fromSplit = from.split('.');
@@ -324,7 +641,7 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 		};
 
 		// update the block & conn representation with the new names
-		const rep = {...this.props.data.contents.representation};
+		const rep = {...oldData.contents.representation};
 		rep.blocks = {...rep.blocks};
 		rep.blocks[newName] = rep.blocks[oldName];
 		delete rep.blocks[oldName];
@@ -347,10 +664,10 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 
 		// update blocks & connections
 		const newContents = {
-			...this.props.data.contents,
-			[set]: this.props.data.contents[set].map(s => s.name === oldName ? {...s, name: newName} : s),
+			...oldData.contents,
+			[set]: oldData.contents[set].map(s => s.name === oldName ? {...s, name: newName} : s),
 			// update the conns, both the names of the blocks and the channel
-			connections: this.props.data.contents.connections.map(c => {
+			connections: oldData.contents.connections.map(c => {
 				if(!c.from.includes(oldName) && !c.to.includes(oldName) && c.channel !== oldName)
 					return c;
 
@@ -368,16 +685,16 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 
 		// if the changed block is a dataset, update the synchronized_channel on all blocks using its old name
 		if(set === 'datasets'){
-			newContents['blocks'] = this.props.data.contents.blocks
+			newContents['blocks'] = oldData.contents.blocks
 			.map(s => s.synchronized_channel === oldName ? {...s, synchronized_channel: newName} : s)
 			;
 
-			newContents['analyzers'] = this.props.data.contents.analyzers
+			newContents['analyzers'] = oldData.contents.analyzers
 			.map(s => s.synchronized_channel === oldName ? {...s, synchronized_channel: newName} : s)
 			;
 		}
 
-		const newGroups = this.props.data.extraContents.groups
+		const newGroups = oldData.extraContents.groups
 		.map(({ name, blocks }) => ({ name, blocks: blocks.map(n => n === oldName ? newName : n) }));
 		this.setState((prevState, props) => ({
 			history: generateNewHistory(prevState, props.data),
@@ -388,18 +705,18 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 			}
 		}));
 
-		this.props.updateFunc({
-			...this.props.data,
+		return {
+			...oldData,
 			contents: newContents,
 			extraContents: {
-				...this.props.data.extraContents,
+				...oldData.extraContents,
 				groups: newGroups,
 			},
-		});
+		};
 	}
 
 	// updates a block's input or output name
-	updateBlockIOName = (blockName: string, set: BlockSet, ioType: 'input' | 'output', oldName: string, newName: string) => {
+	updateBlockIONameFunc = (oldData: any, blockName: string, set: BlockSet, ioType: 'input' | 'output', oldName: string, newName: string) => {
 		const combinedName = `${ blockName }.${ oldName }`;
 		const updateConn = (from, to): string[] => {
 			const fromSplit = from.split('.');
@@ -410,7 +727,7 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 				toSplit[1] = to === combinedName ? newName : toSplit[1];
 			return [fromSplit.join('.'), toSplit.join('.')];
 		};
-		const rep = {...this.props.data.contents.representation};
+		const rep = {...oldData.contents.representation};
 
 		const newRepConns = Object.entries(rep.connections).map(([name, rep]) => {
 			if(!name.includes(`.${ oldName }`))
@@ -424,8 +741,8 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 		.reduce((cs, [name, map]) => ({...cs, [name]: rep}), {})
 		;
 		const newContents = {
-			...this.props.data.contents,
-			[set]: this.props.data.contents[set].map(s => {
+			...oldData.contents,
+			[set]: oldData.contents[set].map(s => {
 				if(s.name !== blockName)
 					return s;
 				const newBlock = {
@@ -437,7 +754,7 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 					newBlock.outputs = newBlock.outputs.map(str => str === oldName ? newName : str);
 				return newBlock;
 			}),
-			connections: this.props.data.contents.connections.map(c => {
+			connections: oldData.contents.connections.map(c => {
 				if(!c.from.includes(`.${ oldName }`) && !c.to.includes(`.${ oldName }`))
 					return c;
 				const updated = updateConn(c.from, c.to);
@@ -453,124 +770,18 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 			},
 		};
 
-		this.setContents(newContents);
-	}
-
-	// helper func for addBlocks()
-	// generates all the new data necessary for a new block,
-	// including the representation (location & dimensions),
-	// the channel color,
-	// and an initial block object
-	generateNewBlockData = (blockName: string, set: BlockSet, x: number, y: number, copyBlock?: ?BlockType) => {
-		let newBlock = copyBlock ? { ...copyBlock } : undefined;
-		if(newBlock === undefined){
-			newBlock = {};
-			if(set === 'blocks' || set === 'analyzers'){
-				newBlock.inputs = [
-					'input',
-				];
-				newBlock.synchronized_channel = '';
-			}
-			if(set === 'blocks' || set === 'datasets'){
-				newBlock.outputs = [
-					'output',
-				];
-			}
-		}
-		newBlock.name = blockName;
-
-		const repBlock = {
-			col: x,
-			row: y,
-			height: 3,
-			width: blockWidth / gridDistance,
-		};
-
-		let newChannelColor = set === 'datasets' ? getRandomBrightColor() : null;
-
 		return {
-			block: newBlock,
-			rep: repBlock,
-			channelColor: newChannelColor,
+			...oldData,
+			contents: newContents,
 		};
 	}
 
-	// adds new blocks at a certain location via an array of arrays of info about the new block(s)
-	// it will also copy connections via looking for connections between copied blocks and copying them to the related new blocks
-	// Parameters:
-	// blockData: each array is info about a new block:
-	// 	- the first string is the new name
-	// 	- the BlockSet is the set to put the block in
-	// 	- the numbers are the x & y coords of the new block
-	// 	- last entry is an optional block object to copy from
-	// connections:
-	// 	if you are inserting blocks from different toolchains, give the new connections as the second arg!
-	// 	these new connections should already be updated to have the new syncd channel names
-	addBlocks = (blockData: [string, BlockSet, number, number, ?BlockType][], connections: ConnectionType[]) => {
-		const newBlocks = blockData.map(d => this.generateNewBlockData(...d));
-		const sets = blockData.map(b => b[1]);
-
-		const rep = {
-			...this.props.data.contents.representation,
-			blocks: {
-				...this.props.data.contents.representation.blocks,
-				...newBlocks.reduce((newReps, b) => ({...newReps, [b.block.name]: b.rep}), {}),
-			},
-			channel_colors: {
-				...this.props.data.contents.representation.channel_colors,
-				...newBlocks.filter(b => b.channelColor !== null)
-				.reduce((newColors, b) => ({...newColors, [b.block.name]: b.channelColor}), {}),
-			}
-		};
-
-
-		const newContents = {
-			blocks: [...this.props.data.contents.blocks, ...newBlocks.filter((b, i) => sets[i] === 'blocks').map(b => b.block)],
-			datasets: [...this.props.data.contents.datasets, ...newBlocks.filter((b, i) => sets[i] === 'datasets').map(b => b.block)],
-			analyzers: [...this.props.data.contents.analyzers, ...newBlocks.filter((b, i) => sets[i] === 'analyzers').map(b => b.block)],
-			representation: rep,
-			connections: this.props.data.contents.connections,
-		};
-
-		// if the blocks werent copied from an external toolchain,
-		// check for connection info from the copied blocks and create new connections between the new blocks
-		if(!connections){
-			const copyBlocks = blockData.filter(bd => bd.length > 4);
-			const newConnections: ConnectionType[] = [...this.props.data.contents.connections].map(({ from, to, channel }) => {
-				const fromBlock = copyBlocks.find(bd => from.startsWith(`${ bd[4].name }.`));
-				const toBlock = copyBlocks.find(bd => to.startsWith(`${ bd[4].name }.`));
-				if(!fromBlock || !toBlock)
-					return false;
-				const newFromBlockData = newBlocks.find(b => b.block.name === fromBlock[0]);
-				if(!newFromBlockData)
-					return false;
-				else {
-					return {
-						from: from.replace(`${ fromBlock[4].name }.`, `${ fromBlock[0] }.`),
-						to: to.replace(`${ toBlock[4].name }.`, `${ toBlock[0] }.`),
-						channel: newFromBlockData.block.synchronized_channel || newFromBlockData.block.name,
-					};
-				}
-			})
-			.filter(c => c);
-			newContents.connections = [...this.props.data.contents.connections, ...newConnections];
-			const newRepConns = generateConnectionRepresentations(newConnections);
-			newContents.representation.connections = {...rep.connections, ...newRepConns};
-		} else {
-			// add the new connections from the copied toolchain
-			const newConnections = connections;
-			newContents.connections = [...this.props.data.contents.connections, ...newConnections];
-			const newRepConns = generateConnectionRepresentations(newConnections);
-			newContents.representation.connections = {...rep.connections, ...newRepConns};
-		}
-
-		this.setContents(newContents);
-	}
 
 	// add a new input or output to a block
-	addBlockIO = (blockName: string, set: BlockSet, ioType: 'input' | 'output', newName: string) => {
+	addBlockIOFunc = (oldData: any, blockName: string, set: BlockSet, ioType: 'input' | 'output', newName: string) => {
 		const newContents = {
-			[set]: this.props.data.contents[set].map(s => {
+			...oldData.contents,
+			[set]: oldData.contents[set].map(s => {
 				if(s.name !== blockName)
 					return s;
 				const newBlock = {
@@ -584,107 +795,16 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 			}),
 		};
 
-		this.setContents(newContents);
-	}
-
-	// deletes a block from the toolchain,
-	// as well as any connections to/from it
-	// if it was the sole member of a group, deletes the group too
-	deleteBlocks = (names: string[]) => {
-		const rep = copyObj(this.props.data.contents.representation);
-
-		names.forEach(name => {
-			delete rep.blocks[name];
-			delete rep.channel_colors[name];
-		});
-
-		rep.connections = Object.entries(rep.connections).filter(([name, rep]) => {
-			const [from, to] = name.split('/');
-			if(names.find(blockName => to.startsWith(`${ blockName }.`) || from.startsWith(`${ blockName }.`)))
-				return false;
-			return true;
-		})
-		.reduce((cs, [name, r]) => ({...cs, [name]: r}), {})
-		;
-
-		//console.log(Object.keys(rep.connections));
-
-		const newContents = {
-			...this.props.data.contents,
-			blocks: this.props.data.contents.blocks.filter(s => !names.includes(s.name)),
-			datasets: this.props.data.contents.datasets.filter(s => !names.includes(s.name)),
-			analyzers: this.props.data.contents.analyzers.filter(s => !names.includes(s.name)),
-			connections: this.props.data.contents.connections.filter(c => {
-				if(names.find(blockName => c.to.startsWith(`${ blockName }.`) || c.from.startsWith(`${ blockName }.`)))
-					return false;
-				return true;
-			}),
-			representation: rep,
-		};
-
-		const newGroups = this.props.data.extraContents.groups
-		.map(({ name, blocks }) => ({ name, blocks: blocks.filter(n => !names.includes(n)) }))
-		.filter(g => g.blocks.length !== 0);
-
-		this.setState({
-			history: generateNewHistory(this.state, this.props.data),
-		});
-
-		this.props.updateFunc({
-			...this.props.data,
+		return {
+			...oldData,
 			contents: newContents,
-			extraContents: {
-				...this.props.data.extraContents,
-				groups: newGroups,
-			},
-		});
-	}
-
-	// deletes a connection
-	// looks at the representation object and the connections array
-	deleteConnection = (cn: ConnectionType) => {
-		const rep = {...this.props.data.contents.representation};
-
-		rep.connections = Object.entries(rep.connections).filter(([name, rep]) => {
-			const [from, to] = name.split('/');
-			if(from === cn.from && to === cn.to)
-				return false;
-			return true;
-		})
-		.reduce((cs, [name, rep]) => ({...cs, [name]: rep}), {})
-		;
-
-		const newConns = this.props.data.contents.connections.filter(c => {
-			if(c.from === cn.from && c.to === cn.to)
-				return false;
-			return true;
-		});
-
-		// if the given block has no conns to it, the syncd channel should become ''
-		const removeInvalidChannels = (b: NormalBlock | AnalyzerBlock): NormalBlock | AnalyzerBlock => {
-			if(newConns.find(c => c.to.split('.')[0] == b.name))
-				return b;
-
-			return {
-				...b,
-				synchronized_channel: '',
-			};
-		};
-
-		const newContents = {
-			connections: newConns,
-			blocks: this.props.data.contents.blocks.map(b => removeInvalidChannels(b)),
-			analyzers: this.props.data.contents.analyzers.map(b => removeInvalidChannels(b)),
-			representation: rep,
 		};
-
-		this.setContents(newContents);
 	}
 
 	// deletes an input or output from a block,
 	// as well as any connections connected to it
-	deleteBlockIO = (blockName: string, set: BlockSet, ioType: 'input' | 'output', ioName: string) => {
-		const rep = {...this.props.data.contents.representation};
+	deleteBlockIOFunc = (oldData: any, blockName: string, set: BlockSet, ioType: 'input' | 'output', ioName: string) => {
+		const rep = {...oldData.contents.representation};
 
 		const connectionLabel = `${ blockName }.${ ioName }`;
 
@@ -702,8 +822,8 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 		.reduce((cs, [name, rep]) => ({...cs, [name]: rep}), {})
 		;
 		const newContents = {
-			...this.props.data.contents,
-			[set]: this.props.data.contents[set].map(s => {
+			...oldData.contents,
+			[set]: oldData.contents[set].map(s => {
 				if(s.name !== blockName)
 					return s;
 				const newBlock = {
@@ -715,7 +835,7 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 					newBlock.outputs = newBlock.outputs.filter(str => str !== ioName);
 				return newBlock;
 			}),
-			connections: this.props.data.contents.connections.filter(c => {
+			connections: oldData.contents.connections.filter(c => {
 				if(ioType === 'input' && c.to === connectionLabel)
 					return false;
 				if(ioType === 'output' && c.from === connectionLabel)
@@ -724,16 +844,19 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 			}),
 		};
 
-		this.setContents(newContents);
+		return {
+			...oldData,
+			contents: newContents,
+		};
 	}
 
 	// changes the block's sync'd channel
 	// also updates all connections from the block
-	updateBlockChannel = (blockName: string, set: BlockSet, channel: string) => {
-		const queue = [this.props.data.contents[set].find(b => b.name === blockName)];
+	updateBlockChannelFunc = (oldData: any, blockName: string, set: BlockSet, channel: string) => {
+		const queue = [oldData.contents[set].find(b => b.name === blockName)];
 		const oldChannel = `${ queue[0].synchronized_channel }`;
-		const bNames = Object.keys(this.props.data.contents.representation.blocks);
-		const conns = this.props.data.contents.connections;
+		const bNames = Object.keys(oldData.contents.representation.blocks);
+		const conns = oldData.contents.connections;
 		const updatedBlocks: { [string]: BlockType } = {};
 		const updatedConnections: { [string]: ConnectionType } = {};
 		while(queue.length > 0){
@@ -757,7 +880,7 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 						.filter(c => c.to.startsWith(`${ curr }.`) && c.channel === oldChannel)
 						.length === 1
 					)
-						queue.push(this.findBlock(curr));
+						queue.push(findBlock(oldData.contents, curr));
 
 					updatedConnections[connectionToId(c)] = {
 						...c,
@@ -768,24 +891,20 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 		}
 
 		const newContents = {
-			...this.props.data.contents,
-			datasets: this.props.data.contents.datasets.map(b => updatedBlocks[b.name] || b),
-			blocks: this.props.data.contents.blocks.map(b => updatedBlocks[b.name] || b),
-			analyzers: this.props.data.contents.analyzers.map(b => updatedBlocks[b.name] || b),
-			connections: this.props.data.contents.connections.map(c => updatedConnections[connectionToId(c)] || c),
+			...oldData.contents,
+			datasets: oldData.contents.datasets.map(b => updatedBlocks[b.name] || b),
+			blocks: oldData.contents.blocks.map(b => updatedBlocks[b.name] || b),
+			analyzers: oldData.contents.analyzers.map(b => updatedBlocks[b.name] || b),
+			connections: oldData.contents.connections.map(c => updatedConnections[connectionToId(c)] || c),
 		};
 
-		this.setContents(newContents);
+		return {
+			...oldData,
+			contents: newContents,
+		};
 	}
 
-	// gets possible channels the block can sync to
-	getPossibleChannels = (blockName: string) => {
-		const relConns = this.props.data.contents.connections.filter(c => c.to.split('.')[0] === blockName) || [];
-		const possibleChannels = Array.from(new Set(relConns.map(c => c.channel)))
-		.reduce((o, channel) => ({...o, [channel]: this.props.data.contents.representation.channel_colors[channel]}), {});
-
-		return possibleChannels;
-	}
+	/* CONTEXT MENUS */
 
 	svgContextMenuLocation: ?[number, number] = undefined
 
@@ -1199,31 +1318,9 @@ export class ToolchainEditor extends React.PureComponent<Props, State> {
 					});
 				}}
 				blockNames={Object.keys(this.props.data.contents.representation.blocks) || []}
-				updateBlockName={(newName: string) => {
-					this.updateBlockName(set, name, newName);
-				}}
-				updateBlockIOName={(ioType: 'input' | 'output', oldName: string, newName: string) => this.updateBlockIOName(
-					name,
-					set,
-					ioType,
-					oldName,
-					newName
-				)}
-				addBlockIO={(ioType: 'input' | 'output', newName: string) => this.addBlockIO(
-					name,
-					set,
-					ioType,
-					newName
-				)}
-				deleteBlockIO={(ioType: 'input' | 'output', ioName: string) => this.deleteBlockIO(
-					name,
-					set,
-					ioType,
-					ioName
-				)}
 				deleteBlock={() => this.deleteBlocks([name])}
 				possibleChannels={possibleChannels}
-				updateBlockChannel={(channel: string) => this.updateBlockChannel(name, set, channel)}
+				updateBlockData={this.updateBlockData}
 			/>
 		);
 	}
diff --git a/conda/js/src/components/toolchain/ToolchainEditor.spec.jsx b/conda/js/src/components/toolchain/ToolchainEditor.spec.jsx
index 3d9b95737928a655728193c4338b5e46ba860b81..cb4123685f12cf28238af33dbf0588457cab8084 100644
--- a/conda/js/src/components/toolchain/ToolchainEditor.spec.jsx
+++ b/conda/js/src/components/toolchain/ToolchainEditor.spec.jsx
@@ -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 () => {
diff --git a/conda/js/src/components/toolchain/ToolchainModal.jsx b/conda/js/src/components/toolchain/ToolchainModal.jsx
index 0ed9a03bb678843d1f1d586c711e4cbfc3b54495..0f24d068da5e257a481b6ec93e76e1063a492f2a 100644
--- a/conda/js/src/components/toolchain/ToolchainModal.jsx
+++ b/conda/js/src/components/toolchain/ToolchainModal.jsx
@@ -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>
 		);