Implement new toolbar for protocols [SCI-8399]

This commit is contained in:
Martin Artnik 2023-05-17 14:12:40 +02:00
parent a52ee99781
commit eb5fb16b1b
9 changed files with 179 additions and 94 deletions

View file

@ -6,7 +6,6 @@
// Global variables
var ProtocolsIndex = (function() {
const ALL_VERSIONS_VALUE = 'All';
var PERMISSIONS = ['archivable', 'restorable', 'copyable', 'readable'];
var rowsSelected = [];
var protocolsTableEl = null;
var protocolsDatatable = null;
@ -28,6 +27,10 @@ var ProtocolsIndex = (function() {
* Initializes page
*/
function init() {
window.initActionToolbar();
window.actionToolbarComponent.setReloadCallback(reloadTable);
// make room for pagination
window.actionToolbarComponent.setBottomOffset(75);
updateButtons();
initProtocolsTable();
initKeywordFiltering();
@ -256,9 +259,7 @@ var ProtocolsIndex = (function() {
DataTableHelpers.initSearchField(dataTableWrapper, I18n.t('protocols.index.search_bar_placeholder'));
dataTableWrapper.find('.main-actions, .pagination-row').removeClass('hidden');
let actionToolBar = $($('#protocolActionToolbar').html());
let generalToolbar = $($('#protocolGeneralToolbar').html());
$('.protocols-container .actions-toolbar').html(actionToolBar);
$('.protocols-container .toolbar').html(generalToolbar);
let protocolFilters = $($('#protocolFilters').html());
@ -285,26 +286,6 @@ var ProtocolsIndex = (function() {
});
}
function checkActionPermission(permission) {
let allProtocols;
allProtocols = rowsSelected.every((id) => {
return protocolsTableEl.find(`tbody tr#${id}`).data(permission);
});
return allProtocols;
}
function loadPermission(id) {
let row = protocolsTableEl.find(`tbody tr#${id}`);
$.get(row.data('permissions-url'), (result) => {
PERMISSIONS.forEach((permission) => {
row.data(permission, result[permission]);
});
updateButtons();
});
}
function initRowSelection() {
let protocolsTableScrollHead = protocolsTableEl.closest('.dataTables_scroll').find('.dataTables_scrollHead');
@ -335,13 +316,12 @@ var ProtocolsIndex = (function() {
updateDataTableSelectAllCheckbox();
if (this.checked) {
loadPermission(rowId);
row.addClass('selected');
} else {
row.removeClass('selected');
updateButtons();
}
updateButtons();
e.stopPropagation();
});
@ -435,7 +415,8 @@ var ProtocolsIndex = (function() {
e.preventDefault();
});
$(protocolsContainer).on('click', '#protocolVersions', function() {
$(protocolsContainer).on('click', '#protocolVersions', function(e) {
e.stopPropagation();
loadVersionModal($(`tr[data-row-id=${rowsSelected[0]}]`).data('versions-url'));
});
}
@ -663,34 +644,7 @@ var ProtocolsIndex = (function() {
}
function updateButtons() {
let actionToolbar = $('.protocols-container .actions-toolbar');
$('.dataTables_wrapper').addClass('show-actions');
if (rowsSelected.length === 0) {
$('.dataTables_wrapper').removeClass('show-actions');
} else if (rowsSelected.length === 1) {
actionToolbar.find('.single-object-action, .multiple-object-action').removeClass('hidden');
} else {
actionToolbar.find('.single-object-action').addClass('hidden');
actionToolbar.find('.multiple-object-action').removeClass('hidden');
}
PERMISSIONS.forEach((permission) => {
if (!checkActionPermission(permission)) {
actionToolbar.find(`.btn[data-for="${permission}"]`).addClass('hidden');
}
});
if (protocolsDatatable) protocolsDatatable.columns.adjust();
actionToolbar.find('.btn').addClass('notransition');
actionToolbar.find('.btn').removeClass('btn-primary').addClass('btn-light');
actionToolbar.find('.btn:visible').first().addClass('btn-primary').removeClass('btn-light');
setTimeout(function() {
actionToolbar.find('.btn').removeClass('notransition');
}, 500);
actionToolbar.find('.emptyPlaceholder').toggleClass('hidden', actionToolbar.find('.btn:visible').length > 0);
window.actionToolbarComponent.fetchActions({ protocol_ids: rowsSelected.join(',') });
}
function initLocalFileImport() {

View file

@ -5,4 +5,20 @@
.btn.btn-light:hover {
background: $color-white;
}
.icon-versions {
display: inline-block;
height: 14px;
background-image: image-url("icon_small/versions-black.svg");
background-position: center center;
background-repeat: no-repeat;
margin-bottom: -2px;
width: 16px;
}
}
@media (max-width: 1000px) {
.sn-action-toolbar__button-text {
display: none;
}
}

View file

@ -921,6 +921,16 @@ class ProtocolsController < ApplicationController
end
end
def actions_toolbar
render json: {
actions:
Toolbars::ProtocolsService.new(
current_user,
protocol_ids: params[:protocol_ids].split(',')
).actions
}
end
private
def set_importer
@ -1001,7 +1011,7 @@ class ProtocolsController < ApplicationController
def check_clone_permissions
load_team_and_type
protocol = Protocol.find_by(id: params[:ids][0])
protocol = Protocol.find_by(id: params[:protocol_ids].split(','))
@original = protocol.latest_published_version_or_self
if @original.blank? ||

View file

@ -1,5 +1,5 @@
<template>
<div v-if="loading || actions.length" class="sn-action-toolbar p-4 w-full fixed bottom-0 rounded-t-md shadow-[0_-12px_24px_-12px_rgba(35,31,32,0.2)]" :style="`width: ${width}px`">
<div v-if="loading || actions.length" class="sn-action-toolbar p-4 w-full fixed bottom-0 rounded-t-md shadow-[0_-12px_24px_-12px_rgba(35,31,32,0.2)]" :style="`width: ${width}px; bottom: ${bottom}px;`">
<div class="sn-action-toolbar__actions flex">
<div v-if="loading && !actions.length" class="sn-action-toolbar__action">
<a class="btn btn-light"></a>
@ -12,9 +12,9 @@
:data-object-type="action.item_type"
:data-object-id="action.item_id"
:data-action="action.type"
@click="doAction(action)">
@click="doAction(action, $event)">
<i class="mr-1" :class="action.icon"></i>
{{ action.label }}
<span class="sn-action-toolbar__button-text">{{ action.label }}</span>
</a>
</div>
</div>
@ -37,7 +37,8 @@
params: {},
reloadCallback: null,
loading: false,
width: 0
width: 0,
bottom: 0
}
},
created() {
@ -51,7 +52,7 @@
this.actions = data.actions;
this.loading = false;
});
}, 200);
}, 10);
},
mounted() {
this.setWidth();
@ -63,6 +64,9 @@
setWidth() {
this.width = $(this.$el).parent().width();
},
setBottomOffset(pixels) {
this.bottom = pixels;
},
fetchActions(params) {
this.loading = true;
this.debouncedFetchActions(params);
@ -70,7 +74,7 @@
setReloadCallback(func) {
this.reloadCallback = func;
},
doAction(action) {
doAction(action, event) {
switch(action.type) {
case 'legacy':
// do nothing, this is handled by legacy code based on the button class
@ -81,7 +85,13 @@
case 'remote-modal':
// do nothing, handled by the data-action="remote-modal" binding
break;
case 'download':
event.stopPropagation();
window.location.href = action.path;
break;
case 'request':
event.stopPropagation();
$.ajax({
type: action.request_method,
url: action.path,

View file

@ -0,0 +1,120 @@
# frozen_string_literal: true
module Toolbars
class ProtocolsService
attr_reader :current_user
include Canaid::Helpers::PermissionsHelper
include Rails.application.routes.url_helpers
include ActionView::Helpers::AssetUrlHelper
def initialize(current_user, protocol_ids: [])
@current_user = current_user
@protocols = Protocol.where(id: protocol_ids)
@single = @protocols.length == 1
end
def actions
return [] if @protocols.none?
[
restore_action,
versions_action,
duplicate_action,
access_action,
export_action,
archive_action
].compact
end
private
def versions_action
return unless @single
{
name: 'versions',
label: I18n.t('protocols.index.toolbar.versions'),
icon: 'icon-versions',
button_id: 'protocolVersions',
type: :legacy
}
end
def duplicate_action
return unless @single
protocol = @protocols.first.latest_published_version_or_self
return unless can_clone_protocol_in_repository?(protocol)
{
name: 'duplicate',
label: I18n.t('protocols.index.toolbar.duplicate'),
icon: 'fas fa-copy',
path: clone_protocols_path,
type: :request,
request_method: :post
}
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: 'fas fa-door-open',
button_class: 'access-btn',
path: path,
type: 'remote-modal'
}
end
def export_action
return unless @single
{
name: 'export',
label: I18n.t('protocols.index.toolbar.export'),
icon: 'fas fa-download',
path: export_protocols_path(protocol_ids: @protocols.pluck(:id)),
type: :download
}
end
def archive_action
return unless @protocols.all? { |p| can_archive_protocol_in_repository?(p) }
{
name: 'archive',
label: I18n.t('protocols.index.toolbar.archive'),
icon: 'fas fa-archive',
path: archive_protocols_path,
type: :request,
request_method: :post
}
end
def restore_action
return unless @protocols.all? { |p| can_restore_protocol_in_repository?(p) }
{
name: 'archive',
label: I18n.t('protocols.index.toolbar.restore'),
icon: 'fas fa-undo',
path: restore_protocols_path,
type: :request,
request_method: :post
}
end
end
end

View file

@ -26,13 +26,16 @@
</div>
<div class="protocols-container">
<%= render partial: "protocols/index/protocols_datatable.html.erb" %>
<div id="actionToolbar" data-behaviour="vue">
<action-toolbar actions-url="<%= actions_toolbar_protocols_url %>" />
</div>
</div>
</div>
<% end %>
<div id="protocolsio-preview-modal-target"></div>
<%= render partial: "protocols/index/general_toolbar.html.erb" %>
<%= render partial: "protocols/index/action_toolbar.html.erb" %>
<%= render partial: "protocols/index/protocol_filters.html.erb" %>
<%= render partial: "protocols/index/delete_draft_modal.html.erb" %>
<%= render partial: "protocols/index/linked_children_modal.html.erb" %>
@ -41,7 +44,7 @@
<%= render partial: "protocols/index/new_protocol_modal.html.erb", locals: {type: 'new'} %>
<%= render partial: "protocols/import_export/import_elements.html.erb" %>
<%= javascript_include_tag "vue_components_action_toolbar" %>
<%= javascript_include_tag "handsontable.full" %>
<!-- Libraries for formulas -->

View file

@ -1,29 +0,0 @@
<template id="protocolActionToolbar">
<button id="restoreProtocol" class="btn btn-light multiple-object-action hidden only-archive" data-url="<%= restore_protocols_path %>" data-for="restorable">
<i class="fas fa-undo"></i>
<span class="button-text"><%= t("protocols.index.action_toolbar.restore") %></span>
</button>
<button id="protocolVersions" class="btn btn-light single-object-action hidden only-active" data-for="readable">
<%= image_tag 'icon/versions.svg' %>
<span class="button-text"><%= t("protocols.index.action_toolbar.versions") %></span>
</button>
<button id="manageProtocolAccess" class="btn btn-light single-object-action hidden">
<i class="fas fa-door-open"></i>
<span class="button-text"><%= t("protocols.index.action_toolbar.access") %></span>
</button>
<button id="duplicateProtocol" class="btn btn-light single-object-action hidden only-active" data-url="<%= clone_protocols_path %>" data-for="copyable">
<i class="fas fa-copy"></i>
<span class="button-text"><%= t("protocols.index.action_toolbar.duplicate") %></span>
</button>
<button id="exportProtocol" class="btn btn-light single-object-action hidden only-active" data-url="<%= export_protocols_path %>" data-for="readable">
<i class="fas fa-download"></i>
<span class="button-text"><%= t("protocols.index.action_toolbar.export") %></span>
</button>
<button id="archiveProtocol" class="btn btn-light multiple-object-action hidden only-active" data-url="<%= archive_protocols_path %>" data-for="archivable">
<i class="fas fa-archive"></i>
<span class="button-text"><%= t("protocols.index.action_toolbar.archive") %></span>
</button>
<div class="emptyPlaceholder hidden">
<%= t("protocols.index.action_toolbar.empty_placeholder") %>
</div>
</template>

View file

@ -2789,9 +2789,9 @@ en:
search_bar_placeholder: 'Filter protocols'
navigation:
archive: "Archive"
action_toolbar:
toolbar:
versions: "Versions"
access: "Manage Access"
access: "Access"
duplicate: "Duplicate"
export: "Export"
archive: "Archive"

View file

@ -655,6 +655,7 @@ Rails.application.routes.draw do
post 'protocolsio_import_save', to: 'protocols#protocolsio_import_save'
get 'export', to: 'protocols#export'
get 'protocolsio', to: 'protocols#protocolsio_index'
get 'actions_toolbar', to: 'protocols#actions_toolbar'
end
end