Merge branch 'features/navigator-resize' into SCI-9318-making-navigator-resizable

This commit is contained in:
Martin Artnik 2023-10-13 10:33:37 +02:00 committed by GitHub
commit 9c302f186b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
121 changed files with 15992 additions and 7351 deletions

View file

@ -0,0 +1,11 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1629_19712)">
<path d="M15.005 28.1194C11.5882 28.025 8.3431 26.6014 5.95975 24.1513C3.57641 21.7013 2.24294 18.4181 2.24294 15C2.24294 11.5819 3.57641 8.29872 5.95975 5.84865C8.3431 3.39859 11.5882 1.97498 15.005 1.88063V0C12.0373 0 9.13624 0.880028 6.66868 2.5288C4.20112 4.17757 2.27789 6.52103 1.14219 9.26284C0.00650146 12.0046 -0.290648 15.0216 0.288324 17.9323C0.867295 20.843 2.29638 23.5167 4.39487 25.6151C6.49336 27.7136 9.16699 29.1427 12.0777 29.7217C14.9884 30.3007 18.0054 30.0035 20.7472 28.8678C23.489 27.7321 25.8324 25.8089 27.4812 23.3413C29.13 20.8738 30.01 17.9727 30.01 15.005H28.1194C28.1141 18.4815 26.7307 21.8141 24.2724 24.2724C21.8141 26.7307 18.4815 28.1141 15.005 28.1194Z" fill="#EAECF0"/>
<path d="M15.005 0V1.88063C18.4833 1.88592 21.8174 3.27069 24.2759 5.73112C26.7345 8.19156 28.1167 11.5267 28.1194 15.005H30.01C30.01 11.0254 28.4291 7.20885 25.6151 4.39486C22.8012 1.58088 18.9846 0 15.005 0Z" fill="#1D2939"/>
</g>
<defs>
<clipPath id="clip0_1629_19712">
<rect width="30" height="30" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -182,8 +182,14 @@ var MyModuleRepositories = (function() {
targets: 0,
className: 'item-name',
render: function(data, type, row) {
var recordName = "<a href='" + row.recordInfoUrl + "'"
+ "class='record-info-link'>" + data + '</a>';
let recordName;
if (row.recordInfoUrl) {
recordName = `<a href="${row.recordInfoUrl}" class="record-info-link">${data}</a>`;
} else {
recordName = `<div class="inline-block my-2 mx-0">${data}</div>`;
}
if (row.hasActiveReminders) {
recordName = `<div class="dropdown row-reminders-dropdown"
data-row-reminders-url="${row.rowRemindersUrl}" tabindex='-1'>

View file

@ -14,6 +14,31 @@ var DateTimeHelper = (function() {
return ('0' + value).slice(-2);
}
function setDateTimePickerOpeningDirection(event) {
const element = $(event.target);
const dateTimePickerWidget = $('.bootstrap-datetimepicker-widget');
const windowHeight = element.closest('table').offset().top;
const inputTop = element.offset().top;
const pickerHeight = $('.bootstrap-datetimepicker-widget').outerHeight();
if (inputTop - windowHeight > pickerHeight) {
dateTimePickerWidget.addClass('top')
.removeClass('bottom')
.css({
top: 'auto',
bottom: '36px',
});
} else {
dateTimePickerWidget.addClass('bottom')
.removeClass('top')
.css({
top: '36px',
bottom: 'auto',
});
}
}
function recalcTimestamp(date, timeStr) {
if (!isValidTimeStr(timeStr)) {
date.setHours(0);
@ -221,7 +246,11 @@ var DateTimeHelper = (function() {
hourFormat: 24
}).mask($cell.find('input[data-mask-type="time"]'));
$cell.find('.calendar-input').datetimepicker({ ignoreReadonly: true, locale: 'en', format: formatJS });
$cell.find('.calendar-input')
.datetimepicker({ ignoreReadonly: true, locale: 'en', format: formatJS })
.on('dp.show', (e) => {
setDateTimePickerOpeningDirection(e);
});
initChangeEvents($cell);
}
@ -273,9 +302,13 @@ var DateTimeHelper = (function() {
$cal1.on('dp.change', function(e) {
$cal2.data('DateTimePicker').minDate(e.date);
}).on('dp.show', (e) => {
setDateTimePickerOpeningDirection(e);
});
$cal2.on('dp.change', function(e) {
$cal1.data('DateTimePicker').maxDate(e.date);
}).on('dp.show', (e) => {
setDateTimePickerOpeningDirection(e);
});
initChangeEvents($cell);

View file

@ -284,10 +284,6 @@ var RepositoryDatatable = (function(global) {
});
}
function updateSelectedRowsForAssignments() {
window.AssignItemsToTaskModalComponent.setShowCallback(() => rowsSelected);
}
function checkAvailableColumns() {
$.ajax({
url: $(TABLE_ID).data('available-columns'),
@ -817,6 +813,11 @@ var RepositoryDatatable = (function(global) {
initRepositoryViewSwitcher();
DataTableHelpers.initLengthAppearance($(TABLE_ID).closest('.dataTables_wrapper'));
$('.dataTables_wrapper').on('click', '.pagination', () => {
const dataTablesScrollBody = document.querySelector('.dataTables_scrollBody');
dataTablesScrollBody.scrollTo(0, 0);
});
$('.dataTables_filter').addClass('hidden');
addRepositorySearch();
@ -882,7 +883,6 @@ var RepositoryDatatable = (function(global) {
})
initRowSelection();
updateSelectedRowsForAssignments();
return TABLE;
}
@ -1018,7 +1018,7 @@ var RepositoryDatatable = (function(global) {
e.preventDefault();
e.stopPropagation();
window.AssignItemsToTaskModalComponentContainer.showModal();
window.AssignItemsToTaskModalComponentContainer.showModal(rowsSelected);
})
.on('click', '#deleteRepositoryRecords', function(e) {
e.preventDefault();

View file

@ -17,7 +17,8 @@ var RepositoryDateColumnType = (function() {
$modal.on('change', `${columnContainer} #date-reminder, ${columnContainer} #date-range`, function() {
let reminderCheckbox = $(columnContainer).find('#date-reminder');
let rangeCheckbox = $(columnContainer).find('#date-range');
rangeCheckbox.attr('disabled', reminderCheckbox.is(':checked'));
const isExistingRecord = $('#new-repo-column-submit').css('display') === 'none';
rangeCheckbox.attr('disabled', isExistingRecord || reminderCheckbox.is(':checked'));
reminderCheckbox.attr('disabled', rangeCheckbox.is(':checked'));
$(columnContainer).find('.reminder-group').toggleClass('hidden', !reminderCheckbox.is(':checked'));
});

View file

@ -17,7 +17,8 @@ var RepositoryDateTimeColumnType = (function() {
$modal.on('change', `${columnContainer} #datetime-reminder, ${columnContainer} #datetime-range`, function() {
let reminderCheckbox = $(columnContainer).find('#datetime-reminder');
let rangeCheckbox = $(columnContainer).find('#datetime-range');
rangeCheckbox.attr('disabled', reminderCheckbox.is(':checked'));
const isExistingRecord = $('#new-repo-column-submit').css('display') === 'none';
rangeCheckbox.attr('disabled', isExistingRecord || reminderCheckbox.is(':checked'));
reminderCheckbox.attr('disabled', rangeCheckbox.is(':checked'));
$(columnContainer).find('.reminder-group').toggleClass('hidden', !reminderCheckbox.is(':checked'));
});

View file

@ -1,6 +1,6 @@
/* global I18n HelperModule truncateLongString animateSpinner RepositoryListColumnType RepositoryStockColumnType */
/* global RepositoryDatatable RepositoryStatusColumnType RepositoryChecklistColumnType dropdownSelector RepositoryDateTimeColumnType */
/* global RepositoryDateColumnType RepositoryDatatable */
/* global RepositoryDateColumnType RepositoryDatatable _ */
/* eslint-disable no-restricted-globals */
@ -297,6 +297,8 @@ var RepositoryColumns = (function() {
} else {
thederName = el.innerText;
}
thederName = _.escape(thederName);
if (['row-name', 'archived-by', 'archived-on'].includes(el.id)) {
visClass = '';
visText = '';

View file

@ -31,6 +31,15 @@
});
}
function initResultComments() {
$(document).on('click', '.shareable-link-open-comments-sidebar', function(e) {
e.preventDefault();
$('.comments-sidebar').removeClass('open');
$($(this).data('objectTarget')).addClass('open');
});
}
function initResultsExpandCollapse() {
$(document).on('click', '#results-collapse-btn', function() {
$('.result .panel-collapse').collapse('hide');
@ -44,6 +53,7 @@
function initMyModuleResultsShow() {
initAttachments();
initResultsExpandCollapse();
initResultComments();
$('.hot-table-container').each(function() {
initializeHandsonTable($(this));

View file

@ -200,6 +200,8 @@ var MarvinJsEditorApi = (function() {
$('#modal_link' + json.id + ' .attachment-label').text(json.file_name);
}
$(marvinJsModal).modal('hide');
config.editor.focus();
config.button.dataset.inProgress = false;
if (MarvinJsEditor.saveCallback) MarvinJsEditor.saveCallback();

View file

@ -0,0 +1,72 @@
/* global PrintModalComponent RepositoryDatatable HelperModule MyModuleRepositories */
(function() {
'use strict';
$(document).on('click', '.record-info-link', function(e) {
const myModuleId = $('.my-modules-protocols-index').data('task-id');
const repositoryRowURL = $(this).attr('href');
e.stopPropagation();
e.preventDefault();
window.repositoryItemSidebarComponent.toggleShowHideSidebar(repositoryRowURL, myModuleId);
});
$(document).on('click', '.print-label-button', function(e) {
var selectedRows = $(this).data('rows');
e.preventDefault();
e.stopPropagation();
if (typeof PrintModalComponent !== 'undefined') {
PrintModalComponent.showModal = true;
if (selectedRows && selectedRows.length) {
$('#modal-info-repository-row').modal('hide');
PrintModalComponent.row_ids = selectedRows;
} else {
PrintModalComponent.row_ids = [...RepositoryDatatable.selectedRows()];
}
}
});
$(document).on('click', '.assign-inventory-button', function(e) {
e.preventDefault();
const assignUrl = $(this).attr('data-assign-url');
const repositoryRowId = $(this).attr('data-repository-row-id');
$.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);
}
window.repositoryItemSidebarComponent.reload();
},
error: function(error) {
HelperModule.flashAlertMsg(error.responseJSON.flash, 'danger');
}
});
});
$(document).on('click', '.export-consumption-button', function(e) {
const selectedRows = $(this).data('rows') || RepositoryDatatable.selectedRows();
e.preventDefault();
window.initExportStockConsumptionModal();
if (window.exportStockConsumptionModalComponent) {
window.exportStockConsumptionModalComponent.fetchRepositoryData(
selectedRows,
{ repository_id: $(this).data('objectId') },
);
$('#modal-info-repository-row').modal('hide');
}
});
}());

View file

@ -1,125 +0,0 @@
/* 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, #results').length) {
params.my_module_id = $('.my-modules-protocols-index, #results').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) {
$('#modal-info-repository-row').find('.modal-body #repository_row-info-table').DataTable().destroy();
$('#modal-info-repository-row').remove();
$('.modal-backdrop').remove();
}
$('body').append($.parseHTML(data.responseJSON.html));
$('[data-toggle="tooltip"]').tooltip();
$('#modal-info-repository-row').modal('show', {
backdrop: true,
keyboard: false
}).on('hidden.bs.modal', function() {
$(this).find('.modal-body #repository_row-info-table').DataTable().destroy();
$(this).remove();
});
let barCodeCanvas = bwipjs.toCanvas('bar-code-canvas', {
bcid: 'qrcode',
text: $('#modal-info-repository-row #bar-code-canvas').data('id').toString(),
scale: 3
});
$('#modal-info-repository-row #bar-code-image').attr('src', barCodeCanvas.toDataURL('image/png'));
$('#repository_row-info-table').DataTable({
dom: 'RBltpi',
stateSave: false,
buttons: [],
processing: true,
colReorder: {
fixedColumnsLeft: 1000000 // Disable reordering
},
columnDefs: [{
targets: 0,
searchable: false,
orderable: false
}],
fnDrawCallback: function(settings, json) {
animateSpinner(this, false);
},
preDrawCallback: function(settings) {
animateSpinner(this);
}
});
});
e.preventDefault();
return false;
});
$(document).on('click', '.export-consumption-button', function() {
let selectedRows = [];
if ($(this).attr('id') === 'exportStockConsumptionButton') {
selectedRows = RepositoryDatatable.selectedRows();
} else {
selectedRows = $('#modal-info-repository-row .print-label-button').data('rows');
}
window.initExportStockConsumptionModal();
if (window.exportStockConsumptionModalComponent) {
window.exportStockConsumptionModalComponent.fetchRepositoryData(
selectedRows,
{ repository_id: $(this).data('objectId') },
);
$('#modal-info-repository-row').modal('hide');
}
});
$(document).on('click', '.print-label-button', function(e) {
var selectedRows = $(this).data('rows');
e.preventDefault();
e.stopPropagation();
if (typeof PrintModalComponent !== 'undefined') {
PrintModalComponent.showModal = true;
if (selectedRows && selectedRows.length) {
$('#modal-info-repository-row').modal('hide');
PrintModalComponent.row_ids = selectedRows;
} else {
PrintModalComponent.row_ids = [...RepositoryDatatable.selectedRows()];
}
}
});
$(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

@ -2,6 +2,7 @@
@import "tailwind/buttons";
@import "tailwind/modals";
@import "tailwind/flyouts";
@import "tailwind/loader.css";
@tailwind base;
@tailwind components;

View file

@ -103,14 +103,3 @@
color: var(--sn-grey);
}
}
@media screen and (max-width: 1395px) {
.task-section-header {
height: 7.44rem;
}
.protocol-buttons-group {
flex-wrap: wrap;
margin: 1rem;
}
}

View file

@ -288,9 +288,6 @@
.dropdown-menu {
@include font-button;
min-width: 200px;
padding: .5em 0;
z-index: 102;
.divider-label {
@include font-small;

View file

@ -2,16 +2,6 @@
// scss-lint:disable NestingDepth
.step-checklist-items {
.sci-inline-edit {
margin-top: 5px;
.sci-inline-edit__content {
margin-bottom: 7px;
margin-left: 0;
margin-right: .5em;
}
}
.step-checklist-item {
padding-left: 5px;
}
@ -59,14 +49,6 @@
.step-checklist-item-ghost {
border: 1px solid $brand-primary;
}
.sci-checkbox-container {
margin: 11px 0;
&.disabled {
pointer-events: none;
}
}
}
.step-checklist-container {

View file

@ -137,4 +137,18 @@
.btn.btn-danger.disabled {
@apply bg-sn-delete-red-disabled;
}
.btn-text-link {
@apply text-sn-blue text-sm cursor-pointer
}
.btn-text-link:visited,
.btn-text-link:hover {
@apply text-sn-blue no-underline
}
.btn-text-link.disabled,
.btn-text-link:disabled {
@apply text-sn-sleepy-grey
}
}

View file

@ -0,0 +1,6 @@
@layer components {
.sci-loader {
@apply flex m-auto h-[30px] w-[30px] animate-spin;
background: image-url("sn-loader.svg") center center no-repeat;
}
}

View file

@ -112,6 +112,6 @@ class CommentsController < ApplicationController
end
def check_manage_permissions
comment_editable?(@comment)
render_403 unless comment_editable?(@comment)
end
end

View file

@ -350,7 +350,7 @@ class ProjectsController < ApplicationController
def notifications
@modules = @project.assigned_modules(current_user).order(due_date: :desc)
render json: {
html: render_to_string(partial: 'notifications')
html: render_to_string(partial: 'notifications', formats: :html)
}
end

View file

@ -10,7 +10,7 @@ class RepositoryRowsController < ApplicationController
before_action :load_repository_row_print, only: %i(print rows_to_print print_zpl validate_label_template_columns)
before_action :load_repository_or_snapshot, only: %i(print rows_to_print print_zpl validate_label_template_columns)
before_action :load_repository_row, only: %i(update assigned_task_list active_reminder_repository_cells)
before_action :check_read_permissions, except: %i(show create update delete_records
before_action :check_read_permissions, except: %i(create update delete_records
copy_records reminder_repository_cells
delete_records archive_records restore_records
actions_toolbar)
@ -42,6 +42,28 @@ class RepositoryRowsController < ApplicationController
render json: { custom_error: I18n.t('repositories.show.repository_filter.errors.value_not_found') }
end
def show
@repository_row = @repository.repository_rows.find_by(id: params[:id])
return render_404 unless @repository_row
@my_module = if params[:my_module_id].present?
MyModule.repository_row_assignable_by_user(current_user).find_by(id: params[:my_module_id])
end
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)
@reminders_present = @repository_row.repository_cells.with_active_reminder(@current_user).any?
end
def create
service = RepositoryRows::CreateRepositoryRowService
.call(repository: @repository, user: current_user, params: update_params)
@ -83,7 +105,7 @@ class RepositoryRowsController < ApplicationController
@private_modules = @assigned_modules - @viewable_modules
render json: {
html: render_to_string(partial: 'repositories/repository_row_info_modal')
html: render_to_string(partial: 'repositories/repository_row_info_modal', formats: :html)
}
end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class RepositoryStockValuesController < ApplicationController
include RepositoryDatatableHelper # for use of display_cell_value method on stock update
include RepositoryDatatableHelper # for use of serialize_repository_cell_value method on stock update
before_action :load_vars
before_action :check_manage_permissions
@ -51,7 +51,7 @@ class RepositoryStockValuesController < ApplicationController
stock_managable: true,
stock_status: @repository_stock_value.status,
manageStockUrl: edit_repository_stock_repository_repository_row_url(@repository, @repository_row)
}.merge(display_cell_value(@repository_stock_value.repository_cell, current_team, @repository))
}.merge(serialize_repository_cell_value(@repository_stock_value.repository_cell, current_team, @repository))
end
private

View file

@ -6,7 +6,7 @@ class TeamRepositoriesController < ApplicationController
# DELETE :team_id/repositories/:repository_id/team_repositories/:id
def destroy
team_shared_object = @repository.team_shared_objects.find(destory_params[:id])
team_shared_object = @repository.team_shared_objects.find(destroy_params[:id])
ActiveRecord::Base.transaction do
log_activity(:unshare_inventory, team_shared_object)
team_shared_object.destroy!
@ -50,7 +50,7 @@ class TeamRepositoriesController < ApplicationController
params.permit(:team_id, :repository_id, :target_team_id, :permission_level)
end
def destory_params
def destroy_params
params.permit(:team_id, :id)
end

View file

@ -144,7 +144,7 @@ module ApplicationHelper
popover_for_user_name(user, team, false, false, base64_encoded_imgs)
end
new_text
sanitize_input(new_text)
end
# Generate smart annotation link for one user object

View file

@ -4,8 +4,16 @@ require 'sanitize'
require 'cgi'
module InputSanitizeHelper
def sanitize_input(html, _tags = [], _attributes = [], sanitizer_config: Constants::INPUT_SANITIZE_CONFIG)
Sanitize.fragment(html, sanitizer_config).html_safe
def sanitize_input(html, _tags = [], _attributes = [], sanitizer_config: nil)
config =
if Rails.application.config.x.custom_sanitizer_config.present?
Rails.application.config.x.custom_sanitizer_config
elsif sanitizer_config.present?
sanitizer_config
else
Constants::INPUT_SANITIZE_CONFIG
end
Sanitize.fragment(html, config).html_safe
end
def escape_input(text)

View file

@ -112,4 +112,53 @@ module MyModulesHelper
''
end
end
def serialize_assigned_my_module_value(my_module)
[
serialize_assigned_my_module_team_data(my_module.team),
serialize_assigned_my_module_project_data(my_module.project),
serialize_assigned_my_module_experiment_data(my_module.experiment),
serialize_assigned_my_module_data(my_module)
]
end
private
def serialize_assigned_my_module_team_data(team)
{
type: team.class.name.underscore,
value: team.name,
url: projects_path(team: team.id),
archived: false
}
end
def serialize_assigned_my_module_project_data(project)
archived = project.archived?
{
type: project.class.name.underscore,
value: project.name,
url: project_path(project, view_mode: archived ? 'archived' : 'active'),
archived: archived
}
end
def serialize_assigned_my_module_experiment_data(experiment)
archived = experiment.archived_branch?
{
type: experiment.class.name.underscore,
value: experiment.name,
url: archived ? module_archive_experiment_path(experiment) : my_modules_experiment_path(experiment),
archived: archived
}
end
def serialize_assigned_my_module_data(my_module)
{
type: my_module.class.name.underscore,
value: my_module.name,
url: protocols_my_module_path(my_module, view_mode: my_module.archived_branch? ? 'archived' : 'active'),
archived: my_module.archived_branch?
}
end
end

View file

@ -43,7 +43,7 @@ module RepositoryDatatableHelper
custom_cells.each do |cell|
row[columns_mappings[cell.repository_column.id]] =
display_cell_value(cell, team, repository, reminders_enabled: reminders_enabled)
serialize_repository_cell_value(cell, team, repository, reminders_enabled: reminders_enabled)
end
if has_stock_management
@ -64,7 +64,12 @@ module RepositoryDatatableHelper
stock_cell = record.repository_cells.find { |cell| cell.value_type == 'RepositoryStockValue' }
# always add stock cell, even if empty
row['stock'] = stock_cell.present? ? display_cell_value(record.repository_stock_cell, team, repository) : {}
row['stock'] =
if stock_cell.present?
serialize_repository_cell_value(record.repository_stock_cell, team, repository)
else
{}
end
row['stock'][:stock_managable] = stock_managable
row['stock']['displayWarnings'] = display_stock_warnings?(repository)
row['stock'][:stock_status] = stock_cell&.value&.status
@ -114,7 +119,6 @@ module RepositoryDatatableHelper
DT_RowId: record.id,
DT_RowAttr: { 'data-state': row_style(record) },
'0': escape_input(record.name),
recordInfoUrl: Rails.application.routes.url_helpers.repository_repository_row_path(record.repository, record),
rowRemindersUrl:
Rails.application.routes.url_helpers
.active_reminder_repository_cells_repository_repository_row_url(
@ -123,6 +127,11 @@ module RepositoryDatatableHelper
)
}
unless record.repository.is_a?(RepositorySnapshot)
row['recordInfoUrl'] = Rails.application.routes.url_helpers.repository_repository_row_path(record.repository,
record)
end
if reminders_enabled
row['hasActiveReminders'] = record.has_active_stock_reminders || record.has_active_datetime_reminders
end
@ -132,7 +141,12 @@ module RepositoryDatatableHelper
consumption_managable = stock_consumption_managable?(record, repository, my_module)
row['stock'] = stock_present ? display_cell_value(record.repository_stock_cell, record.repository.team, repository) : {}
row['stock'] =
if stock_present
serialize_repository_cell_value(record.repository_stock_cell, record.repository.team, repository)
else
{}
end
row['stock']['displayWarnings'] = display_stock_warnings?(repository)
row['stock'][:stock_status] = record.repository_stock_cell&.value&.status
@ -140,7 +154,9 @@ module RepositoryDatatableHelper
if record.repository.is_a?(RepositorySnapshot)
row['consumedStock'] =
if record.repository_stock_consumption_value.present?
display_cell_value(record.repository_stock_consumption_cell, record.repository.team, repository)
serialize_repository_cell_value(record.repository_stock_consumption_cell,
record.repository.team,
repository)
else
{}
end
@ -184,24 +200,26 @@ module RepositoryDatatableHelper
'2': escape_input(record.name),
'3': I18n.l(record.created_at, format: :full),
'4': escape_input(record.created_by.full_name),
'recordInfoUrl': Rails.application.routes.url_helpers.repository_repository_row_path(repository_snapshot, record)
'recordInfoUrl': Rails.application.routes.url_helpers
.repository_repository_row_path(repository_snapshot, record)
}
# Add custom columns
record.repository_cells.each do |cell|
row[columns_mappings[cell.repository_column.id]] = display_cell_value(cell, team, repository_snapshot)
row[columns_mappings[cell.repository_column.id]] =
serialize_repository_cell_value(cell, team, repository_snapshot)
end
if has_stock_management
row['stock'] = if record.repository_stock_cell.present?
display_cell_value(record.repository_stock_cell, team, repository_snapshot)
serialize_repository_cell_value(record.repository_stock_cell, team, repository_snapshot)
else
{ value_type: 'RepositoryStockValue' }
end
row['consumedStock'] =
if record.repository_stock_consumption_cell.present?
display_cell_value(record.repository_stock_consumption_cell, team, repository_snapshot)
serialize_repository_cell_value(record.repository_stock_consumption_cell, team, repository_snapshot)
else
{}
end
@ -220,13 +238,6 @@ module RepositoryDatatableHelper
}
end
def can_perform_repository_actions(repository)
can_read_repository?(repository) ||
can_manage_repository?(repository) ||
can_create_repositories?(repository.team) ||
can_manage_repository_rows?(repository)
end
def repository_default_columns(record)
{
'1': assigned_row(record),
@ -252,7 +263,7 @@ module RepositoryDatatableHelper
}
end
def display_cell_value(cell, team, repository, options = {})
def serialize_repository_cell_value(cell, team, repository, options = {})
serializer_class = "RepositoryDatatable::#{cell.repository_column.data_type}Serializer".constantize
serializer_class.new(
cell.value,

View file

@ -20,6 +20,7 @@ function initAssignItemsToTaskModalComponent() {
data() {
return {
visibility: false,
rowsToAssign: [],
urls: {
assign: container.data('assign-url'),
projects: container.data('projects-url'),
@ -29,7 +30,8 @@ function initAssignItemsToTaskModalComponent() {
};
},
methods: {
showModal() {
showModal(repositoryRows) {
this.rowsToAssign = repositoryRows;
this.visibility = true;
},
closeModal() {

View file

@ -3,16 +3,13 @@
// eg v-click-outside="{handler: 'handlerToTrigger', exclude: [refs to ignore on click (eg 'searchInput', 'searchInputBtn')]}"
// eslint-enable-next-line max-len
let handleOutsideClick;
export default {
bind(el, binding, vnode) {
const { handler, exclude } = binding.value;
handleOutsideClick = (e) => {
el._vueClickOutside_ = (e) => {
e.stopPropagation();
let clickedOnExcludedEl = false;
const { exclude } = binding.value;
exclude.forEach(refName => {
if (!clickedOnExcludedEl) {
const excludedEl = vnode.context.$refs[refName];
@ -21,15 +18,17 @@ export default {
});
if (!el.contains(e.target) && !clickedOnExcludedEl) {
const { handler } = binding.value;
vnode.context[handler]();
}
};
document.addEventListener('click', handleOutsideClick);
document.addEventListener('touchstart', handleOutsideClick);
document.addEventListener('click', el._vueClickOutside_);
document.addEventListener('touchstart', el._vueClickOutside_);
},
unbind() {
document.removeEventListener('click', handleOutsideClick);
document.removeEventListener('touchstart', handleOutsideClick);
unbind(el) {
document.removeEventListener('click', el._vueClickOutside_);
document.removeEventListener('touchstart', el._vueClickOutside_);
el._vueClickOutside_ = null;
}
};

View file

@ -0,0 +1,21 @@
/* global notTurbolinksPreview */
import TurbolinksAdapter from 'vue-turbolinks';
import ScrollSpy from 'vue2-scrollspy';
import Vue from 'vue/dist/vue.esm';
import RepositoryItemSidebar from '../../vue/repository_item_sidebar/RepositoryItemSidebar.vue';
Vue.use(TurbolinksAdapter);
Vue.use(ScrollSpy);
Vue.prototype.i18n = window.I18n;
function initRepositoryItemSidebar() {
new Vue({
el: '#repositoryItemSidebar',
components: {
RepositoryItemSidebar
}
});
}
initRepositoryItemSidebar();

View file

@ -145,11 +145,11 @@ export default {
name: "AssignItemsToTaskModalContainer",
props: {
visibility: Boolean,
urls: Object
urls: Object,
rowsToAssign: Array
},
data() {
return {
rowsToAssign: [],
projects: [],
experiments: [],
tasks: [],
@ -159,15 +159,11 @@ export default {
projectsLoading: null,
experimentsLoading: null,
tasksLoading: null,
showCallback: null
};
},
components: {
SelectSearch
},
created() {
window.AssignItemsToTaskModalComponent = this;
},
mounted() {
$(this.$refs.modal).on("shown.bs.modal", () => {
this.projectsLoading = true;
@ -239,8 +235,6 @@ export default {
methods: {
showModal() {
$(this.$refs.modal).modal("show");
this.rowsToAssign = this.showCallback();
},
hideModal() {
$(this.$refs.modal).modal("hide");
@ -317,11 +311,9 @@ export default {
}).always(() => {
this.resetSelectors();
this.reloadTable();
window.repositoryItemSidebarComponent.reload();
});
},
setShowCallback(callback) {
this.showCallback = callback;
},
reloadTable() {
$('.repository-row-selector:checked').trigger('click');
$('.repository-table')

View file

@ -3,10 +3,12 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<button type="button" class="close" data-dismiss="modal" :aria-label="i18n.t('general.close')">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title">{{ i18n.t(`protocols.import_modal.${state}.title`) }}</h4>
</div>
<div class="modal-body text-xs" v-html="i18n.t(`protocols.import_modal.${state}.body_html`, { url: protocolTemplateTableUrl })">
<div class="modal-body text-sm" v-html="i18n.t(`protocols.import_modal.${state}.body_html`, { url: protocolTemplateTableUrl })">
</div>
<div class="modal-footer">
<button v-if="state === 'confirm'" type="button"

View file

@ -0,0 +1,352 @@
<template>
<div ref="wrapper"
class='bg-white overflow-auto gap-2.5 self-stretch rounded-tl-4 rounded-bl-4 transition-transform ease-in-out transform shadow-lg'
:class="{ 'translate-x-0 w-[565px] h-full': isShowing, 'transition-transform ease-in-out duration-400 transform translate-x-0 translate-x-full w-0': !isShowing }">
<div id="repository-item-sidebar" class="w-full h-auto pb-6 px-6 bg-white flex flex-col">
<div id="sticky-header-wrapper" class="sticky top-0 right-0 bg-white flex z-50 flex-col h-[102px] pt-6">
<div class="header flex w-full h-[30px]">
<h4 class="item-name my-auto truncate" :title="defaultColumns?.name">
{{ defaultColumns?.archived ? i18n.t('labels.archived') : '' }}
{{ defaultColumns?.name }}
</h4>
<i id="close-icon" @click="toggleShowHideSidebar(currentItemUrl)"
class="sn-icon sn-icon-close ml-auto cursor-pointer my-auto mx-0"></i>
</div>
<div id="divider" class="w-500 bg-sn-light-grey flex items-center self-stretch h-px my-6"></div>
</div>
<div v-if="dataLoading" class="h-full flex flex-grow-1">
<div class="sci-loader"></div>
</div>
<div v-else id="body-wrapper" class="flex flex-1 flex-grow-1 justify-between">
<div id="left-col" class="flex flex-col gap-4">
<!-- INFORMATION -->
<div id="information">
<div ref="information-label" id="information-label"
class="font-inter text-base font-semibold leading-7 mb-4 transition-colors duration-300">{{
i18n.t('repositories.item_card.section.information') }}
</div>
<div>
<div class="flex flex-col gap-4">
<!-- REPOSITORY NAME -->
<div class="flex flex-col ">
<span class="inline-block font-semibold pb-[6px]">{{
i18n.t('repositories.item_card.default_columns.repository_name') }}</span>
<span class="repository-name flex text-sn-dark-grey" :title="repository?.name">
{{ repository?.name }}
</span>
</div>
<div class="sci-divider"></div>
<!-- CODE -->
<div class="flex flex-col ">
<span class="inline-block font-semibold pb-[6px]">{{ i18n.t('repositories.item_card.default_columns.id')
}}</span>
<span class="inline-block text-sn-dark-grey" :title="defaultColumns?.code">
{{ defaultColumns?.code }}
</span>
</div>
<div class="sci-divider"></div>
<!-- ADDED ON -->
<div class="flex flex-col ">
<span class="inline-block font-semibold pb-[6px]">{{
i18n.t('repositories.item_card.default_columns.added_on')
}}</span>
<span class="inline-block text-sn-dark-grey" :title="defaultColumns?.added_on">
{{ defaultColumns?.added_on }}
</span>
</div>
<div class="sci-divider"></div>
<!-- ADDED BY -->
<div class="flex flex-col ">
<span class="inline-block font-semibold pb-[6px]">{{
i18n.t('repositories.item_card.default_columns.added_by')
}}</span>
<span class="inline-block text-sn-dark-grey" :title="defaultColumns?.added_by">
{{ defaultColumns?.added_by }}
</span>
</div>
</div>
</div>
</div>
<div id="divider" class="w-500 bg-sn-light-grey flex items-center self-stretch h-px "></div>
<!-- CUSTOM COLUMNS, ASSIGNED, QR CODE -->
<div id="custom-col-assigned-qr-wrapper" class="flex flex-col gap-4">
<!-- CUSTOM COLUMNS -->
<div id="custom-columns-wrapper" class="flex flex-col min-h-[64px] h-auto">
<div ref="custom-columns-label" id="custom-columns-label"
class="font-inter text-base font-semibold leading-7 pb-4 transition-colors duration-300">
{{ i18n.t('repositories.item_card.custom_columns_label') }}
</div>
<div v-if="customColumns?.length > 0" class="flex flex-col gap-4 w-[350px] h-auto">
<div v-for="(column, index) in customColumns" class="flex flex-col gap-4 w-[350px] h-auto relative">
<span class="absolute right-2 top-6" v-if="column?.value?.reminder === true">
<Reminder :value="column?.value" :valueType="column?.value_type" />
</span>
<component :is="column?.data_type" :key="index" :data_type="column?.data_type" :colId="column?.id"
:colName="column?.name" :colVal="column?.value" :repositoryRowId="repositoryRowId"
:repositoryId="repository?.id"
:permissions="permissions" @closeSidebar="toggleShowHideSidebar(null)" />
<div class="sci-divider" :class="{ 'hidden': index === customColumns?.length - 1 }"></div>
</div>
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t('repositories.item_card.no_custom_columns_label') }}
</div>
</div>
<div id="divider" class="w-500 bg-sn-light-grey flex px-8 items-center self-stretch h-px"></div>
<!-- ASSIGNED -->
<section id="assigned_wrapper" class="flex flex-col">
<div class="flex flex-row text-base font-semibold w-[350px] pb-4 leading-7 items-center justify-between" ref="assigned-label">
{{ i18n.t('repositories.item_card.section.assigned', {
count: assignedModules ?
assignedModules.total_assigned_size : 0
}) }}
<a v-if="actions?.assign_repository_row || (inRepository && !defaultColumns?.archived)"
class="btn-text-link font-normal"
:class= "{'assign-inventory-button': actions?.assign_repository_row,
'disabled': actions?.assign_repository_row && actions.assign_repository_row.disabled }"
:data-assign-url="actions?.assign_repository_row ? actions.assign_repository_row.assign_url : ''"
:data-repository-row-id="repositoryRowId"
@click="showRepositoryAssignModal">
{{ i18n.t('repositories.item_card.assigned.assign') }}
</a>
</div>
<div v-if="assignedModules && assignedModules.total_assigned_size > 0">
<div v-if="privateModuleSize() > 0" class="pb-6">
{{ i18n.t('repositories.item_card.assigned.private', { count: privateModuleSize() }) }}
<hr v-if="assignedModules.viewable_modules.length > 0"
class="h-1 w-[350px] m-0 mt-6 border-dashed border-1 border-sn-light-grey" />
</div>
<div v-for="(assigned, index) in assignedModules.viewable_modules" :key="`assigned_module_${index}`"
class="flex flex-col w-[350px] mb-6 h-auto">
<div class="flex flex-col gap-3">
<div v-for="(item, index_assigned) in assigned" :key="`assigned_element_${index_assigned}`">
{{ i18n.t(`repositories.item_card.assigned.labels.${item.type}`) }}
<a :href="item.url" class="text-sn-science-blue">
{{ item.archived ? i18n.t('labels.archived') : '' }} {{ item.value }}
</a>
</div>
</div>
<hr v-if="index < assignedModules.viewable_modules.length - 1"
class="h-1 w-[350px] mt-6 mb-0 border-dashed border-1 border-sn-light-grey" />
</div>
</div>
<div v-else class="mb-3">
{{ i18n.t('repositories.item_card.assigned.empty') }}
</div>
</section>
<div id="divider" class="w-500 bg-sn-light-grey flex px-8 items-center self-stretch h-px "></div>
<!-- QR -->
<section id="qr-wrapper" ref="QR-label">
<div class="font-inter text-base font-semibold leading-7 mb-4 mt-0">{{ i18n.t('repositories.item_card.section.qr') }}</div>
<div class="bar-code-container">
<canvas id="bar-code-canvas" class="hidden"></canvas>
<img :src="barCodeSrc" />
</div>
</section>
</div>
</div>
<!-- NAVIGATION -->
<div ref="navigationRef" id="navigation"
class="flex item-end gap-x-4 min-w-[130px] min-h-[130px] h-fit absolute top-[102px] right-[24px] ">
<scroll-spy :itemsToCreate="[
{ id: 'highlight-item-1', textId: 'text-item-1', labelAlias: 'information_label', label: 'information-label' },
{ id: 'highlight-item-2', textId: 'text-item-2', labelAlias: 'custom_columns_label', label: 'custom-columns-label' },
{ id: 'highlight-item-3', textId: 'text-item-3', labelAlias: 'assigned_label', label: 'assigned-label' },
{ id: 'highlight-item-4', textId: 'text-item-4', labelAlias: 'QR_label', label: 'QR-label' }
]" :stickyHeaderHeightPx="102" :cardTopPaddingPx="null">
</scroll-spy>
</div>
</div>
<!-- BOTTOM -->
<div id="bottom" class="h-[100px] flex flex-col justify-end mt-4" :class="{ 'pb-6': customColumns?.length }">
<div id="divider" class="w-500 bg-sn-light-grey flex px-8 items-center self-stretch h-px mb-6"></div>
<div id="bottom-button-wrapper" class="flex h-10 justify-end">
<button type="button" class="btn btn-primary print-label-button" :data-rows="JSON.stringify([repositoryRowId])">
{{ i18n.t('repositories.item_card.print_label') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import RepositoryStockValue from './repository_values/RepositoryStockValue.vue';
import RepositoryTextValue from './repository_values/RepositoryTextValue.vue';
import RepositoryNumberValue from './repository_values/RepositoryNumberValue.vue';
import RepositoryAssetValue from './repository_values/RepositoryAssetValue.vue';
import RepositoryListValue from './repository_values/RepositoryListValue.vue';
import RepositoryChecklistValue from './repository_values/RepositoryChecklistValue.vue';
import RepositoryStatusValue from './repository_values/RepositoryStatusValue.vue';
import RepositoryDateTimeValue from './repository_values/RepositoryDateTimeValue.vue';
import RepositoryDateTimeRangeValue from './repository_values/RepositoryDateTimeRangeValue.vue';
import RepositoryDateValue from './repository_values/RepositoryDateValue.vue';
import RepositoryDateRangeValue from './repository_values/RepositoryDateRangeValue.vue';
import RepositoryTimeRangeValue from './repository_values/RepositoryTimeRangeValue.vue'
import RepositoryTimeValue from './repository_values/RepositoryTimeValue.vue'
import ScrollSpy from './repository_values/ScrollSpy.vue';
import Reminder from './reminder.vue'
export default {
name: 'RepositoryItemSidebar',
components: {
Reminder,
RepositoryStockValue,
RepositoryTextValue,
RepositoryNumberValue,
RepositoryAssetValue,
RepositoryListValue,
RepositoryChecklistValue,
RepositoryStatusValue,
RepositoryDateTimeValue,
RepositoryDateTimeRangeValue,
RepositoryDateValue,
RepositoryDateRangeValue,
RepositoryTimeRangeValue,
RepositoryTimeValue,
'scroll-spy': ScrollSpy
},
data() {
return {
currentItemUrl: null,
dataLoading: false,
repositoryRowId: null,
repository: null,
defaultColumns: null,
customColumns: null,
assignedModules: null,
isShowing: false,
barCodeSrc: null,
permissions: null,
repositoryRowUrl: null,
actions: null,
myModuleId: null,
inRepository: false
}
},
created() {
window.repositoryItemSidebarComponent = this;
},
mounted() {
// Add a click event listener to the document
document.addEventListener('click', this.handleOutsideClick);
this.inRepository = $('.assign-items-to-task-modal-container').length > 0;
},
beforeDestroy() {
delete window.repositoryItemSidebarComponent;
document.removeEventListener('click', this.handleDocumentClick);
},
methods: {
handleOutsideClick(event) {
if (!this.isShowing) return
const sidebar = this.$refs.wrapper;
// Check if the clicked element is not within the sidebar and it's not another item link
if (!sidebar.contains(event.target) && !event.target.closest('a')) {
this.toggleShowHideSidebar(null)
}
},
toggleShowHideSidebar(repositoryRowUrl, myModuleId = null) {
// initial click
if (this.currentItemUrl === null) {
this.myModuleId = myModuleId;
this.isShowing = true;
this.loadRepositoryRow(repositoryRowUrl);
this.currentItemUrl = repositoryRowUrl;
return
}
// click on the same item - should just open/close it
else if (this.currentItemUrl === repositoryRowUrl) {
this.isShowing = false;
this.currentItemUrl = null;
this.myModuleId = null;
return
}
// explicit close (from emit)
else if (repositoryRowUrl === null) {
this.isShowing = false;
this.currentItemUrl = null;
this.myModuleId = null;
return
}
// click on a different item - should just fetch new data
else {
this.myModuleId = myModuleId;
this.loadRepositoryRow(repositoryRowUrl);
this.currentItemUrl = repositoryRowUrl;
return
}
},
loadRepositoryRow(repositoryRowUrl) {
this.dataLoading = true
$.ajax({
method: 'GET',
url: repositoryRowUrl,
data: { my_module_id: this.myModuleId },
dataType: 'json',
success: (result) => {
this.repositoryRowId = result?.id
this.repository = result?.repository;
this.defaultColumns = result?.default_columns;
this.customColumns = result?.custom_columns;
this.assignedModules = result?.assigned_modules;
this.permissions = result?.permissions
this.actions = result?.actions;
this.dataLoading = false
this.$nextTick(() => {
this.generateBarCode(this?.defaultColumns?.code);
});
}
});
},
reload() {
if(this.isShowing) {
this.loadRepositoryRow(this.currentItemUrl);
}
},
showRepositoryAssignModal() {
if (this.inRepository) {
window.AssignItemsToTaskModalComponentContainer.showModal([this.repositoryRowId]);
}
},
generateBarCode(text) {
if(!text) return;
const barCodeCanvas = bwipjs.toCanvas('bar-code-canvas', {
bcid: 'qrcode',
text,
scale: 3
});
this.barCodeSrc = barCodeCanvas.toDataURL('image/png');
},
privateModuleSize() {
return this.assignedModules.total_assigned_size - this.assignedModules.viewable_modules.length;
}
}
}
</script>

View file

@ -0,0 +1,25 @@
<template v-if="value.reminder === true">
<div class="inline-block float-right cursor-pointer relative" data-placement="top" data-toggle="tooltip" :title="value.text"
tabindex='-1'>
<i class="sn-icon sn-icon-notifications row-reminders-icon"></i>
<span :class="`inline-block absolute rounded-full w-2 h-2 right-1 top-0.5 ${reminderColor}`"></span>
</div>
</template>
<script>
export default {
name: 'Reminder',
props: {
valueType: null,
value: null,
},
computed: {
reminderColor() {
if (this.value.reminder && (this.value.stock_amount > 0 || this.value.days_left > 0)) {
return 'bg-sn-alert-brittlebush'
}
return 'bg-sn-alert-passion';
}
}
}
</script>

View file

@ -0,0 +1,61 @@
<template>
<div id="repository-asset-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
<div class="font-inter text-sm font-semibold leading-5">
{{ colName }}
</div>
<div v-if="file_name" @mouseover="tooltipShowing = true" @mouseout="tooltipShowing = false"
class="w-fit cursor-pointer text-sn-science-blue relative">
<a @click="$emit('closeSidebar')" class="file-preview-link" :id="modalPreviewLinkId" data-no-turbolink="true"
data-id="true" data-status="asset-present" :data-preview-url=this?.preview_url :href=this?.url>
{{ file_name }}
</a>
<tooltip-preview v-if="tooltipShowing && medium_preview_url" :id="id" :url="url" :file_name="file_name"
:preview_url="preview_url" :icon_html="icon_html" :medium_preview_url="medium_preview_url">
</tooltip-preview>
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t('repositories.item_card.repository_asset_value.no_asset') }}
</div>
</div>
</template>
<script>
import TooltipPreview from './TooltipPreview.vue'
export default {
name: 'RepositoryAssetvalue',
components: {
"tooltip-preview": TooltipPreview
},
data() {
return {
tooltipShowing: false,
id: null,
url: null,
preview_url: null,
file_name: null,
icon_html: null,
medium_preview_url: null,
}
},
props: {
data_type: String,
colId: Number,
colName: String,
colVal: Object
},
created() {
this.id = this?.colVal?.id
this.url = this?.colVal?.url
this.preview_url = this?.colVal?.preview_url
this.file_name = this?.colVal?.file_name
this.icon_html = this?.colVal?.icon_html
this.medium_preview_url = this?.colVal?.medium_preview_url
},
computed: {
modalPreviewLinkId() {
return `modal_link${this?.id}`
}
},
}
</script>

View file

@ -0,0 +1,53 @@
<template>
<div id="repository-checklist-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
<div class="font-inter text-sm font-semibold leading-5">
{{ colName }}
</div>
<div v-if="allChecklistItems">
<div v-if="isEditing"
class="text-sn-dark-grey font-inter text-sm font-normal leading-5 grid grid-rows-2 grid-cols-2 overflow-auto h-12">
<div v-for="(checklistItem, index) in allChecklistItems" :key="index">
<div class="sci-checkbox-container">
<input type="checkbox" class="sci-checkbox" :value="checklistItem?.value" v-model="selectedChecklistItems" />
<span class="sci-checkbox-label"></span>
</div>
{{ checklistItem?.label }}
</div>
</div>
<div v-else
class="text-sn-dark-grey font-inter text-sm font-normal leading-5 h-10 w-[370px] overflow-x-auto flex flex-wrap">
<div v-for="(checklistItem, index) in allChecklistItems" :key="index">
<div id="checklist-item" class="flex w-fit h-[18px] break-words mx-1">
{{ `${checklistItem?.label} |` }}
</div>
</div>
</div>
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t('repositories.item_card.repository_checklist_value.no_checklist') }}
</div>
</div>
</template>
<script>
export default {
name: 'RepositoryChecklistValue',
data() {
return {
isEditing: false,
id: null,
allChecklistItems: [],
selectedChecklistItems: []
}
},
props: {
data_type: String,
colId: Number,
colName: String,
colVal: Array
},
created() {
this.allChecklistItems = this?.colVal
}
}
</script>

View file

@ -0,0 +1,36 @@
<template>
<div id="repository-date-range-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
<div class="font-inter text-sm font-semibold leading-5">
{{ colName }}
</div>
<div v-if="start_time?.formatted && end_time?.formatted"
class="text-sn-dark-grey font-inter text-sm font-normal leading-5 flex">
<div>{{ start_time?.formatted }} - {{ end_time?.formatted }}</div>
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t('repositories.item_card.repository_date_range_value.no_date_range') }}
</div>
</div>
</template>
<script>
export default {
name: 'RepositoryDateRangeValue',
data() {
return {
start_time: null,
end_time: null
}
},
props: {
data_type: String,
colId: Number,
colName: String,
colVal: Object
},
created() {
this.start_time = this?.colVal?.start_time
this.end_time = this?.colVal?.end_time
}
}
</script>

View file

@ -0,0 +1,36 @@
<template>
<div id="repository-date-time-range-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
<div class="font-inter text-sm font-semibold leading-5">
{{ colName }}
</div>
<div v-if="start_time?.formatted && end_time?.formatted"
class="text-sn-dark-grey font-inter text-sm font-normal leading-5 flex">
<div>{{ start_time?.formatted }} - {{ end_time?.formatted }}</div>
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t('repositories.item_card.repository_date_time_range_value.no_date_time_range') }}
</div>
</div>
</template>
<script>
export default {
name: 'RepositoryDateTimeRangeValue',
data() {
return {
start_time: null,
end_time: null
}
},
props: {
data_type: String,
colId: Number,
colName: String,
colVal: Object
},
created() {
this.start_time = this?.colVal?.start_time
this.end_time = this?.colVal?.end_time
}
}
</script>

View file

@ -0,0 +1,39 @@
<template>
<div id="repository-date-time-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
<div class="font-inter text-sm font-semibold leading-5">
{{ colName }}
</div>
<div v-if="formatted" class="text-sn-dark-grey font-inter text-sm font-normal leading-5 flex">
<div>{{ formatted }}</div>
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t('repositories.item_card.repository_date_time_value.no_date_time') }}
</div>
</div>
</template>
<script>
export default {
name: 'RepositoryDateTimeValue',
data() {
return {
formatted: null,
date_formatted: null,
time_formatted: null,
datetime: null
}
},
props: {
data_type: String,
colId: Number,
colName: String,
colVal: Object
},
created() {
this.formatted = this?.colVal?.formatted
this.date_formatted = this?.colVal?.date_formatted
this.time_formatted = this?.colVal?.time_formatted
this.formatdatetimeted = this?.colVal?.datetime
}
}
</script>

View file

@ -0,0 +1,35 @@
<template>
<div id="repository-date-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
<div class="font-inter text-sm font-semibold leading-5">
{{ colName }}
</div>
<div v-if="formatted" class="text-sn-dark-grey font-inter text-sm font-normal leading-5 flex">
<div>{{ formatted }}</div>
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t('repositories.item_card.repository_date_value.no_date') }}
</div>
</div>
</template>
<script>
export default {
name: 'RepositoryDateValue',
data() {
return {
formatted: null,
datetime: null
}
},
props: {
data_type: String,
colId: Number,
colName: String,
colVal: Object
},
created() {
this.formatted = this?.colVal?.formatted
this.datetime = this?.colVal?.datetime
}
}
</script>

View file

@ -0,0 +1,35 @@
<template>
<div id="repository-list-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
<div class="font-inter text-sm font-semibold leading-5">
{{ colName }}
</div>
<div v-if="text" class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ text }}
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t('repositories.item_card.repository_list_value.no_list') }}
</div>
</div>
</template>
<script>
export default {
name: 'RepositoryListValue',
data() {
return {
id: null,
text: null
}
},
props: {
data_type: String,
colId: Number,
colName: String,
colVal: Object
},
created() {
this.id = this?.colVal?.id
this.text = this?.colVal?.text
}
}
</script>

View file

@ -0,0 +1,25 @@
<template>
<div id="repository-number-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
<div class="font-inter text-sm font-semibold leading-5">
{{ colName }}
</div>
<div v-if="colVal" class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ colVal }}
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t('repositories.item_card.repository_number_value.no_number') }}
</div>
</div>
</template>
<script>
export default {
name: 'RepositoryNumberValue',
props: {
data_type: String,
colId: Number,
colName: String,
colVal: Number
},
}
</script>

View file

@ -0,0 +1,47 @@
<template>
<div id="repository-status-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
<div class="font-inter text-sm font-semibold leading-5">
{{ colName }}
</div>
<div v-if="status && icon"
class="flex flex-row items-center text-sn-dark-grey font-inter text-sm font-normal leading-5 ">
<div v-html="parseEmoji(icon)" class="flex mr-1.5 h-6"></div>
{{ status }}
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t('repositories.item_card.repository_status_value.no_status') }}
</div>
</div>
</template>
<script>
import twemoji from 'twemoji';
export default {
name: 'RepositoryStatusValue',
data() {
return {
id: null,
icon: null,
status: null
}
},
props: {
data_type: String,
colId: Number,
colName: String,
colVal: Object
},
created() {
this.id = this?.colVal?.id
this.icon = this?.colVal?.icon
this.status = this?.colVal?.status
},
methods: {
parseEmoji(content) {
return twemoji.parse(content);
}
}
}
</script>

View file

@ -0,0 +1,46 @@
<template>
<div id="repository-stock-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
<div class="font-inter text-sm font-semibold leading-5 relative">
<span>{{ colName }}</span>
<a style="text-decoration: none;" class="absolute right-0 text-sn-science-blue visited:text-sn-science-blue hover:text-sn-science-blue
font-inter text-sm font-normal cursor-pointer export-consumption-button"
v-if="permissions?.can_export_repository_stock === true" :data-rows="JSON.stringify([repositoryRowId])"
:data-object-id="repositoryId">
{{ i18n.t('repositories.item_card.stock_export') }}
</a>
</div>
<div v-if="colVal?.stock_formatted" class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ colVal?.stock_formatted }}
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t('repositories.item_card.repository_stock_value.no_stock') }}
</div>
</div>
</template>
<script>
export default {
name: 'RepositoryStockValue',
data() {
return {
stock_formatted: null,
stock_amount: null,
low_stock_threshold: null
}
},
props: {
data_type: String,
colId: Number,
colName: String,
colVal: Object,
repositoryId: Number,
repositoryRowId: null,
permissions: null
},
created() {
this.stock_formatted = this?.colVal?.stock_formatted
this.stock_amount = this?.colVal?.stock_amount
this.low_stock_threshold = this?.colVal?.low_stock_threshold
}
}
</script>

View file

@ -0,0 +1,35 @@
<template>
<div id="repository-text-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
<div class="font-inter text-sm font-semibold leading-5">
{{ colName }}
</div>
<div v-if="edit" class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ edit }}
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t('repositories.item_card.repository_text_value.no_text') }}
</div>
</div>
</template>
<script>
export default {
name: 'RepositoryTextValue',
data() {
return {
edit: null,
view: null,
}
},
props: {
data_type: String,
colId: Number,
colName: String,
colVal: Object
},
created() {
this.edit = this?.colVal?.edit
this.view = this?.colVal?.view
}
}
</script>

View file

@ -0,0 +1,36 @@
<template>
<div id="repository-time-range-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
<div class="font-inter text-sm font-semibold leading-5">
{{ colName }}
</div>
<div v-if="start_time?.formatted && end_time?.formatted"
class="text-sn-dark-grey font-inter text-sm font-normal leading-5 flex">
<div>{{ start_time?.formatted }} - {{ end_time?.formatted }}</div>
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">{{
i18n.t('repositories.item_card.repository_time_range_value.no_time_range') }}
</div>
</div>
</template>
<script>
export default {
name: 'RepositoryTimeRangeValue',
data() {
return {
start_time: null,
end_time: null
}
},
props: {
data_type: String,
colId: Number,
colName: String,
colVal: Object
},
created() {
this.start_time = this?.colVal?.start_time
this.end_time = this?.colVal?.end_time
}
}
</script>

View file

@ -0,0 +1,37 @@
<template>
<div id="repository-time-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
<div class="font-inter text-sm font-semibold leading-5">
{{ colName }}
</div>
<div v-if="formatted" class="text-sn-dark-grey font-inter text-sm font-normal leading-5 flex">
<div>
{{ formatted }}
</div>
</div>
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5">
{{ i18n.t('repositories.item_card.repository_time_value.no_time') }}
</div>
</div>
</template>
<script>
export default {
name: 'RepositoryTimeValue',
props: {
data_type: String,
colId: Number,
colName: String,
colVal: Object
},
data() {
return {
formatted: null,
datetime: null
}
},
created() {
this.formatted = this?.colVal?.formatted
this.datetime = this?.colVal?.datetime
}
}
</script>

View file

@ -0,0 +1,94 @@
<template>
<!-- This will be re-implemented using vue2-scrollspy library in a following ticket -->
<div class="flex gap-3">
<div id="navigation-text">
<div class="flex flex-col py-2 px-0 gap-3 self-stretch w-[130px] h-[130px] justify-center items-center">
<div v-for="(itemObj, index) in itemsToCreate" :key="index"
class="flex flex-col w-[130px] h-[130px] justify-between text-right">
<div @click="handleSideNavClick" :id="itemObj?.textId" class="hover:cursor-pointer text-sn-grey"
:class="{ 'text-sn-science-blue': selectedNavText === itemObj?.textId }">{{
i18n.t(`repositories.highlight_component.${itemObj?.labelAlias}`) }}
</div>
</div>
</div>
</div>
<div id="highlight-container" class="w-[1px] h-[130px] flex flex-col justify-evenly bg-sn-light-grey">
<div v-for="(itemObj, index) in itemsToCreate" :key="index">
<div :id="itemObj?.id" class="w-[5px] h-[28px] rounded-[11px]"
:class="{ 'bg-sn-science-blue relative left-[-2px]': itemObj?.id === selectedNavIndicator }"></div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ScrollSpy',
props: {
itemsToCreate: Array,
stickyHeaderHeightPx: Number || null,
cardTopPaddingPx: Number || null
},
data() {
return {
rootContainerEl: null,
selectedNavText: null,
selectedNavIndicator: null,
positions: []
}
},
created() {
this.rootContainerEl = this.$parent.$refs.wrapper
this.rootContainerEl?.addEventListener('scroll', this.handleScrollBehaviour)
},
methods: {
handleScrollBehaviour() {
// used for keeping scroll spy sticky
this.updateNavigationPositionOnScroll()
},
updateNavigationPositionOnScroll() {
const navigationDom = this?.$parent?.$refs?.navigationRef
// Get the current scroll position
const scrollPosition = this?.rootContainerEl?.scrollTop
// Adjust navigationDom position equal to the scrollPosition + the header height and the card top padding (if present)
navigationDom.style.top = `${scrollPosition + this?.stickyHeaderHeightPx + this?.cardTopPaddingPx}px`;
},
handleSideNavClick(e) {
if (!this.rootContainerEl) {
return
}
let refToScrollTo
const targetId = e.target.id
const foundObj = this.itemsToCreate.find((obj) => obj?.textId === targetId)
if (!foundObj) return
refToScrollTo = foundObj.label
this.selectedNavText = foundObj?.textId
this.selectedNavIndicator = foundObj?.id
const sectionLabels = this.itemsToCreate.map((obj) => obj?.label)
const labelsToUnhighlight = sectionLabels.filter((i) => i !== refToScrollTo)
// scrolling to desired section
const domElToScrollTo = this.$parent.$refs[refToScrollTo]
this.rootContainerEl.scrollTo({
top: domElToScrollTo.offsetTop - this?.stickyHeaderHeightPx - this?.cardTopPaddingPx,
behavior: "auto"
})
// flashing the title color to blue and back over 300ms
const timeoutId = setTimeout(() => {
// wrapped in timeout to ensure that the color-change animation happens after the scrolling animation is completed
domElToScrollTo?.classList.add('text-sn-science-blue')
labelsToUnhighlight.forEach(id => document.getElementById(id)?.classList.remove('text-sn-science-blue'))
setTimeout(() => {
domElToScrollTo?.classList.remove('text-sn-science-blue')
}, 400)
clearTimeout(timeoutId)
}, 500)
}
},
}
</script>

View file

@ -0,0 +1,46 @@
<template>
<div>
<img :src="this?.medium_preview_url" @load="onImageLoaded($event)"
class="absolute bg-sn-light-grey text-sn-black rounded pointer-events-none flex shadow-lg z-10"
:class="{ hidden: !showImage, 'top-0 transform -translate-y-full': showTop }" />
</div>
</template>
<script>
export default {
name: 'TooltipPreview',
data() {
return {
showTop: false,
showImage: false,
}
},
props: {
tooltipId: String,
url: String,
preview_url: String,
file_name: String,
icon_html: String || null,
medium_preview_url: String || null,
},
methods: {
onImageLoaded(event) {
this.showTop = !this.isInViewPort(event.target);
this.showImage = true;
},
isInViewPort(el) {
if (!el) return;
const height = el.naturalHeight;
const rect = el.parentElement.getBoundingClientRect();
return (
(rect.bottom + height) <=
(window.innerHeight || document.documentElement.clientHeight)
);
}
}
}
</script>

View file

@ -14,7 +14,7 @@
<h4 class="modal-title"> {{ i18n.t('zip_export.consumption_modal_label') }} </h4>
</div>
<div class="modal-body">
<p>{{ i18n.t('zip_export.consumption_header_html', { repository: this.repository?.name }) }} </p>
<p>{{ i18n.t('zip_export.consumption_header_html', { repository: repository?.name }) }} </p>
<p v-html="i18n.t('zip_export.consumption_body_html')"> </p>
<p class='pb-0' v-html="i18n.t('zip_export.consumption_footer_html')"></p>
</div>

View file

@ -34,19 +34,22 @@ module Reports
proxy.set_user(user, scope: :user, store: false)
ApplicationController.renderer.defaults[:http_host] = Rails.application.routes.default_url_options[:host]
renderer = ApplicationController.renderer.new(warden: proxy)
Rails.application.config.x.custom_sanitizer_config = build_custom_sanitizer_config
file << renderer.render(
pdf: 'report', header: { html: { template: "reports/templates/#{template}/header",
locals: { report: report, user: user, logo: report_logo },
layout: 'reports/footer_header' } },
footer: { html: { template: "reports/templates/#{template}/footer",
locals: { report: report, user: user, logo: report_logo },
layout: 'reports/footer_header' } },
assigns: { settings: report.settings },
locals: { report: report },
disable_javascript: false,
template: 'reports/report',
formats: :pdf
pdf: 'report',
header: { html: { template: "reports/templates/#{template}/header",
locals: { report: report, user: user, logo: report_logo },
layout: 'reports/footer_header' } },
footer: { html: { template: "reports/templates/#{template}/footer",
locals: { report: report, user: user, logo: report_logo },
layout: 'reports/footer_header' } },
assigns: { settings: report.settings },
locals: { report: report },
disable_javascript: false,
disable_external_links: true,
template: 'reports/report',
formats: :pdf
)
file.rewind
@ -69,6 +72,7 @@ module Reports
)
notification.create_user_notification(user)
ensure
Rails.application.config.x.custom_sanitizer_config = nil
I18n.backend.date_format = nil
file.close(true)
end
@ -178,6 +182,15 @@ module Reports
'scinote_logo.svg'
end
def build_custom_sanitizer_config
sanitizer_config = Constants::INPUT_SANITIZE_CONFIG.deep_dup
sanitizer_config[:protocols] = {
'a' => { 'href' => ['http', 'https', :relative] },
'img' => { 'src' => %w(data) }
}
sanitizer_config
end
# Overrides method from FailedDeliveryNotifiableJob concern
def failed_notification_title
I18n.t('projects.reports.index.generation.error_pdf_notification_title')

View file

@ -110,13 +110,17 @@ module TinyMceImages
next if asset.object == self
next unless asset.can_read?(user)
else
url = image['src']
image_type = FastImage.type(url).to_s
next unless image_type
image_type = nil
begin
new_image = Down.download(url, max_size: Rails.configuration.x.file_max_size_mb.megabytes)
rescue Down::TooLarge => e
uri = URI.parse(image['src'])
if uri.scheme != 'https'
uri.scheme = Rails.application.config.force_ssl ? 'https' : 'http'
end
image_type = FastImage.type(uri.to_s).to_s
next unless image_type
new_image = Down.download(uri.to_s, max_size: Rails.configuration.x.file_max_size_mb.megabytes)
rescue StandardError => e
Rails.logger.error e.message
next
end

View file

@ -13,7 +13,8 @@ module RepositoryDatatable
url: rails_blob_path(asset.file, disposition: 'attachment'),
preview_url: asset_file_preview_path(asset),
file_name: escape_input(asset.file_name),
icon_html: sn_icon_for(asset)
icon_html: sn_icon_for(asset),
medium_preview_url: asset.previewable? && rails_representation_url(asset.medium_preview)
}
rescue StandardError => e
Rails.logger.error e.message

View file

@ -12,9 +12,22 @@ module RepositoryDatatable
if scope.dig(:options, :reminders_enabled) &&
!scope[:repository].is_a?(RepositorySnapshot) &&
scope[:column].reminder_value.present? && scope[:column].reminder_unit.present?
scope[:column].reminder_value.present? &&
scope[:column].reminder_unit.present?
reminder_delta = scope[:column].reminder_value.to_i * scope[:column].reminder_unit.to_i
data[:reminder] = reminder_delta + DateTime.now.to_i >= value_object.data.to_i
data[:reminder_message] = scope[:column].reminder_message
days_left = ((value_object.data - Time.now.utc) / 1.day).ceil
if data[:reminder] && days_left.positive?
data[:days_left] = days_left
date_expiration =
"#{days_left} #{I18n.t("repositories.item_card.reminders.day.#{days_left == 1 ? 'one' : 'other'}")}"
data[:text] =
"#{I18n.t('repositories.item_card.reminders.date_expiration', date_expiration: date_expiration)}\n
#{data[:reminder_message]}"
elsif data[:reminder]
data[:text] = "#{I18n.t('repositories.item_card.reminders.item_expired')}\n#{data[:reminder_message]}"
end
end
data

View file

@ -10,11 +10,23 @@ module RepositoryDatatable
if scope.dig(:options, :reminders_enabled) &&
!scope[:repository].is_a?(RepositorySnapshot) &&
scope[:column].reminder_value.present? && scope[:column].reminder_unit.present?
scope[:column].reminder_value.present? &&
scope[:column].reminder_unit.present?
reminder_delta = scope[:column].reminder_value.to_i * scope[:column].reminder_unit.to_i
data[:reminder] = reminder_delta + DateTime.now.to_i >= value_object.data.to_i
data[:reminder_message] = scope[:column].reminder_message
days_left = ((value_object.data - Time.now.utc) / 1.day).ceil
if data[:reminder] && days_left.positive?
data[:days_left] = days_left
date_expiration =
"#{days_left} #{I18n.t("repositories.item_card.reminders.day.#{days_left == 1 ? 'one' : 'other'}")}"
data[:text] =
"#{I18n.t('repositories.item_card.reminders.date_expiration', date_expiration: date_expiration)}\n
#{data[:reminder_message]}"
elsif data[:reminder]
data[:text] = "#{I18n.t('repositories.item_card.reminders.item_expired')}\n#{data[:reminder_message]}"
end
end
data
end
end

View file

@ -5,11 +5,23 @@ module RepositoryDatatable
include Canaid::Helpers::PermissionsHelper
def value
{
data = {
stock_formatted: value_object.formatted,
stock_amount: value_object.data,
low_stock_threshold: value_object.low_stock_threshold
}
if scope.dig(:options, :reminders_enabled) &&
!scope[:repository].is_a?(RepositorySnapshot) &&
value_object.data.present? &&
value_object.low_stock_threshold.present?
data[:reminder] = value_object.low_stock_threshold > value_object.data
if data[:reminder] && (data[:stock_amount]).positive?
data[:text] = I18n.t('repositories.item_card.reminders.stock_low', stock_formated: data[:stock_formatted])
elsif data[:reminder]
data[:text] = I18n.t('repositories.item_card.reminders.stock_empty')
end
end
data
end
end
end

View file

@ -5,7 +5,7 @@
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
<h4 class="modal-title">
<%= link_to assignable_path, remote: true, class: 'pull-left spacer', data: { action: 'swap-remote-container', target: '#user_assignments_modal' } do %>
<i class="fas fa-arrow-left"></i>
<i class="fas fa-arrow-right"></i>
<% end %>
<%= t '.title', resource_name: assignable.name %>
</h4>

View file

@ -101,6 +101,7 @@
</div>
<%= render "shared/comments/comments_sidebar" %>
<%= render "shared/repository_row_sidebar" %>
</div>
<%= render partial: 'shared/flash_alerts',
@ -121,5 +122,7 @@
<span style="display: none;" data-hook="application-body-end-html"></span>
<%= javascript_include_tag 'prism' %>
<%= javascript_include_tag "vue_components_repository_item_sidebar" %>
</body>
</html>

View file

@ -4,9 +4,9 @@
<% if current_team.shareable_links_enabled? && can_share_my_module?(@my_module) %>
<div class="share-task-container" data-behaviour="vue">
<share-task-container
<%= 'shared' if @my_module.shared? %>
shareable-link-url="<%= my_module_shareable_link_path(@my_module) %>"
<%= 'disabled' if !can_manage_my_module?(current_user, @my_module) %> />
:shared="<%= @my_module.shared? %>"
:disabled="<%= !can_share_my_module?(@my_module) %>" />
</div>
<%= javascript_include_tag 'vue_share_task_container' %>

View file

@ -75,9 +75,4 @@
<% end %>
</li>
<% end %>
<li class="form-dropdown-item">
<div class="form-dropdown-item-info">
<small><%= t('experiments.experiment_id') %>: <strong><%= experiment.code %></strong></small>
</div>
</li>
</ul>

View file

@ -15,10 +15,10 @@
<div class="report-element-body">
<% if step_text.text.present? %>
<%= custom_auto_link(step_text.prepare_for_report(:text),
team: current_team,
simple_format: false,
tags: %w(img),
base64_encoded_imgs: true) %>
team: current_team,
simple_format: false,
tags: %w(img),
base64_encoded_imgs: true) %>
<% else %>
<em><%= t('projects.reports.elements.step.no_description') %></em>
<% end %>

View file

@ -7,6 +7,7 @@
>
<assign-items-to-task-modal-container
:visibility="visibility"
:rows-to-assign="rowsToAssign"
:urls="urls"
@close="closeModal"
/>

View file

@ -0,0 +1,51 @@
# frozen_string_literal: true
json.id @repository_row.id
json.repository do
json.id @repository.id
json.name @repository.name
end
json.permissions do
json.can_export_repository_stock can_export_repository_stock?(@repository_row.repository)
end
json.actions do
if @my_module.present?
json.assign_repository_row do
json.assign_url my_module_repositories_path(@my_module.id)
json.disabled @my_module_assign_error.present?
end
end
end
json.default_columns do
json.name @repository_row.name
json.code @repository_row.code
json.added_on I18n.l(@repository_row.created_at, format: :full)
json.added_by @repository_row.created_by&.full_name
json.archived @repository_row.archived?
end
json.custom_columns do
json.array! @repository_row.repository.repository_columns.each do |repository_column|
repository_cell = @repository_row.repository_cells.find_by(repository_column: repository_column)
if repository_cell
json.merge! **serialize_repository_cell_value(repository_cell, @repository.team, @repository, reminders_enabled: @reminders_present).merge(
**repository_cell.repository_column.as_json(only: %i(id name data_type))
)
else
json.merge! repository_column.as_json(only: %i(id name data_type))
end
end
end
json.assigned_modules do
json.total_assigned_size @assigned_modules.size
json.viewable_modules do
json.array! @viewable_modules do |my_module|
json.merge! serialize_assigned_my_module_value(my_module)
end
end
end

View file

@ -7,7 +7,7 @@
</span>
</div>
<div class="my-5 max-w-4xl flex-1 bg-sn-white">
<div class="my-5 flex-1">
<div class="my-module-position-container">
<!-- Header Actions -->
<%= render partial: 'shareable_links/my_modules/header_actions' %>
@ -27,10 +27,6 @@
<%= @my_module.code %>
</span>
</div>
<div class="flex items-center gap-3">
<%= render partial: 'shareable_links/my_modules/task_flow_button', locals: { my_module: @my_module } if @my_module.my_module_status_flow %>
</div>
</div>
<div id="details-container" class="task-details" data-shareable-link=<%= @shareable_link.uuid %>>
<%= render partial: 'shareable_links/my_modules/my_module_details' %>

View file

@ -6,24 +6,22 @@
</span>
</div>
<div class="my-5 max-w-4xl flex-1 bg-sn-white">
<div class="content-pane flexible">
<div class="my-5 flex-1">
<div class="content-pane with-grey-background flexible">
<%= render partial: 'shareable_links/my_modules/header_actions' %>
<div class="px-4">
<div class="my-5" id="results-toolbar">
<div class="sci-btn-group collapse-expand-result">
<button class="btn btn-light" id="results-collapse-btn">
<span class="sn-icon sn-icon-up"></span>
<span class="hidden-xs-custom"><%= t'my_modules.results.collapse_label' %></span>
<div>
<div class="my-4 p-3 bg-sn-white" id="results-toolbar">
<div class="flex items-center gap-4 collapse-expand-result">
<button class="btn btn-secondary" id="results-collapse-btn">
<span><%= t'my_modules.results.collapse_label' %></span>
</button>
<button class="btn btn-light" id="results-expand-btn">
<span class="sn-icon sn-icon-down"></span>
<span class="hidden-xs-custom"><%= t'my_modules.results.expand_label' %></span>
<button class="btn btn-secondary" id="results-expand-btn">
<span><%= t'my_modules.results.expand_label' %></span>
</button>
</div>
<div class="sort-result-dropdown dropdown">
<button id="sort-result-button" class="btn btn-light icon-btn" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<i class="sn-icon sn-icon-sort-up"></i>
<i class="sn-icon sn-icon-sort"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="sort-result-button">
<li><%= link_to t('general.sort_new.atoz'), shared_protocol_results_path(@shareable_link.uuid, page: params[:page], order: 'atoz'), class: (@results_order == 'atoz' ? 'selected' : '') %></li>
@ -38,6 +36,7 @@
<div id="results">
<% @results.each do |result| %>
<%= render partial: "shareable_links/my_modules/results/result", locals: { result: result, gallery: @gallery } %>
<%= render partial: "shareable_links/my_modules/result_comments_sidebar", locals: { result: result } %>
<% end %>
</div>
<div class="kaminari-pagination">

View file

@ -1,4 +1,5 @@
<div class="step-attachments">
<div class="sci-divider my-6"></div>
<div class="attachments-actions">
<div class="title">
<h3> <%= t('protocols.steps.files', count: attachments.length) %> </h3>

View file

@ -1,5 +1,5 @@
<% user= nil %>
<% step.comments.order(created_at: :asc).each do |comment| %>
<% oject.comments.order(created_at: :asc).each do |comment| %>
<div class="comment-container" data-comment-id="<%= comment.id %>">
<% unless user == comment.user%>
<div class="comment-header">

View file

@ -1,12 +1,12 @@
<div class="header-actions px-5 border-solid border-0 border-b border-sn-light-grey">
<div class="flex items-center uppercase">
<a class="px-4 py-3 border-b-4 border-transparent hover:no-underline capitalize <%= is_module_protocols? ? "text-sn-blue" : "text-sn-black" %>"
<div class="sticky-header-element bg-sn-white border-b border-solid border-0 border-sn-sleepy-grey rounded-t px-4 py-2 top-0 sticky flex items-center flex-wrap z-[106]">
<div class="flex items-center gap-4 mr-auto w-full">
<a class="p-3 border-b-4 border-transparent hover:no-underline uppercase text-bold capitalize <%= is_module_protocols? ? "text-sn-blue" : "text-sn-black" %>"
href="<%= shared_protocol_url(@shareable_link.uuid) %>"
title="<%= t("nav2.modules.steps") %>"
>
<%= t("nav2.modules.steps") %>
</a>
<a class="px-4 py-3 border-b-4 border-transparent hover:no-underline capitalize <%= is_module_results? ? "text-sn-blue" : "text-sn-black" %>"
<a class="p-3 border-b-4 border-transparent hover:no-underline uppercase text-bold capitalize <%= is_module_results? ? "text-sn-blue" : "text-sn-black" %>"
href="<%= shared_protocol_results_path(@shareable_link.uuid) %>"
title="<%= t("nav2.modules.results") %>"
>
@ -16,5 +16,9 @@
<sup class="navigation-results-counter"><%= @my_module.archived_branch? ? @my_module.results.size : @active_results_size %></sup>
<% end %>
</a>
<div class="flex items-center gap-3 ml-auto">
<%= render partial: 'shareable_links/my_modules/task_flow_button', locals: { my_module: @my_module } if @my_module.my_module_status_flow %>
</div>
</div>
</div>

View file

@ -19,49 +19,47 @@
<% end %>
</div>
</div>
<div class="actions-block gap-4">
<% if protocol.steps.length > 0 %>
<button class="btn btn-secondary" id="steps-collapse-btn" tabindex="0">
<%= t("protocols.steps.collapse_label") %>
</button>
<button class="btn btn-secondary" id="steps-expand-btn" tabindex="0">
<%= t("protocols.steps.expand_label") %>
</button>
<% end %>
</div>
</div>
<div id="protocol-content" class="protocol-content collapse in" aria-expanded="true">
<div class="protocol-description">
<div class="protocol-name">
<span>
<%= protocol.name %>
</span>
</div>
<div>
<div id="protocol-description-container" >
<% if protocol.description.present? %>
<div>
<%= smart_annotation_text(protocol.shareable_tinymce_render(:description)) %>
</div>
<% else %>
<div class="empty-protocol-description">
<%= t("protocols.no_text_placeholder") %>
</div>
<% end %>
</div>
</div>
<div id="protocol-content" class="protocol-content collapse in " aria-expanded="true">
<div class="sci-divider my-4"></div>
<div class="protocol-name">
<span>
<%= protocol.name %>
</span>
</div>
<div>
<div id="protocol-steps-container">
<% if protocol.steps.length > 0 %>
<div class="protocol-step-actions">
<button class="btn btn-light" id="steps-collapse-btn" tabindex="0">
<span class="sn-icon sn-icon-collapse"></span>
<%= t("protocols.steps.collapse_label") %>
</button>
<button class="btn btn-light" id="steps-expand-btn" tabindex="0">
<span class="sn-icon sn-icon-expand"></span>
<%= t("protocols.steps.expand_label") %>
</button>
<div id="protocol-description-container" >
<% if protocol.description.present? %>
<div>
<%= smart_annotation_text(protocol.shareable_tinymce_render(:description)) %>
</div>
<% else %>
<div class="empty-protocol-description">
<%= t("protocols.no_text_placeholder") %>
</div>
<% end %>
</div>
</div>
<div class="sci-divider my-4"></div>
<div>
<div id="protocol-steps-container">
<div class="protocol-steps">
<% protocol.steps.sort_by(&:position).each do |step| %>
<div class="step-block">
<%= render partial: "shareable_links/my_modules/step", locals: { step: step } %>
<%= render partial: "shareable_links/my_modules/comments_sidebar", locals: { step: step } %>
<%= render partial: "shareable_links/my_modules/step_comments_sidebar", locals: { step: step } %>
</div>
<% end %>
</div>

View file

@ -0,0 +1,26 @@
<div class="comments-sidebar !top-0 !h-screen" id="Result<%= result.id %>">
<div class="sidebar-content">
<div class="sidebar-header">
<div class="comments-subject-title">
<%= "#{result.name}" %>
</div>
<div class="btn btn-light icon-btn close-btn">
<i class="sn-icon sn-icon-close"></i>
</div>
</div>
<div class="sidebar-body">
<% if result.comments.present? %>
<div class="comments-list">
<%= render partial: "shareable_links/my_modules/comments_list", locals: { object: result } %>
</div>
<% else %>
<div class="no-comments-placeholder !block">
<%= image_tag 'comments/placeholder.svg', class: 'no-comments-image' %>
<h1><%= t('comments.empty_state.title') %></h1>
<p class="description"><%= t('comments.empty_state.description') %></p>
</div>
<% end %>
</div>
<div class="sidebar-footer"></div>
</div>
</div>

View file

@ -1,26 +1,26 @@
<div class="step-container mt-[6px] mr-0 mb-6 ml-[-1em] pt-[8px] pr-[24px] pb-[8px] pl-0 border-solid border-[1px] border-[#fff]" id="stepContainer<%= step.id %>" >
<div class="step-container" id="stepContainer<%= step.id %>" >
<div class="step-header">
<div class="step-element-header no-hover">
<div class="step-controls flex items-center">
<div class="step-element-grip-placeholder"></div>
<div class="flex items-center gap-4">
<a class="step-collapse-link hover:no-underline focus:no-underline"
href="#stepBody<%= step.id %>"
data-toggle="collapse"
data-remote="true">
<span class="sn-icon sn-icon-right "></span>
</a>
<div class="step-complete-container mx-2 step-element--locked">
<div class="step-complete-container step-element--locked">
<div class="step-state <%= step.completed ? 'completed' : '' %>"
tabindex="0"
></div>
</div>
<div class="step-position">
<%= step.position + 1 %> .
<%= step.position + 1 %>.
</div>
</div>
<div class="step-name-container">
<div class="step-name-container basis-[calc(100%_-_100px)] relative">
<%= render partial: "shareable_links/my_modules/inline_view", locals: { text: step.name, smart_annotation_enabled: false } %>
<span class="mt-2 whitespace-nowrap truncate text-xs font-normal w-full absolute -bottom-5"><%= t('protocols.steps.timestamp_iso_html', date: step.created_at.iso8601, user: step.user.full_name) %></span>
</div>
</div>
<div class="elements-actions-container">
@ -41,10 +41,7 @@
</div>
<div class="collapse in" id="stepBody<%= step.id %>">
<div class="step-elements !pl-[4.5rem]">
<div class="step-timestamp !m-0">
<%= t('protocols.steps.timestamp_iso_html', date: step.created_at.iso8601, user: step.user.full_name) %>
</div>
<div class="step-elements">
<% step.step_orderable_elements.sort_by(&:position).each do |element| %>
<% if element.orderable_type == 'StepText' %>
<%= render partial: "shareable_links/my_modules/step_elements/text", locals: { element: element.orderable } %>
@ -59,4 +56,5 @@
<% end %>
</div>
</div>
<div class="sci-divider my-6"></div>
</div>

View file

@ -1,14 +0,0 @@
<hr>
<div class="col-xs-12 comments-title">
<h4>
<%=t('my_modules.results.comments_tab') %>
(<span><%= comments.count %></span>)
</h4>
</div>
<div class="comments-container">
<div class="content-comments inline_scroll_block">
<div class="comments-list">
<%= render partial: "shareable_links/my_modules/results/comments_list", locals: { comments: comments } %>
</div>
</div>
</div>

View file

@ -1,18 +0,0 @@
<% comments.order(created_at: :asc).each do |comment| %>
<div class="comment-container">
<div class="avatar-placehodler">
<span class='global-avatar-container'>
<%= image_tag user_avatar_absolute_url(comment.user, :icon_small, true), class: 'user-avatar' %>
</span>
</div>
<div class="content-placeholder">
<div class="comment-name"><%= comment.user.full_name %></div>
<div class="comment-right !w-fit">
<div class="comment-datetime !w-fit"><%= l(comment.created_at, format: :full) %></div>
</div>
<div class="comment-message">
<div class="view-mode"><%= smart_annotation_text(comment.message) %></div>
</div>
</div>
</div>
<% end %>

View file

@ -1,37 +1,40 @@
<div class="result">
<div class="panel panel-default">
<div class="panel-heading">
<div class="panel-options pull-right">
</div>
<a class="result-panel-collapse-link" href="#result-panel-<%= result.id %>" data-toggle="collapse">
<div class="bg-white p-4 mb-4 rounded">
<div class="result-header flex justify-between">
<div class="result-head-left flex items-start flex-grow gap-4">
<a class="result-collapse-link hover:no-underline focus:no-underline text-sn-black" href="#result-panel-<%= result.id %>" data-toggle="collapse">
<span class="sn-icon sn-icon-right"></span>
<strong><%= result.name %></strong> |
<span><%= t('my_modules.results.published_on_iso_html', timestamp: result.created_at.iso8601, user: h(result.user.full_name)) %></span>
</a>
</div>
<div class="panel-collapse collapse in" id="result-panel-<%= result.id %>" role="tabpanel">
<div class="panel-body">
<div class="row">
<div class="col-xs-12">
<% result.result_orderable_elements.sort_by(&:position).each do |element| %>
<% if element.orderable_type == 'ResultText' %>
<%= render partial: "shareable_links/my_modules/step_elements/text", locals: { element: element.orderable } %>
<% elsif element.orderable_type == 'ResultTable'%>
<%= render partial: "shareable_links/my_modules/step_elements/table", locals: { element: element.orderable.table } %>
<% end %>
<% end %>
<% if result.result_assets.present? %>
<%= render partial: "shareable_links/my_modules/attachments", locals: { attachments: result.assets, step: result } %>
<% end %>
</div>
</div>
<div class="row">
<div class="result-comment"
id="result-comments-<%= result.id %>">
<%= render partial: "shareable_links/my_modules/results/comments", locals: { comments: result.comments } %>
</div>
</div>
<div class="w-full relative flex flex-grow font-bold text-base">
<strong><%= result.name %></strong>
<span class="mt-2 whitespace-nowrap truncate text-xs font-normal absolute bottom-[-1rem] w-full"><%= t('my_modules.results.published_on_iso_html', timestamp: result.created_at.iso8601, user: h(result.user.full_name)) %></span>
</div>
<div class="elements-actions-container">
<a href="#"
class="shareable-link-open-comments-sidebar btn icon-btn btn-light"
data-turbolinks="false"
data-object-type="Result"
data-object-id="<%= result.id %>"
data-object-target="#Result<%= result.id %>">
<i class="sn-icon sn-icon-comments"></i>
<span class="comments-counter"
id="comment-count-<%= result.id %>"
>
<%= result.comments.count %>
</span>
</a>
</div>
</div>
</div>
<div class="panel-collapse collapse in pl-10" id="result-panel-<%= result.id %>" role="tabpanel">
<% result.result_orderable_elements.sort_by(&:position).each do |element| %>
<% if element.orderable_type == 'ResultText' %>
<%= render partial: "shareable_links/my_modules/step_elements/text", locals: { element: element.orderable } %>
<% elsif element.orderable_type == 'ResultTable'%>
<%= render partial: "shareable_links/my_modules/step_elements/table", locals: { element: element.orderable.table } %>
<% end %>
<% end %>
<% if result.result_assets.present? %>
<%= render partial: "shareable_links/my_modules/attachments", locals: { attachments: result.assets, step: result } %>
<% end %>
</div>
</div>

View file

@ -3,10 +3,9 @@
<i class="sn-icon sn-icon-more-hori"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownAssetContextMenu">
<ul class="dropdown-menu dropdown-menu-right rounded !p-2.5 sn-shadow-menu-sm" aria-labelledby="dropdownAssetContextMenu">
<li>
<%= link_to shared_protocol_asset_download_path(@shareable_link.uuid, asset), data: { turbolinks: false } do %>
<span class="sn-icon sn-icon-export"></span>
<%= link_to shared_protocol_asset_download_path(@shareable_link.uuid, asset), data: { turbolinks: false }, class: "!px-3 !py-2.5 hover:!bg-sn-super-light-blue !text-sn-blue" do %>
<%= t('Download') %>
<% end %>
</li>

View file

@ -1,18 +1,17 @@
<div class="attachment-container asset">
<div class="attachment-container asset group">
<%= link_to '#',
class: "shareable-file-preview-link",
id: "modal_link#{asset.id}",
data: {
no_turbolink: true,
id: asset.id
} do %>
class: "file-preview-link group-hover:hidden",
id: "modal_link#{asset.id}",
data: {
no_turbolink: true,
id: asset.id
} do %>
<div class="attachment-preview <%= asset.file.attached? ? asset.file.metadata['asset_type'] : '' %>">
<% if asset.previewable? && asset.medium_preview&.image&.attached? %>
<%= image_tag asset.medium_preview.url(expires_in: Constants::URL_SHORT_EXPIRE_TIME.minutes),
class: 'asset-preview-image',
style: 'opacity: 0' %>
class: 'rounded-sm' %>
<% else %>
<i class="fas <%= file_fa_icon_class(asset) if asset.file_name %>"></i>
<div class="w-[186px] h-[186px] bg-sn-super-light-grey rounded-sm"></div>
<% end %>
</div>
<div class="attachment-label"
@ -21,12 +20,28 @@
title="<%= asset.render_file_name %>">
<%= asset.render_file_name %>
</div>
<div class="attachment-metadata">
<%= t('assets.placeholder.modified_label') %> <span class="iso-formatted-date"><%= asset.updated_at.iso8601 if asset.updated_at %></span><br>
<% end %>
<div class="tw-hidden group-hover:block hovered-thumbnail h-full">
<a
href="#"
class="shareable-file-preview-link"
id="modal_link<%= asset.id %>"
data-no-turbolink="true"
data-id="<%= asset.id %>"
>
<%= asset.render_file_name %>
</a>
<div class="absolute bottom-16 text-sn-grey">
<%= number_to_human_size(asset.file_size) %>
</div>
<% end %>
<% if defined?(show_context) && show_context %>
<%= render partial: "shareable_links/my_modules/step_attachments/context_menu", locals: { asset: asset } %>
<% end %>
<div class="absolute bottom-4 w-[184px] grid grid-cols-[repeat(4,_2.5rem)] justify-between">
<%= link_to shared_protocol_asset_download_path(@shareable_link.uuid, asset), class: "btn btn-light icon-btn thumbnail-action-btn",data: { turbolinks: false } do %>
<span class="sn-icon sn-icon-export"></span>
<% end %>
</div>
<% if defined?(show_context) && show_context %>
<%= render partial: "shareable_links/my_modules/step_attachments/context_menu", locals: { asset: asset } %>
<% end %>
</div>
</div>

View file

@ -1,7 +1,7 @@
<div class="step-checklist-container" >
<div class="step-element-header no-hover">
<div class="step-element-grip-placeholder"></div>
<div class="step-element-name font-bold">
<div class="content__checklist-container" >
<div class="sci-divider my-6"></div>
<div class="checklist-header flex rounded mb-1 items-center relative w-full group/checklist-header">
<div class="grow-1 text-ellipsis whitespace-nowrap grow my-1 font-bold">
<%= render partial: "shareable_links/my_modules/inline_view", locals: { text: element.name, smart_annotation_enabled: true } %>
</div>
</div>

View file

@ -1,14 +1,14 @@
<div class="step-checklist-item step-element--locked">
<div class="step-element-header locked">
<div class="step-element-grip-placeholder"></div>
<div class="step-element-name <%= 'done' if checklist_item.checked %>">
<div class="sci-checkbox-container disabled">
<div class="content__checklist-item step-element--locked my-2">
<div class="checklist-item-header flex rounded pl-10 ml-[-2.325rem] items-center relative w-full group/checklist-item-header locked">
<div class="absolute cursor-grab justify-center left-0 px-2 tw-hidden text-sn-grey step-element-grip-placeholder"></div>
<div class="flex items-start gap-2 grow <%= 'done' if checklist_item.checked %>">
<div class="sci-checkbox-container disabled my-0.5">
<input type="checkbox"
class="sci-checkbox"
<%= 'checked' if checklist_item.checked %> />
<span class="sci-checkbox-label" ></span>
</div>
<div class="step-checklist-text !mt-1 step-element--locked">
<div class="pr-24 relative flex items-start max-w-[90ch] step-element--locked">
<%= render partial: "shareable_links/my_modules/inline_view", locals: { text: checklist_item.text, smart_annotation_enabled: true } %>
</div>
</div>

View file

@ -1,12 +1,13 @@
<div class="step-table-container">
<div class="step-element-header step-element--locked">
<div class="content__table-container">
<div class="sci-divider my-6"></div>
<div class="table-header h-9 flex rounded mb-3 items-center relative w-full group/table-header step-element--locked">
<% if element.name.present? %>
<div class="step-element-name font-bold">
<div class="grow-1 text-ellipsis whitespace-nowrap grow my-1 font-bold">
<%= render partial: "shareable_links/my_modules/inline_view", locals: { text: element.name, smart_annotation_enabled: false } %>
</div>
<% end %>
</div>
<div class="step-table view locked" tabindex="0">
<div class="table-body group/table-body relative border-solid border-transparent view locked" tabindex="0">
<input type="hidden" class="hot-table-contents" value="<%= element.contents_utf_8 %>" />
<input type="hidden" class="hot-table-metadata" value="<%= element.metadata ? element.metadata.to_json : nil %>" />
<div class="hot-table-container"></div>

View file

@ -1,13 +1,14 @@
<div class="step-text-container step-element--locked locked" tabindex="0">
<div class="content__text-container locked" tabindex="0">
<div class="sci-divider my-6"></div>
<% if element.name.present? %>
<div class="step-element-header step-element--locked mt-4">
<div class="step-element-name font-bold">
<%= render partial: "shareable_links/my_modules/inline_view", locals: { text: element.name, smart_annotation_enabled: false } %>
</div>
<div class="text-header h-9 flex rounded mb-1 items-center relative w-full group/text-header">
<div class="grow-1 text-ellipsis whitespace-nowrap grow my-1 font-bold">
<%= render partial: "shareable_links/my_modules/inline_view", locals: { text: element.name, smart_annotation_enabled: false } %>
</div>
</div>
<% end %>
<% if element.text.present? %>
<div class="view-text-element">
<div class="flex rounded min-h-[2.25rem] mb-4 relative group/text_container content__text-body max-w-[90ch]">
<%= smart_annotation_text(element.shareable_tinymce_render(:text)) %>
</div>
<% else %>

View file

@ -0,0 +1,7 @@
<div
id="repositoryItemSidebar"
data-behaviour="vue"
class="fixed top-0 right-0 h-full z-[2000]"
>
<repository-item-sidebar />
</div>

View file

@ -60,6 +60,8 @@ module Scinote
config.x.connected_devices_enabled = ENV['CONNECTED_DEVICES_ENABLED'] == 'true'
config.x.custom_sanitizer_config = nil
# Logging
config.log_formatter = proc do |severity, datetime, progname, msg|
"[#{datetime}] #{severity}: #{msg}\n"

View file

@ -326,7 +326,7 @@ class Constants
config = Sanitize::Config::RELAXED.deep_dup
config[:attributes][:all] << 'id'
config[:attributes][:all] << 'contenteditable'
config[:attributes][:all] << :data
config[:attributes]['img'] << 'data-mce-token'
config[:protocols]['img']['src'] << 'data'
INPUT_SANITIZE_CONFIG = Sanitize::Config.freeze_config(config)

View file

@ -510,7 +510,7 @@ class Extends
protocol_repository: [80, 103, 89, 87, 79, 90, 91, 88, 85, 86, 84, 81, 82,
83, 101, 112, 123, 125, 117, 119, 129, 131, 170, 173, 179, 187, 186,
190, 191, *204..215, 220, 221, 223, 227, 228, 229, *230..235,
*237..240, *253..256, *259..283],
*237..240, *253..256, *279..283],
team: [92, 94, 93, 97, 104, 244, 245],
label_templates: [*216..219]
}

View file

@ -1,7 +0,0 @@
# This code will include the listed helpers in all the assets
Rails.application.config.assets.configure do |env|
env.context_class.class_eval do
# This is required for repository_datatable.js.erb
include RepositoryDatatableHelper
end
end

View file

@ -2152,7 +2152,7 @@ en:
disabled_placeholder: "Select Experiment to enable Task"
no_options_placeholder: "No tasks available to assign items"
assign:
text: "Assign to this task"
text: "Assign to task"
flash_all_assignments_success: "Successfully assigned %{count} item(s) to the task."
flash_some_assignments_success: "Successfully assigned %{assigned_count} item(s) to the task. %{skipped_count} item(s) were already assigned to the task."
flash_assignments_failure: "Failed to assign item(s) to task."
@ -2219,6 +2219,71 @@ en:
invalid_arguments: "Can't find %{key}"
my_module_assigned_snapshot_service:
invalid_arguments: "Can't find %{key}"
item_card:
section:
information: "Information"
assigned: "Assigned (%{count})"
qr: "QR"
print_label: "Print label"
assigned:
empty: "This item is not assigned to any task."
assign: "Assign to task"
private:
one: "Assigned to %{count} private task"
other: "Assigned to %{count} private tasks"
labels:
team: "Team:"
project: "Project:"
experiment: "Experiment:"
my_module: "Task:"
default_columns:
repository_name: "Inventory"
id: "Item ID"
added_on: "Added on"
added_at: "Added at"
added_by: "Added by"
reminders:
stock_low: "Only %{stock_formated} left."
stock_empty: "No stock left"
date_expiration: "This item is expiring in %{date_expiration}."
item_expired: "This item has expired."
day:
one: "day"
other: "days"
stock_export: "Export"
custom_columns_label: "Custom columns"
no_custom_columns_label: "This item has no custom columns"
repository_time_range_value:
no_time_range: 'No time range'
repository_text_value:
no_text: 'No text'
repository_stock_value:
no_stock: 'No stock'
repository_status_value:
no_status: 'No selection'
repository_number_value:
no_number: 'No number'
repository_list_value:
no_list: 'No selection'
repository_date_value:
no_date: 'No date'
repository_date_time_value:
no_date_time: 'No date and time'
repository_date_time_range_value:
no_date_time_range: 'No date and time range'
repository_date_range_value:
no_date_range: 'No date range'
repository_checklist_value:
no_checklist: 'No selection'
repository_asset_value:
no_asset: 'No file'
repository_time_value:
no_time: 'No time'
highlight_component:
information_label: 'Information'
custom_columns_label: 'Custom columns'
assigned_label: 'Assigned'
QR_label: 'QR'
repository_stock_values:
manage_modal:
title: "Stock %{item}"
@ -2412,7 +2477,7 @@ 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: "Assign to task"
assign_to_task_error:
no_access: "You can only view this task"
already_assigned: "This item is already assigned to this task"
@ -3436,7 +3501,7 @@ en:
description: "Once you create items in the inventory, they will appear here."
buttons:
insert: "Insert"
assign: "Assign to this task"
assign: "Assign to task"
projects: PROJECTS
experiments: EXPERIMENTS
tasks: TASKS

View file

@ -38,6 +38,7 @@ const entryList = {
vue_share_task_container: './app/javascript/packs/vue/share_task_container.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_repository_item_sidebar: './app/javascript/packs/vue/repository_item_sidebar.js',
vue_components_action_toolbar: './app/javascript/packs/vue/action_toolbar.js',
vue_components_open_vector_editor: './app/javascript/packs/vue/open_vector_editor.js',
vue_navigation_breadcrumbs: './app/javascript/packs/vue/navigation/breadcrumbs.js',

View file

@ -82,6 +82,7 @@
"vue-template-compiler": "^2.6.12",
"vue-turbolinks": "^2.2.1",
"vue2-perfect-scrollbar": "^1.5.56",
"vue2-scrollspy": "^2.3.1",
"vuedraggable": "^2.24.3",
"webpack": "^5.64.4",
"webpack-cli": "^4.10.0",

View file

@ -58,7 +58,7 @@ describe AccessPermissions::ExperimentsController, type: :controller do
{
id: experiment.id,
project_id: project.id,
experiment_member: {
user_assignment: {
user_role_id: technician_role.id,
user_id: viewer_user.id
}

View file

@ -61,7 +61,7 @@ describe AccessPermissions::MyModulesController, type: :controller do
id: my_module.id,
experiment_id: experiment.id,
project_id: project.id,
my_module_member: {
user_assignment: {
user_role_id: technician_role.id,
user_id: viewer_user.id
}

View file

@ -100,7 +100,7 @@ describe AccessPermissions::ProjectsController, type: :controller do
let(:valid_params) do
{
id: project.id,
project_member: {
user_assignment: {
user_role_id: technician_role.id,
user_id: normal_user.id
}
@ -127,7 +127,7 @@ describe AccessPermissions::ProjectsController, type: :controller do
let(:valid_params) do
{
id: project.id,
access_permissions_new_user_project_form: {
access_permissions_new_user_form: {
resource_members: {
0 => {
assign: '1',
@ -193,8 +193,7 @@ describe AccessPermissions::ProjectsController, type: :controller do
it 'removes the user project and user assigment record' do
expect {
delete :destroy, params: valid_params, format: :json
}.to change(UserProject, :count).by(-1).and \
change(UserAssignment, :count).by(-1)
}.to change(UserAssignment, :count).by(-1)
end
it 'renders 403 if user does not have manage permissions on project' do

View file

@ -68,8 +68,10 @@ describe ProjectsController, type: :controller do
let(:action) { put :update, params: params }
let(:params) do
{ id: projects.first.id,
project: { name: projects.first.name, team_id: projects.first.team.id,
visibility: projects.first.visibility } }
project: { name: projects.first.name,
team_id: projects.first.team.id,
visibility: projects.first.visibility,
default_public_user_role_id: projects.first.default_public_user_role.id } }
end
it 'returns redirect response' do
@ -78,10 +80,10 @@ describe ProjectsController, type: :controller do
expect(response.media_type).to eq 'text/html'
end
it 'calls create activity service (change_project_visibility)' do
it 'calls create activity service (project_grant_access_to_all_team_members)' do
params[:project][:visibility] = 'visible'
expect(Activities::CreateActivityService).to receive(:call)
.with(hash_including(activity_type: :change_project_visibility))
.with(hash_including(activity_type: :project_grant_access_to_all_team_members))
action
end

View file

@ -5,7 +5,7 @@ require 'rails_helper'
describe ProtocolsController, type: :controller do
login_user
include_context 'reference_project_structure'
include_context 'reference_project_structure', { team_role: :owner }
describe 'POST create' do
let(:action) { post :create, params: params, format: :json }
@ -78,10 +78,12 @@ describe ProtocolsController, type: :controller do
protocol: {
name: 'my_test_protocol',
description: 'description',
authors: 'authors'
authors: 'authors',
elnVersion: '1.0',
}
}
end
let(:action) { post :import, params: params, format: :json }
it 'calls create activity for importing protocols' do
@ -98,9 +100,9 @@ describe ProtocolsController, type: :controller do
end
end
describe 'PUT description' do
describe 'PATCH description' do
let(:protocol) do
create :protocol, :in_public_repository, team: team, added_by: user
create :protocol, :in_repository_draft, team: team, added_by: user
end
let(:params) do
{
@ -110,8 +112,7 @@ describe ProtocolsController, type: :controller do
}
}
end
let(:action) { put :update_description, params: params, format: :json }
let(:action) { patch :update_description, params: params, format: :json }
it 'calls create activity for updating description' do
expect(Activities::CreateActivityService)
.to(receive(:call)
@ -126,14 +127,14 @@ describe ProtocolsController, type: :controller do
end
end
describe 'POST update_keywords' do
describe 'PATCH update_keywords' do
let(:protocol) do
create :protocol, :in_public_repository, team: team, added_by: user
create :protocol, :in_repository_draft, team: team, added_by: user
end
let(:action) { put :update_keywords, params: params, format: :json }
let(:params) do
{ id: protocol.id, keywords: ['keyword-1', 'keyword-2'] }
end
let(:action) { patch :update_keywords, params: params, format: :json }
it 'calls create activity for updating keywords' do
expect(Activities::CreateActivityService)
@ -151,7 +152,7 @@ describe ProtocolsController, type: :controller do
context 'update protocol' do
let(:protocol_repo) do
create :protocol, :in_public_repository, name: ' test protocol',
create :protocol, :in_repository_published_original, name: ' test protocol',
team: team,
added_by: user
end
@ -165,7 +166,7 @@ describe ProtocolsController, type: :controller do
let(:params) { { id: protocol.id } }
describe 'POST revert' do
let(:action) { put :revert, params: params, format: :json }
let(:action) { post :revert, params: params, format: :json }
it 'calls create activity for updating protocol in task from repository' do
expect(Activities::CreateActivityService)
@ -184,13 +185,13 @@ describe ProtocolsController, type: :controller do
describe 'POST load_from_repository' do
let(:protocol_source) do
create :protocol, :in_public_repository, team: team, added_by: user
create :protocol, :in_repository_published_original, team: team, added_by: user
end
let(:protocol) { create :protocol, team: team, added_by: user, my_module: my_module }
let(:action) { put :load_from_repository, params: params, format: :json }
let(:params) do
{ source_id: protocol_source.id, id: protocol.id }
end
let(:action) { post :load_from_repository, params: params, format: :json }
it 'calls create activity for loading protocol to task from repository' do
expect(Activities::CreateActivityService)
@ -210,13 +211,14 @@ describe ProtocolsController, type: :controller do
let(:protocol) do
create :protocol, my_module: my_module, team: team, added_by: user
end
let(:action) { put :load_from_file, params: params, format: :json }
let(:params) do
{ id: protocol.id,
protocol: { name: 'my_test_protocol',
description: 'description',
authors: 'authors' } }
authors: 'authors',
elnVersion: '1.1'} }
end
let(:action) { post :load_from_file, params: params, format: :json }
it 'calls create activity for loading protocol to task from file' do
expect(Activities::CreateActivityService)

View file

@ -16,7 +16,8 @@ describe ResultTablesController, type: :controller do
result:
{ name: 'result name created',
table_attributes:
{ contents: '{\"data\":[[\"a\",\"b\",\"1\",null,null]]}' } } }
{ contents: '{\"data\":[[\"a\",\"b\",\"1\",null,null]]}',
metadata: "{\"cells\":[{\"row\":\"0\",\"col\":\"0\",\"className\":\"\",\"calculated\":\"\"}]}" } } }
end
it 'calls create activity service' do

View file

@ -10,7 +10,7 @@ describe StepsController, type: :controller do
}
let(:protocol_repo) do
create :protocol, :in_public_repository, team: team, added_by: user
create :protocol, :in_repository_draft, team: team, added_by: user
end
let(:step_repo) { create :step, protocol: protocol_repo }

View file

@ -12,11 +12,11 @@ describe TeamRepositoriesController, type: :controller do
describe 'DELETE destroy' do
let(:second_team) { create :team, created_by: user }
let(:team_repository) { create :team_repository, :read, team: second_team, repository: repository }
let(:team_repository) { create :team_shared_object, :read, team: second_team, shared_object: repository }
context 'when resource can be deleted' do
let(:action) do
delete :destroy, params: { repository_id: repository.id, team_id: team.id, id: team_repository.id }
delete :destroy, params: { team_id: team.id, id: team_repository.id }
end
it 'renders 204' do

View file

@ -4,5 +4,15 @@ FactoryBot.define do
factory :settings do
type { Faker::Lorem.sentence.split(' ').sample }
values { { key_of_data: Faker::Lorem.sentence.split(' ').sample } }
trait :with_load_values_from_env_defined do
after(:build) do |application_settings|
application_settings.define_singleton_method(:load_values_from_env) do
{
some_key: Faker::Lorem.sentence.split(' ').sample
}
end
end
end
end
end

View file

@ -1,10 +1,7 @@
# frozen_string_literal: true
# frozen_string_literal: true
FactoryBot.define do
factory :team_shared_object do
repository
trait :read do
permission_level { :shared_read }
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
FactoryBot.define do
factory :user_assignment do
end

View file

@ -2,9 +2,7 @@ FactoryBot.define do
factory :user_role do
factory :owner_role do
name { I18n.t('user_roles.predefined.owner') }
permissions { ProjectPermissions.constants.map { |const| ProjectPermissions.const_get(const) } +
ExperimentPermissions.constants.map { |const| ExperimentPermissions.const_get(const) } +
MyModulePermissions.constants.map { |const| MyModulePermissions.const_get(const) } }
permissions { PredefinedRoles::OWNER_PERMISSIONS }
predefined { true }
end

Some files were not shown because too many files have changed in this diff Show more