From cb3e7928bdb8aeeaadd2ba826413f9194c0243a9 Mon Sep 17 00:00:00 2001
From: Jaden Diefenbaugh <jaden.diefenbaugh@idiap.ch>
Date: Tue, 31 Jul 2018 11:18:06 -0700
Subject: [PATCH] [js][db] added basic protocol template insertion support, #74

---
 .../components/database/DatabaseEditor.jsx    | 147 ++++++++++++++----
 1 file changed, 121 insertions(+), 26 deletions(-)

diff --git a/conda/js/src/components/database/DatabaseEditor.jsx b/conda/js/src/components/database/DatabaseEditor.jsx
index b7f506e8..eaec5a44 100644
--- a/conda/js/src/components/database/DatabaseEditor.jsx
+++ b/conda/js/src/components/database/DatabaseEditor.jsx
@@ -19,7 +19,7 @@ import {
 	FormFeedback,
 	Alert,
 	InputGroupAddon,
-	UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
+	Dropdown, UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
 	ButtonGroup,
 	ListGroup, ListGroupItem,
 	UncontrolledTooltip,
@@ -32,6 +32,7 @@ import './DatabaseEditor.css';
 import { changeObjFieldName, copyObj } from '@helpers';
 import { getValidDatabaseObj as getValidObj } from '@helpers/beat';
 import type { BeatObject } from '@helpers/beat';
+import fuse from 'fuse.js';
 
 import * as Selectors from '@store/selectors.js';
 
@@ -50,10 +51,23 @@ type Props = {
 	databases: BeatObject[],
 	dataformats: BeatObject[],
 	updateFunc: (BeatObject) => any,
+	protocols: SelectorProtocol[]
 };
 
+export type Protocol = {
+	name: string,
+	template: string,
+	sets: Set[],
+};
+
+// the protocols from the databaseProtocols selector has an additional field
+// that's not in the protocols in database objects
+type SelectorProtocol = Protocol & { database: string };
+
 type State = {
 	activeProtocol: number,
+	insertProtocolOpen: boolean,
+	searchResults: SelectorProtocol[],
 };
 
 export type Set = {
@@ -68,12 +82,17 @@ export type Set = {
 	},
 };
 
-export type Protocol = {
-	name: string,
-	template: string,
-	sets: Set[],
+const FuseOptions = {
+	shouldSort: true,
+	tokenize: true,
+	threshold: 0.6,
+	location: 0,
+	distance: 100,
+	maxPatternLength: 32,
+	minMatchCharLength: 1,
+	keys: ['template'],
 };
-
+let Fuse;
 
 export class DatabaseEditor extends React.Component<Props, State> {
 	constructor(props: Props) {
@@ -82,6 +101,8 @@ export class DatabaseEditor extends React.Component<Props, State> {
 
 	state = {
 		activeProtocol: 0,
+		insertProtocolOpen: false,
+		searchResults: [],
 	}
 
 	setContents = (newContents: any) => {
@@ -94,6 +115,21 @@ export class DatabaseEditor extends React.Component<Props, State> {
 		});
 	}
 
+	componentDidMount = () => {
+		this.updateFuseInstance();
+	}
+
+	componentDidUpdate = (prevProps: Props, prevState: State) => {
+		this.updateFuseInstance(prevProps);
+	}
+
+	updateFuseInstance = (prevProps?: Props) => {
+		// update fuse & reset results if search data changed
+		if(!prevProps || this.props.protocols !== prevProps.protocols){
+			Fuse = new fuse(this.props.protocols, FuseOptions);
+		}
+	}
+
 	activateProtocol = (index: number) => {
 		this.setState({ activeProtocol: index });
 	}
@@ -116,6 +152,18 @@ export class DatabaseEditor extends React.Component<Props, State> {
 		);
 	}
 
+	toggleInsertProtocol = (val: boolean = !this.state.insertProtocolOpen) => {
+		this.setState({ insertProtocolOpen: val });
+	};
+
+	protocolSearch = (str: string) => {
+		const res = Fuse.search(str);
+		this.setState(prevState => ({
+			searchResults: res.slice(0, 5),
+			insertProtocolOpen: res.length > 0,
+		}));
+	};
+
 	renderProtocol = (index: number, protocol: Protocol) => (
 		<Container className='protocol'>
 			<FormGroup row>
@@ -604,28 +652,74 @@ export class DatabaseEditor extends React.Component<Props, State> {
 							>
 								Clone
 							</Button>
-						</ButtonGroup>
-						<Button
-							className='ml-3'
-							color='success'
-							id='newProtocol'
-							onClick={e => {
-								const protocols = [...this.props.data.contents.protocols, {
-									name: '',
-									template: '',
-									sets: [],
-								}];
+							<Button
+								outline
+								color='success'
+								id='newProtocol'
+								onClick={e => {
+									const protocols = [...this.props.data.contents.protocols, {
+										name: '',
+										template: '',
+										sets: [],
+									}];
 
-								this.setContents({
-									...this.props.data.contents,
-									protocols
-								});
+									this.setContents({
+										...this.props.data.contents,
+										protocols
+									});
 
-								this.activateProtocol(protocols.length - 1);
-							}}
+									this.activateProtocol(protocols.length - 1);
+								}}
+							>
+								Create
+							</Button>
+						</ButtonGroup>
+						<Dropdown
+							isOpen={this.state.insertProtocolOpen}
+							toggle={e => this.toggleInsertProtocol(false)}
+							style={{flex: 1}}
+							className='ml-2 mr-2'
 						>
-							+
-						</Button>
+							<DropdownToggle
+								tag='span'
+							>
+								<Input
+									onChange={e => {
+										this.protocolSearch(e.target.value);
+									}}
+									placeholder={`Insert protocol from template...`}
+									title={`You may insert a protocol from a template from any database`}
+								/>
+							</DropdownToggle>
+							<DropdownMenu>
+								{
+									this.state.searchResults.map((r, i) =>
+										<DropdownItem
+											tag={Button}
+											key={i}
+											onClick={e => {
+												const newProtocol = copyObj(r);
+												delete newProtocol['database'];
+
+												const newProtocols = [...this.props.data.contents.protocols, newProtocol];
+												this.setContents({
+													...this.props.data.contents,
+													protocols: newProtocols,
+												});
+
+												this.activateProtocol(newProtocols.length - 1);
+											}}
+										>
+											{ `${ r.template } (${ r.database })` }
+											{' '}
+											<small className='text-muted'>
+												Sets: { r.sets.length }
+											</small>
+										</DropdownItem>
+									)
+								}
+							</DropdownMenu>
+						</Dropdown>
 					</div>
 					{ protocol && this.renderProtocol(
 						protocolIdx,
@@ -805,7 +899,8 @@ const mapStateToProps = (state, ownProps) => {
 	const obj = {
 		databases: dbs,
 		dataformats: Selectors.dataformatGet(state),
-		data: dbs[ownProps.index] || getValidObj()
+		data: dbs[ownProps.index] || getValidObj(),
+		protocols: Selectors.databaseProtocols(state),
 	};
 	return obj;
 };
-- 
GitLab