Skip to content

Commit

Permalink
iGEM source (#327)
Browse files Browse the repository at this point in the history
* refactor SnapGene component

* closes #326

* fix IndexJsonSelector
  • Loading branch information
manulera authored Dec 10, 2024
1 parent 44439fe commit 352a0e3
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 62 deletions.
2 changes: 1 addition & 1 deletion ShareYourCloning_backend
34 changes: 34 additions & 0 deletions cypress/e2e/source_repository_id.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,38 @@ describe('RepositoryId Source', () => {
cy.get('li#source-1 button.MuiButtonBase-root').click();
cy.get('.MuiAlert-message', { timeout: 20000 }).should('be.visible');
});
it('works with iGEM', () => {
clickMultiSelectOption('Select repository', 'iGEM', 'li#source-1');
// Cannot submit if nothing selected
cy.get('li#source-1 button').contains('Submit').should('not.exist');

// When clicking in the input, you can already select
clickMultiSelectOption('Plasmid name', 'BBa_B0012', 'li#source-1');

// Can clear the input
cy.get('li#source-1').contains('Plasmid name').siblings('div').children('input')
.click();
cy.get('li#source-1 button.MuiAutocomplete-clearIndicator').click();
cy.get('body').click(0, 0);

clickMultiSelectOption('Plasmid name', 'BBa_B0012', 'li#source-1');

// Displays the right links
cy.get('li#source-1').contains('Plasmid BBa_B0012 containing part BBa_J428091 in backbone pSB1C5SD from 2024 iGEM Distribution').should('be.visible');
cy.get('li#source-1 a[href="https://raw.githubusercontent.com/manulera/annotated-igem-distribution/master/results/plasmids/1.gb"]').should('exist');
cy.get('li#source-1 a[href="https://parts.igem.org/Part:BBa_J428091"]').should('exist');
cy.get('li#source-1 a[href="https://airtable.com/appgWgf6EPX5gpnNU/shrb0c8oYTgpZDRgH/tblNqHsHbNNQP2HCX"]').should('exist');

cy.get('li#source-1 button').contains('Submit').click();

// Shows the plasmid name
cy.get('li#sequence-2').contains('BBa_J428091');
cy.get('li#sequence-2').contains('2432 bps');

// Links to https://www.snapgene.com/plasmids/insect_cell_vectors/pFastBac1
cy.get('li#source-1 a[href="https://raw.githubusercontent.com/manulera/annotated-igem-distribution/master/results/plasmids/1.gb"]').should('exist');
cy.get('li#source-1 a[href="https://parts.igem.org/Part:BBa_J428091"]').should('exist');
cy.get('li#source-1 a[href="https://airtable.com/appgWgf6EPX5gpnNU/shrb0c8oYTgpZDRgH/tblNqHsHbNNQP2HCX"]').should('exist');
cy.get('li#source-1 a[href="https://github.com/manulera/annotated-igem-distribution/blob/master/results/reports/1.csv"]').should('exist');
});
});
58 changes: 58 additions & 0 deletions src/components/sources/FinishedSource.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,63 @@ function PlannotateAnnotationMessage({ source }) {
);
}

function IGEMMessage({ source }) {
// Split repository_id by the first -, the first part is the part name, the rest is the backbone,
// but there may be more than one -
const indexOfDash = source.repository_id.indexOf('-');
if (indexOfDash === -1) {
return (
<>
iGEM plasmid
{' '}
{source.repository_id}
{' '}
from
{' '}
<a href="https://airtable.com/appgWgf6EPX5gpnNU/shrb0c8oYTgpZDRgH/tblNqHsHbNNQP2HCX" target="_blank" rel="noopener noreferrer">2024 iGEM Distribution</a>
</>
);
}
const partName = source.repository_id.substring(0, indexOfDash);
const backbone = source.repository_id.substring(indexOfDash + 1);
const indexInCollection = source.sequence_file_url.match(/(\d+)\.gb$/)[1];
return (
<>
<div>
iGEM
{' '}
<a href={source.sequence_file_url} target="_blank" rel="noopener noreferrer">
plasmid
</a>
{' '}
containing part
{' '}
<a href={`https://parts.igem.org/Part:${partName}`} target="_blank" rel="noopener noreferrer">{partName}</a>
{' '}
in backbone
{' '}
{backbone}
{' '}
from
{' '}
<a href="https://airtable.com/appgWgf6EPX5gpnNU/shrb0c8oYTgpZDRgH/tblNqHsHbNNQP2HCX" target="_blank" rel="noopener noreferrer">2024 iGEM Distribution</a>
</div>
<div style={{ marginTop: '10px' }}>
Annotated with
{' '}
<a href="https://github.com/mmcguffi/pLannotate" target="_blank" rel="noopener noreferrer">
pLannotate
</a>
,
{' '}
see report
{' '}
<a href={`https://github.com/manulera/annotated-igem-distribution/blob/master/results/reports/${indexInCollection}.csv`} target="_blank" rel="noopener noreferrer">here</a>
</div>
</>
);
}

function FinishedSource({ sourceId }) {
const source = useSelector((state) => state.cloning.sources.find((s) => s.id === sourceId), shallowEqual);
const primers = useSelector((state) => state.cloning.primers, shallowEqual);
Expand Down Expand Up @@ -207,6 +264,7 @@ function FinishedSource({ sourceId }) {
);
break;
case 'AnnotationSource': message = <PlannotateAnnotationMessage source={source} />; break;
case 'IGEMSource': message = <IGEMMessage source={source} />; break;
default: message = '';
}
return (
Expand Down
171 changes: 110 additions & 61 deletions src/components/sources/SourceRepositoryId.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,46 +45,90 @@ const inputLabels = {
euroscarf: 'Euroscarf ID',
};

const checkOption = (option, inputValue) => option.name.toLowerCase().includes(inputValue.toLowerCase());
const formatOption = (option, plasmidSet, plasmidSetName) => ({ name: option.name, path: `${plasmidSet}/${option.subpath}`, plasmidSetName, plasmidSet });
const snapgeneCheckOption = (option, inputValue) => option.name.toLowerCase().includes(inputValue.toLowerCase());
const snapgeneFormatOption = (option, plasmidSet, plasmidSetName) => ({ name: option.name, path: `${plasmidSet}/${option.subpath}`, plasmidSetName, plasmidSet });
const snapgeneGetOptions = (data, inputValue) => Object.entries(data)
.flatMap(([plasmidSet, category]) => category.plasmids
.filter((option) => snapgeneCheckOption(option, inputValue))
.map((option) => snapgeneFormatOption(option, plasmidSet, data[plasmidSet].name)));
function SnapgeneSuccessComponent({ option }) {
return (
<Alert severity="info" sx={{ mb: 1 }}>
Plasmid
{' '}
<a href={`https://www.snapgene.com/plasmids/${option.path}`} target="_blank" rel="noopener noreferrer">{option.name}</a>
{' '}
from set
{' '}
<a href={`https://www.snapgene.com/plasmids/${option.plasmidSet}`} target="_blank" rel="noopener noreferrer">{option.plasmidSetName}</a>
</Alert>
);
}

function SnapGenePlasmidSelector({ setInputValue }) {
const url = 'https://raw.githubusercontent.com/manulera/SnapGene_crawler/master/index.json';
const iGEMGetOptions = (plasmids, inputValue) => plasmids.map((p) => ({
name: `${p['Short Desc / Name']} / ${p['Part Name']} / ${p['Plasmid Backbone']}`,
url: `https://raw.githubusercontent.com/manulera/annotated-igem-distribution/master/results/plasmids/${p['Index ID']}.gb`,
table_name: p['Short Desc / Name'],
part_name: p['Part Name'],
part_url: p['Part URL'],
backbone: p['Plasmid Backbone'],
})).filter((p) => p.name.toLowerCase().includes(inputValue.toLowerCase()));

function iGEMSuccessComponent({ option }) {
return (
<Alert severity="info" sx={{ mb: 1 }}>
Plasmid
{' '}
<a href={option.url} target="_blank" rel="noopener noreferrer">{option.table_name}</a>
{' '}
containing part
{' '}
<a href={option.part_url} target="_blank" rel="noopener noreferrer">{option.part_name}</a>
{' '}
in backbone
{' '}
{option.backbone}
{' '}
from
{' '}
<a href="https://airtable.com/appgWgf6EPX5gpnNU/shrb0c8oYTgpZDRgH/tblNqHsHbNNQP2HCX" target="_blank" rel="noopener noreferrer">2024 iGEM Distribution</a>
</Alert>
);
}

function IndexJsonSelector({ url, setInputValue, getOptions, noOptionsText, inputLabel, SuccessComponent, requiredInput = 3 }) {
const [userInput, setUserInput] = React.useState('');
const [data, setData] = React.useState(null);
const [options, setOptions] = React.useState([]);
// const [filter, setFilter] = React.useState('');

React.useEffect(() => {
const fetchOptions = async () => {
const resp = await axios.get(url);
setData(resp.data);
if (requiredInput === 0) {
setOptions(getOptions(resp.data, ''));
}
};
fetchOptions();
}, []);

const onInputChange = (newInputValue) => {
if (newInputValue === undefined) {
// When clearing the input via x button
setUserInput('');
setOptions([]);
if (requiredInput === 0) {
setOptions(getOptions(data, ''));
} else {
setOptions([]);
}
return;
}
setUserInput(newInputValue);
if (newInputValue.length < 3) {
if (newInputValue.length < requiredInput) {
setOptions([]);
return;
}
// if (filter !== '') {
// setOptions(data[filter].plasmids
// .filter((option) => checkOption(option, newInputValue))
// .map((option) => formatOption(option, filter, data[filter].name)));
// } else {
setOptions(Object.entries(data)
.flatMap(([plasmidSet, category]) => category.plasmids
.filter((option) => checkOption(option, newInputValue))
.map((option) => formatOption(option, plasmidSet, data[plasmidSet].name))));
// }

setOptions(getOptions(data, newInputValue));
};

if (data === null) {
Expand All @@ -95,59 +139,34 @@ function SnapGenePlasmidSelector({ setInputValue }) {

return (
<>
{/* <FormControl fullWidth>
<InputLabel id="plasmid-set-label">Filter by set</InputLabel>
<Select
value={filter}
onChange={(event) => setFilter(event.target.value)}
labelId="plasmid-set-label"
label="Filter by set"
>
<MenuItem key="all" value="">All</MenuItem>
{Object.keys(data).map((plasmidSet) => (
<MenuItem key={plasmidSet} value={plasmidSet}>{data[plasmidSet].name}</MenuItem>
))}
</Select>
</FormControl> */}

<FormControl fullWidth>
<Autocomplete
onChange={(event, value) => { onInputChange(value?.name); value && setInputValue(value.path); }}
onChange={(event, value) => {
onInputChange(value?.name);
if (value) {
setInputValue(value);
} else {
setInputValue('');
}
}}
// Change options only when input changes (not when an option is picked)
onInputChange={(event, newInputValue, reason) => (reason === 'input') && onInputChange(newInputValue)}
id="tags-standard"
options={options}
noOptionsText={(
<div>
Type at least 3 characters to search, see
{' '}
<a href="https://www.snapgene.com/plasmids" target="_blank" rel="noopener noreferrer">SnapGene plasmids</a>
{' '}
for options
</div>
)}
noOptionsText={noOptionsText}
getOptionLabel={(o) => o.name}
isOptionEqualToValue={(o1, o2) => o1.subpath === o2.subpath}
inputValue={userInput}
renderInput={(params) => (
<TextField
{...params}
label="Plasmid name"
label={inputLabel}
/>
)}
/>
</FormControl>
{selectedOption && (
<Alert severity="info" sx={{ mb: 1 }}>
Plasmid
{' '}
<a href={`https://www.snapgene.com/plasmids/${selectedOption.path}`}>{selectedOption.name}</a>
{' '}
from set
{' '}
<a href={`https://www.snapgene.com/plasmids/${selectedOption.plasmidSet}`}>{selectedOption.plasmidSetName}</a>
</Alert>
)}
{selectedOption && <SuccessComponent option={selectedOption} />}
</>
);
}
Expand All @@ -157,7 +176,7 @@ function SnapGenePlasmidSelector({ setInputValue }) {
function SourceRepositoryId({ source, requestStatus, sendPostRequest }) {
const { id: sourceId } = source;
const [inputValue, setInputValue] = React.useState('');
const [selectedRepository, setSelectedRepository] = React.useState(source.repository_name);
const [selectedRepository, setSelectedRepository] = React.useState(source.repository_name || '');
const [error, setError] = React.useState('');

React.useEffect(() => {
Expand All @@ -179,12 +198,19 @@ function SourceRepositoryId({ source, requestStatus, sendPostRequest }) {

const onSubmit = (event) => {
event.preventDefault();
let repositoryId = inputValue;
const extra = { repository_id: inputValue };
if (selectedRepository === 'benchling') {
// Remove /edit from the end of the URL and add .gb
repositoryId = repositoryId.replace(/\/edit$/, '.gb');
extra.repository_id = inputValue.replace(/\/edit$/, '.gb');
}
if (selectedRepository === 'snapgene') {
extra.repository_id = inputValue.path;
}
if (selectedRepository === 'igem') {
extra.repository_id = `${inputValue.part_name}-${inputValue.backbone}`;
extra.sequence_file_url = inputValue.url;
}
const requestData = { id: sourceId, repository_id: repositoryId, repository_name: selectedRepository };
const requestData = { id: sourceId, ...extra, repository_name: selectedRepository };
sendPostRequest({ endpoint: `repository_id/${selectedRepository}`, requestData, source });
};
const helperText = error || (exampleIds[selectedRepository] && `Example: ${exampleIds[selectedRepository]}`);
Expand All @@ -203,11 +229,12 @@ function SourceRepositoryId({ source, requestStatus, sendPostRequest }) {
<MenuItem value="benchling">Benchling</MenuItem>
<MenuItem value="snapgene">SnapGene</MenuItem>
<MenuItem value="euroscarf">Euroscarf</MenuItem>
<MenuItem value="igem">iGEM</MenuItem>
</Select>
</FormControl>
{selectedRepository !== '' && (
<form onSubmit={onSubmit}>
{selectedRepository !== 'snapgene' && (
{selectedRepository !== 'snapgene' && selectedRepository !== 'igem' && (
<>
<FormControl fullWidth>
<TextField
Expand All @@ -216,7 +243,7 @@ function SourceRepositoryId({ source, requestStatus, sendPostRequest }) {
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
helperText={helperText}
error={error}
error={error !== ''}
/>
</FormControl>
{/* Extra info for benchling case */}
Expand All @@ -230,7 +257,29 @@ function SourceRepositoryId({ source, requestStatus, sendPostRequest }) {
)}
</>
)}
{selectedRepository === 'snapgene' && <SnapGenePlasmidSelector setInputValue={setInputValue} />}
{selectedRepository === 'snapgene'
&& (
<IndexJsonSelector
url="https://raw.githubusercontent.com/manulera/SnapGene_crawler/master/index.json"
setInputValue={setInputValue}
getOptions={snapgeneGetOptions}
noOptionsText="Type at least 3 characters to search, see SnapGene plasmids for options"
inputLabel="Plasmid name"
SuccessComponent={SnapgeneSuccessComponent}
requiredInput={3}
/>
)}
{selectedRepository === 'igem' && (
<IndexJsonSelector
url="https://raw.githubusercontent.com/manulera/annotated-igem-distribution/master/results/index.json"
setInputValue={setInputValue}
getOptions={iGEMGetOptions}
noOptionsText=""
inputLabel="Plasmid name"
SuccessComponent={iGEMSuccessComponent}
requiredInput={0}
/>
)}
{inputValue && !error && (<SubmitButtonBackendAPI requestStatus={requestStatus}>Submit</SubmitButtonBackendAPI>)}

</form>
Expand Down
1 change: 1 addition & 0 deletions src/utils/sourceFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const classNameToEndPointMap = {
BenchlingUrlSource: 'repository_id',
SnapGenePlasmidSource: 'repository_id',
EuroscarfSource: 'repository_id',
IGEMSource: 'repository_id',
GenomeCoordinatesSource: 'genome_coordinates',
ManuallyTypedSource: 'manually_typed',
OligoHybridizationSource: 'oligonucleotide_hybridization',
Expand Down

0 comments on commit 352a0e3

Please sign in to comment.