Prepare styled listing of protocols [SCI-3535] (#1870)

Prepare styled listing of protocols [SCI-3535]
This commit is contained in:
Jure Grabnar 2019-06-28 12:59:50 +02:00 committed by GitHub
commit 692a5aff15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 403 additions and 249 deletions

View file

@ -1,25 +1,214 @@
function applyClickCallbackOnProtocolCards() {
$('.protocol-card').off('click').on('click', function(e) {
var currProtocolCard = $(this);
// Check whether this card is already active and deactivate it
if ($(currProtocolCard).hasClass('active')) {
resetPreviewPanel();
$(currProtocolCard).removeClass('active');
} else {
$('.protocol-card').removeClass('active');
currProtocolCard.addClass('active');
$.ajax({
url: $(currProtocolCard).data('show-url'),
type: 'GET',
dataType: 'json',
data: {
protocol_source: $(currProtocolCard).data('protocol-source'),
protocol_id: $(currProtocolCard).data('show-protocol-id')
},
beforeSend: animateSpinner($('.protocol-preview-panel'), true),
success: function(data) {
$('.empty-preview-panel').hide();
$('.full-preview-panel').show();
$('.btn-holder').html($(currProtocolCard).find('.external-import-btn').clone());
$('.preview-iframe').contents().find('body').html(data.html);
initLoadProtocolModalPreview();
animateSpinner($('.protocol-preview-panel'), false);
},
error: function(_error) {
// TODO: we should probably show some alert bubble
resetPreviewPanel();
animateSpinner($('.protocol-preview-panel'), false);
}
});
}
e.preventDefault();
return false;
});
}
// Resets preview to the default state
function resetPreviewPanel() {
$('.empty-preview-panel').show();
$('.full-preview-panel').hide();
}
// Reset whole view to the default state
function setDefaultViewState() {
resetPreviewPanel();
$('.empty-text').show();
$('.list-wrapper').hide();
}
// Apply AJAX callbacks onto the search box
function applySearchCallback() {
var timeout;
// Submit form on every input in the search box
$('input[name="key"]').off('input').on('input', function() {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(function() {
$('form.protocols-search-bar').submit();
}, 500);
});
// Submit form when clicking on sort buttons
$('input[name="sort_by"]').off('change').on('change', function() {
$('form.protocols-search-bar').submit();
});
// Bind ajax calls on the form
$('form.protocols-search-bar').off('ajax:success').off('ajax:error')
.bind('ajax:success', function(evt, data, status, xhr) {
if (data.html) {
resetPreviewPanel();
$('.empty-text').hide();
$('.list-wrapper').show();
$('.list-wrapper').html(data.html);
applyClickCallbackOnProtocolCards();
initLoadProtocolModalPreview();
} else {
setDefaultViewState();
}
})
.bind("ajax:error", function(evt, xhr, status, error) {
setDefaultViewState();
console.log(xhr.responseText);
});
}
function resetFormErrors(modal) {
// Reset all errors
modal.find('form > .form-group.has-error').removeClass('has-error');
modal.find('form > .form-group>span.help-block').html('');
modal.find('.general-error > span').html('');
}
function showFormErrors(modal, errors) {
resetFormErrors(modal);
// AR valdiation errors
Object.keys(errors.protocol).forEach(function(key) {
var input = modal.find('#protocol_' + key);
var msg;
msg = key.charAt(0).toUpperCase() + key.slice(1) + ': ' + errors.protocol[key].join(', ');
if ((input.length > 0) && (errors.protocol[key].length > 0)) {
input.parent().next('span.help-block').html(msg);
input.parent().parent().addClass('has-error');
} else if (errors.protocol[key].length > 0) {
modal.find('.general-error > span').append(msg + '<br/>');
}
});
}
function renderTable(table) {
$(table).handsontable('render');
// Yet another dirty hack to solve HandsOnTable problems
if (parseInt($(table).css('height'), 10) < parseInt($(table).css('max-height'), 10) - 30) {
$(table).find('.ht_master .wtHolder').css({ height: '100%', width: '100%' });
}
}
// Expand all steps
function expandAllSteps() {
$('.step .panel-collapse').collapse('show');
$(document).find("[data-role='step-hot-table']").each(function() {
renderTable($(this));
});
$(document).find('span.collapse-step-icon').each(function() {
$(this).addClass('fa-caret-square-up');
$(this).removeClass('fa-caret-square-down');
});
}
function handleFormSubmit(modal) {
var form = modal.find('form');
form.on('submit', function(e) {
var url = form.attr('action');
e.preventDefault(); // avoid to execute the actual submit of the form.
e.stopPropagation();
animateSpinner(modal, true);
$.ajax({
url: $(this).data('show-url'),
type: 'GET',
dataType: 'json',
data: {
protocol_source: $(this).data('protocol-source'),
protocol_id: $(this).data('show-protocol-id')
},
beforeSend: animateSpinner($('.protocol-preview-panel'), true),
type: 'POST',
url: url,
data: form.serialize(), // serializes the form's elements.
success: function(data) {
$('.empty-preview-panel').hide();
$('.full-preview-panel').show();
$('.preview-iframe').contents().find('body').html(data.html);
animateSpinner($('.protocol-preview-panel'), false);
animateSpinner(modal, false);
window.location.replace(data.redirect_url);
},
error: function(_error) {
// TODO: we should probably show some alert bubble
$('.empty-preview-panel').show();
$('.full-preview-panel').hide();
animateSpinner($('.protocol-preview-panel'), false);
error: function(data) {
showFormErrors(modal, data.responseJSON.errors);
},
complete: function() {
animateSpinner(modal, false);
}
});
});
}
function initLoadProtocolModalPreview() {
$('.external-import-btn').off('click').on('click', function(e) {
var link = $(this).parents('.protocol-card');
// When clicking on the banner button, we have no protocol-card parent
if (link.length === 0) {
link = $('.protocol-card.active');
}
animateSpinner(null, true);
$.ajax({
url: link.data('url'),
type: 'GET',
data: {
protocol_source: link.data('protocol-source'),
protocol_client_id: link.data('id')
},
dataType: 'json',
success: function(data) {
var modal = $('#protocol-preview-modal');
var modalTitle = modal.find('.modal-title');
var modalBody = modal.find('.modal-body');
var modalFooter = modal.find('.modal-footer');
modalTitle.html(data.title);
modalBody.html(data.html);
modalFooter.html(data.footer);
initHandsOnTable(modalBody);
modal.modal('show');
expandAllSteps();
initHandsOnTable(modalBody);
if (data.validation_errors) {
showFormErrors(modal, data.validation_errors);
}
initFormSubmits();
handleFormSubmit(modal);
},
error: function(error) {
console.log(error.responseJSON.errors);
alert('Server error');
},
complete: function() {
animateSpinner(null, false);
}
});
e.preventDefault();
@ -27,4 +216,14 @@ function applyClickCallbackOnProtocolCards() {
});
}
applyClickCallbackOnProtocolCards();
function initFormSubmits() {
var modal = $('#protocol-preview-modal');
modal.find('button[data-action=import_protocol]').off('click').on('click', function() {
var form = modal.find('form');
var hiddenField = form.find('#protocol_protocol_type');
hiddenField.attr('value', $(this).data('import_type'));
form.submit();
});
}
applySearchCallback();

View file

@ -1,142 +0,0 @@
/* global animateSpinner initHandsOnTable */
/* eslint-disable no-restricted-globals, no-alert */
var ExternalProtocols = (function() {
function resetFormErrors(modal) {
// Reset all errors
modal.find('form > .form-group.has-error').removeClass('has-error');
modal.find('form > .form-group>span.help-block').html('');
modal.find('.general-error > span').html('');
}
function showFormErrors(modal, errors) {
resetFormErrors(modal);
// AR valdiation errors
Object.keys(errors.protocol).forEach(function(key) {
var input = modal.find('#protocol_' + key);
var msg;
msg = key.charAt(0).toUpperCase() + key.slice(1) + ': ' + errors.protocol[key].join(', ');
if ((input.length > 0) && (errors.protocol[key].length > 0)) {
input.parent().next('span.help-block').html(msg);
input.parent().parent().addClass('has-error');
} else if (errors.protocol[key].length > 0) {
modal.find('.general-error > span').append(msg + '<br/>');
}
});
}
function renderTable(table) {
$(table).handsontable('render');
// Yet another dirty hack to solve HandsOnTable problems
if (parseInt($(table).css('height'), 10) < parseInt($(table).css('max-height'), 10) - 30) {
$(table).find('.ht_master .wtHolder').css({ height: '100%', width: '100%' });
}
}
// Expand all steps
function expandAllSteps() {
$('.step .panel-collapse').collapse('show');
$(document).find("[data-role='step-hot-table']").each(function() {
renderTable($(this));
});
$(document).find('span.collapse-step-icon').each(function() {
$(this).addClass('fa-caret-square-up');
$(this).removeClass('fa-caret-square-down');
});
}
function handleFormSubmit(modal) {
var form = modal.find('form');
form.on('submit', function(e) {
var url = form.attr('action');
e.preventDefault(); // avoid to execute the actual submit of the form.
e.stopPropagation();
animateSpinner(modal, true);
$.ajax({
type: 'POST',
url: url,
data: form.serialize(), // serializes the form's elements.
success: function(data) {
animateSpinner(modal, false);
window.location.replace(data.redirect_url);
},
error: function(data) {
showFormErrors(modal, data.responseJSON.errors);
},
complete: function() {
animateSpinner(modal, false);
}
});
});
}
function initLoadProtocolModalPreview() {
var externalProtocols = $('.external-protocol-result');
externalProtocols.on('click', 'a[data-action="external-import"]', function(e) {
var link = $(this);
animateSpinner(null, true);
$.ajax({
url: link.attr('data-url'),
type: 'GET',
data: {
protocol_source: link.attr('data-source'),
protocol_client_id: link.attr('data-id')
},
dataType: 'json',
success: function(data) {
var modal = $('#protocol-preview-modal');
var modalTitle = modal.find('.modal-title');
var modalBody = modal.find('.modal-body');
var modalFooter = modal.find('.modal-footer');
modalTitle.html(data.title);
modalBody.html(data.html);
modalFooter.html(data.footer);
initHandsOnTable(modalBody);
modal.modal('show');
expandAllSteps();
initHandsOnTable(modalBody);
if (data.validation_errors) {
showFormErrors(modal, data.validation_errors);
}
handleFormSubmit(modal);
},
error: function(error) {
console.log(error.responseJSON.errors);
alert('Server error');
},
complete: function() {
animateSpinner(null, false);
}
});
e.preventDefault();
return false;
});
}
function initFormSubmits() {
var modal = $('#protocol-preview-modal');
modal.on('click', 'button[data-action=import_protocol]', function() {
var form = modal.find('form');
var hiddenField = form.find('#protocol_protocol_type');
hiddenField.attr('value', $(this).data('import_type'));
form.submit();
});
}
return {
init: () => {
if ($('.external-protocols-tab').length > 0) {
initLoadProtocolModalPreview();
initFormSubmits();
}
}
};
}());
$(document).on('turbolinks:load', function() {
ExternalProtocols.init();
});

View file

@ -74,6 +74,12 @@
margin-bottom: 15px;
padding-right: 0;
.service-provider {
display: flex;
align-items: center;
width: 50%;
}
.protocolsio-logo {
height: 30px;
width: 30px;
@ -82,25 +88,20 @@
.protocolsio-title {
color: $brand-primary;
font-size: 14px;
margin-left: 3px;
vertical-align: middle;
}
.protocols-search-bar {
padding-right: 0;
width: 70%;
.input-group {
margin-bottom: 0;
.input-group {
margin-bottom: 0;
width: 100%;
.form-control {
border-right: 0;
}
.form-control {
border-right: 0;
}
.input-group-addon {
background: $color-white;
color: $color-silver-chalice;
}
.input-group-addon {
background: $color-white;
color: $color-silver-chalice;
}
}
}
@ -149,6 +150,40 @@
font-size: 13px;
margin-top: 20px;
}
.list-wrapper {
height: 100%;
overflow: auto;
}
.protocol-card {
border-bottom: 1px solid $color-gainsboro;
margin-right: 20px;
padding: 12px 11px 7px 11px;
&.active {
border: 2px solid $brand-primary;
border-radius: 2px;
box-shadow: 0 1px 4px 0 $color-black;
}
&:hover {
background-color: rgba(64,161,215,0.1);
}
}
.protocol-title {
color: $brand-primary;
font-size: 16px;
}
.info-line {
color: $color-dove-gray;
font-size: 13px;
padding-left: 0;
padding-right: 0;
}
}
.protocol-preview-panel {
@ -188,10 +223,35 @@
}
.full-preview-panel {
height: 100%;
}
.preview-banner {
align-items: center;
background-color: $color-white;
border-bottom: 1px solid $color-alto;
color: $color-dove-gray;
display: flex;
font-size: 16px;
height: 40px;
padding-left: 21px;
.txt-holder {
padding-left: 0;
}
.btn-holder {
padding-right: 21px;
}
}
.preview-holder {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
padding-bottom: 40px;
padding-left: 21px;
padding-right: 21px;
width: 100%;
@ -204,4 +264,13 @@
}
}
}
.external-import-btn {
background-color: $brand-primary;
border: none;
color: $color-white;
font-size: 12px;
padding-bottom: 3px;
padding-top: 3px;
}
}

View file

@ -7,7 +7,8 @@ class ExternalProtocolsController < ApplicationController
# GET list_external_protocols
def index
service_call = ProtocolImporters::SearchProtocolsService
.call(protocol_source: 'protocolsio/v3', query_params: index_params)
.call(protocol_source: index_params[:protocol_source],
query_params: index_params)
if service_call.succeed?
render json: {
@ -101,7 +102,7 @@ class ExternalProtocolsController < ApplicationController
end
def index_params
params.permit(:protocol_source, :key, :page_id, :page_size, :order_field, :order_dir)
params.permit(:protocol_source, :key, :page_id, :page_size, :sort_by)
end
def show_params

View file

@ -11,7 +11,7 @@ module ProtocolImporters
def initialize(protocol_source:, query_params: {})
@protocol_source = protocol_source
@query_params = query_params
@query_params = query_params.except(:protocol_source)
@errors = Hash.new { |h, k| h[k] = {} }
end
@ -51,16 +51,6 @@ module ProtocolImporters
@errors[:invalid_params][:page_id] = 'Page needs to be positive'
end
# try if order_field is ok
if @query_params[:order_field] && CONSTANTS[:available_order_fields].exclude?(@query_params[:order_field]&.to_sym)
@errors[:invalid_params][:order_field] = 'Order field is not ok'
end
# try if order dir is ok
if @query_params[:order_field] && CONSTANTS[:available_order_dirs].exclude?(@query_params[:order_dir]&.to_sym)
@errors[:invalid_params][:order_dir] = 'Order dir is not ok'
end
# try if endpints exists
@errors[:invalid_params][:source_endpoint] = 'Wrong source endpoint' unless endpoint_name&.is_a?(String)

View file

@ -44,8 +44,13 @@ module ProtocolImporters
# Default is 1.
def protocol_list(query_params = {})
response = with_handle_network_errors do
sort_mappings = CONSTANTS[:sort_mappings]
query = CONSTANTS.dig(:endpoints, :protocols, :default_query_params)
.merge(query_params)
.merge(query_params.except(:sort_by))
if sort_mappings[query_params[:sort_by]&.to_sym]
query = query.merge(sort_mappings[query_params[:sort_by].to_sym])
end
self.class.get('/protocols', query: query)
end

View file

@ -50,11 +50,17 @@ module ProtocolImporters
original_order = protocol_hash[:steps].map { |m| [m[:previous_id], m[:id]] }.to_h
current_position = 0
while next_step_id
current_position += 1
steps[next_step_id][:position] = current_position
next_step_id = original_order[next_step_id]
end
# Check if step name are valid
steps.each do |step|
step[1][:name] = "Step #{(step[1][:position] + 1)}" if step[1][:name].blank?
end
{ protocol: normalized_data }
rescue StandardError => e
raise ProtocolImporters::ProtocolsIO::V3::NormalizerError.new(e.class.to_s.downcase.to_sym), e.message
@ -69,6 +75,7 @@ module ProtocolImporters
{
id: e[:id],
title: e[:title],
source: Constants::PROTOCOLS_IO_V3_API[:source_id],
created_on: e[:created_on],
authors: e[:authors].map { |a| a[:name] }.join(', '),
nr_of_steps: e[:stats][:number_of_steps],

View file

@ -1,5 +1,4 @@
<% protocols.each do |protocol| %>
<% protocols[:protocols].each do |protocol| %>
<%= render partial: 'protocol_importers/protocol_card',
locals: { protocol: protocol } %>
<hr>
<% end %>

View file

@ -1,3 +1,23 @@
<div class='protocol-card'>
<h2><%= protocol[:name] %></h2>
<div class='protocol-card'
data-id='<%= protocol[:id] %>'
data-protocol-source='<%= protocol[:source] %>'
data-url='<%= team_build_external_protocol_path(current_team.id,) %>'
data-show-url='<%= team_show_external_protocol_path(current_team.id) %>'
data-show-protocol-id='<%= protocol[:uri] %>'>
<p class='protocol-title'><%= protocol[:title] %></p>
<div class='row'>
<div class='row col-md-12 info-line'>
<%= l(Time.at(protocol[:created_on]).to_datetime, format: :full_date) %> • <%= protocol[:authors] %>
</div>
<div class='row'>
<div class='col-md-6 info-line'>
<%= t('protocol_importers.card.views_and_steps', nr_of_views: protocol[:nr_of_views], nr_of_steps: protocol[:nr_of_steps]) %>
</div>
<div class='col-md-6 info-line'>
<button type="button" class='external-import-btn btn btn-primary pull-right'><%= t('protocol_importers.card.import_button_text') %></button>
</div>
</div>
</div>
</div>

View file

@ -1,92 +1,75 @@
<div class='external-protocols-tab'>
<div class='row'>
<div class='col-md-5 protocols-search-bar-panel'>
<%= form_tag team_list_external_protocol_path(@current_team.id),
method: :get,
class: 'protocols-search-bar',
remote: true do %>
<div class='col-md-5 protocols-search-bar-panel'>
<div class='service-provider'>
<%= image_tag 'external_protocols/protocolsio_logo.png',
class: 'protocolsio-logo' %>
<span class='protocolsio-title'><%= t('protocols.index.external_protocols.protocolsio_title') %></span>
<%= hidden_field_tag 'protocol_source', 'protocolsio/v3' %>
</div>
<div>
<%= image_tag 'external_protocols/protocolsio_logo.png',
class: 'protocolsio-logo' %>
<span class='protocolsio-title'><%= t('protocols.index.external_protocols.protocolsio_title') %></span>
<div class='input-group'>
<input class='form-control'
type='text'
name='key'
placeholder="<%= t('protocols.index.external_protocols.search_bar_placeholder') %>" >
</input>
<span class='input-group-addon'><i class='fas fa-search '></i></span>
</div>
</div>
<%= form_tag '#',
method: :get,
class: 'protocols-search-bar' do %>
<div class='input-group'>
<input class='form-control'
type="text"
name="q"
placeholder="<%= t('protocols.index.external_protocols.search_bar_placeholder') %>" >
</input>
<span class='input-group-addon'><i class='fas fa-search '></i></span>
</div>
<% end %>
</div>
<div class='col-md-7'>
<div class='protocol-sort'>
<span><%= t('protocols.index.external_protocols.sort_by.title') %></span>
<div class='btn-group' data-toggle='buttons' >
<div class='btn-group' data-toggle='buttons'>
<label class='btn btn-link active'>
<input type='radio' name='sory_by' id='alphabetically' value='alpha'>
<input type='radio' name='sort_by' id='alphabetically' value='alpha' checked>
<%= t('protocols.index.external_protocols.sort_by.alphabetically') %>
</label>
<label class='btn btn-link'>
<input type='radio' name='sory_by' id='newest' value='newest'>
<input type='radio' name='sort_by' id='newest' value='newest'>
<%= t('protocols.index.external_protocols.sort_by.newest') %>
</label>
<label class='btn btn-link'>
<input type='radio' name='sory_by' id='oldest' value='oldest'>
<input type='radio' name='sort_by' id='oldest' value='oldest'>
<%= t('protocols.index.external_protocols.sort_by.oldest') %>
</label>
</label>
</div>
</div>
</div>
</div>
<% end %>
</div>
<div class='row main-protocol-panel'>
<div class='col-md-5 protocol-list-side-panel'>
<div class='row main-protocol-panel'>
<div class='col-md-5 protocol-list-side-panel'>
<div class='row empty-text'>
<%= t('protocols.index.external_protocols.list_panel.empty_text') %>
</div>
<div class="external-protocol-result">
<a href="#" data-source="protocolsio/v3" data-id="11176" data-action="external-import" data-url="<%= team_build_external_protocol_path(current_team.id,) %>">Protocols IO, 11176</a>
</div>
<div class='protocol-card'
data-protocol-source='protocolsio/v3'
data-show-url='<%= team_show_external_protocol_path(current_team.id) %>'
data-show-protocol-id='Extracting-DNA-from-bananas-esvbee6'>
Banana protocol (click me)
</div>
<div class='protocol-card'
data-protocol-source='protocolsio/v3'
data-show-url='<%= team_show_external_protocol_path(current_team.id) %>'
data-show-protocol-id='cut-run-targeted-in-situ-genome-wide-profiling-wit-mgjc3un'>
Cut run targeted protocol (click me)
</div>
<div class='protocol-card'
data-protocol-source='protocolsio/3'
data-show-url='<%= team_show_external_protocol_path(current_team.id) %>'
data-show-protocol-id='errorr'>
Error protocol (click me, should default to default screen)
</div>
<div class='list-wrapper'></div>
</div>
<div class='col-md-7 protocol-preview-panel'>
<div class='empty-preview-panel'>
<div class='row'>
<div class='text-rows protocol-preview-text'>
<%= t('protocols.index.external_protocols.preview_panel.empty_title') %>
</div>
</div>
<div class='row'>
<div class='text-separator'> <hr> </div>
</div>
<div class='row'>
<div class='text-rows protocol-preview-subtext'>
<%= t('protocols.index.external_protocols.preview_panel.empty_subtext') %>
@ -95,7 +78,18 @@
</div>
<div class='full-preview-panel' style='display: none;'>
<iframe class='preview-iframe'></iframe>
<div class='row preview-banner'>
<div class='col-md-6 txt-holder'>
<span>
<%= t('protocols.index.external_protocols.preview_panel.banner_text') %>
</span>
</div>
<div class='col-md-6 btn-holder'>
</div>
</div>
<div class='preview-holder'>
<iframe class='preview-iframe'></iframe>
</div>
</div>
</div>
</div>

View file

@ -203,8 +203,11 @@ class Constants
base_uri: 'https://www.protocols.io/api/v3/',
default_timeout: 10,
debug_level: :debug,
available_order_fields: %i(title created_on),
available_order_dirs: %i(asc desc),
sort_mappings: {
alpha: { order_field: :name, order_dir: :asc },
newest: { order_field: :date, order_dir: :desc },
oldest: { order_field: :date, order_dir: :asc }
},
endpoints: {
protocols: {
default_query_params: {

View file

@ -1749,6 +1749,7 @@ en:
preview_panel:
empty_title: 'PROTOCOL PREVIEW'
empty_subtext: 'Click on the protocol in the list to preview it here'
banner_text: 'Protocol Preview'
steps:
completed: 'Completed'

View file

@ -2,6 +2,9 @@ en:
protocol_importers:
new:
modal_title: 'Import protocol - %{protocol_name}'
card:
views_and_steps: '%{nr_of_views} views • %{nr_of_steps} steps'
import_button_text: 'Import...'
templates:
amount:
title: 'Amount'
@ -44,8 +47,3 @@ en:
title: 'Temperature'
warning:
title: 'Warning'

View file

@ -3,6 +3,7 @@
{
"id": 22532,
"title": "Producing rooted cassava plantlets for use in pot experiments",
"source": "protocolsio/v3",
"created_on": 1556012701,
"authors": "Matema Imakumbili",
"nr_of_steps": 13,
@ -12,6 +13,7 @@
{
"id": 832,
"title": "Preparation of Virus DNA from Seawater for Metagenomics",
"source": "protocolsio/v3",
"created_on": 1434495014,
"authors": "Matthew Sullivan Lab",
"nr_of_steps": 11,
@ -21,6 +23,7 @@
{
"id": 14506,
"title": "MALE CIRCUMCISION FOR PREVENTION OF HETEROSEXUAL TRANSMISSION OF HIV IN ADULT MALES IN SOWETO, what do indicators and incidence rates show?untitled protocol",
"source": "protocolsio/v3",
"created_on": 1533553359,
"authors": "Hillary Mukudu, Neil Martinson, Benn Sartorius",
"nr_of_steps": 0,
@ -30,6 +33,7 @@
{
"id": 10927,
"title": "physiological and biochemical parameters",
"source": "protocolsio/v3",
"created_on": 1521454618,
"authors": "Amor Slama, Elhem Mallek-Maalej, Hatem Ben Mohamed, Thouraya Rhim, Leila Radhouane, Amor SLAMA",
"nr_of_steps": 0,
@ -39,6 +43,7 @@
{
"id": 822,
"title": "Iron Chloride Precipitation of Viruses from Seawater",
"source": "protocolsio/v3",
"created_on": 1434402745,
"authors": "Seth John, Bonnie Poulos, Christine Schirmer",
"nr_of_steps": 17,
@ -48,6 +53,7 @@
{
"id": 12115,
"title": "Measuring specific leaf area and water content",
"source": "protocolsio/v3",
"created_on": 1526074093,
"authors": "Etienne Laliberté",
"nr_of_steps": 33,
@ -57,6 +63,7 @@
{
"id": 1842,
"title": "Purification of viral assemblages from seawater in CsCl gradients",
"source": "protocolsio/v3",
"created_on": 1445287995,
"authors": "Janice E. Lawrence and Grieg F. Steward",
"nr_of_steps": 23,
@ -66,6 +73,7 @@
{
"id": 12540,
"title": "An improved primer set and PCR amplification protocol with increased specificity and sensitivity targeting the Symbiodinium ITS2 region using the SymVar primer pair",
"source": "protocolsio/v3",
"created_on": 1527418313,
"authors": "Benjamin Hume, Maren Ziegler, Christian Voolstra, Julie Poulain, Xavier Pochon, Sarah Romac, Emilie Boissin, Colomban de Vargas, Serge Planes, Patrick Wincker",
"nr_of_steps": 1,
@ -75,6 +83,7 @@
{
"id": 985,
"title": "NATURAL SEAWATER-BASED PRO99 MEDIUM",
"source": "protocolsio/v3",
"created_on": 1435347035,
"authors": "Chisholm Lab",
"nr_of_steps": 10,
@ -84,6 +93,7 @@
{
"id": 1033,
"title": "SN Maintenance Medium for Synechococcus",
"source": "protocolsio/v3",
"created_on": 1435778857,
"authors": "JB Waterbury \u0026 JM Willey",
"nr_of_steps": 6,