Merge pull request #5417 from scinote-eln/features/sa-item-assigning

Features/sa item assigning
This commit is contained in:
Alex Kriuchykhin 2023-05-17 11:45:13 +02:00 committed by GitHub
commit 5a06f649d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 743 additions and 52 deletions

View file

@ -18,7 +18,8 @@ function initEditMyModuleDescription() {
{
onSaveCallback: () => {
Prism.highlightAllUnder(viewObject.get(0));
}
},
assignableMyModuleId: $('#my_module_description_textarea').data('object-id')
}
);
}).on('click', 'a', function(e) {

View file

@ -841,6 +841,9 @@ var MyModuleRepositories = (function() {
reloadFullViewTable: () => {
if (!FULL_VIEW_TABLE) return;
FULL_VIEW_TABLE.ajax.reload(null, false);
},
reloadRepositoriesList: (repositoryId, expand = false) => {
reloadRepositoriesList(repositoryId, expand);
}
};
}());

View file

@ -25,7 +25,7 @@ var MyModuleStockConsumption = (function() {
$manageModal.find('.modal-content').html(result.html);
$manageModal.modal('show');
focusStockConsumption();
SmartAnnotation.init($(CONSUMPTION_MODAL + ' #comment')[0]);
SmartAnnotation.init($(CONSUMPTION_MODAL + ' #comment')[0], false);
$('#stock_consumption').on('input', function() {
let initialValue = new Decimal($(this).data('initial-value') || 0);

View file

@ -33,7 +33,7 @@ $.fn.dataTable.render.newRepositoryTextValue = function(formId, columnId, $cell)
data-type="RepositoryTextValue">
</div>`);
SmartAnnotation.init($cell.find('input'));
SmartAnnotation.init($cell.find('input'), false);
};
$.fn.dataTable.render.newRepositoryListValue = function(formId, columnId, $cell) {

View file

@ -266,6 +266,10 @@ var RepositoryDatatable = (function(global) {
});
}
function updateSelectedRowsForAssignments() {
window.AssignItemsToTaskModalComponent.setShowCallback(() => rowsSelected);
}
function checkAvailableColumns() {
$.ajax({
url: $(TABLE_ID).data('available-columns'),
@ -730,6 +734,7 @@ var RepositoryDatatable = (function(global) {
})
initRowSelection();
updateSelectedRowsForAssignments();
// $(window).resize(() => {
// setTimeout(() => {
// adjustTableHeader();

View file

@ -120,7 +120,7 @@ var RepositoryStockValues = (function() {
this.value = formatDecimalValue(this.value, decimals);
});
SmartAnnotation.init($('#repository-stock-value-comment')[0]);
SmartAnnotation.init($('#repository-stock-value-comment')[0], false);
$('#repository-stock-value-comment').on('input', function() {
$(this).closest('.sci-input-container').toggleClass(

View file

@ -27,7 +27,9 @@
formAjaxResultText($form);
Results.initCancelFormButton($form, initNewReslutText);
Results.toggleResultEditButtons(false);
TinyMCE.init('#result_text_attributes_textarea');
TinyMCE.init('#result_text_attributes_textarea', {
assignableMyModuleId: $('#result_text_attributes_textarea').data('my-module-id')
});
$('#result_name').focus();
},
error: function() {
@ -58,7 +60,9 @@
Results.toggleResultEditButtons(true);
});
Results.toggleResultEditButtons(false);
TinyMCE.init('#result_text_attributes_textarea');
TinyMCE.init('#result_text_attributes_textarea', {
assignableMyModuleId: $('#result_text_attributes_textarea').data('my-module-id')
});
$('#result_name').focus();
});
}

View file

@ -22,7 +22,7 @@ var inlineEditing = (function() {
function initSmartAnnotation(container) {
if (container.data('smart-annotation')) {
SmartAnnotation.init(inputField(container));
SmartAnnotation.init(inputField(container), false);
}
}

View file

@ -1,4 +1,4 @@
/* global _ */
/* global PerfectScrollbar MyModuleRepositories HelperModule _ */
var SmartAnnotation = (function() {
'use strict';
@ -11,7 +11,7 @@ var SmartAnnotation = (function() {
});
}
function SetAtWho(field, deferred) {
function SetAtWho(field, deferred, assignableMyModuleId) {
var FilterTypeEnum = Object.freeze({
USER: { tag: 'users', dataUrl: $(document.body).attr('data-atwho-users-url') },
TASK: { tag: 'sa-tasks', dataUrl: $(document.body).attr('data-atwho-task-url') },
@ -67,6 +67,7 @@ var SmartAnnotation = (function() {
let activeRepository = repositoryTab.find('.btn-primary');
if (activeRepository.length) {
params.repository_id = activeRepository.data('object-id');
params.assignable_my_module_id = assignableMyModuleId;
}
}
$.getJSON(filterType.dataUrl, params, function(data) {
@ -81,6 +82,10 @@ var SmartAnnotation = (function() {
$currentAtWho.find(`.repository-object[data-object-id="${data.repository}"]`)
.addClass('btn-primary').removeClass('btn-light');
}
if ($('.atwho-scroll-container')[0]) {
// eslint-disable-next-line no-new
new PerfectScrollbar($('.atwho-scroll-container')[0]);
}
});
return true;
},
@ -141,6 +146,24 @@ var SmartAnnotation = (function() {
$(this).addClass('btn-primary').removeClass('btn-light');
$(field).click().focus();
});
$currentAtWho.on('click', '.atwho-assign-button', function() {
let el = $(this);
$.ajax({
method: 'POST',
url: el.data('assign-url'),
data: { repository_row_id: el.data('repository-row-id') },
dataType: 'json',
success: function(data) {
if (typeof MyModuleRepositories !== 'undefined') {
MyModuleRepositories.reloadRepositoriesList(el.data('repository-id'));
}
HelperModule.flashAlertMsg(data.flash, 'success');
},
error: function(response) {
HelperModule.flashAlertMsg(response.responseJSON.flash, 'danger');
}
});
});
if ($currentAtWho.find('.tab-pane.active').length === 0) {
let filterType = DEFAULT_SEARCH_FILTER.tag;
@ -223,8 +246,8 @@ var SmartAnnotation = (function() {
$('.atwho-header-res').find('.fa-times').click();
}
function initialize(field, deferred) {
var atWho = new SetAtWho(field, deferred);
function initialize(field, deferred, assignableMyModuleId) {
var atWho = new SetAtWho(field, deferred, assignableMyModuleId);
atWho.init();
}
@ -240,7 +263,7 @@ var SmartAnnotation = (function() {
(function() {
$(document).on('focus', '[data-atwho-edit]', function() {
if (_.isUndefined($(this).data('atwho'))) {
SmartAnnotation.init(this);
SmartAnnotation.init(this, false);
}
});

View file

@ -147,7 +147,7 @@ var CommentsSidebar = (function() {
function initInputField() {
if ($(SIDEBAR).find('.comment-input-field').length) {
SmartAnnotation.init($(SIDEBAR).find('.comment-input-field'));
SmartAnnotation.init($(SIDEBAR).find('.comment-input-field'), false);
}
}

View file

@ -1,13 +1,18 @@
/* global bwipjs PrintModalComponent RepositoryDatatable */
/* global bwipjs PrintModalComponent RepositoryDatatable HelperModule MyModuleRepositories */
(function() {
'use strict';
$(document).on('click', '.record-info-link', function(e) {
var that = $(this);
let params = {};
if ($('.my-modules-protocols-index').length) {
params.my_module_id = $('.my-modules-protocols-index').data('task-id');
}
$.ajax({
method: 'GET',
url: that.attr('href'),
data: params,
dataType: 'json'
}).done(function(xhr, settings, data) {
if ($('#modal-info-repository-row').length) {
@ -16,6 +21,7 @@
$('.modal-backdrop').remove();
}
$('body').append($.parseHTML(data.responseJSON.html));
$('[data-toggle="tooltip"]').tooltip();
$('#modal-info-repository-row').modal('show', {
backdrop: true,
keyboard: false
@ -73,4 +79,27 @@
}
}
});
$(document).on('click', '.assign-inventory-button', function(e) {
e.preventDefault();
let assignUrl = $(this).data('assignUrl');
let repositoryRowId = $(this).data('repositoryRowId');
$.ajax({
url: assignUrl,
type: 'POST',
data: { repository_row_id: repositoryRowId },
dataType: 'json',
success: function(data) {
HelperModule.flashAlertMsg(data.flash, 'success');
$('#modal-info-repository-row').modal('hide');
if (typeof MyModuleRepositories !== 'undefined') {
MyModuleRepositories.reloadRepositoriesList(repositoryRowId);
}
},
error: function(error) {
HelperModule.flashAlertMsg(error.responseJSON.flash, 'danger');
}
});
});
}());

View file

@ -0,0 +1,38 @@
.assign-items-to-task-modal-container {
.modal-header {
color: $color-volcano;
display: flex;
font-size: $font-size-h2;
font-weight: bold;
padding: 1rem;
.close {
margin-left: auto;
}
}
.modal-body {
color: $color-volcano;
display: flex;
flex-direction: column;
font-size: $font-size-base;
row-gap: 1rem;
.level-selector {
display: flex;
flex-direction: column;
row-gap: .25rem;
}
label {
font-size: $font-size-h6;
font-weight: bold;
margin-bottom: 0;
}
}
.modal-footer {
padding: 1rem;
}
}

View file

@ -3,4 +3,15 @@
display: flex;
justify-content: flex-end;
}
.modal-footer[data-assign-item-button="true"] {
align-items: center;
display: flex;
gap: .5em;
width: 100%;
.print-label-button {
margin-right: auto;
}
}
}

View file

@ -83,7 +83,6 @@
max-height: 300px;
overflow: hidden;
overflow-y: scroll;
position: absolute;
top: 2.5em;
width: 100%;
z-index: 9999;

View file

@ -1,3 +1,6 @@
// scss-lint:disable SelectorDepth
// scss-lint:disable NestingDepth
.atwho-view {
background: $color_white;
border-radius: $border-radius-default;
@ -107,15 +110,49 @@
.item {
cursor: pointer;
margin-left: -.5em;
line-height: 2.25em;
overflow: hidden;
padding: .25em .5em;
padding: 0 .5em;
position: relative;
text-overflow: ellipsis;
width: calc(100% + 1em);
vertical-align: middle;
white-space: nowrap;
width: 100%;
.atwho-button-container {
background: linear-gradient(90deg,
transparent,
$color-concrete 15%,
$color-concrete 100%);
display: inline;
opacity: 0;
padding-left: 2em;
position: absolute;
right: 0;
.atwho-assign-button-form {
display: inline;
}
.atwho-insert-button,
.atwho-assign-button {
background: $color-concrete;
color: $brand-primary;
height: 2.25em;
margin-right: .5em;
padding: 0 .5em;
text-align: center;
width: auto;
}
}
&.cur {
background: $color-concrete;
color: $brand-primary;
.atwho-button-container {
opacity: 1;
}
}
.atwho-highlight {

View file

@ -40,7 +40,7 @@ class AtWhoController < ApplicationController
end
if repository && can_read_repository?(repository)
items = SmartAnnotation.new(current_user, current_team, @query)
.repository_rows(repository)
.repository_rows(repository, params[:assignable_my_module_id])
repository_id = repository.id
else
items = []
@ -51,7 +51,7 @@ class AtWhoController < ApplicationController
render json: {
res: [
render_to_string(partial: 'shared/smart_annotation/repository_items.html.erb',
locals: { repository_rows: items })
locals: { repository_rows: items, repository: repository })
],
repository: repository_id,
team: current_team.id

View file

@ -9,8 +9,9 @@ class ExperimentsController < ApplicationController
include Breadcrumbs
before_action :load_project, only: %i(new create archive_group restore_group)
before_action :load_experiment, except: %i(new create archive_group restore_group actions_toolbar)
before_action :check_read_permissions, except: %i(edit archive clone move new create archive_group restore_group actions_toolbar)
before_action :load_experiment, except: %i(new create archive_group restore_group experiment_filter actions_toolbar)
before_action :check_read_permissions, except: %i(edit archive clone move new create
archive_group restore_group experiment_filter actions_toolbar)
before_action :check_canvas_read_permissions, only: %i(canvas)
before_action :check_create_permissions, only: %i(new create)
before_action :check_manage_permissions, only: %i(edit batch_clone_my_modules)
@ -433,6 +434,20 @@ class ExperimentsController < ApplicationController
end
end
def experiment_filter
project = Project.readable_by_user(current_user).find_by(id: params[:project_id])
return render_404 if project.blank?
experiments = project.experiments
.readable_by_user(current_user)
.search(current_user, false, params[:query], 1, current_team)
.pluck(:id, :name)
return render plain: [].to_json if experiments.blank?
render json: experiments
end
def actions_dropdown
if stale?([@experiment, @experiment.project])
render json: {

View file

@ -4,11 +4,11 @@ class MyModuleRepositoriesController < ApplicationController
include ApplicationHelper
before_action :load_my_module
before_action :load_repository, except: %i(repositories_dropdown_list repositories_list_html)
before_action :load_repository, except: %i(repositories_dropdown_list repositories_list_html create)
before_action :check_my_module_view_permissions, except: %i(update consume_modal update_consumption)
before_action :check_repository_view_permissions, except: %i(repositories_dropdown_list repositories_list_html)
before_action :check_repository_view_permissions, except: %i(repositories_dropdown_list repositories_list_html create)
before_action :check_repository_row_consumption_permissions, only: %i(consume_modal update_consumption)
before_action :check_assign_repository_records_permissions, only: :update
before_action :check_assign_repository_records_permissions, only: %i(update create)
def index_dt
@draw = params[:draw].to_i
@ -41,6 +41,34 @@ class MyModuleRepositoriesController < ApplicationController
render rows_view
end
def create
repository_row = RepositoryRow.find(params[:repository_row_id])
repository = repository_row.repository
return render_403 unless can_read_repository?(repository)
ActiveRecord::Base.transaction do
@my_module.my_module_repository_rows.create!(repository_row: repository_row, assigned_by: current_user)
Activities::CreateActivityService.call(activity_type: :assign_repository_record,
owner: current_user,
team: @my_module.experiment.project.team,
project: @my_module.experiment.project,
subject: @my_module,
message_items: { my_module: @my_module.id,
repository: repository.id,
record_names: repository_row.name })
render json: {
flash: t('my_modules.assigned_items.direct_assign.success')
}
end
rescue StandardError => e
Rails.logger.error e.message
render json: {
flash: t('my_modules.repository.flash.update_error')
}, status: :bad_request
end
def update
service = RepositoryRows::MyModuleAssignUnassignService.call(my_module: @my_module,
repository: @repository,

View file

@ -8,14 +8,14 @@ class MyModulesController < ApplicationController
include MyModulesHelper
include Breadcrumbs
before_action :load_vars, except: %i(restore_group create new save_table_state actions_toolbar)
before_action :load_vars, except: %i(restore_group create new save_table_state my_module_filter actions_toolbar)
before_action :load_experiment, only: %i(create new)
before_action :check_create_permissions, only: %i(new create)
before_action :check_archive_permissions, only: %i(update)
before_action :check_manage_permissions, only: %i(
description due_date update_description update_protocol_description update_protocol
)
before_action :check_read_permissions, except: %i(create new update update_description
before_action :check_read_permissions, except: %i(create new update update_description my_module_filter
update_protocol_description restore_group
save_table_state actions_toolbar)
before_action :check_update_state_permissions, only: :update_state
@ -456,6 +456,20 @@ class MyModulesController < ApplicationController
render json: { provisioning_status: @my_module.provisioning_status }
end
def my_module_filter
experiment = Experiment.readable_by_user(current_user).find_by(id: params[:experiment_id])
return render_404 if experiment.blank?
my_modules = experiment.my_modules
.readable_by_user(current_user)
.search(current_user, false, params[:query], 1, current_team)
.pluck(:id, :name)
return render plain: [].to_json if my_modules.blank?
render json: my_modules
end
private
def load_vars

View file

@ -17,7 +17,7 @@ class ProjectsController < ApplicationController
sidebar experiments_cards view_type actions_dropdown create_tag)
before_action :load_current_folder, only: %i(index cards new show)
before_action :check_view_permissions, except: %i(index cards new create edit update archive_group restore_group
users_filter actions_dropdown actions_toolbar)
users_filter actions_dropdown project_filter actions_toolbar)
before_action :check_create_permissions, only: %i(new create)
before_action :check_manage_permissions, only: :edit
before_action :load_exp_sort_var, only: :show
@ -104,6 +104,16 @@ class ProjectsController < ApplicationController
}
end
def project_filter
projects = Project.readable_by_user(current_user)
.search(current_user, false, params[:query], 1, current_team)
.pluck(:id, :name)
return render plain: [].to_json if projects.blank?
render json: projects
end
def new
@project = current_team.projects.new(project_folder: current_folder)
respond_to do |format|

View file

@ -64,9 +64,19 @@ class RepositoryRowsController < ApplicationController
def show
@repository_row = RepositoryRow.find_by(id: params[:id])
@my_module = MyModule.find_by(id: params[:my_module_id])
return render_404 unless @repository_row
return render_404 unless @repository_row.repository_id == params[:repository_id].to_i
return render_403 unless can_read_repository?(@repository_row.repository)
return render_403 if @my_module && !can_read_my_module?(@my_module)
if @my_module
@my_module_assign_error = if !can_assign_my_module_repository_rows?(@my_module)
I18n.t('repository_row.modal_info.assign_to_task_error.no_access')
elsif @repository_row.my_modules.where(id: @my_module.id).any?
I18n.t('repository_row.modal_info.assign_to_task_error.already_assigned')
end
end
@assigned_modules = @repository_row.my_modules.joins(experiment: :project)
@viewable_modules = @assigned_modules.viewable_by_user(current_user, current_user.teams)

View file

@ -377,7 +377,7 @@ window.TinyMCE = (() => {
editor.selection.select(editor.getBody(), true);
editor.selection.collapse(false);
SmartAnnotation.init($(editor.contentDocument.activeElement));
SmartAnnotation.init($(editor.contentDocument.activeElement), false, options.assignableMyModuleId);
SmartAnnotation.preventPropagation('.atwho-user-popover');
if (options.afterInitCallback) { options.afterInitCallback(); }

View file

@ -0,0 +1,37 @@
import TurbolinksAdapter from 'vue-turbolinks';
import Vue from 'vue/dist/vue.esm';
import AssignItemsToTaskModalContainer from '../../vue/assign_items_to_tasks_modal/container.vue';
Vue.use(TurbolinksAdapter);
Vue.prototype.i18n = window.I18n;
function initAssignItemsToTaskModalComponent() {
const container = $('.assign-items-to-task-modal-container');
if (container.length) {
window.AssignItemsToTaskModalComponentContainer = new Vue({
el: '.assign-items-to-task-modal-container',
name: 'AssignItemsToTaskModalComponent',
components: {
'assign-items-to-task-modal-container': AssignItemsToTaskModalContainer
},
data() {
return {
visibility: false,
urls: {
assign: container.data('assign-url'),
projects: container.data('projects-url'),
experiments: container.data('experiments-url'),
tasks: container.data('tasks-url')
}
};
},
methods: {
closeModal() {
this.visibility = false;
}
}
});
}
}
initAssignItemsToTaskModalComponent();

View file

@ -0,0 +1,290 @@
<template>
<div
ref="modal"
class="modal fade"
id="assign-items-to-task-modal"
tabindex="-1"
role="dialog"
aria-labelledby="assignItemsToTaskModalLabel"
>
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
{{ i18n.t("repositories.modal_assign_items_to_task.title") }}
</h4>
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="description">
{{
i18n.t("repositories.modal_assign_items_to_task.body.description")
}}
</div>
<div class="project-selector level-selector">
<label>
{{
i18n.t(
"repositories.modal_assign_items_to_task.body.project_select.label"
)
}}
</label>
<SelectSearch
ref="projectsSelector"
@change="changeProject"
:options="projects"
:placeholder="
i18n.t(
'repositories.modal_assign_items_to_task.body.project_select.placeholder'
)
"
:searchPlaceholder="
i18n.t(
'repositories.modal_assign_items_to_task.body.project_select.placeholder'
)
"
/>
</div>
<div class="experiment-selector level-selector">
<label>
{{
i18n.t(
"repositories.modal_assign_items_to_task.body.experiment_select.label"
)
}}
</label>
<SelectSearch
:disabled="!selectedProject"
ref="experimentsSelector"
@change="changeExperiment"
:options="experiments"
:placeholder="experimentsSelectorPlaceholder"
:searchPlaceholder="
i18n.t(
'repositories.modal_assign_items_to_task.body.experiment_select.placeholder'
)
"
/>
</div>
<div class="task-selector level-selector">
<label>
{{
i18n.t(
"repositories.modal_assign_items_to_task.body.task_select.label"
)
}}
</label>
<SelectSearch
:disabled="!selectedExperiment"
ref="tasksSelector"
@change="changeTask"
:options="tasks"
:placeholder="
i18n.t(
'repositories.modal_assign_items_to_task.body.task_select.disabled_placeholder'
)
"
:searchPlaceholder="
i18n.t(
'repositories.modal_assign_items_to_task.body.task_select.placeholder'
)
"
/>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-primary"
data-dismiss="modal"
:disabled="!selectedTask"
@click="assign"
>
{{ i18n.t("repositories.modal_assign_items_to_task.assign") }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import SelectSearch from "../shared/select_search.vue";
export default {
name: "AssignItemsToTaskModalContainer",
props: {
visibility: Boolean,
urls: Object
},
data() {
return {
rowsToAssign: [],
projects: [],
experiments: [],
tasks: [],
selectedProject: null,
selectedExperiment: null,
selectedTask: null,
showCallback: null
};
},
components: {
SelectSearch
},
created() {
window.AssignItemsToTaskModalComponent = this;
},
mounted() {
$(this.$refs.modal).on("shown.bs.modal", () => {
$.get(this.projectURL, data => {
if (Array.isArray(data)) {
this.projects = data;
return false;
}
this.projects = [];
});
});
$(this.$refs.modal).on("hidden.bs.modal", () => {
this.$emit("close");
});
},
beforeDestroy() {
delete window.AssignItemsToTaskModalComponent;
},
computed: {
experimentsSelectorPlaceholder() {
if (this.selectedProject) {
return this.i18n.t(
"repositories.modal_assign_items_to_task.body.experiment_select.placeholder"
);
}
return this.i18n.t(
"repositories.modal_assign_items_to_task.body.experiment_select.disabled_placeholder"
);
},
tasksSelectorPlaceholder() {
if (this.selectedExperiment) {
return this.i18n.t(
"repositories.modal_assign_items_to_task.body.task_select.placeholder"
);
}
return this.i18n.t(
"repositories.modal_assign_items_to_task.body.task_select.disabled_placeholder"
);
},
projectURL() {
return `${this.urls.projects}`;
},
experimentURL() {
return `${this.urls.experiments}?project_id=${this.selectedProject ||
""}`;
},
taskURL() {
return `${this.urls.tasks}?experiment_id=${this.selectedExperiment ||
""}`;
},
assignURL() {
return this.urls.assign.replace(":module_id", this.selectedTask);
}
},
watch: {
visibility() {
if (this.visibility) {
this.showModal();
} else {
this.hideModal();
}
}
},
methods: {
showModal() {
$(this.$refs.modal).modal("show");
this.rowsToAssign = this.showCallback();
},
hideModal() {
$(this.$refs.modal).modal("hide");
},
changeProject(value) {
this.selectedProject = value;
this.resetExperimentSelector();
this.resetTaskSelector();
$.get(this.experimentURL, data => {
if (Array.isArray(data)) {
this.experiments = data;
return false;
}
this.experiments = [];
});
},
changeExperiment(value) {
this.selectedExperiment = value;
this.resetTaskSelector();
$.get(this.taskURL, data => {
if (Array.isArray(data)) {
this.tasks = data;
return false;
}
this.tasks = [];
});
},
changeTask(value) {
this.selectedTask = value;
},
resetProjectSelector() {
this.projects = [];
this.selectedProject = null;
},
resetExperimentSelector() {
this.experiments = [];
this.selectedExperiment = null;
},
resetTaskSelector() {
this.tasks = [];
this.selectedTask = null;
},
resetSelectors() {
this.resetTaskSelector();
this.resetExperimentSelector();
this.resetProjectSelector();
},
assign() {
if (!this.selectedTask) return;
$.ajax({
url: this.assignURL,
type: "PATCH",
dataType: "json",
data: { rows_to_assign: this.rowsToAssign }
}).always(() => {
this.resetSelectors();
this.deselectRows();
});
},
setShowCallback(callback) {
this.showCallback = callback;
},
deselectRows() {
$('.repository-row-selector:checked').trigger('click');
}
}
};
</script>

View file

@ -76,6 +76,7 @@
:objectId="parseInt(protocol.id)"
:fieldName="'protocol[description]'"
:lastUpdated="protocol.attributes.updated_at"
:assignableMyModuleId="protocol.attributes.assignable_my_module_id"
:characterLimit="1000000"
@update="updateDescription"
/>
@ -136,6 +137,7 @@
@stepUpdated="refreshProtocolStatus"
@step:insert="updateStepsPosition"
:reorderStepUrl="steps.length > 1 ? urls.reorder_steps_url : null"
:assignableMyModuleId="protocol.attributes.assignable_my_module_id"
/>
</div>
</template>

View file

@ -136,6 +136,7 @@
:element.sync="elements[index]"
:inRepository="inRepository"
:reorderElementUrl="elements.length > 1 ? urls.reorder_elements_url : ''"
:assignableMyModuleId="assignableMyModuleId"
:isNew="element.isNew"
@component:delete="deleteElement"
@update="updateElement"
@ -213,6 +214,10 @@
},
reorderStepUrl: {
required: false
},
assignableMyModuleId: {
type: Number,
required: false
}
},
data() {

View file

@ -104,6 +104,10 @@
isNew: {
type: Boolean,
default: false
},
assignableMyModuleId: {
type: Number,
required: false
}
},
data() {

View file

@ -83,6 +83,10 @@
},
isNew: {
type: Boolean, default: false
},
assignableMyModuleId: {
type: Number,
required: false
}
},
data() {

View file

@ -26,6 +26,7 @@
:objectId="element.attributes.orderable.id"
:fieldName="'step_text[text]'"
:lastUpdated="element.attributes.orderable.updated_at"
:assignableMyModuleId="assignableMyModuleId"
:characterLimit="1000000"
@update="update"
@editingDisabled="disableEditMode"
@ -64,6 +65,10 @@
isNew: {
type: Boolean,
default: false
},
assignableMyModuleId: {
type: Number,
required: false
}
},
data() {

View file

@ -144,7 +144,7 @@
this.$refs.input.select();
}
if (this.smartAnnotation) {
SmartAnnotation.init($(this.$refs.input));
SmartAnnotation.init($(this.$refs.input), false);
}
})
this.$emit('editingEnabled');

View file

@ -48,7 +48,7 @@
setTimeout(() => {
this.isOpen = false;
this.$emit('blur');
}, 100)
}, 200)
},
toggle() {
this.isOpen = !this.isOpen;
@ -70,10 +70,19 @@
this.$emit('change', this.value);
},
updateOptionPosition() {
let rect = this.$refs.container.getBoundingClientRect();
let top =rect.top + rect.height;
let left = rect.left;
const container = this.$refs.container;
const rect = container.getBoundingClientRect();
let width = rect.width;
let top = rect.top + rect.height;
let left = rect.left;
const modal = $(container).parents('.modal-content');
if (modal.length > 0) {
const modalRect = modal.get(0).getBoundingClientRect();
top -= modalRect.top;
left -= modalRect.left;
}
this.optionPositionStyle = `position: fixed; top: ${top}px; left: ${left}px; width: ${width}px`
}

View file

@ -1,5 +1,5 @@
<template>
<Select class="sn-select--search" :options="currentOptions" :placeholder="placeholder" @change="change" @blur="blur" @open="open" @close="close">
<Select class="sn-select--search" :options="currentOptions" :placeholder="placeholder" v-bind:disabled="disabled" @change="change" @blur="blur" @open="open" @close="close">
<input ref="focusElement" v-model="query" type="text" class="sn-select__search-input" :placeholder="searchPlaceholder" />
<span class="sn-select__value">{{ valueLabel || (placeholder || i18n.t('general.select')) }}</span>
<span class="sn-select__caret caret"></span>
@ -15,7 +15,8 @@
options: { type: Array, default: () => [] },
optionsUrl: { type: String },
placeholder: { type: String },
searchPlaceholder: { type: String }
searchPlaceholder: { type: String },
disabled: { type: Boolean }
},
components: { Select },
data() {
@ -41,20 +42,25 @@
} else {
this.currentOptions = this.options.filter((o) => o[1].toLowerCase().includes(this.query.toLowerCase()));
}
},
options() {
this.currentOptions = this.options;
}
},
computed: {
valueLabel() {
let option = this.options.find((o) => o[0] === this.value);
let option = this.currentOptions.find((o) => o[0] === this.value);
return option && option[1];
}
},
methods: {
blur() {
this.isOpen = false;
this.$emit('blur');
},
change(value) {
this.value = value;
this.isOpen = false;
this.$emit('change', this.value);
},
open() {
@ -66,7 +72,7 @@
this.$emit('close');
},
fetchOptions() {
$.get(`${this.optionsUrl}?query=${this.query}`,
$.get(`${this.optionsUrl}?query=${this.query || ''}`,
(data) => {
this.currentOptions = data;
}

View file

@ -68,6 +68,7 @@
fieldName: String,
lastUpdated: Number,
inEditMode: Boolean,
assignableMyModuleId: Number,
characterLimit: {
type: Number,
default: null
@ -141,7 +142,8 @@
this.initCharacterCount();
this.$emit('editingEnabled');
},
placeholder: this.placeholder
placeholder: this.placeholder,
assignableMyModuleId: this.assignableMyModuleId
}
)
},

View file

@ -10,7 +10,7 @@ class ProtocolSerializer < ActiveModel::Serializer
attributes :name, :id, :urls, :description, :description_view, :updated_at, :in_repository,
:created_at_formatted, :updated_at_formatted, :added_by, :authors, :keywords, :version,
:code, :published, :version_comment, :archived, :linked, :has_draft,
:published_on_formatted, :published_by, :created_from_version
:published_on_formatted, :published_by, :created_from_version, :assignable_my_module_id
def updated_at
object.updated_at.to_i
@ -116,6 +116,12 @@ class ProtocolSerializer < ActiveModel::Serializer
object.linked?
end
def assignable_my_module_id
return if in_repository
object.my_module&.id
end
private
def load_from_repo_url

View file

@ -35,19 +35,33 @@ class SmartAnnotation
.limit(Constants::ATWHO_SEARCH_LIMIT + 1)
end
def repository_rows(repository)
def repository_rows(repository, my_module_id)
res = RepositoryRow
.active
.where(repository: repository)
.search_by_name_and_id(@current_user, @current_team, @query)
.limit(Constants::ATWHO_SEARCH_LIMIT + 1)
if my_module_id.present?
res = res.joins('LEFT OUTER JOIN "my_module_repository_rows" "current_my_module_repository_rows"'\
'ON "current_my_module_repository_rows"."repository_row_id" = "repository_rows"."id" '\
'AND "current_my_module_repository_rows"."my_module_id" = ' + my_module_id.to_s)
.select('repository_rows.*',
'CASE WHEN current_my_module_repository_rows.id IS NOT NULL '\
'THEN true ELSE false END as row_assigned')
end
rep_items_list = []
res.each do |rep_row|
rep_item = {}
rep_item[:id] = rep_row.id.base62_encode
rep_item[:id] = rep_row.id
rep_item[:id_encoded] = rep_row.id.base62_encode
rep_item[:name] = escape_input(rep_row.name)
rep_item[:code] = escape_input(rep_row.code)
if my_module_id.present?
rep_item[:row_assigned] = rep_row&.row_assigned
rep_item[:my_module_id] = my_module_id
end
rep_items_list << rep_item
end
rep_items_list

View file

@ -0,0 +1,15 @@
<div
class="assign-items-to-task-modal-container"
data-assign-url="<%= my_module_repository_path(":module_id") %>"
data-projects-url="<%= project_filter_projects_path %>"
data-experiments-url="<%= experiment_filter_experiments_path %>"
data-tasks-url="<%= module_filter_my_modules_path %>"
>
<assign-items-to-task-modal-container
:visibility="visibility"
:urls="urls"
@close="closeModal"
/>
</div>
<%= javascript_include_tag 'vue_repository_assign_items_to_task_modal' %>

View file

@ -111,10 +111,27 @@
<% end %>
<% end %>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t('general.close')%></button>
<button type="button" class="btn btn-primary print-label-button" data-rows="[<%= @repository_row.id %>]"><%= t('repository_row.modal_print_label.print_label') %></button>
<div class="modal-footer" data-assign-item-button="<%= @my_module.present? %>">
<% if @my_module %>
<button type="button" class="btn btn-secondary print-label-button" data-rows="[<%= @repository_row.id %>]"><%= t('repository_row.modal_print_label.print_label') %></button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t('general.close')%></button>
<span <% if @my_module_assign_error.present? %>
data-toggle="tooltip"
data-placement="bottom"
title="<%= @my_module_assign_error %>"
<% end %>
>
<button type="button" class="btn btn-primary assign-inventory-button"
data-assign-url=<%= my_module_repositories_path(@my_module) %>
data-repository-row-id=<%= @repository_row.id %>
<%= 'disabled' if @my_module_assign_error.present? %>>
<%= t('repository_row.modal_info.assign_to_task') %>
</button>
</span>
<% else %>
<button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t('general.close')%></button>
<button type="button" class="btn btn-primary print-label-button" data-rows="[<%= @repository_row.id %>]"><%= t('repository_row.modal_print_label.print_label') %></button>
<% end %>
</div>
</div>
</div>

View file

@ -84,6 +84,7 @@
<%= render partial: 'repository_columns/manage_column_modal', locals: { my_module_page: false } %>
<%= render partial: "repository_stock_values/manage_modal" %>
<%= render partial: "toolbar_buttons" %>
<%= render partial: "assign_items_to_task_modal" %>
<% if @repository.is_a?(BmtRepository) %>
<%= render partial: 'save_bmt_filter_modal' %>

View file

@ -10,6 +10,7 @@
autocomplete: 'off',
data: { object_type: 'result_text',
object_id: @result.result_text.id,
my_module_id: @result.my_module_id,
last_updated: @result.updated_at.to_i * 1000 }) %>
<% end %><br />
<div class="align-right edit-result-texts-buttons'">

View file

@ -9,6 +9,7 @@
autocomplete: 'off',
data: { object_type: 'result_text',
object_id: @result.result_text.id,
my_module_id: @result.my_module_id,
last_updated: @result.updated_at.to_i * 1000 }) %>
<% end %><br />
<div class="align-right new-result-texts-buttons'">

View file

@ -0,0 +1,12 @@
<div class="atwho-button-container">
<button type="button" class="btn icon-btn btn-light atwho-insert-button"><%= I18n.t("atwho.buttons.insert") %></button>
<% if defined?(row) && !row[:row_assigned] && row[:my_module_id].present? %>
<button type="button"
class="btn icon-btn btn-light atwho-assign-button"
data-assign-url= <%= my_module_repositories_path(row[:my_module_id]) %>
data-repository-id=<%= repository[:id] %>
data-repository-row-id=<%= row[:id] %>>
<%= I18n.t("atwho.buttons.assign") %>
</button>
<%end%>
</div>

View file

@ -11,6 +11,7 @@
<span class='sa-type'><%= experiment.code %></span>
<span class="dot">&middot;</span>
<span class="item-text"><%= experiment.name %></span>
<%= render partial: 'shared/smart_annotation/atwho_control_buttons.html.erb' %>
</li>
<% end %>
</div>

View file

@ -13,6 +13,7 @@
<span class='sa-type'><%= task.code %></span>
<span class="dot">&middot;</span>
<span class="item-text"><%= task.name %></span>
<%= render partial: 'shared/smart_annotation/atwho_control_buttons.html.erb' %>
</li>
<% end %>
</div>

View file

@ -5,6 +5,7 @@
<span class='sa-type'><%= project.code %></span>
<span class="dot">&middot;</span>
<span class="item-text"><%= project.name %></span>
<%= render partial: 'shared/smart_annotation/atwho_control_buttons.html.erb' %>
</li>
<% end %>
<% if limit_reached %>

View file

@ -1,10 +1,11 @@
<% limit_reached = repository_rows.length == Constants::ATWHO_SEARCH_LIMIT + 1 %>
<div class="atwho-scroll-container">
<% repository_rows.take(Constants::ATWHO_SEARCH_LIMIT).each do |row| %>
<li class="item" data-name="<%= row[:name] %>" data-id="<%= row[:id] %>" data-type="rep_item">
<li class="item" data-name="<%= row[:name] %>" data-id="<%= row[:id_encoded] %>" data-type="rep_item">
<span class='sa-type'><%= row[:code] %></span>
<span class="dot">&middot;</span>
<span class="item-text"><%= row[:name] %></span>
<%= render partial: 'shared/smart_annotation/atwho_control_buttons.html.erb', locals: { row: row, repository: repository } %>
</li>
<% end %>
<% if limit_reached %>

View file

@ -1041,6 +1041,8 @@ en:
assigned_items:
title: "Assigned items"
assign_from: "Assign from"
direct_assign:
success: "Successfully assigned an item to the task."
protocol:
title: "Protocol"
options_dropdown:
@ -1974,6 +1976,22 @@ en:
columns_delete: "Delete"
columns_changed: "Someone removed/added a new column to the inventory in use. To prevent data inconsistency we will reload this page for you."
columns_visibility: "Visible columns"
modal_assign_items_to_task:
title: "Assign to task"
body:
description: "Type in the fields below to find the right task."
project_select:
label: "Project"
placeholder: "Enter project name"
experiment_select:
label: "Experiment"
placeholder: "Enter Experiment name"
disabled_placeholder: "Select Project to enable Experiment"
task_select:
label: "Task"
placeholder: "Enter Task name"
disabled_placeholder: "Select Experiment to enable Task"
assign: "Assign to this task"
modal_delete_record:
title: "Delete items"
notice: "Are you sure you want to delete the selected item(s)?"
@ -2209,6 +2227,10 @@ en:
no_tasks: "This item in not assigned to any task."
amount: "Amount: %{value}"
unit: "Unit: %{unit}"
assign_to_task: "Assign to this task"
assign_to_task_error:
no_access: "You can only view this task"
already_assigned: "This item is already assigned to this task"
modal_print_label:
head_title: "Print label - %{repository_row}"
head_title_multiple: "Print label - %{repository_rows} rows"
@ -3198,6 +3220,9 @@ en:
repository_rows: "Items with this name/ID were not found"
users: "Users with this name were not found"
description: "Please make sure there are no typos, or erase letters one by one unless you see some results"
buttons:
insert: "Insert"
assign: "Assign to this task"
projects: PROJECTS
experiments: EXPERIMENTS
tasks: TASKS
@ -3206,7 +3231,7 @@ en:
users:
title: "People"
header: "Type the name or email of the user you want to mention (they will be notified)"
help: "Navigate: ↑ ↓ • Submit: Enter / Tab • Dismiss: Esc"
help: "Navigate: ↑ ↓ • Insert: Enter / Tab • Dismiss: Esc"
popover_html: "<span class='silver'>Team:</span>&nbsp;%{team} <br> <span class='silver'>Role:</span>&nbsp;%{role} <br> <span class='silver'>Joined:</span>&nbsp;%{time}"
res:
archived: "(archived)"

View file

@ -344,8 +344,8 @@ Rails.application.routes.draw do
end
resources :experiments, only: %i(new create), defaults: { format: 'json' } do
collection do
post 'archive_group' # archive group of experements
post 'restore_group' # restore group of experementss
post 'archive_group' # archive group of experiments
post 'restore_group' # restore group of experiments
end
end
member do
@ -359,6 +359,7 @@ Rails.application.routes.draw do
end
collection do
get 'project_filter'
get 'cards', to: 'projects#cards'
get 'users_filter'
post 'archive_group'
@ -382,6 +383,7 @@ Rails.application.routes.draw do
resources :experiments, only: %i(show edit update) do
collection do
get 'experiment_filter'
get 'edit', action: :edit
get 'clone_modal', action: :clone_modal
get 'move_modal', action: :move_modal
@ -411,7 +413,7 @@ Rails.application.routes.draw do
post 'clone' # clone experiment
get 'move_modal' # return modal with move options
post 'move' # move experiment
get 'fetch_workflow_img' # Get udated workflow img
get 'fetch_workflow_img' # Get updated workflow img
get 'modules/new', to: 'my_modules#new'
post 'modules', to: 'my_modules#create'
post 'restore_my_modules', to: 'my_modules#restore_group'
@ -427,6 +429,7 @@ Rails.application.routes.draw do
# as well as 'module info' page for single module (HTML)
resources :my_modules, path: '/modules', only: [:show, :update] do
post 'save_table_state', on: :collection, defaults: { format: 'json' }
get 'module_filter', to: 'my_modules#my_module_filter', on: :collection, defaults: { format: 'json' }
collection do
get 'actions_toolbar'
@ -463,7 +466,7 @@ Rails.application.routes.draw do
get :repositories_dropdown_list, controller: :my_module_repositories
get :repositories_list_html, controller: :my_module_repositories
resources :repositories, controller: :my_module_repositories, only: :update do
resources :repositories, controller: :my_module_repositories, only: %i(update create) do
member do
get :full_view_table
post :index_dt

View file

@ -32,6 +32,7 @@ const entryList = {
vue_repository_filter: './app/javascript/packs/vue/repository_filter.js',
vue_repository_search: './app/javascript/packs/vue/repository_search.js',
vue_repository_print_modal: './app/javascript/packs/vue/repository_print_modal.js',
vue_repository_assign_items_to_task_modal: './app/javascript/packs/vue/assign_items_to_task_modal.js',
vue_navigation_top_menu: './app/javascript/packs/vue/navigation/top_menu.js',
vue_navigation_navigator: './app/javascript/packs/vue/navigation/navigator.js',
vue_components_action_toolbar: './app/javascript/packs/vue/action_toolbar.js'