diff --git a/app/assets/javascripts/my_modules/protocols.js b/app/assets/javascripts/my_modules/protocols.js index c96749baa..183dc905c 100644 --- a/app/assets/javascripts/my_modules/protocols.js +++ b/app/assets/javascripts/my_modules/protocols.js @@ -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) { diff --git a/app/assets/javascripts/my_modules/repositories.js b/app/assets/javascripts/my_modules/repositories.js index 6f0701c28..480865a6c 100644 --- a/app/assets/javascripts/my_modules/repositories.js +++ b/app/assets/javascripts/my_modules/repositories.js @@ -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); } }; }()); diff --git a/app/assets/javascripts/my_modules/stock.js b/app/assets/javascripts/my_modules/stock.js index 688b60bbd..fd40c5408 100644 --- a/app/assets/javascripts/my_modules/stock.js +++ b/app/assets/javascripts/my_modules/stock.js @@ -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); diff --git a/app/assets/javascripts/repositories/renderers/new_renderers.js b/app/assets/javascripts/repositories/renderers/new_renderers.js index c626f98d0..7310690dd 100644 --- a/app/assets/javascripts/repositories/renderers/new_renderers.js +++ b/app/assets/javascripts/repositories/renderers/new_renderers.js @@ -33,7 +33,7 @@ $.fn.dataTable.render.newRepositoryTextValue = function(formId, columnId, $cell) data-type="RepositoryTextValue"> `); - SmartAnnotation.init($cell.find('input')); + SmartAnnotation.init($cell.find('input'), false); }; $.fn.dataTable.render.newRepositoryListValue = function(formId, columnId, $cell) { diff --git a/app/assets/javascripts/repositories/repository_datatable.js b/app/assets/javascripts/repositories/repository_datatable.js index a9f4befd8..d391882cf 100644 --- a/app/assets/javascripts/repositories/repository_datatable.js +++ b/app/assets/javascripts/repositories/repository_datatable.js @@ -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(); diff --git a/app/assets/javascripts/repositories/stock.js b/app/assets/javascripts/repositories/stock.js index 306543708..82a43a644 100644 --- a/app/assets/javascripts/repositories/stock.js +++ b/app/assets/javascripts/repositories/stock.js @@ -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( diff --git a/app/assets/javascripts/results/result_texts.js b/app/assets/javascripts/results/result_texts.js index 814d48b41..38ac50000 100644 --- a/app/assets/javascripts/results/result_texts.js +++ b/app/assets/javascripts/results/result_texts.js @@ -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(); }); } diff --git a/app/assets/javascripts/shared/inline_editing.js b/app/assets/javascripts/shared/inline_editing.js index b6f568059..e9950ece1 100644 --- a/app/assets/javascripts/shared/inline_editing.js +++ b/app/assets/javascripts/shared/inline_editing.js @@ -22,7 +22,7 @@ var inlineEditing = (function() { function initSmartAnnotation(container) { if (container.data('smart-annotation')) { - SmartAnnotation.init(inputField(container)); + SmartAnnotation.init(inputField(container), false); } } diff --git a/app/assets/javascripts/sitewide/atwho_res.js b/app/assets/javascripts/sitewide/atwho_res.js index 8f9b81cf3..9e68c28df 100644 --- a/app/assets/javascripts/sitewide/atwho_res.js +++ b/app/assets/javascripts/sitewide/atwho_res.js @@ -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); } }); diff --git a/app/assets/javascripts/sitewide/comments_sidebar.js b/app/assets/javascripts/sitewide/comments_sidebar.js index f59f5075f..d6a35193a 100644 --- a/app/assets/javascripts/sitewide/comments_sidebar.js +++ b/app/assets/javascripts/sitewide/comments_sidebar.js @@ -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); } } diff --git a/app/assets/javascripts/sitewide/repository_row_info_modal.js b/app/assets/javascripts/sitewide/repository_row_info_modal.js index 4d01b7e20..130f9a7b9 100644 --- a/app/assets/javascripts/sitewide/repository_row_info_modal.js +++ b/app/assets/javascripts/sitewide/repository_row_info_modal.js @@ -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'); + } + }); + }); }()); diff --git a/app/assets/stylesheets/repository/assign_items_to_task_modal.scss b/app/assets/stylesheets/repository/assign_items_to_task_modal.scss new file mode 100644 index 000000000..dedd7be94 --- /dev/null +++ b/app/assets/stylesheets/repository/assign_items_to_task_modal.scss @@ -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; + } +} diff --git a/app/assets/stylesheets/repository/repository_row_modal.scss b/app/assets/stylesheets/repository/repository_row_modal.scss index a25126b89..3399f7ec9 100644 --- a/app/assets/stylesheets/repository/repository_row_modal.scss +++ b/app/assets/stylesheets/repository/repository_row_modal.scss @@ -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; + } + } } diff --git a/app/assets/stylesheets/shared/select.scss b/app/assets/stylesheets/shared/select.scss index f31a78c6a..69195c5f9 100644 --- a/app/assets/stylesheets/shared/select.scss +++ b/app/assets/stylesheets/shared/select.scss @@ -83,7 +83,6 @@ max-height: 300px; overflow: hidden; overflow-y: scroll; - position: absolute; top: 2.5em; width: 100%; z-index: 9999; diff --git a/app/assets/stylesheets/shared/smart_annotation.scss b/app/assets/stylesheets/shared/smart_annotation.scss index 7cdb403c6..2faedd274 100644 --- a/app/assets/stylesheets/shared/smart_annotation.scss +++ b/app/assets/stylesheets/shared/smart_annotation.scss @@ -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 { diff --git a/app/controllers/at_who_controller.rb b/app/controllers/at_who_controller.rb index 6c01dc05a..892889aa2 100644 --- a/app/controllers/at_who_controller.rb +++ b/app/controllers/at_who_controller.rb @@ -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 diff --git a/app/controllers/experiments_controller.rb b/app/controllers/experiments_controller.rb index 4ecd67f60..d0c77e685 100644 --- a/app/controllers/experiments_controller.rb +++ b/app/controllers/experiments_controller.rb @@ -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: { diff --git a/app/controllers/my_module_repositories_controller.rb b/app/controllers/my_module_repositories_controller.rb index 5b5bee11f..5a1258762 100644 --- a/app/controllers/my_module_repositories_controller.rb +++ b/app/controllers/my_module_repositories_controller.rb @@ -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, diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index 959b1592c..19a2e415a 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -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 diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index dcdb02ef3..e45eda1fc 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -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| diff --git a/app/controllers/repository_rows_controller.rb b/app/controllers/repository_rows_controller.rb index cec9100d9..9e3c04ccb 100644 --- a/app/controllers/repository_rows_controller.rb +++ b/app/controllers/repository_rows_controller.rb @@ -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) diff --git a/app/javascript/packs/tiny_mce.js b/app/javascript/packs/tiny_mce.js index 8917a3479..863454cb4 100644 --- a/app/javascript/packs/tiny_mce.js +++ b/app/javascript/packs/tiny_mce.js @@ -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(); } diff --git a/app/javascript/packs/vue/assign_items_to_task_modal.js b/app/javascript/packs/vue/assign_items_to_task_modal.js new file mode 100644 index 000000000..7fd3cbc50 --- /dev/null +++ b/app/javascript/packs/vue/assign_items_to_task_modal.js @@ -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(); diff --git a/app/javascript/vue/assign_items_to_tasks_modal/container.vue b/app/javascript/vue/assign_items_to_tasks_modal/container.vue new file mode 100644 index 000000000..fc266fc9d --- /dev/null +++ b/app/javascript/vue/assign_items_to_tasks_modal/container.vue @@ -0,0 +1,290 @@ + + + + + + + {{ i18n.t("repositories.modal_assign_items_to_task.title") }} + + + × + + + + + {{ + i18n.t("repositories.modal_assign_items_to_task.body.description") + }} + + + + + {{ + i18n.t( + "repositories.modal_assign_items_to_task.body.project_select.label" + ) + }} + + + + + + + + {{ + i18n.t( + "repositories.modal_assign_items_to_task.body.experiment_select.label" + ) + }} + + + + + + + + {{ + i18n.t( + "repositories.modal_assign_items_to_task.body.task_select.label" + ) + }} + + + + + + + + + + + + diff --git a/app/javascript/vue/protocol/container.vue b/app/javascript/vue/protocol/container.vue index c32a910f3..f98c61746 100644 --- a/app/javascript/vue/protocol/container.vue +++ b/app/javascript/vue/protocol/container.vue @@ -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" /> diff --git a/app/javascript/vue/protocol/step.vue b/app/javascript/vue/protocol/step.vue index b0b10ba33..28e9603ad 100644 --- a/app/javascript/vue/protocol/step.vue +++ b/app/javascript/vue/protocol/step.vue @@ -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() { diff --git a/app/javascript/vue/protocol/step_elements/checklist.vue b/app/javascript/vue/protocol/step_elements/checklist.vue index 47f87571b..03d07a818 100644 --- a/app/javascript/vue/protocol/step_elements/checklist.vue +++ b/app/javascript/vue/protocol/step_elements/checklist.vue @@ -104,6 +104,10 @@ isNew: { type: Boolean, default: false + }, + assignableMyModuleId: { + type: Number, + required: false } }, data() { diff --git a/app/javascript/vue/protocol/step_elements/table.vue b/app/javascript/vue/protocol/step_elements/table.vue index 3c92349f5..2a369f9f6 100644 --- a/app/javascript/vue/protocol/step_elements/table.vue +++ b/app/javascript/vue/protocol/step_elements/table.vue @@ -83,6 +83,10 @@ }, isNew: { type: Boolean, default: false + }, + assignableMyModuleId: { + type: Number, + required: false } }, data() { diff --git a/app/javascript/vue/protocol/step_elements/text.vue b/app/javascript/vue/protocol/step_elements/text.vue index 4c6f06242..396d11699 100644 --- a/app/javascript/vue/protocol/step_elements/text.vue +++ b/app/javascript/vue/protocol/step_elements/text.vue @@ -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() { diff --git a/app/javascript/vue/shared/inline_edit.vue b/app/javascript/vue/shared/inline_edit.vue index 6ba45790c..d624f096c 100644 --- a/app/javascript/vue/shared/inline_edit.vue +++ b/app/javascript/vue/shared/inline_edit.vue @@ -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'); diff --git a/app/javascript/vue/shared/select.vue b/app/javascript/vue/shared/select.vue index 9cb048dfa..8a3862184 100644 --- a/app/javascript/vue/shared/select.vue +++ b/app/javascript/vue/shared/select.vue @@ -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` } diff --git a/app/javascript/vue/shared/select_search.vue b/app/javascript/vue/shared/select_search.vue index 44827b109..88249ef97 100644 --- a/app/javascript/vue/shared/select_search.vue +++ b/app/javascript/vue/shared/select_search.vue @@ -1,5 +1,5 @@ - + {{ valueLabel || (placeholder || i18n.t('general.select')) }} @@ -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; } diff --git a/app/javascript/vue/shared/tinymce.vue b/app/javascript/vue/shared/tinymce.vue index cc9d85011..7473aa0de 100644 --- a/app/javascript/vue/shared/tinymce.vue +++ b/app/javascript/vue/shared/tinymce.vue @@ -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 } ) }, diff --git a/app/serializers/protocol_serializer.rb b/app/serializers/protocol_serializer.rb index 25c527af0..c8866fa2c 100644 --- a/app/serializers/protocol_serializer.rb +++ b/app/serializers/protocol_serializer.rb @@ -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 diff --git a/app/utilities/smart_annotation.rb b/app/utilities/smart_annotation.rb index 37c72624f..e5a69c3bf 100644 --- a/app/utilities/smart_annotation.rb +++ b/app/utilities/smart_annotation.rb @@ -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 diff --git a/app/views/repositories/_assign_items_to_task_modal.html.erb b/app/views/repositories/_assign_items_to_task_modal.html.erb new file mode 100644 index 000000000..95b51f151 --- /dev/null +++ b/app/views/repositories/_assign_items_to_task_modal.html.erb @@ -0,0 +1,15 @@ +" + data-projects-url="<%= project_filter_projects_path %>" + data-experiments-url="<%= experiment_filter_experiments_path %>" + data-tasks-url="<%= module_filter_my_modules_path %>" + > + + + +<%= javascript_include_tag 'vue_repository_assign_items_to_task_modal' %> diff --git a/app/views/repositories/_repository_row_info_modal.html.erb b/app/views/repositories/_repository_row_info_modal.html.erb index 003acaca6..1ac3f5581 100644 --- a/app/views/repositories/_repository_row_info_modal.html.erb +++ b/app/views/repositories/_repository_row_info_modal.html.erb @@ -111,10 +111,27 @@ <% end %> <% end %> - diff --git a/app/views/repositories/show.html.erb b/app/views/repositories/show.html.erb index 19483a9e9..a8936d576 100644 --- a/app/views/repositories/show.html.erb +++ b/app/views/repositories/show.html.erb @@ -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' %> diff --git a/app/views/result_texts/_edit.html.erb b/app/views/result_texts/_edit.html.erb index 267c51161..2c080e1e8 100644 --- a/app/views/result_texts/_edit.html.erb +++ b/app/views/result_texts/_edit.html.erb @@ -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 %> diff --git a/app/views/result_texts/_new.html.erb b/app/views/result_texts/_new.html.erb index c0931f6c1..b65af3c08 100644 --- a/app/views/result_texts/_new.html.erb +++ b/app/views/result_texts/_new.html.erb @@ -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 %> diff --git a/app/views/shared/smart_annotation/_atwho_control_buttons.html.erb b/app/views/shared/smart_annotation/_atwho_control_buttons.html.erb new file mode 100644 index 000000000..d3e6bbbc4 --- /dev/null +++ b/app/views/shared/smart_annotation/_atwho_control_buttons.html.erb @@ -0,0 +1,12 @@ + + <%= I18n.t("atwho.buttons.insert") %> + <% if defined?(row) && !row[:row_assigned] && row[:my_module_id].present? %> + + data-repository-id=<%= repository[:id] %> + data-repository-row-id=<%= row[:id] %>> + <%= I18n.t("atwho.buttons.assign") %> + + <%end%> + diff --git a/app/views/shared/smart_annotation/_experiment_items.html.erb b/app/views/shared/smart_annotation/_experiment_items.html.erb index fb9202cb3..708c0be54 100644 --- a/app/views/shared/smart_annotation/_experiment_items.html.erb +++ b/app/views/shared/smart_annotation/_experiment_items.html.erb @@ -11,6 +11,7 @@ <%= experiment.code %> · <%= experiment.name %> + <%= render partial: 'shared/smart_annotation/atwho_control_buttons.html.erb' %> <% end %> diff --git a/app/views/shared/smart_annotation/_my_module_items.html.erb b/app/views/shared/smart_annotation/_my_module_items.html.erb index 530570186..1f440577b 100644 --- a/app/views/shared/smart_annotation/_my_module_items.html.erb +++ b/app/views/shared/smart_annotation/_my_module_items.html.erb @@ -13,6 +13,7 @@ <%= task.code %> · <%= task.name %> + <%= render partial: 'shared/smart_annotation/atwho_control_buttons.html.erb' %> <% end %> diff --git a/app/views/shared/smart_annotation/_project_items.html.erb b/app/views/shared/smart_annotation/_project_items.html.erb index a6903364b..bfd57367c 100644 --- a/app/views/shared/smart_annotation/_project_items.html.erb +++ b/app/views/shared/smart_annotation/_project_items.html.erb @@ -5,6 +5,7 @@ <%= project.code %> · <%= project.name %> + <%= render partial: 'shared/smart_annotation/atwho_control_buttons.html.erb' %> <% end %> <% if limit_reached %> diff --git a/app/views/shared/smart_annotation/_repository_items.html.erb b/app/views/shared/smart_annotation/_repository_items.html.erb index dbf9d1473..3c8e47c2b 100644 --- a/app/views/shared/smart_annotation/_repository_items.html.erb +++ b/app/views/shared/smart_annotation/_repository_items.html.erb @@ -1,10 +1,11 @@ <% limit_reached = repository_rows.length == Constants::ATWHO_SEARCH_LIMIT + 1 %> <% repository_rows.take(Constants::ATWHO_SEARCH_LIMIT).each do |row| %> - + <%= row[:code] %> · <%= row[:name] %> + <%= render partial: 'shared/smart_annotation/atwho_control_buttons.html.erb', locals: { row: row, repository: repository } %> <% end %> <% if limit_reached %> diff --git a/config/locales/en.yml b/config/locales/en.yml index a9b70f48b..c1c22bbab 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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: "Team: %{team} Role: %{role} Joined: %{time}" res: archived: "(archived)" diff --git a/config/routes.rb b/config/routes.rb index d49444f4b..128f0808c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/config/webpack/webpack.config.js b/config/webpack/webpack.config.js index 323d18830..747b8438a 100644 --- a/config/webpack/webpack.config.js +++ b/config/webpack/webpack.config.js @@ -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'