Merge pull request #6864 from aignatov-bio/ai-sci-9802-migrate-protocols-table

Migrate protocols table [SCI-9802]
This commit is contained in:
aignatov-bio 2024-01-03 15:04:59 +01:00 committed by GitHub
commit e93635f061
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1321 additions and 187 deletions

View file

@ -1,7 +1,8 @@
//= require protocols/import_export/import
/* eslint-disable no-use-before-define, no-underscore-dangle, max-len, no-param-reassign */
/* global ProtocolRepositoryHeader PdfPreview DataTableHelpers importProtocolFromFile _ PerfectSb protocolsIO
protocolSteps dropdownSelector filterDropdown I18n animateSpinner initHandsOnTable inlineEditing HelperModule */
/* global ProtocolRepositoryHeader PdfPreview DataTableHelpers importProtocolFromFile
protocolFileImportModal PerfectSb protocolsIO
protocolSteps dropdownSelector filterDropdown I18n animateSpinner initHandsOnTable inlineEditing HelperModule */
// Global variables
var ProtocolsIndex = (function() {
@ -27,10 +28,10 @@ var ProtocolsIndex = (function() {
* Initializes page
*/
function init() {
window.initActionToolbar();
window.actionToolbarComponent.setReloadCallback(reloadTable);
// window.initActionToolbar();
// window.actionToolbarComponent.setReloadCallback(reloadTable);
// make room for pagination
window.actionToolbarComponent.setBottomOffset(68);
// window.actionToolbarComponent.setBottomOffset(68);
updateButtons();
initProtocolsTable();
initKeywordFiltering();
@ -38,6 +39,7 @@ var ProtocolsIndex = (function() {
initLinkedChildrenModal();
initModals();
initVersionsModal();
initLocalFileImport();
}
function reloadTable() {
@ -267,7 +269,6 @@ var ProtocolsIndex = (function() {
let protocolFilters = $($('#protocolFilters').html());
$(protocolFilters).appendTo('.protocols-container .protocol-filters');
initLocalFileImport();
initProtocolsFilters();
initRowSelection();
},
@ -649,8 +650,6 @@ var ProtocolsIndex = (function() {
}
function updateButtons() {
window.actionToolbarComponent.fetchActions({ protocol_ids: rowsSelected });
$('.dataTables_scrollBody').css('margin-bottom', `${rowsSelected.length > 0 ? 46 : 0}px`);
}
function initLocalFileImport() {
@ -673,7 +672,6 @@ var ProtocolsIndex = (function() {
var importUrl = fileInput.attr('data-import-url');
var teamId = fileInput.attr('data-team-id');
var type = fileInput.attr('data-type');
if(ev.target.files[0].name.split('.').pop() === 'eln') {
importProtocolFromFile(
ev.target.files[0],
@ -691,14 +689,14 @@ var ProtocolsIndex = (function() {
if (nrSuccessful) {
HelperModule.flashAlertMsg(I18n.t('protocols.index.import_results.message_ok_html', { count: nrSuccessful }), 'success');
reloadTable();
window.protocolsTable.$refs.table.updateTable();
} else {
HelperModule.flashAlertMsg(I18n.t('protocols.index.import_results.message_failed'), 'danger');
}
}
);
} else {
protocolFileImportModal.init(ev.target.files, reloadTable);
protocolFileImportModal.init(ev.target.files, window.protocolsTable.$refs.table.updateTable());
}
// $(this).val('');
});

View file

@ -215,7 +215,7 @@ var protocolsIO = function() {
animateSpinner(modal, false);
modal.modal('hide');
HelperModule.flashAlertMsg(data.message, 'success');
ProtocolsIndex.reloadTable();
window.protocolsTable.$refs.table.updateTable();
},
error: function(data) {
showFormErrors(modal, data.responseJSON.validation_errors);

View file

@ -2,18 +2,20 @@
module AccessPermissions
class ProtocolsController < ApplicationController
include InputSanitizeHelper
before_action :set_protocol
before_action :check_read_permissions, only: %i(show)
before_action :check_manage_permissions, except: %i(show)
before_action :available_users, only: %i(new create)
def show; end
def show
render json: @protocol.user_assignments.includes(:user_role, :user).order('users.full_name ASC'),
each_serializer: UserAssignmentSerializer
end
def new
@user_assignment = UserAssignment.new(
assignable: @protocol,
assigned_by: current_user,
team: current_team
)
render json: @available_users, each_serializer: UserSerializer
end
def edit; end
@ -21,38 +23,34 @@ module AccessPermissions
def create
ActiveRecord::Base.transaction do
created_count = 0
permitted_create_params[:resource_members].each do |_k, user_assignment_params|
next unless user_assignment_params[:assign] == '1'
if user_assignment_params[:user_id] == 'all'
@protocol.update!(default_public_user_role_id: user_assignment_params[:user_role_id])
log_activity(:protocol_template_access_granted_all_team_members,
if permitted_create_params[:user_id] == 'all'
@protocol.update!(visibility: :visible, default_public_user_role_id: permitted_create_params[:user_role_id])
log_activity(:protocol_template_access_granted_all_team_members,
{ team: @protocol.team.id, role: @protocol.default_public_user_role&.name })
else
user_assignment = UserAssignment.find_or_initialize_by(
assignable: @protocol,
user_id: user_assignment_params[:user_id],
team: current_team
)
else
user_assignment = UserAssignment.find_or_initialize_by(
assignable: @protocol,
user_id: permitted_create_params[:user_id],
team: current_team
)
user_assignment.update!(
user_role_id: user_assignment_params[:user_role_id],
assigned_by: current_user,
assigned: :manually
)
user_assignment.update!(
user_role_id: permitted_create_params[:user_role_id],
assigned_by: current_user,
assigned: :manually
)
created_count += 1
log_activity(:protocol_template_access_granted, { user_target: user_assignment.user.id,
log_activity(:protocol_template_access_granted, { user_target: user_assignment.user.id,
role: user_assignment.user_role.name })
end
created_count += 1
end
@message = if created_count.zero?
t('access_permissions.create.success', count: t('access_permissions.all_team'))
t('access_permissions.create.success', member_name: t('access_permissions.all_team'))
else
t('access_permissions.create.success', count: created_count)
t('access_permissions.create.success', member_name: escape_input(user_assignment.user.name))
end
render :edit
render json: { message: @message }
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error e.message
errors = @protocol.errors.present? ? @protocol.errors&.map(&:message)&.join(',') : e.message
@ -146,8 +144,17 @@ module AccessPermissions
end
def permitted_create_params
params.require(:access_permissions_new_user_form)
.permit(resource_members: %i(assign user_id user_role_id))
params.require(:user_assignment)
.permit(%i(user_id user_role_id))
end
def available_users
# automatically assigned or not assigned to project
@available_users = current_team.users.where(
id: @protocol.user_assignments.automatically_assigned.select(:user_id)
).or(
current_team.users.where.not(id: @protocol.users.select(:id))
).order('users.full_name ASC')
end
def set_protocol

View file

@ -19,6 +19,7 @@ class ProtocolsController < ApplicationController
protocol_status_bar
linked_children
linked_children_datatable
versions_list
permissions
)
before_action :switch_team_with_param, only: %i(index protocolsio_index)
@ -75,7 +76,20 @@ class ProtocolsController < ApplicationController
layout 'fluid'
def index; end
def index
respond_to do |format|
format.json do
protocols = Lists::ProtocolsService.new(Protocol.latest_available_versions(@current_team), params).call
render json: protocols,
each_serializer: Lists::ProtocolSerializer,
user: current_user,
meta: pagination_dict(protocols)
end
format.html do
render 'index'
end
end
end
def datatable
render json: ::ProtocolsDatatable.new(
@ -90,8 +104,17 @@ class ProtocolsController < ApplicationController
return render_403 unless @protocol.in_repository_published_original? || @protocol.initial_draft?
@published_versions = @protocol.published_versions_with_original.order(version_number: :desc)
if @protocol.draft.present?
draft = @protocol.initial_draft? ? @protocol : @protocol.draft
draft_hash = ProtocolDraftSerializer.new(draft, scope: current_user).as_json
end
render json: {
html: render_to_string(partial: 'protocols/index/protocol_versions_modal')
draft: draft_hash,
versions: @published_versions.map do |version|
ProtocolVersionSerializer.new(version, scope: current_user).as_json
end
}
end
@ -100,14 +123,35 @@ class ProtocolsController < ApplicationController
end
def linked_children
render json: {
title: I18n.t('protocols.index.linked_children.title',
protocol: escape_input(@protocol.name)),
html: render_to_string(partial: 'protocols/index/linked_children_modal_body',
locals: { protocol: @protocol })
if params[:version].present?
records = @protocol.published_versions_with_original
.find_by!(version_number: params[:version])
.linked_children
else
records = Protocol.where(protocol_type: Protocol.protocol_types[:linked])
records = records.where(parent_id: @protocol.published_versions)
.or(records.where(parent_id: @protocol.id))
end
records.preload(my_module: { experiment: :project }).distinct
render json: records.map { |record|
{
my_module_name: record.my_module.name,
experiment_name: record.my_module.experiment.name,
project_name: record.my_module.experiment.project.name,
my_module_url: protocols_my_module_path(record.my_module),
experiment_url: my_modules_path(experiment_id: record.my_module.experiment.id),
project_url: experiments_path(project_id: record.my_module.experiment.project.id)
}
}
end
def versions_list
render json: { versions: (@protocol.parent || @protocol).published_versions_with_original
.order(version_number: :desc)
.map(&:version_number) }
end
def linked_children_datatable
render json: ::ProtocolLinkedChildrenDatatable.new(
view_context,
@ -155,8 +199,12 @@ class ProtocolsController < ApplicationController
nil,
protocol: @protocol.id)
flash[:success] = I18n.t('protocols.delete_draft_modal.success')
redirect_to protocols_path
if params[:version_modal]
render json: { message: I18n.t('protocols.delete_draft_modal.success') }
else
flash[:success] = I18n.t('protocols.delete_draft_modal.success')
redirect_to protocols_path
end
rescue ActiveRecord::RecordNotDestroyed => e
Rails.logger.error e.message
render json: { message: e.message }, status: :unprocessable_entity
@ -328,17 +376,15 @@ class ProtocolsController < ApplicationController
draft = @protocol.save_as_draft(current_user)
if draft.invalid?
flash[:error] = draft.errors.full_messages.join(', ')
redirect_to protocols_path
render json: { error: draft.errors.messages.map { |_, value| value }.join(' ') }, status: :unprocessable_entity
else
log_activity(:protocol_template_draft_created, nil, protocol: @protocol.id)
redirect_to protocol_path(draft)
render json: { url: protocol_path(draft) }
end
rescue StandardError => e
Rails.logger.error(e.message)
Rails.logger.error(e.backtrace.join("\n"))
flash[:error] = I18n.t('errors.general')
redirect_to protocols_path
render json: { error: I18n.t('errors.general') }, status: :unprocessable_entity
raise ActiveRecord::Rollback
end
end
@ -823,7 +869,7 @@ class ProtocolsController < ApplicationController
actions:
Toolbars::ProtocolsService.new(
current_user,
protocol_ids: params[:protocol_ids].split(',')
protocol_ids: JSON.parse(params[:items]).map { |i| i['id'] }
).actions
}
end

View file

@ -0,0 +1,12 @@
import { createApp } from 'vue/dist/vue.esm-bundler.js';
import PerfectScrollbar from 'vue3-perfect-scrollbar';
import ProtocolsTable from '../../vue/protocols/table.vue';
import { mountWithTurbolinks } from './helpers/turbolinks.js';
const app = createApp();
app.component('ProtocolsTable', ProtocolsTable);
app.config.globalProperties.i18n = window.I18n;
app.use(PerfectScrollbar);
window.protocolsTable = mountWithTurbolinks(app, '#ProtocolsTable', () => {
delete window.protocolsTable;
});

View file

@ -61,8 +61,8 @@
py-2.5 hover:bg-sn-super-light-grey"
@click="createTag()"
>
<div class="h-8 w-8 rounded relative border-sn-gray border-solid">
<div class="absolute top-1 left-1 rounded-full w-1 h-1 bg-white border-sn-gray border-solid"></div>
<div class="h-8 w-8 rounded relative border-sn-grey border-solid">
<div class="absolute top-1 left-1 rounded-full w-1 h-1 bg-white border-sn-grey border-solid"></div>
</div>
<div>{{ i18n.t('experiments.canvas.modal_manage_tags.create_new') }}</div>
</div>

View file

@ -332,8 +332,8 @@ export default {
},
move(event, rows) {
this.objectsToMove = rows;
},
},
}
}
};
</script>

View file

@ -2,7 +2,8 @@
<div class="protocol-section protocol-information mb-4">
<div id="protocol-details" class="protocol-section-header">
<div class="protocol-details-container">
<a class="protocol-section-caret" role="button" data-toggle="collapse" href="#details-container" aria-expanded="false" aria-controls="details-container">
<a class="protocol-section-caret" role="button" data-toggle="collapse"
href="#details-container" aria-expanded="false" aria-controls="details-container">
<i class="sn-icon sn-icon-right"></i>
<span id="protocolDetailsLabel" class="protocol-section-title">
<h2>
@ -13,12 +14,21 @@
</a>
</div>
<div class="actions-block">
<a class="btn btn-light icon-btn pull-right" :href="protocol.attributes.urls.print_protocol_url" target="_blank">
<a class="btn btn-light icon-btn pull-right"
:href="protocol.attributes.urls.print_protocol_url" target="_blank">
<span class="sn-icon sn-icon-printer" aria-hidden="true"></span>
</a>
<button class="btn btn-light" @click="openVersionsModal">{{ i18n.t("protocols.header.versions") }}</button>
<button v-if="protocol.attributes.urls.publish_url" @click="$emit('publish')" class="btn btn-primary">{{ i18n.t("protocols.header.publish") }}</button>
<button v-if="protocol.attributes.urls.save_as_draft_url" v-bind:disabled="protocol.attributes.has_draft" @click="saveAsdraft" class="btn btn-secondary">{{ i18n.t("protocols.header.save_as_draft") }}</button>
<button class="btn btn-light" @click="openVersionsModal">
{{ i18n.t("protocols.header.versions") }}
</button>
<button v-if="protocol.attributes.urls.publish_url"
@click="$emit('publish')" class="btn btn-primary">
{{ i18n.t("protocols.header.publish") }}</button>
<button v-if="protocol.attributes.urls.save_as_draft_url"
v-bind:disabled="protocol.attributes.has_draft"
@click="saveAsdraft" class="btn btn-secondary">
{{ i18n.t("protocols.header.save_as_draft") }}
</button>
</div>
</div>
<div id="details-container" class="protocol-details collapse in">
@ -87,77 +97,95 @@
</div>
</div>
</div>
<Teleport to="body">
<VersionsModal v-if="VersionsModalObject" :protocol="VersionsModalObject"
@close="VersionsModalObject = null"
@reloadPage="reloadPage"
@redirectToProtocols="redirectToProtocols"/>
</Teleport>
</template>
<script>
/* global HelperModule */
import InlineEdit from '../shared/inline_edit.vue';
import DropdownSelector from '../shared/legacy/dropdown_selector.vue';
import VersionsModal from '../protocols/modals/versions.vue';
import InlineEdit from '../shared/inline_edit.vue'
import DropdownSelector from '../shared/legacy/dropdown_selector.vue'
export default {
name: 'ProtocolMetadata',
components: { InlineEdit, DropdownSelector },
props: {
protocol: {
type: Object,
required: true
},
export default {
name: 'ProtocolMetadata',
components: { InlineEdit, DropdownSelector, VersionsModal },
props: {
protocol: {
type: Object,
required: true
},
computed: {
titleVersion() {
const createdFromVersion = this.protocol.attributes.created_from_version;
},
data() {
return {
VersionsModalObject: null
};
},
computed: {
titleVersion() {
const createdFromVersion = this.protocol.attributes.created_from_version;
if (this.protocol.attributes.published) {
return this.protocol.attributes.version;
}
if (!createdFromVersion) {
return this.i18n.t('protocols.draft');
}
return this.i18n.t('protocols.header.draft_with_from_version', {nr: createdFromVersion});
if (this.protocol.attributes.published) {
return this.protocol.attributes.version;
}
if (!createdFromVersion) {
return this.i18n.t('protocols.draft');
}
return this.i18n.t('protocols.header.draft_with_from_version', { nr: createdFromVersion });
}
},
methods: {
saveAsdraft() {
$.post(this.protocol.attributes.urls.save_as_draft_url);
},
methods: {
saveAsdraft() {
$.post(this.protocol.attributes.urls.save_as_draft_url)
},
updateAuthors(authors) {
$.ajax({
type: 'PATCH',
url: this.protocol.attributes.urls.update_protocol_authors_url,
data: { protocol: { authors: authors } },
success: (result) => {
this.$emit('update', result.data.attributes)
},
error: (data) => {
HelperModule.flashAlertMsg(data.responseJSON ? Object.values(data.responseJSON).join(', ') : I18n.t('errors.general'), 'danger');
updateAuthors(authors) {
$.ajax({
type: 'PATCH',
url: this.protocol.attributes.urls.update_protocol_authors_url,
data: { protocol: { authors } },
success: (result) => {
this.$emit('update', result.data.attributes);
},
error: (data) => {
let message;
if (data.responseJSON) {
message = Object.values(data.responseJSON).join(', ');
} else {
message = this.i18n.t('errors.general');
}
});
},
updateKeywords(keywords) {
$.ajax({
type: 'PATCH',
url: this.protocol.attributes.urls.update_protocol_keywords_url,
data: { keywords: keywords },
success: (result) => {
this.$emit('update', result.data.attributes)
}
});
},
openVersionsModal() {
$.get(this.protocol.attributes.urls.versions_modal_url, (data) => {
let versionsModal = '#protocol-versions-modal'
$('.protocols-show').append($.parseHTML(data.html));
$(versionsModal).modal('show');
inlineEditing.init();
$(versionsModal).find('[data-toggle="tooltip"]').tooltip();
// Remove modal when it gets closed
$(versionsModal).on('hidden.bs.modal', () => {
$(versionsModal).remove();
});
});
}
HelperModule.flashAlertMsg(message);
}
});
},
updateKeywords(keywords) {
$.ajax({
type: 'PATCH',
url: this.protocol.attributes.urls.update_protocol_keywords_url,
data: { keywords },
success: (result) => {
this.$emit('update', result.data.attributes);
}
});
},
openVersionsModal() {
this.VersionsModalObject = {
id: this.protocol.id,
urls: {
versions_modal: this.protocol.attributes.urls.versions_modal
}
};
},
reloadPage() {
window.location.reload();
},
redirectToProtocols() {
window.location.href = this.protocol.attributes.urls.redirect_to_protocols;
}
}
};
</script>

View file

@ -0,0 +1,104 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block">
{{ i18n.t('protocols.index.linked_children.title', { protocol: protocol.name }) }}
</h4>
</div>
<div class="modal-body">
<div class="max-h-96 overflow-y-auto">
<div v-for="myModule in linkedMyModules" class="flex items-center gap-2 px-3 py-2">
<a :href="myModule.project_url"
:title="myModule.project_name"
class="hover:no-underline flex items-center gap-1 shrink-0">
<i class="sn-icon sn-icon-projects"></i>
<span class="truncate max-w-[160px]">{{ myModule.project_name }}</span>
</a>
<span>/</span>
<a :href="myModule.experiment_url"
:title="myModule.experiment_name"
class="hover:no-underline flex items-center gap-1 shrink-0">
<i class="sn-icon sn-icon-experiment"></i>
<span class="truncate max-w-[160px]">{{ myModule.experiment_name }}</span>
</a>
<span>/</span>
<a :href="myModule.my_module_url"
:title="myModule.my_module_name"
class="hover:no-underline flex items-center gap-1 shrink-0">
<i class="sn-icon sn-icon-task"></i>
<span class="truncate max-w-[160px]">{{ myModule.my_module_name }}</span>
</a>
</div>
</div>
</div>
<div class="modal-footer items-center">
{{ i18n.t("protocols.index.linked_children.show_version") }}
<div class="w-48 mr-auto">
<SelectDropdown
:options="versionsList"
:value="selectedVersion"
@change="changeSelectedVersion"
></SelectDropdown>
</div>
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
</div>
</div>
</div>
</div>
</template>
<script>
import SelectDropdown from '../../shared/select_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
export default {
name: 'NewProtocolModal',
props: {
protocol: Object
},
mixins: [modalMixin],
components: {
SelectDropdown
},
data() {
return {
linkedMyModules: [],
versionsList: [],
selectedVersion: 'All'
};
},
mounted() {
this.loadLinkedMyModules();
this.loadVersions();
},
methods: {
loadLinkedMyModules() {
const urlParams = {};
if (this.selectedVersion !== 'All') {
urlParams.version = this.selectedVersion;
}
axios.get(this.protocol.urls.linked_my_modules, { params: urlParams })
.then((response) => {
this.linkedMyModules = response.data;
});
},
loadVersions() {
axios.get(this.protocol.urls.versions_list)
.then((response) => {
this.versionsList = [['All', 'All']].concat(response.data.versions.map((version) => [version, version]));
});
},
changeSelectedVersion(version) {
this.selectedVersion = version;
this.loadLinkedMyModules();
}
}
};
</script>

View file

@ -0,0 +1,90 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block" id="edit-project-modal-label">
{{ i18n.t("protocols.new_protocol_modal.title_new") }}
</h4>
</div>
<div class="modal-body">
<div class="mb-6">
<label class="sci-label">{{ i18n.t("protocols.new_protocol_modal.name_label") }}</label>
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
<input type="text" v-model="name"
class="sci-input-field"
autofocus="true"
:placeholder="i18n.t('protocols.new_protocol_modal.name_placeholder')" />
</div>
</div>
<div class="flex gap-2 text-xs items-center">
<div class="sci-checkbox-container">
<input type="checkbox" class="sci-checkbox" v-model="visible" value="visible"/>
<span class="sci-checkbox-label"></span>
</div>
<span v-html="i18n.t('protocols.new_protocol_modal.access_label')"></span>
</div>
<div class="mt-6" :class="{'hidden': !visible}">
<label class="sci-label">{{ i18n.t("protocols.new_protocol_modal.role_label") }}</label>
<SelectDropdown :optionsUrl="userRolesUrl" :value="defaultRole" @change="changeRole" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button class="btn btn-primary" @click="submit" type="submit">
{{ i18n.t('protocols.new_protocol_modal.create_new') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import SelectDropdown from '../../shared/select_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
export default {
name: 'NewProtocolModal',
props: {
createUrl: String,
userRolesUrl: String
},
mixins: [modalMixin],
components: {
SelectDropdown
},
data() {
return {
name: '',
visible: false,
defaultRole: null,
error: null
};
},
methods: {
submit() {
axios.post(this.createUrl, {
protocol: {
name: this.name,
visibility: (this.visible ? 'visible' : 'hidden'),
default_public_user_role_id: this.defaultRole
}
}).then(() => {
this.error = null;
this.$emit('create');
}).catch((error) => {
this.error = error.response.data.name;
});
},
changeRole(role) {
this.defaultRole = role;
}
}
};
</script>

View file

@ -0,0 +1,181 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block">
{{ i18n.t('protocols.index.versions.title', { protocol: protocol.name }) }}
</h4>
</div>
<div class="modal-body">
<div class="max-h-[400px] overflow-y-auto">
<div v-if="draft">
<div class="flex items-center gap-4">
<a :href="draft.urls.show" class="hover:no-underline cursor-pointer shrink-0">
<span v-if="draft.previous_number"
v-html="i18n.t('protocols.index.versions.draft_html', {
parent_version: draft.previous_number
})"
></span>
<span v-else v-html="i18n.t('protocols.index.versions.first_draft_html')"></span>
</a>
<span class="text-xs" v-if="draft.modified_by">
{{
i18n.t('protocols.index.versions.draft_full_modification_info', {
modified_on: draft.modified_on,
modified_by: draft.modified_by
})
}}
</span>
<span class="text-xs" v-else>
{{
i18n.t('protocols.index.versions.draft_update_modification_info', {
modified_on: draft.modified_on
})
}}
</span>
<div class="flex items-center gap-2 ml-auto">
<button v-if="draft.urls.publish" class="btn btn-light" @click="publishDraft">
{{ i18n.t('protocols.index.versions.publish') }}
</button>
<button v-if="draft.urls.destroy" @click="destroyDraft" class="btn btn-light icon-btn">
<i class="sn-icon sn-icon-delete"></i>
</button>
</div>
</div>
<InlineEdit
:class="{ 'pointer-events-none': !draft.urls.comment }"
class="mb-4"
:value="draft.comment"
:characterLimit="10000"
:placeholder="i18n.t('protocols.index.versions.comment_placeholder')"
:allowBlank="true"
:singleLine="false"
:attributeName="`${i18n.t('Draft')} ${i18n.t('comment')}`"
@update="updateComment"
/>
</div>
<div v-for="version in publishedVersions" :key="version.number">
<div class="flex items-center gap-4">
<a :href="version.urls.show" class="hover:no-underline cursor-pointer shrink-0">
<b>
{{ i18n.t('protocols.index.versions.revision', { version: version.number }) }}
</b>
</a>
<span class="text-xs">
{{
i18n.t('protocols.index.versions.revision_publishing_info', {
published_on: version.published_on,
published_by: version.published_by
})
}}
</span>
<button
v-if="version.urls.save_as_draft"
class="btn btn-light icon-btn ml-auto"
:title="i18n.t('protocols.index.versions.save_as_draft')"
@click="saveAsDraft(version.urls.save_as_draft)"
:disabled="draft"
>
<i class="sn-icon sn-icon-duplicate"></i>
</button>
</div>
<div class="mb-4">
{{ version.comment }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<ConfirmationModal
:title="i18n.t('protocols.delete_draft_modal.title')"
:description="`
<p>${i18n.t('protocols.delete_draft_modal.description_1')}</p>
<p><b>${i18n.t('protocols.delete_draft_modal.description_2')}</b></p>
`"
:confirmClass="'btn btn-danger'"
:confirmText="i18n.t('protocols.delete_draft_modal.confirm')"
ref="destroyModal"
></ConfirmationModal>
</template>
<script>
/* global HelperModule */
import SelectDropdown from '../../shared/select_dropdown.vue';
import InlineEdit from '../../shared/inline_edit.vue';
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
import ConfirmationModal from '../../shared/confirmation_modal.vue';
export default {
name: 'VersionsModal',
props: {
protocol: Object
},
emits: ['refresh', 'close'],
mixins: [modalMixin],
components: {
SelectDropdown,
InlineEdit,
ConfirmationModal
},
data() {
return {
draft: null,
publishedVersions: []
};
},
mounted() {
this.loadModalData();
},
methods: {
loadModalData() {
axios.get(this.protocol.urls.versions_modal).then((response) => {
this.publishedVersions = response.data.versions;
this.draft = response.data.draft;
});
},
updateComment(comment) {
axios.put(this.draft.urls.comment, { protocol: { version_comment: comment } }).then(() => {
this.draft.comment = comment;
});
},
async destroyDraft() {
const ok = await this.$refs.destroyModal.show();
if (ok) {
axios.post(this.draft.urls.destroy, {
version_modal: true
}).then((response) => {
document.body.style.overflow = 'hidden';
this.$emit('refresh');
this.$emit('redirectToProtocols');
this.loadModalData();
HelperModule.flashAlertMsg(response.data.message, 'success');
});
} else {
document.body.style.overflow = 'hidden';
}
},
saveAsDraft(url) {
axios.post(url).then((response) => {
window.location.href = response.data.url;
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
},
publishDraft() {
axios.post(this.draft.urls.publish).then(() => {
this.loadModalData();
this.$emit('reloadPage');
});
}
}
};
</script>

View file

@ -0,0 +1,23 @@
<template>
<div class="flex items-center gap-2 truncate h-full">
<div v-for="keyword in params.data.keywords" :key="keyword.id"
@click="params.dtComponent.setSearchValue(keyword)"
class="px-2 py-1 rounded-sm bg-sn-super-light-grey gap-1 leading-5 cursor-pointer hover:bg-sn-light-grey">
{{ keyword }}
</div>
<div v-if="params.data.keywords.length === 0">
</div>
</div>
</template>
<script>
export default {
name: 'KeywordsRenderer',
props: {
params: {
required: true
}
}
};
</script>

View file

@ -0,0 +1,22 @@
<template>
<div v-if="params.data.linked_tasks > 0"
@click.stop="params.dtComponent.$emit('linked_my_modules', {}, [params.data])"
class="cursor-pointer text-sn-blue"
>
{{ params.data.linked_tasks }}
</div>
<div v-else>
0
</div>
</template>
<script>
export default {
name: 'LinkedMyModulesRenderer',
props: {
params: {
required: true
}
}
};
</script>

View file

@ -0,0 +1,33 @@
<template>
<div v-if="params.data.urls.show" class="flex items-center gap-2 text-sn-blue">
<span class="cursor-pointer"
@click.stop="params.dtComponent.$emit('versions', {}, [params.data])"
v-if="params.data.nr_of_versions > 0">
{{ params.data.nr_of_versions }}
</span>
<span v-if="params.data.nr_of_versions > 0 && params.data.has_draft">/</span>
<a v-if="params.data.has_draft" :href="params.data.urls.show_draft" class="hover:no-underline">
{{ i18n.t("protocols.index.table.draft") }}
</a>
</div>
<div v-else class="flex items-center gap-2 text-sn-grey">
<span v-if="params.data.nr_of_versions > 0">
{{ params.data.nr_of_versions }}
</span>
<span v-if="params.data.nr_of_versions > 0 && params.data.has_draft">/</span>
<span v-if="params.data.has_draft" class="hover:no-underline ">
{{ i18n.t("protocols.index.table.draft") }}
</span>
</div>
</template>
<script>
export default {
name: 'VersionsRenderer',
props: {
params: {
required: true
}
}
};
</script>

View file

@ -0,0 +1,298 @@
<template>
<div class="h-full">
<DataTable :columnDefs="columnDefs"
:tableId="'protocolTemplates'"
:dataUrl="dataSource"
:reloadingTable="reloadingTable"
:currentViewMode="currentViewMode"
:toolbarActions="toolbarActions"
:activePageUrl="activePageUrl"
:archivedPageUrl="archivedPageUrl"
:actionsUrl="actionsUrl"
@create="create"
@archive="archive"
@restore="restore"
@export="exportProtocol"
@duplicate="duplicate"
@versions="versions"
@tableReloaded="reloadingTable = false"
@import_file="importFile"
@import_protocols_io="importProtocolsIo"
@import_docx="importDocx"
@access="access"
@linked_my_modules="linkedMyModules"
/>
</div>
<NewProtocolModal v-if="newProtocol" :createUrl="createUrl"
:userRolesUrl="userRolesUrl"
@close="newProtocol = false" @create="updateTable" />
<AccessModal v-if="accessModalParams" :params="accessModalParams"
@close="accessModalParams = null" @refresh="this.reloadingTable = true" />
<LinkedMyModulesModal v-if="linkedMyModulesModalObject" :protocol="linkedMyModulesModalObject"
@close="linkedMyModulesModalObject = null"/>
<VersionsModal v-if="VersionsModalObject" :protocol="VersionsModalObject"
@close="VersionsModalObject = null" @refresh="this.reloadingTable = true"/>
</template>
<script>
/* global HelperModule */
import axios from '../../packs/custom_axios.js';
import DataTable from '../shared/datatable/table.vue';
import UsersRenderer from '../projects/renderers/users.vue';
import NewProtocolModal from './modals/new.vue';
import AccessModal from '../shared/access_modal/modal.vue';
import KeywordsRenderer from './renderers/keywords.vue';
import LinkedMyModulesRenderer from './renderers/linked_my_modules.vue';
import LinkedMyModulesModal from './modals/linked_my_modules.vue';
import VersionsRenderer from './renderers/versions.vue';
import VersionsModal from './modals/versions.vue';
export default {
name: 'LabelTemplatesTable',
components: {
DataTable,
UsersRenderer,
NewProtocolModal,
AccessModal,
KeywordsRenderer,
LinkedMyModulesRenderer,
LinkedMyModulesModal,
VersionsRenderer,
VersionsModal
},
props: {
dataSource: {
type: String,
required: true
},
actionsUrl: {
type: String,
required: true
},
createUrl: {
type: String
},
currentViewMode: {
type: String,
required: true
},
docxParserEnabled: {
type: Boolean,
required: true
},
activePageUrl: {
type: String,
required: true
},
archivedPageUrl: {
type: String,
required: true
},
userRolesUrl: {
type: String,
required: true
}
},
data() {
return {
reloadingTable: false,
newProtocol: false,
accessModalParams: null,
linkedMyModulesModalObject: null,
VersionsModalObject: null
};
},
computed: {
columnDefs() {
const columns = [{
field: 'name',
headerName: this.i18n.t('protocols.index.thead.name'),
sortable: true,
notSelectable: true,
cellRenderer: this.nameRenderer
},
{
field: 'code',
headerName: this.i18n.t('protocols.index.thead.id'),
sortable: true
},
{
field: 'versions',
headerName: this.i18n.t('protocols.index.thead.versions'),
sortable: true,
cellRenderer: 'VersionsRenderer',
notSelectable: true
},
{
field: 'keywords',
headerName: this.i18n.t('protocols.index.thead.keywords'),
sortable: true,
cellRenderer: 'KeywordsRenderer',
notSelectable: true
},
{
field: 'linked_tasks',
headerName: this.i18n.t('protocols.index.thead.nr_of_linked_children'),
sortable: true,
cellRenderer: 'LinkedMyModulesRenderer'
},
{
field: 'assigned_users',
headerName: this.i18n.t('protocols.index.thead.access'),
sortable: true,
cellRenderer: 'UsersRenderer',
minWidth: 210,
notSelectable: true
},
{
field: 'published_by',
headerName: this.i18n.t('protocols.index.thead.published_by'),
sortable: true
},
{
field: 'published_on',
headerName: this.i18n.t('protocols.index.thead.published_on'),
sortable: true
},
{
field: 'updated_at',
headerName: this.i18n.t('protocols.index.thead.updated_at'),
sortable: true
}];
if (this.currentViewMode === 'archived') {
columns.push({
field: 'archived_by',
headerName: this.i18n.t('protocols.index.thead.archived_by'),
sortable: true
});
columns.push({
field: 'archived_on',
headerName: this.i18n.t('protocols.index.thead.archived_on'),
sortable: true
});
}
return columns;
},
toolbarActions() {
const left = [];
if (this.createUrl) {
left.push({
name: 'create',
icon: 'sn-icon sn-icon-new-task',
label: this.i18n.t('protocols.index.create_new'),
type: 'emit',
path: this.createUrl,
buttonStyle: 'btn btn-primary'
});
const importMenu = {
name: 'import',
icon: 'sn-icon sn-icon-import',
label: this.i18n.t('protocols.index.import'),
type: 'menu',
path: this.createUrl,
buttonStyle: 'btn btn-light',
menuItems: [
{
emit: 'import_file',
text: this.i18n.t('protocols.index.import_eln')
}
]
};
if (this.docxParserEnabled) {
importMenu.menuItems.push({
emit: 'import_docx',
text: this.i18n.t('protocols.index.import_docx')
});
}
importMenu.menuItems.push({
emit: 'import_protocols_io',
text: this.i18n.t('protocols.index.import_protocols_io')
});
left.push(importMenu);
}
return {
left,
right: []
};
}
},
methods: {
updateTable() {
this.newProtocol = false;
this.reloadingTable = true;
},
create() {
this.newProtocol = true;
},
duplicate(event, rows) {
axios.post(event.path, { protocol_ids: rows.map((row) => row.id) }).then((response) => {
this.updateTable();
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
},
versions(_event, rows) {
[this.VersionsModalObject] = rows;
},
archive(event, rows) {
axios.post(event.path, { protocol_ids: rows.map((row) => row.id) }).then((response) => {
this.updateTable();
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
},
restore(event, rows) {
axios.post(event.path, { protocol_ids: rows.map((row) => row.id) }).then((response) => {
this.updateTable();
HelperModule.flashAlertMsg(response.data.message, 'success');
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
},
exportProtocol(event) {
const link = document.createElement('a');
link.href = event.path;
link.click();
},
importFile() {
const fileInput = document.querySelector('#importFileInput');
fileInput.click();
},
importProtocolsIo() {
const protocolIoButton = document.querySelector('#importProtocolsIo');
protocolIoButton.click();
},
importDocx() {
const docxButton = document.querySelector('#importDocx');
docxButton.click();
},
access(_event, rows) {
this.accessModalParams = {
object: rows[0],
roles_path: this.userRolesUrl
};
},
linkedMyModules(_event, rows) {
[this.linkedMyModulesModalObject] = rows;
},
// renderers
nameRenderer(params) {
const { urls, name } = params.data;
if (urls.show) {
return `<a href="${urls.show}">${name}</a>`;
}
return `<span class="text-sn-grey">${name}</span>`;
}
}
};
</script>

View file

@ -1,18 +1,30 @@
<template>
<div class="flex py-4 items-center justify-between">
<div class="flex items-center gap-4">
<a v-for="action in toolbarActions.left" :key="action.label"
:class="action.buttonStyle"
:href="action.path"
@click="doAction(action, $event)">
<i :class="action.icon"></i>
{{ action.label }}
</a>
<template v-for="action in toolbarActions.left" :key="action.label">
<a v-if="action.type === 'emit'"
:class="action.buttonStyle"
:href="action.path"
@click="doAction(action, $event)">
<i :class="action.icon"></i>
{{ action.label }}
</a>
<MenuDropdown
v-if="action.type === 'menu'"
:listItems="action.menuItems"
:btnClasses="action.buttonStyle"
:btnText="action.label"
:btnIcon="action.icon"
:caret="true"
:position="'right'"
@dtEvent="handleEvent"
></MenuDropdown>
</template>
</div>
<div>
<div class="flex items-center gap-2">
<MenuDropdown
v-if="archivedPageUrl"
v-if="viewRenders"
:listItems="this.viewRendersMenu"
:btnClasses="'btn btn-light icon-btn'"
:btnText="i18n.t(`toolbar.${currentViewRender}_view`)"
@ -59,7 +71,9 @@
:placeholder="'Search...'"
@change="$emit('search:change', $event.target.value)"
/>
<i class="sn-icon sn-icon-search !m-2.5 !ml-auto right-0"></i>
<i v-if="searchValue.length === 0" class="sn-icon sn-icon-search !m-2.5 !ml-auto right-0"></i>
<i v-else class="sn-icon sn-icon-close !m-2.5 !ml-auto right-0 cursor-pointer z-10"
@click="$emit('search:change', '')"></i>
</div>
<FilterDropdown v-else :filters="filters" @applyFilters="applyFilters" />
</div>
@ -105,7 +119,6 @@ export default {
default: () => []
},
viewRenders: {
type: Array,
required: true
},
currentViewRender: {
@ -184,6 +197,9 @@ export default {
},
applyFilters(filters) {
this.$emit('applyFilters', filters);
},
handleEvent(event) {
this.$emit('toolbar:action', { name: event });
}
}
};

View file

@ -160,6 +160,7 @@ class Protocol < ApplicationRecord
dependent: :destroy
has_many :protocol_keywords, through: :protocol_protocol_keywords
has_many :steps, inverse_of: :protocol, dependent: :destroy
has_many :users, through: :user_assignments
def self.search(user,
include_archived,

View file

@ -0,0 +1,104 @@
# frozen_string_literal: true
module Lists
class ProtocolSerializer < ActiveModel::Serializer
include Canaid::Helpers::PermissionsHelper
include Rails.application.routes.url_helpers
attributes :name, :code, :keywords, :linked_tasks, :nr_of_versions, :assigned_users, :published_by,
:published_on, :updated_at, :archived_by, :archived_on, :urls, :default_public_user_role_id,
:hidden, :top_level_assignable, :has_draft, :team
def keywords
object.protocol_keywords.map(&:name)
end
def team
object.team.name
end
def linked_tasks
object.nr_of_linked_tasks
end
def assigned_users
object.user_assignments.map do |ua|
{
avatar: avatar_path(ua.user, :icon_small),
full_name: ua.user_name_with_role
}
end
end
def has_draft
if object.in_repository_published_original? || object.in_repository_published_version?
parent = object.parent || object
parent.draft.present?
else
object.in_repository_draft?
end
end
def published_by
object.published_by&.full_name
end
def published_on
I18n.l(object.published_on, format: :full) if object.published_on
end
def updated_at
I18n.l(object.updated_at, format: :full) if object.updated_at
end
def archived_by
object.archived_by&.full_name
end
def archived_on
I18n.l(object.archived_on, format: :full) if object.archived_on
end
delegate :default_public_user_role_id, to: :object
def hidden
object.hidden?
end
def top_level_assignable
true
end
def urls
urls_list = {
show_access: access_permissions_protocol_path(object),
versions_list: versions_list_protocol_path(object),
linked_my_modules: linked_children_protocol_path(object.parent || object),
versions_modal: versions_modal_protocol_path(object.parent || object)
}
if can_read_protocol_in_repository?(object)
urls_list[:show] = if object.in_repository_published_original? && object.latest_published_version.present?
protocol_path(object.latest_published_version)
else
protocol_path(object)
end
end
if has_draft
object.initial_draft? ? object : object.draft
urls_list[:show_draft] = protocol_path(object)
end
if can_manage_protocol_users?(object)
urls_list[:update_access] = access_permissions_protocol_path(object)
urls_list[:new_access] = new_access_permissions_protocol_path(id: object.id)
urls_list[:create_access] = access_permissions_protocols_path(id: object.id)
urls_list[:default_public_user_role_path] =
update_default_public_user_role_access_permissions_protocol_path(object)
end
urls_list
end
end
end

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
class ProtocolDraftSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
include Canaid::Helpers::PermissionsHelper
attributes :id, :previous_number, :modified_on, :modified_by, :comment, :urls
def previous_number
object.previous_version&.version_number
end
def modified_on
I18n.l(object.updated_at, format: :full_date) if object.updated_at
end
def modified_by
object.last_modified_by&.full_name
end
def comment
object.version_comment
end
def urls
current_user = scope
urls_list = {
show: protocol_path(object)
}
urls_list[:publish] = publish_protocol_path(object) if can_publish_protocol_in_repository?(current_user, object)
if can_delete_protocol_draft_in_repository?(current_user, object)
urls_list[:destroy] = destroy_draft_protocol_path(object)
end
if can_manage_protocol_draft_in_repository?(current_user, object) &&
can_publish_protocol_in_repository?(current_user, object)
urls_list[:comment] = update_version_comment_protocol_path(object)
end
urls_list
end
end

View file

@ -96,7 +96,9 @@ class ProtocolSerializer < ActiveModel::Serializer
save_as_draft_url: save_as_draft_url,
versions_modal_url: versions_modal_url,
version_comment_url: version_comment_url,
print_protocol_url: print_protocol_url
print_protocol_url: print_protocol_url,
versions_modal: versions_modal_protocol_path(object.parent || object),
redirect_to_protocols: protocols_path
}
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
class ProtocolVersionSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
include Canaid::Helpers::PermissionsHelper
attributes :id, :number, :published_on, :published_by, :comment, :urls
def urls
current_user = scope
urls_list = {
show: protocol_path(object)
}
if can_save_protocol_version_as_draft?(current_user, object)
urls_list[:save_as_draft] = save_as_draft_protocol_path(object)
end
urls_list
end
def number
object.version_number
end
def published_on
I18n.l(object.published_on, format: :full_date) if object.published_on
end
def published_by
object.published_by&.full_name
end
def comment
object.version_comment
end
end

View file

@ -47,6 +47,8 @@ class UserAssignmentSerializer < ActiveModel::Serializer
parent.user_assignments.find_by(user: object.user)
when Experiment
parent_assignment(parent.permission_parent)
when Team
object
end
end

View file

@ -0,0 +1,88 @@
# frozen_string_literal: true
module Lists
class ProtocolsService < BaseService
PREFIXED_ID_SQL = "('#{Protocol::ID_PREFIX}' || COALESCE(\"protocols\".\"parent_id\", \"protocols\".\"id\"))".freeze
private
def fetch_records
@records = @raw_data.preload(:parent, :latest_published_version, :draft,
:protocol_keywords, user_assignments: %i(user user_role))
.joins("LEFT OUTER JOIN protocols protocol_versions " \
"ON protocol_versions.protocol_type = #{
Protocol.protocol_types[:in_repository_published_version]} " \
"AND protocol_versions.parent_id = protocols.parent_id")
.joins("LEFT OUTER JOIN protocols self_linked_task_protocols " \
"ON self_linked_task_protocols.protocol_type = #{
Protocol.protocol_types[:linked]} " \
"AND self_linked_task_protocols.parent_id = protocols.id")
.joins("LEFT OUTER JOIN protocols parent_linked_task_protocols " \
"ON parent_linked_task_protocols.protocol_type = #{
Protocol.protocol_types[:linked]} " \
"AND parent_linked_task_protocols.parent_id = protocols.parent_id")
.joins("LEFT OUTER JOIN protocols version_linked_task_protocols " \
"ON version_linked_task_protocols.protocol_type = #{
Protocol.protocol_types[:linked]} " \
"AND version_linked_task_protocols.parent_id = protocol_versions.id " \
"AND version_linked_task_protocols.parent_id != protocols.id")
.joins('LEFT OUTER JOIN "protocol_protocol_keywords" ' \
'ON "protocol_protocol_keywords"."protocol_id" = "protocols"."id"')
.joins('LEFT OUTER JOIN "protocol_keywords" ' \
'ON "protocol_protocol_keywords"."protocol_keyword_id" = "protocol_keywords"."id"')
.joins('LEFT OUTER JOIN "users" "archived_users"
ON "archived_users"."id" = "protocols"."archived_by_id"')
.joins('LEFT OUTER JOIN "users" ON "users"."id" = "protocols"."published_by_id"')
.joins('LEFT OUTER JOIN "user_assignments" "all_user_assignments" ' \
'ON "all_user_assignments"."assignable_type" = \'Protocol\' ' \
'AND "all_user_assignments"."assignable_id" = "protocols"."id"')
.group('"protocols"."id"')
.select(
'"protocols".*',
'COALESCE("protocols"."parent_id", "protocols"."id") AS adjusted_parent_id',
'STRING_AGG(DISTINCT("protocol_keywords"."name"), \', \') AS "protocol_keywords_str"',
"CASE WHEN protocols.protocol_type = #{Protocol.protocol_types[:in_repository_draft]} " \
"THEN 0 ELSE COUNT(DISTINCT(\"protocol_versions\".\"id\")) + 1 " \
"END AS nr_of_versions",
'(COUNT(DISTINCT("self_linked_task_protocols"."id")) + ' \
'COUNT(DISTINCT("parent_linked_task_protocols"."id")) + ' \
'COUNT(DISTINCT("version_linked_task_protocols"."id"))) AS nr_of_linked_tasks',
'COUNT(DISTINCT("all_user_assignments"."id")) AS "nr_of_assigned_users"',
'MAX("users"."full_name") AS "full_username_str"', # "Hack" to get single username
'MAX("archived_users"."full_name") AS "archived_full_username_str"'
)
view_mode = @params[:view_mode] || 'active'
@records = @records.archived if view_mode == 'archived'
@records = @records.active if view_mode == 'active'
end
def filter_records
return if @params[:search].blank?
@records = @records.where(
"LOWER(\"protocols\".\"name\") LIKE :search OR
LOWER(\"protocol_keywords\".\"name\") LIKE :search OR
LOWER(#{PREFIXED_ID_SQL}) LIKE :search",
search: "%#{@params[:search].to_s.downcase}%"
)
end
def sortable_columns
@sortable_columns ||= {
name: 'name',
parent_id: 'adjusted_parent_id',
versions: 'nr_of_versions',
keywords: 'protocol_keywords_str',
linked_tasks: 'nr_of_linked_tasks',
assigned_users: 'nr_of_assigned_users',
published_by: 'full_username_str',
published_on: 'published_on',
udpated_at: 'updated_at',
archived_by: 'archived_full_username_str',
archived_on: 'archived_on'
}
end
end
end

View file

@ -39,8 +39,7 @@ module Toolbars
name: 'versions',
label: I18n.t('protocols.index.toolbar.versions'),
icon: 'sn-icon sn-icon-versions',
button_id: 'protocolVersions',
type: :legacy
type: :emit
}
end
@ -55,29 +54,18 @@ module Toolbars
label: I18n.t('protocols.index.toolbar.duplicate'),
icon: 'sn-icon sn-icon-duplicate',
path: clone_protocols_path,
type: :request,
request_method: :post
type: :emit
}
end
def access_action
return unless @single
protocol = @protocols.first
path = if can_manage_protocol_users?(protocol)
edit_access_permissions_protocol_path(protocol)
else
access_permissions_protocol_path(protocol)
end
{
name: 'access',
label: I18n.t('protocols.index.toolbar.access'),
icon: 'sn-icon sn-icon-project-member-access',
button_class: 'access-btn',
path: path,
type: 'remote-modal'
type: :emit
}
end
@ -91,7 +79,7 @@ module Toolbars
label: I18n.t('protocols.index.toolbar.export'),
icon: 'sn-icon sn-icon-export',
path: export_protocols_path(protocol_ids: @protocols.pluck(:id)),
type: :download
type: :emit
}
end
@ -103,8 +91,7 @@ module Toolbars
label: I18n.t('protocols.index.toolbar.archive'),
icon: 'sn-icon sn-icon-archive',
path: archive_protocols_path,
type: :request,
request_method: :post
type: :emit
}
end
@ -112,12 +99,11 @@ module Toolbars
return unless @protocols.all? { |p| can_restore_protocol_in_repository?(p) }
{
name: 'archive',
name: 'restore',
label: I18n.t('protocols.index.toolbar.restore'),
icon: 'sn-icon sn-icon-restore',
path: restore_protocols_path,
type: :request,
request_method: :post
type: :emit
}
end
end

View file

@ -25,42 +25,54 @@
</div>
</div>
<div class="protocols-container">
<%= render partial: "protocols/index/protocols_datatable" %>
<div id="actionToolbar" data-behaviour="vue">
<action-toolbar actions-url="<%= actions_toolbar_protocols_url %>" />
<div id="ProtocolsTable" class="fixed-content-body">
<protocols-table
ref="table"
:actions-url="'<%= actions_toolbar_protocols_url %>'"
:data-source="'<%= protocols_path(format: :json) %>'"
:active-page-url="'<%= protocols_path %>'"
:archived-page-url="'<%= protocols_path(view_mode: :archived) %>'"
current-view-mode="<%= params[:view_mode] || :active %>"
:docx-parser-enabled="<%= Protocol.docx_parser_enabled? %>"
user-roles-url="<%= user_roles_projects_path %>"
:create-url="'<%= protocols_path if can_create_protocols_in_repository?(current_team) %>'"
/>
</div>
</div>
</div>
<% end %>
<%= javascript_include_tag 'vue_protocols_list' %>
<!-- Legacy code -->
<a class="btn-open-file tw-hidden" data-action='import' >
<input type="file" ref="importFileBtn" value="" id="importFileInput" accept=".eln" data-role="import-file-input"
data-team-id="<%= current_team.id %>" data-import-url="<%= import_protocols_path %>">
</a>
<a class="btn-open-file tw-hidden" data-action='import' >
<input type="file" value="" id="importDocx" accept=".docx" data-role="import-file-input"
data-team-id="<%= @current_team.id %>" data-import-url="<%= import_protocols_path %>">
</a>
<%= link_to t("protocols.index.import_protocols_io"), '',
id: "importProtocolsIo",
class: 'tw-hidden',
data: { target: '#protocolsioModal', toggle: 'modal' } %>
<%= render partial: "protocols/index/protocolsio_modal" %>
<div id="protocolsio-preview-modal-target"></div>
<%= javascript_include_tag "protocols/index" %>
<%= javascript_include_tag "handsontable.full" %>
<%= render partial: "shared/formulas_libraries" %>
<div id="protocolFileImportModal">
<protocol-file-import-modal
import-url="<%= import_docx_protocols_path %>"
protocol-template-table-url="<%= protocols_path %>"
/>
</div>
<%= javascript_include_tag 'vue_protocol_file_import_modal' %>
<div id="protocolsio-preview-modal-target"></div>
<%= render partial: "protocols/index/general_toolbar" %>
<%= render partial: "protocols/index/protocol_filters" %>
<%= render partial: "protocols/index/delete_draft_modal" %>
<%= render partial: "protocols/index/linked_children_modal" %>
<%= render partial: "protocols/index/protocol_preview_modal" %>
<%= render partial: "protocols/index/protocolsio_modal" %>
<%= render partial: "protocols/index/new_protocol_modal", locals: {type: 'new'} %>
<%= render partial: "protocols/import_export/import_elements" %>
<%= javascript_include_tag "vue_components_action_toolbar" %>
<%= javascript_include_tag "handsontable.full" %>
<!-- Libraries for formulas -->
<%= render partial: "shared/formulas_libraries" %>
<%= stylesheet_link_tag 'datatables' %>
<%= javascript_include_tag 'vue_protocol_file_import_modal' %>
<%= javascript_include_tag "assets/wopi/create_wopi_file" %>
<%= javascript_include_tag "protocols/index" %>
<%= javascript_include_tag "protocols/new_protocol" %>
<%= javascript_include_tag 'pdf_js' %>
<%= stylesheet_link_tag 'pdf_js_styles' %>

View file

@ -633,6 +633,7 @@ Rails.application.routes.draw do
post :publish
post :destroy_draft
post :save_as_draft
get :versions_list
get 'version_comment', to: 'protocols#version_comment'
get 'print', to: 'protocols#print'
get 'linked_children', to: 'protocols#linked_children'

View file

@ -51,8 +51,9 @@ const entryList = {
vue_projects_list: './app/javascript/packs/vue/projects_list.js',
vue_experiments_list: './app/javascript/packs/vue/experiments_list.js',
vue_my_modules_list: './app/javascript/packs/vue/my_modules_list.js',
vue_design_system_select: './app/javascript/packs/vue/design_system/select.js'
}
vue_design_system_select: './app/javascript/packs/vue/design_system/select.js',
vue_protocols_list: './app/javascript/packs/vue/protocols_list.js'
};
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949
// Get paths to all engines' folders