Commit 3ac81317 authored by Jaden DIEFENBAUGH's avatar Jaden DIEFENBAUGH
Browse files

[js] split clone functionality into "version" and "copy", closes #90

parent 99e77f91
Pipeline #20296 passed with stages
in 50 minutes and 8 seconds
......@@ -92,7 +92,6 @@ parts
develop-eggs
sphinx
doc/api
src
dist
.nfs*
.gdb_history
......
......@@ -3,7 +3,7 @@ import React from 'react';
import { expect } from 'chai';
import { mount } from 'enzyme';
import sinon from 'sinon';
import { spies } from '../../test';
import { spies } from '@test';
import C from './CacheInput.jsx';
......
......@@ -64,6 +64,7 @@ class EntityHome extends React.Component<Props, State> {
toggle={this.toggleNew}
isOpen={this.state.newOpen}
entity={this.props.entity}
nameOrVersion={true}
/>
</div>
</div>
......
......@@ -32,7 +32,15 @@ import ValidSchemaBadge from './ValidSchemaBadge.jsx';
import NewEntityModal from './NewEntityModal.jsx';
const El = ({ data, entity, copyFunc, deleteFunc }: any) => (
type ElProps = {
data: BeatObject,
entity: BeatEntity,
copyFunc: () => any,
deleteFunc: () => any,
versionFunc?: () => any,
};
const El = ({ data, entity, copyFunc, deleteFunc, versionFunc }: ElProps) => (
<ListGroupItem className='d-flex align-items-center'>
<span>
<Link to={`/${ entity }/${ data.name }`}>
......@@ -43,6 +51,7 @@ const El = ({ data, entity, copyFunc, deleteFunc }: any) => (
</span>
<div className='ml-auto'>
{ versionFunc && <Button outline color='success' onClick={() => versionFunc()}>New Version</Button> }{' '}
<Button outline color='success' onClick={() => copyFunc()}>Clone</Button>{' '}
<Button outline color='danger' onClick={() => deleteFunc()}>Delete</Button>
</div>
......@@ -58,9 +67,10 @@ type Props = {
};
type State = {
copyOpen: boolean,
newModalOpen: boolean,
copyObj: {},
deleteModalData: false | BeatObject,
nameOrVersion: boolean,
};
export class EntityList extends React.Component<Props, State> {
......@@ -69,27 +79,59 @@ export class EntityList extends React.Component<Props, State> {
}
state = {
copyOpen: false,
newModalOpen: false,
copyObj: {},
deleteModalData: false,
nameOrVersion: false,
}
toggleDeleteModal = (e: false | BeatObject = false) => this.setState({ deleteModalData: e || false });
toggleCopy = () => this.setState({ copyOpen: !this.state.copyOpen });
toggleModal = () => this.setState({ newModalOpen: !this.state.newModalOpen });
copyFunc = (obj: BeatObject) => this.setState({ copyObj: obj, copyOpen: true });
copyFunc = (obj: BeatObject) => this.setState({ copyObj: obj, newModalOpen: true, nameOrVersion: true });
versionFunc = (obj: BeatObject) => this.setState({ copyObj: obj, newModalOpen: true, nameOrVersion: false });
render () {
// name to be deleted if any
const deleteName = this.state.deleteModalData ? this.state.deleteModalData.name : '';
// need to find which objects can be versioned (which objects have no newer versions)
let versionableNames;
if(this.props.entity === 'experiment')
versionableNames = [];
else {
const getNameAndVersion = name => {
const segs = name.split('/');
const n = segs.slice(0, -1).join('/');
const version = Number.parseInt(segs[segs.length - 1]);
return [n, version];
};
// sort the names so that all versions of an object are together,
// and the versions are sorted from first to laste
const sortedNames = this.props.data.map(d => d.name).sort();
const nameAndVersions = sortedNames.map(n => getNameAndVersion(n));
// find the complete names of the highest versions of each obj
// since we sorted we just find the last version of each obj
versionableNames = sortedNames.filter((n, i) => {
if(i === sortedNames.length - 1)
return true;
if(nameAndVersions[i + 1][0] === nameAndVersions[i][0])
return false;
return true;
});
}
return (
<React.Fragment>
<ListGroup>
<NewEntityModal
toggle={this.toggleCopy}
isOpen={this.state.copyOpen}
toggle={this.toggleModal}
isOpen={this.state.newModalOpen}
entity={this.props.entity}
copyObj={this.state.copyObj}
nameOrVersion={this.state.nameOrVersion}
/>
{ this.props.data.map((d, i) =>
<El
......@@ -97,6 +139,7 @@ export class EntityList extends React.Component<Props, State> {
data={d}
entity={this.props.entity}
copyFunc={() => this.copyFunc(d)}
versionFunc={versionableNames.includes(d.name) ? () => this.versionFunc(d) : undefined}
deleteFunc={() => this.toggleDeleteModal(d)}
/>)
}
......
......@@ -77,11 +77,12 @@ export default class EntityTemplateGenerationButton extends React.Component<Prop
return (
<React.Fragment>
<Button outline
color='danger'
className='mx-auto'
onClick={this.click}
title={`Some types of objects (databases, algorithms, and libraries) need Python files to be used in experiments. You may generate a template Python file to provide a starting point for development.`}
>
Generate Template File
Generate Python File
</Button>
<br />
<Modal color='info' isOpen={this.state.infoOpen} toggle={e => this.toggleInfo(false)}>
......
......@@ -49,6 +49,9 @@ type Props = {
data: BeatObject[],
toolchains: BeatObject[],
algorithms: BeatObject[],
// if true, the name is being changed but the version is stuck at 1 (for copying or creating new)
// if false, the name is stuck and the version is incremented (for making a new version)
nameOrVersion: boolean,
};
type State = {
......@@ -56,13 +59,17 @@ type State = {
numSegs: number,
};
const getStartNameSegs = (copyObj?: BeatObject, existingNames: string[], entity: BeatEntity) => {
const getStartNameSegs = (copyObj?: BeatObject, existingNames: string[], entity: BeatEntity, nameOrVersion: boolean) => {
if(copyObj){
const newName = generateNewKey(copyObj.name, existingNames);
return newName.split('/');
const newName = copyObj.name || '';
const segs = newName.split('/');
const version = Number.parseInt(segs[segs.length - 1]);
segs[segs.length - 1] = `${ nameOrVersion ? 1 : version + 1 }`;
return segs;
}
return Array.apply(null, Array(nameSegmentsForEntity(entity))).map(u => '');
const len = nameSegmentsForEntity(entity);
return Array.apply(null, Array(len)).map((u, i) => i === len - 1 ? '1' : '');
};
export class NewEntityModal extends React.Component<Props, State> {
......@@ -71,13 +78,13 @@ export class NewEntityModal extends React.Component<Props, State> {
}
state = {
nameSegs: getStartNameSegs(this.props.copyObj, this.props.data.map(d => d.name), this.props.entity),
nameSegs: getStartNameSegs(this.props.copyObj, this.props.data.map(d => d.name), this.props.entity, this.props.nameOrVersion),
numSegs: nameSegmentsForEntity(this.props.entity),
}
componentWillReceiveProps = (newProps: Props) => {
this.setState({
nameSegs: getStartNameSegs(newProps.copyObj, newProps.data.map(d => d.name), newProps.entity),
nameSegs: getStartNameSegs(newProps.copyObj, newProps.data.map(d => d.name), newProps.entity, this.props.nameOrVersion),
numSegs: nameSegmentsForEntity(newProps.entity),
});
}
......@@ -118,6 +125,7 @@ export class NewEntityModal extends React.Component<Props, State> {
render () {
const nameValidity = this.nameIsValid();
const nameOrVersion = this.props.nameOrVersion;
return (
<Modal isOpen={this.props.isOpen} toggle={this.props.toggle} size='lg'>
<ModalHeader toggle={this.props.toggle}>
......@@ -147,6 +155,7 @@ export class NewEntityModal extends React.Component<Props, State> {
onChange={(e) => this.handleInput(e.target.value)}
fieldTest
autoFocus
disabled={!nameOrVersion}
/>
</Col>
<Col sm={6}>
......@@ -159,6 +168,7 @@ export class NewEntityModal extends React.Component<Props, State> {
value={this.state.nameSegs[1]}
validateFunc={(str) => `${ Number.parseInt(str) }` === str}
onChange={(e) => this.handleInput(undefined, e.target.value)}
disabled
/>
</Col>
</React.Fragment>
......@@ -177,6 +187,7 @@ export class NewEntityModal extends React.Component<Props, State> {
onChange={(e) => this.handleInput(e.target.value)}
fieldTest
autoFocus
disabled={!nameOrVersion}
/>
</Col>
<Col sm={4}>
......@@ -189,6 +200,7 @@ export class NewEntityModal extends React.Component<Props, State> {
value={this.state.nameSegs[1]}
onChange={(e) => this.handleInput(undefined, e.target.value)}
fieldTest
disabled={!nameOrVersion}
/>
</Col>
<Col sm={4}>
......@@ -201,6 +213,7 @@ export class NewEntityModal extends React.Component<Props, State> {
value={this.state.nameSegs[2]}
validateFunc={(str) => `${ Number.parseInt(str) }` === str}
onChange={(e) => this.handleInput(undefined, undefined, e.target.value)}
disabled
/>
</Col>
</React.Fragment>
......@@ -219,6 +232,7 @@ export class NewEntityModal extends React.Component<Props, State> {
onChange={(e) => this.handleInput(e.target.value)}
fieldTest
autoFocus
disabled={!nameOrVersion}
/>
</Col>
<Col sm={4}>
......@@ -231,6 +245,7 @@ export class NewEntityModal extends React.Component<Props, State> {
value={this.state.nameSegs[1]}
onChange={(e) => this.handleInput(undefined, e.target.value)}
fieldTest
disabled={!nameOrVersion}
/>
</Col>
<Col sm={4}>
......@@ -243,6 +258,7 @@ export class NewEntityModal extends React.Component<Props, State> {
value={this.state.nameSegs[2]}
validateFunc={(str) => `${ Number.parseInt(str) }` === str}
onChange={(e) => this.handleInput(undefined, undefined, e.target.value)}
disabled
/>
</Col>
</React.Fragment>
......@@ -261,6 +277,7 @@ export class NewEntityModal extends React.Component<Props, State> {
onChange={(e) => this.handleInput(e.target.value)}
fieldTest
autoFocus
disabled={!nameOrVersion}
/>
</Col>
<Col sm={4}>
......@@ -273,6 +290,7 @@ export class NewEntityModal extends React.Component<Props, State> {
value={this.state.nameSegs[1]}
onChange={(e) => this.handleInput(undefined, e.target.value)}
fieldTest
disabled={!nameOrVersion}
/>
</Col>
<Col sm={4}>
......@@ -285,6 +303,7 @@ export class NewEntityModal extends React.Component<Props, State> {
value={this.state.nameSegs[2]}
validateFunc={(str) => `${ Number.parseInt(str) }` === str}
onChange={(e) => this.handleInput(undefined, undefined, e.target.value)}
disabled
/>
</Col>
</React.Fragment>
......@@ -303,6 +322,7 @@ export class NewEntityModal extends React.Component<Props, State> {
onChange={(e) => this.handleInput(e.target.value)}
fieldTest
autoFocus
disabled={!nameOrVersion}
/>
</Col>
<Col sm={4}>
......@@ -315,6 +335,7 @@ export class NewEntityModal extends React.Component<Props, State> {
value={this.state.nameSegs[1]}
onChange={(e) => this.handleInput(undefined, e.target.value)}
fieldTest
disabled={!nameOrVersion}
/>
</Col>
<Col sm={4}>
......@@ -327,6 +348,7 @@ export class NewEntityModal extends React.Component<Props, State> {
value={this.state.nameSegs[2]}
validateFunc={(str) => `${ Number.parseInt(str) }` === str}
onChange={(e) => this.handleInput(undefined, undefined, e.target.value)}
disabled
/>
</Col>
</React.Fragment>
......@@ -393,6 +415,7 @@ export class NewEntityModal extends React.Component<Props, State> {
onChange={(e) => this.handleInput(e.target.value)}
fieldTest
autoFocus
disabled={!nameOrVersion}
/>
</Col>
<Col sm={4}>
......@@ -405,6 +428,7 @@ export class NewEntityModal extends React.Component<Props, State> {
value={this.state.nameSegs[1]}
onChange={(e) => this.handleInput(undefined, e.target.value)}
fieldTest
disabled={!nameOrVersion}
/>
</Col>
<Col sm={4}>
......@@ -417,6 +441,7 @@ export class NewEntityModal extends React.Component<Props, State> {
value={this.state.nameSegs[2]}
validateFunc={(str) => `${ Number.parseInt(str) }` === str}
onChange={(e) => this.handleInput(undefined, undefined, e.target.value)}
disabled
/>
</Col>
</React.Fragment>
......@@ -435,6 +460,7 @@ export class NewEntityModal extends React.Component<Props, State> {
onChange={(e) => this.handleInput(e.target.value)}
fieldTest
autoFocus
disabled={!nameOrVersion}
/>
</Col>
<Col sm={4}>
......@@ -447,6 +473,7 @@ export class NewEntityModal extends React.Component<Props, State> {
value={this.state.nameSegs[1]}
onChange={(e) => this.handleInput(undefined, e.target.value)}
fieldTest
disabled={!nameOrVersion}
/>
</Col>
<Col sm={4}>
......@@ -459,6 +486,7 @@ export class NewEntityModal extends React.Component<Props, State> {
value={this.state.nameSegs[2]}
validateFunc={(str) => `${ Number.parseInt(str) }` === str}
onChange={(e) => this.handleInput(undefined, undefined, e.target.value)}
disabled
/>
</Col>
</React.Fragment>
......
// @flow
import React from 'react';
import { expect } from 'chai';
import { mount } from 'enzyme';
import sinon from 'sinon';
import { NewEntityModal as C } from './NewEntityModal.jsx';
import test_dbs from '@test/test_dbs.json';
import test_dfs from '@test/test_dfs.json';
import test_libs from '@test/test_libs.json';
import test_algs from '@test/test_algs.json';
import test_tcs from '@test/test_tcs.json';
import test_exps from '@test/test_exps.json';
type Props = {
isOpen: boolean,
toggle: () => mixed,
entity: BeatEntity,
copyObj?: BeatObject,
create: (string, BeatObject[], BeatObject[], ?BeatObject) => mixed,
data: BeatObject[],
toolchains: BeatObject[],
algorithms: BeatObject[],
// if true, the name is being changed but the version is stuck at 1 (for copying or creating new)
// if false, the name is stuck and the version is incremented (for making a new version)
nameOrVersion: boolean,
};
describe.only('<NewEntityModal />', () => {
let wrapper;
const entities = [
'database',
'dataformat',
'library',
'algorithm',
'toolchain',
'experiment',
'plotter',
'plotterparameter',
];
const isOpen = true;
const create = sinon.spy();
const toggle = sinon.spy();
afterEach(() => {
if(wrapper && wrapper.unmount)
wrapper.unmount();
});
it(`has 9 required props and is missing 1 optional`, () => {
const entity = 'database';
const nameOrVersion = true;
wrapper = mount(
<C
isOpen={isOpen}
toggle={toggle}
create={create}
entity={entity}
data={test_dbs}
algorithms={test_algs}
toolchains={test_tcs}
nameOrVersion={nameOrVersion}
/>
);
expect(wrapper).to.have.props(
['isOpen', 'toggle', 'create', 'entity', 'data', 'algorithms', 'toolchains', 'nameOrVersion']
).deep.equal(
[isOpen, toggle, create, entity, test_dbs, test_algs, test_tcs, nameOrVersion]
);
expect(wrapper).to.not.have.props(
['copyObj']
);
});
it(`has all 10 props`, () => {
const entity = 'database';
const nameOrVersion = true;
const copyObj = test_dbs[0];
wrapper = mount(
<C
isOpen={isOpen}
toggle={toggle}
create={create}
entity={entity}
data={test_dbs}
algorithms={test_algs}
toolchains={test_tcs}
nameOrVersion={nameOrVersion}
copyObj={copyObj}
/>
);
expect(wrapper).to.have.props(
['isOpen', 'toggle', 'create', 'entity', 'data', 'algorithms', 'toolchains', 'nameOrVersion', 'copyObj']
).deep.equal(
[isOpen, toggle, create, entity, test_dbs, test_algs, test_tcs, nameOrVersion, copyObj]
);
});
describe('for databases', () => {
const entity = 'database';
it(`properly handles creating a new object`, () => {
const nameOrVersion = true;
const copyObj = undefined;
const newNameSegs = ['', `1`];
wrapper = mount(
<C
isOpen={isOpen}
toggle={toggle}
create={create}
entity={entity}
data={test_dbs}
algorithms={test_algs}
toolchains={test_tcs}
nameOrVersion={nameOrVersion}
copyObj={copyObj}
/>
);
expect(wrapper.state('numSegs')).to.equal(2);
expect(wrapper.state('nameSegs')).to.deep.equal(newNameSegs);
const inputs = wrapper.find('input');
expect(inputs).to.have.length(2);
expect(inputs.get(0).props.value).to.equal(newNameSegs[0]);
expect(inputs.get(0).props.disabled).to.equal(false);
expect(inputs.get(1).props.value).to.equal(newNameSegs[1]);
expect(inputs.get(1).props.disabled).to.equal(true);
});
it(`properly handles copying`, () => {
const nameOrVersion = true;
const copyObj = test_dbs[0];
const oldNameSegs = copyObj.name.split('/');
const copyVersion = Number.parseInt(oldNameSegs.slice(-1)[0]);
const newNameSegs = [...oldNameSegs.slice(0, -1), `1`];
wrapper = mount(
<C
isOpen={isOpen}
toggle={toggle}
create={create}
entity={entity}
data={test_dbs}
algorithms={test_algs}
toolchains={test_tcs}
nameOrVersion={nameOrVersion}
copyObj={copyObj}
/>
);
expect(wrapper.state('numSegs')).to.equal(2);
expect(wrapper.state('nameSegs')).to.deep.equal(newNameSegs);
const inputs = wrapper.find('input');
expect(inputs).to.have.length(2);
expect(inputs.get(0).props.value).to.equal(newNameSegs[0]);
expect(inputs.get(0).props.disabled).to.equal(false);
expect(inputs.get(1).props.value).to.equal(newNameSegs[1]);
expect(inputs.get(1).props.disabled).to.equal(true);
});
it(`properly handles creating a new version`, () => {
const nameOrVersion = false;
const copyObj = test_dbs[0];
const oldNameSegs = copyObj.name.split('/');
const copyVersion = Number.parseInt(oldNameSegs.slice(-1)[0]);
const newNameSegs = [...oldNameSegs.slice(0, -1), `${ copyVersion + 1 }`];
wrapper = mount(
<C
isOpen={isOpen}
toggle={toggle}
create={create}
entity={entity}
data={test_dbs}
algorithms={test_algs}
toolchains={test_tcs}
nameOrVersion={nameOrVersion}
copyObj={copyObj}
/>
);
expect(wrapper.state('numSegs')).to.equal(2);
expect(wrapper.state('nameSegs')).to.deep.equal(newNameSegs);
const inputs = wrapper.find('input');
expect(inputs).to.have.length(2);
expect(inputs.get(0).props.value).to.equal(newNameSegs[0]);
expect(inputs.get(0).props.disabled).to.equal(true);
expect(inputs.get(1).props.value).to.equal(newNameSegs[1]);
expect(inputs.get(1).props.disabled).to.equal(true);
});
});
describe('for databases', () => {
const entity = 'database';
it(`properly handles creating a new object`, () => {
const nameOrVersion = true;
const copyObj = undefined;
const newNameSegs = ['', `1`];
wrapper = mount(
<C
isOpen={isOpen}
toggle={toggle}
create={create}
entity={entity}
data={test_dbs}
algorithms={test_algs}
toolchains={test_tcs}
nameOrVersion={nameOrVersion}
copyObj={copyObj}
/>
);
expect(wrapper.state('numSegs')).to.equal(2);
expect(wrapper.state('nameSegs')).to.deep.equal(newNameSegs);
const inputs = wrapper.find('input');
expect(inputs).to.have.length(2);
expect(inputs.get(0).props.value).to.equal(newNameSegs[0]);
expect(inputs.get(0).props.disabled).to.equal(false);
expect(inputs.get(1).props.value).to.equal(newNameSegs[1]);
expect(inputs.get(1).props.disabled).to.equal(true);
});
it(`properly handles copying`, () => {
const nameOrVersion = true;
const copyObj = test_dbs[0];
const oldNameSegs = copyObj.name.split('/');
const copyVersion = Number.parseInt(oldNameSegs.slice(-1)[0]);
const newNameSegs = [...oldNameSegs.slice(0, -1), `1`];
wrapper = mount(
<C