Merge branch 'develop' into features/file-versioning

This commit is contained in:
Martin Artnik 2024-10-08 11:04:33 +02:00
commit f7dde1b24c
71 changed files with 559 additions and 391 deletions

View file

@ -62,6 +62,7 @@ gem 'logging', '~> 2.0.0'
gem 'nested_form_fields'
gem 'nokogiri', '~> 1.16.5' # HTML/XML parser
gem 'noticed'
gem 'oj'
gem 'rails_autolink', '~> 1.1', '>= 1.1.6'
gem 'rgl' # Graph framework for project diagram calculations
gem 'roo', '~> 2.10.0' # Spreadsheet parser

View file

@ -97,9 +97,9 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_model_serializers (0.10.13)
actionpack (>= 4.1, < 7.1)
activemodel (>= 4.1, < 7.1)
active_model_serializers (0.10.14)
actionpack (>= 4.1)
activemodel (>= 4.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.0.8.4)
@ -195,6 +195,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
rouge (>= 1.0.0)
bigdecimal (3.1.8)
bindata (2.5.0)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
@ -202,7 +203,7 @@ GEM
msgpack (~> 1.2)
brakeman (6.1.2)
racc
builder (3.2.4)
builder (3.3.0)
bullet (7.0.7)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
@ -316,7 +317,7 @@ GEM
railties (>= 5)
down (5.4.1)
addressable (~> 2.8)
erubi (1.12.0)
erubi (1.13.0)
et-orbi (1.2.11)
tzinfo
execjs (2.8.1)
@ -364,7 +365,7 @@ GEM
httparty (0.21.0)
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.14.5)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
i18n-js (3.9.2)
i18n (>= 0.6.6)
@ -372,7 +373,7 @@ GEM
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
iniparse (1.5.0)
jbuilder (2.11.5)
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jmespath (1.6.2)
@ -435,8 +436,7 @@ GEM
mime-types-data (3.2023.0218.1)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.23.1)
minitest (5.25.1)
msgpack (1.7.1)
multi_json (1.15.0)
multi_test (1.1.0)
@ -475,6 +475,9 @@ GEM
rack (>= 1.2, < 4)
snaky_hash (~> 2.0)
version_gem (~> 1.1)
oj (3.16.6)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (2.1.2)
hashie (>= 3.4.6)
rack (>= 2.2.3)
@ -509,6 +512,7 @@ GEM
validate_url
webfinger (~> 2.0)
orm_adapter (0.5.0)
ostruct (0.6.0)
overcommit (0.60.0)
childprocess (>= 0.6.3, < 5)
iniparse (~> 1.4)
@ -835,6 +839,7 @@ DEPENDENCIES
newrelic_rpm
nokogiri (~> 1.16.5)
noticed
oj
omniauth (~> 2.1)
omniauth-azure-activedirectory-v2
omniauth-linkedin-oauth2

View file

@ -433,10 +433,10 @@ var MyModuleRepositories = (function() {
}
function initSimpleTable() {
$('#assigned-items-container').on('shown.bs.collapse', '.assigned-repository-container.readable-repository', function() {
$('#assigned-items-container').on('shown.bs.collapse', '.assigned-repository-container', function() {
var repositoryContainer = $(this);
var repositoryTable = repositoryContainer.find('.repository-table');
var initializedTable = repositoryContainer.find('.dataTables_wrapper .repository-table');
var repositoryTable = repositoryContainer.find('.table');
var initializedTable = repositoryContainer.find('.dataTables_wrapper table');
// do not try to re-initialized already initialized table
if (initializedTable.length) {

View file

@ -317,9 +317,7 @@ var RepositoryDatatable = (function(global) {
checkAvailableColumns();
RepositoryDatatableRowEditor.switchRowToEditMode(row);
changeToEditMode();
RepositoryDatatableRowEditor.switchRowToEditMode(row, changeToEditMode);
});
}
@ -692,6 +690,7 @@ var RepositoryDatatable = (function(global) {
},
rowCallback: function(row, data) {
$(row).attr('data-editable', data.recordEditable);
$(row).attr('data-info-url', data.recordInfoUrl);
$(row).attr('data-manage-stock-url', data.manageStockUrl);
// Get row ID
let rowId = data.DT_RowId;
@ -1003,10 +1002,8 @@ var RepositoryDatatable = (function(global) {
$(TABLE_ID).find('.repository-row-edit-icon').remove();
rowsSelected.forEach(function(rowNumber) {
RepositoryDatatableRowEditor.switchRowToEditMode(TABLE.row('#' + rowNumber));
RepositoryDatatableRowEditor.switchRowToEditMode(TABLE.row('#' + rowNumber), changeToEditMode);
});
changeToEditMode();
})
.on('click', '#assignRepositoryRecords', function(e) {
e.preventDefault();

View file

@ -173,11 +173,17 @@ var RepositoryDatatableRowEditor = (function() {
TABLE.columns.adjust();
}
function switchRowToEditMode(row) {
function enableEditMode(row, isEditable) {
if (!isEditable) {
HelperModule.flashAlertMsg(I18n.t('repositories.table.row_locked'), 'danger');
return false;
}
let $row = $(row.node());
let itemId = row.id();
let formId = `repositoryRowForm${itemId}`;
let requestUrl = $(TABLE.table().node()).data('current-uri');
let rowForm = $(`
<form id="${formId}"
class="${EDIT_FORM_CLASS_NAME} ${GLOBAL_CONSTANTS.HAS_UNSAVED_DATA_CLASS_NAME}"
@ -213,6 +219,26 @@ var RepositoryDatatableRowEditor = (function() {
initAssetCellActions($row);
TABLE.columns.adjust();
return true;
}
function switchRowToEditMode(row, editEnabledCallback) {
// Editable property was already preloaded
if (row.data().editable !== undefined) {
if (enableEditMode(row, row.data().editable)) editEnabledCallback();
return;
}
// Need to fetch editable property
$.ajax({
url: row.data().recordInfoUrl,
type: 'GET',
dataType: 'json',
success: (data) => {
if (enableEditMode(row, data.editable)) editEnabledCallback();
}
});
}
return Object.freeze({

View file

@ -15,7 +15,14 @@ class MyModuleRepositoriesController < ApplicationController
@draw = params[:draw].to_i
per_page = params[:length].to_i < 1 ? Constants::REPOSITORY_DEFAULT_PAGE_SIZE : params[:length].to_i
page = (params[:start].to_i / per_page) + 1
datatable_service = RepositoryDatatableService.new(@repository, params, current_user, @my_module)
if params[:simple_view]
rows_view = 'repository_rows/simple_view_index'
preload_cells = false
else
rows_view = 'repository_rows/index'
preload_cells = true
end
datatable_service = RepositoryDatatableService.new(@repository, params, current_user, @my_module, preload_cells: preload_cells)
@datatable_params = {
view_mode: params[:view_mode],
@ -26,21 +33,9 @@ class MyModuleRepositoriesController < ApplicationController
@all_rows_count = datatable_service.all_count
@columns_mappings = datatable_service.mappings
if params[:simple_view]
repository_rows = datatable_service.repository_rows
rows_view = 'repository_rows/simple_view_index'
else
repository_rows = datatable_service.repository_rows
.preload(:repository_columns,
:created_by,
:archived_by,
:last_modified_by,
repository_cells: { value: @repository.cell_preload_includes })
rows_view = 'repository_rows/index'
end
repository_rows = datatable_service.repository_rows
@repository_rows = repository_rows.page(page).per(per_page)
@filtered_rows_count = @repository_rows.load.take&.filtered_count || 0
render rows_view
end

View file

@ -16,20 +16,16 @@ class MyModuleRepositorySnapshotsController < ApplicationController
@all_rows_count = datatable_service.all_count
@columns_mappings = datatable_service.mappings
repository_rows = datatable_service.repository_rows
if params[:simple_view]
repository_rows = datatable_service.repository_rows
@repository = @repository_snapshot
rows_view = 'repository_rows/simple_view_index'
else
repository_rows =
datatable_service.repository_rows
.preload(:repository_columns,
:created_by,
repository_cells: { value: @repository_snapshot.cell_preload_includes })
rows_view = 'repository_rows/snapshot_index'
end
@repository_rows = repository_rows.page(page).per(per_page)
@filtered_rows_count = @repository_rows.load.take&.filtered_count || 0
render rows_view
end

View file

@ -62,7 +62,7 @@ class MyModuleShareableLinksController < ApplicationController
@draw = params[:draw].to_i
per_page = params[:length].to_i < 1 ? Constants::REPOSITORY_DEFAULT_PAGE_SIZE : params[:length].to_i
page = (params[:start].to_i / per_page) + 1
datatable_service = RepositoryDatatableService.new(@repository, params, nil, @my_module)
datatable_service = RepositoryDatatableService.new(@repository, params, nil, @my_module, preload_cells: false)
@datatable_params = {
view_mode: params[:view_mode],
@ -76,6 +76,7 @@ class MyModuleShareableLinksController < ApplicationController
@columns_mappings = datatable_service.mappings
@repository_rows = datatable_service.repository_rows.page(page).per(per_page)
@filtered_rows_count = @repository_rows.load.take&.filtered_count || 0
render 'repository_rows/simple_view_index'
end
@ -84,13 +85,14 @@ class MyModuleShareableLinksController < ApplicationController
@draw = params[:draw].to_i
per_page = params[:length].to_i < 1 ? Constants::REPOSITORY_DEFAULT_PAGE_SIZE : params[:length].to_i
page = (params[:start].to_i / per_page) + 1
datatable_service = RepositorySnapshotDatatableService.new(@repository_snapshot, params, nil, @my_module)
datatable_service = RepositorySnapshotDatatableService.new(@repository_snapshot, params, nil, @my_module, preload_cells: false)
@all_rows_count = datatable_service.all_count
@columns_mappings = datatable_service.mappings
@repository = @repository_snapshot
@repository_rows = datatable_service.repository_rows.page(page).per(per_page)
@filtered_rows_count = @repository_rows.load.take&.filtered_count || 0
render 'repository_rows/simple_view_index'
end

View file

@ -304,7 +304,7 @@ class MyModulesController < ApplicationController
def protocols
@protocol = @my_module.protocol
@assigned_repositories = @my_module.live_and_snapshot_repositories_list
@assigned_repositories = @my_module.readable_live_and_snapshot_repositories_list(current_user)
end
def protocol

View file

@ -44,11 +44,12 @@ class RepositoriesController < ApplicationController
end
def list
results = @repositories
results = @repositories.select(:id, :name, 'LOWER(repositories.name)')
results = results.name_like(params[:query]) if params[:query].present?
results = results.joins(:repository_rows).distinct if params[:non_empty].present?
results = results.active if params[:active].present?
render json: { data: results.map { |r| [r.id, r.name] } }
render json: { data: results.order('LOWER(repositories.name) asc').map { |r| [r.id, r.name] } }
end
def rows_list
@ -59,7 +60,15 @@ class RepositoriesController < ApplicationController
params[:query]
)
end
render json: { data: results.map { |r| [r.id, r.name] } }
results = results.active if params[:active].present?
results = results.order('LOWER(repository_rows.name) asc').page(params[:page])
render json: {
paginated: true,
next_page: results.next_page,
data: results.map { |r| [r.id, r.name] }
}
end
def sidebar

View file

@ -28,16 +28,10 @@ class RepositoryRowsController < ApplicationController
@all_rows_count = datatable_service.all_count
@columns_mappings = datatable_service.mappings
@repository_rows = datatable_service.repository_rows
.preload(:repository_columns,
:created_by,
:archived_by,
:last_modified_by,
repository_cells: { value: @repository.cell_preload_includes })
.page(page)
.per(per_page)
@repository_rows = @repository_rows.where(archived: params[:archived]) unless @repository.archived?
repository_rows = datatable_service.repository_rows
repository_rows = repository_rows.where(archived: params[:archived]) unless @repository.archived?
@repository_rows = repository_rows.page(page).per(per_page)
@filtered_rows_count = @repository_rows.load.take&.filtered_count || 0
rescue RepositoryFilters::ColumnNotFoundException
render json: { custom_error: I18n.t('repositories.show.repository_filter.errors.column_not_found') }
rescue RepositoryFilters::ValueNotFoundException

View file

@ -26,12 +26,14 @@ class StorageLocationRepositoryRowsController < ApplicationController
created_by: current_user
)
if @storage_location_repository_row.save
log_activity(:storage_location_repository_row_created)
render json: @storage_location_repository_row,
serializer: Lists::StorageLocationRepositoryRowSerializer
else
render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity
@storage_location_repository_row.with_lock do
if @storage_location_repository_row.save
log_activity(:storage_location_repository_row_created)
render json: @storage_location_repository_row,
serializer: Lists::StorageLocationRepositoryRowSerializer
else
render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity
end
end
end
end
@ -131,7 +133,7 @@ class StorageLocationRepositoryRowsController < ApplicationController
.call(activity_type: type_of,
owner: current_user,
team: @storage_location.team,
subject: @storage_location_repository_row.repository_row,
subject: @storage_location_repository_row.storage_location,
message_items: {
storage_location: @storage_location_repository_row.storage_location_id,
repository_row: @storage_location_repository_row.repository_row_id,

View file

@ -9,9 +9,9 @@ class StorageLocationsController < ApplicationController
before_action :switch_team_with_param, only: %i(index show)
before_action :check_storage_locations_enabled, except: :unassign_rows
before_action :load_storage_location, only: %i(update destroy duplicate move show available_positions unassign_rows export_container import_container)
before_action :check_read_permissions, except: %i(index create tree actions_toolbar)
before_action :check_read_permissions, except: %i(index create tree actions_toolbar import_container unassign_rows)
before_action :check_create_permissions, only: :create
before_action :check_manage_permissions, only: %i(update destroy duplicate move unassign_rows import_container)
before_action :check_manage_permissions, only: %i(update destroy duplicate move)
before_action :set_breadcrumbs_items, only: %i(index show)
def index
@ -86,7 +86,7 @@ class StorageLocationsController < ApplicationController
def duplicate
ActiveRecord::Base.transaction do
new_storage_location = @storage_location.duplicate!
new_storage_location = @storage_location.duplicate!(current_user)
if new_storage_location
@storage_location = new_storage_location
log_activity('storage_location_created')
@ -123,8 +123,17 @@ class StorageLocationsController < ApplicationController
end
def tree
records = StorageLocation.viewable_by_user(current_user, current_team).where(parent: nil, container: [false, params[:container] == 'true'])
render json: storage_locations_recursive_builder(records)
records = StorageLocation.viewable_by_user(current_user, current_team)
.where(
parent: nil,
container: [false, params[:container] == 'true']
)
records = records.where(team_id: params[:team_id]) if params[:team_id]
render json: {
locations: storage_locations_recursive_builder(records),
movable_to_root: params[:team_id] && current_team.id == params[:team_id].to_i
}
end
def available_positions
@ -253,7 +262,7 @@ class StorageLocationsController < ApplicationController
end
def storage_locations_recursive_builder(storage_locations)
storage_locations.map do |storage_location|
storage_locations.order('LOWER(storage_locations.name) ASC').map do |storage_location|
{
storage_location: storage_location,
can_manage: (can_manage_storage_location?(storage_location) unless storage_location.parent_id),

View file

@ -163,9 +163,8 @@ module Users
error_message ||= I18n.t('devise.okta.errors.generic')
redirect_to after_omniauth_failure_path_for(resource_name)
ensure
if user&.errors.present? || missing_attribute.present?
missing_attribute ||= user.errors.first.attribute.capitalize
set_flash_message(:alert, :missing_attribute, attribute: missing_attribute)
if user&.errors.present?
set_flash_message(:alert, :missing_attribute, attribute: user.errors.first.attribute.capitalize)
elsif error_message
set_flash_message(:alert, :failure, kind: I18n.t('devise.okta.provider_name'), reason: error_message)
else

View file

@ -141,10 +141,9 @@ module ApplicationHelper
# Check if text have smart annotations of users
# and outputs a popover with user information
def smart_annotation_filter_users(text, team, base64_encoded_imgs: false)
sa_user = /\[\@(.*?)~([0-9a-zA-Z]+)\]/
text.gsub(sa_user) do |el|
match = el.match(sa_user)
user = User.find_by_id(match[2].base62_decode)
text.gsub(SmartAnnotations::TagToHtml::USER_REGEX) do |el|
match = el.match(SmartAnnotations::TagToHtml::USER_REGEX)
user = User.find_by(id: match[2].base62_decode)
next unless user
popover_for_user_name(user, team, false, false, base64_encoded_imgs)

View file

@ -3,6 +3,7 @@
module GlobalActivitiesHelper
include ActionView::Helpers::AssetTagHelper
include ActionView::Helpers::UrlHelper
include Canaid::Helpers::PermissionsHelper
include InputSanitizeHelper
def generate_activity_content(activity, no_links: false, no_custom_links: false)
@ -60,6 +61,9 @@ module GlobalActivitiesHelper
when Repository
path = repository_path(obj, team: obj.team.id)
when RepositoryRow
# Handle private repository rows
return I18n.t('storage_locations.show.hidden') unless can_read_repository?(obj.repository)
return current_value unless obj.repository
path = repository_path(obj.repository, team: obj.repository.team.id)
@ -127,6 +131,8 @@ module GlobalActivitiesHelper
message_item['type'].constantize.new
end
return I18n.t('storage_locations.show.hidden') if obj.is_a?(RepositoryRow) && !can_read_repository?(obj.repository)
return I18n.t('projects.index.breadcrumbs_root') if obj.is_a?(ProjectFolder) && obj.new_record?
return I18n.t('storage_locations.index.breadcrumbs_root') if obj.is_a?(StorageLocation) && obj.new_record?

View file

@ -46,9 +46,8 @@ module InputSanitizeHelper
sanitizer_config = Constants::INPUT_SANITIZE_CONFIG.deep_dup
text = sanitize_input(text, tags, sanitizer_config: sanitizer_config)
if text =~ SmartAnnotations::TagToHtml::USER_REGEX || text =~ SmartAnnotations::TagToHtml::REGEX
text = smart_annotation_parser(text, team, base64_encoded_imgs, preview_repository)
end
text = smart_annotation_parser(text, team, base64_encoded_imgs, preview_repository) if text.match?(SmartAnnotations::TagToHtml::ALL_REGEX)
auto_link(
text,
html: { target: '_blank' },

View file

@ -5,44 +5,38 @@ module RepositoryDatatableHelper
include Rails.application.routes.url_helpers
def prepare_row_columns(repository_rows, repository, columns_mappings, team, options = {})
# repository_rows collection is already preloaded in controllers, do not modify scopes or query params
# otherwise it will result in duplicated SQL queries
has_stock_management = repository.has_stock_management?
stock_management_column_exists = repository.repository_columns.stock_type.exists?
repository_row_connections_enabled = Repository.repository_row_connections_enabled?
reminders_enabled = Repository.reminders_enabled?
repository_rows = reminders_enabled ? with_reminders_status(repository_rows, repository) : repository_rows
stock_managable = has_stock_management && !options[:disable_stock_management] &&
can_manage_repository_stock?(repository) &&
!repository.is_a?(SoftLockedRepository)
stock_consumption_permitted = has_stock_management && options[:include_stock_consumption] && options[:my_module] &&
stock_consumption_permitted?(repository, options[:my_module])
default_columns_method_name = "#{repository.class.name.underscore}_default_columns"
repository_rows.map do |record|
row = public_send("#{repository.class.name.underscore}_default_columns", record)
row.merge!(
DT_RowId: record.id,
DT_RowAttr: { 'data-state': row_style(record), 'data-e2e': "e2e-TR-invInventory-bodyRow-#{record.id}" },
recordInfoUrl: Rails.application.routes.url_helpers.repository_repository_row_path(repository, record),
rowRemindersUrl:
Rails.application.routes.url_helpers
.active_reminder_repository_cells_repository_repository_row_url(
repository,
record
),
relationshipsUrl:
Rails.application.routes.url_helpers
.relationships_repository_repository_row_url(record.repository_id, record.id),
relationships_enabled: repository_row_connections_enabled,
code: record.code
)
if reminders_enabled
row['hasActiveReminders'] = record.has_active_stock_reminders || record.has_active_datetime_reminders
end
row = public_send(default_columns_method_name, record)
row['code'] = record.code
row['DT_RowId'] = record.id
row['DT_RowAttr'] = { 'data-state': row_style(record), 'data-e2e': "e2e-TR-invInventory-bodyRow-#{record.id}" }
row['recordInfoUrl'] = Rails.application.routes.url_helpers.repository_repository_row_path(repository.id, record.id)
row['rowRemindersUrl'] = Rails.application.routes.url_helpers
.active_reminder_repository_cells_repository_repository_row_url(repository.id, record.id)
row['relationshipsUrl'] = Rails.application.routes.url_helpers
.relationships_repository_repository_row_url(record.repository_id, record.id)
row['relationships_enabled'] = repository_row_connections_enabled
row['hasActiveReminders'] = record.has_active_reminders if reminders_enabled
unless options[:view_mode] || repository.is_a?(SoftLockedRepository)
row['recordUpdateUrl'] =
Rails.application.routes.url_helpers.repository_repository_row_path(repository, record)
row['recordEditable'] = record.editable?
# if the editable? property will be checked in a separate request, we can default it to true
row['recordEditable'] = options[:omit_editable] ? true : record.editable?
end
row['0'] = record[:row_assigned] if options[:my_module]
@ -51,8 +45,7 @@ module RepositoryDatatableHelper
custom_cells = record.repository_cells.filter { |cell| cell.value_type != 'RepositoryStockValue' }
custom_cells.each do |cell|
row[columns_mappings[cell.repository_column.id]] =
serialize_repository_cell_value(cell, team, repository, reminders_enabled: reminders_enabled)
row[columns_mappings[cell.repository_column_id]] = serialize_repository_cell_value(cell, team, repository, reminders_enabled: reminders_enabled)
end
if has_stock_management
@ -105,9 +98,10 @@ module RepositoryDatatableHelper
end
def prepare_simple_view_row_columns(repository_rows, repository, my_module, options = {})
# repository_rows collection is already preloaded in controllers, do not modify scopes or query params
# otherwise it will result in duplicated SQL queries
has_stock_management = repository.has_stock_management?
reminders_enabled = !options[:disable_reminders] && Repository.reminders_enabled?
repository_rows = reminders_enabled ? with_reminders_status(repository_rows, repository) : repository_rows
# Always disabled in a simple view
stock_managable = false
stock_consumption_permitted = has_stock_management && stock_consumption_permitted?(repository, my_module)
@ -126,9 +120,7 @@ module RepositoryDatatableHelper
)
}
if reminders_enabled
row['hasActiveReminders'] = record.has_active_stock_reminders || record.has_active_datetime_reminders
end
row['hasActiveReminders'] = record.has_active_reminders if reminders_enabled
if has_stock_management
stock_present = record.repository_stock_cell.present?
@ -193,15 +185,14 @@ module RepositoryDatatableHelper
'1': record.code,
'2': escape_input(record.name),
'3': I18n.l(record.created_at, format: :full),
'4': escape_input(record.created_by.full_name),
'4': escape_input(record.created_by_full_name),
'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]] =
serialize_repository_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
@ -239,11 +230,11 @@ module RepositoryDatatableHelper
'3': escape_input(record.name),
'4': "#{record.parent_connections_count || 0} / #{record.child_connections_count || 0}",
'5': I18n.l(record.created_at, format: :full),
'6': escape_input(record.created_by.full_name),
'6': escape_input(record.created_by_full_name),
'7': (record.updated_at ? I18n.l(record.updated_at, format: :full) : ''),
'8': escape_input(record.last_modified_by.full_name),
'8': escape_input(record.last_modified_by_full_name),
'9': (record.archived_on ? I18n.l(record.archived_on, format: :full) : ''),
'10': escape_input(record.archived_by&.full_name)
'10': escape_input(record.archived_by_full_name)
}
end
@ -254,9 +245,9 @@ module RepositoryDatatableHelper
'3': escape_input(record.name),
'4': "#{record.parent_connections_count || 0} / #{record.child_connections_count || 0}",
'5': I18n.l(record.created_at, format: :full),
'6': escape_input(record.created_by.full_name),
'6': escape_input(record.created_by_full_name),
'7': (record.archived_on ? I18n.l(record.archived_on, format: :full) : ''),
'8': escape_input(record.archived_by&.full_name)
'8': escape_input(record.archived_by_full_name)
}
end
@ -266,9 +257,9 @@ module RepositoryDatatableHelper
'2': record.code,
'3': escape_input(record.name),
'4': I18n.l(record.created_at, format: :full),
'5': escape_input(record.created_by.full_name),
'5': escape_input(record.created_by_full_name),
'6': (record.archived_on ? I18n.l(record.archived_on, format: :full) : ''),
'7': escape_input(record.archived_by&.full_name),
'7': escape_input(record.archived_by_full_name),
'8': escape_input(record.external_id)
}
end
@ -313,35 +304,6 @@ module RepositoryDatatableHelper
''
end
def with_reminders_status(repository_rows, repository)
# don't load reminders for archived repositories or snapshots
if repository.archived? || repository.is_a?(RepositorySnapshot)
return repository_rows.select('FALSE AS has_active_stock_reminders')
.select('FALSE AS has_active_datetime_reminders')
end
repository_cells = RepositoryCell.joins(
"INNER JOIN repository_columns ON repository_columns.id = repository_cells.repository_column_id " \
"AND repository_columns.repository_id = #{repository.id}"
)
repository_rows
.joins(
"LEFT OUTER JOIN (#{RepositoryCell.stock_reminder_repository_cells_scope(repository_cells, current_user)
.select(:id, :repository_row_id).to_sql}) " \
"AS repository_cells_with_active_stock_reminders " \
"ON repository_cells_with_active_stock_reminders.repository_row_id = repository_rows.id"
)
.joins(
"LEFT OUTER JOIN (#{RepositoryCell.date_time_reminder_repository_cells_scope(repository_cells, current_user)
.select(:id, :repository_row_id).to_sql}) " \
"AS repository_cells_with_active_datetime_reminders " \
"ON repository_cells_with_active_datetime_reminders.repository_row_id = repository_rows.id"
)
.select('COUNT(repository_cells_with_active_stock_reminders.id) > 0 AS has_active_stock_reminders')
.select('COUNT(repository_cells_with_active_datetime_reminders.id) > 0 AS has_active_datetime_reminders')
end
def stock_consumption_permitted?(repository, my_module)
return false unless repository.is_a?(Repository) && current_user

View file

@ -1,5 +1,7 @@
module StorageLocationsHelper
def storage_locations_placeholder
return if StorageLocation.storage_locations_enabled?
"<div class=\"p-4 rounded bg-sn-super-light-blue\">
#{I18n.t('storage_locations.storage_locations_disabled')}
</div>"

View file

@ -7,7 +7,10 @@
{{ i18n.t('repositories.locations.assign') }}
</button>
</div>
<template v-if="repositoryRow.storage_locations.enabled" v-for="(location, index) in repositoryRow.storage_locations.locations" :key="location.id">
<div class="mb-4">
<div v-html="repositoryRow.storage_locations.placeholder"></div>
</div>
<template v-for="(location, index) in repositoryRow.storage_locations.locations" :key="location.id">
<div>
<div class="sci-divider my-4" v-if="index > 0"></div>
<div class="flex gap-2 mb-3">
@ -30,15 +33,13 @@
</div>
</div>
</template>
<div v-else>
<div v-html="repositoryRow.storage_locations.placeholder"></div>
</div>
<Teleport to="body">
<AssignModal
v-if="openAssignModal"
assignMode="assign"
:selectedRow="repositoryRow.id"
@close="openAssignModal = false; $emit('reloadRow')"
:selectedRowName="repositoryRow.default_columns.name"
@close="openAssignModal = false; $emit('reloadRow'); reloadStorageLocations()"
></AssignModal>
<ConfirmationModal
:title="i18n.t('storage_locations.show.unassign_modal.title')"
@ -86,6 +87,11 @@ export default {
}
return '';
},
reloadStorageLocations() {
if (window.StorageLocationsContainer) {
window.StorageLocationsContainer.$refs.container.reloadingTable = true;
}
},
numberToLetter(number) {
return String.fromCharCode(96 + number);
},
@ -95,9 +101,7 @@ export default {
axios.post(unassign_rows_storage_location_path({ id: locationId }), { ids: [rowId] })
.then(() => {
this.$emit('reloadRow');
if (window.StorageLocationsContainer) {
window.StorageLocationsContainer.$refs.container.reloadingTable = true;
}
this.reloadStorageLocations();
});
}
}

View file

@ -24,10 +24,13 @@
</template>
<script>
import axios from '../../../packs/custom_axios.js';
export default {
name: 'ActionToolbar',
props: {
actionsUrl: { type: String, required: true },
actionsMethod: { type: String, default: 'get' },
params: { type: Object },
},
data() {
@ -51,8 +54,14 @@ export default {
loadActions() {
this.loading = true;
this.loaded = false;
$.get(`${this.actionsUrl}?${new URLSearchParams(this.params).toString()}`, (data) => {
this.actions = data.actions;
axios.request({
method: this.actionsMethod,
url: this.actionsUrl,
params: this.actionsMethod === 'get' && this.params,
data: this.actionsMethod !== 'get' && this.params
}).then((response) => {
this.actions = response.data.actions;
this.loading = false;
this.loaded = true;
});

View file

@ -72,6 +72,7 @@
<ActionToolbar
v-if="selectedRows.length > 0 && actionsUrl"
:actionsUrl="actionsUrl"
:actionsMethod="actionsMethod"
:params="actionsParams"
@toolbar:action="emitAction" />
</div>
@ -145,6 +146,9 @@ export default {
actionsUrl: {
type: String
},
actionsMethod: {
type: String
},
toolbarActions: {
type: Object,
required: true

View file

@ -28,7 +28,7 @@
v-else
v-model="query"
:placeholder="placeholderRender"
@keyup="fetchOptions"
@keyup="reloadItems"
@change.stop
class="w-full bg-transparent border-0 outline-none pl-0 placeholder:text-sn-grey" />
</template>
@ -70,7 +70,7 @@
{{ i18n.t('general.select_all') }}
</div>
</div>
<perfect-scrollbar class="p-2.5 flex flex-col max-h-80 relative" :class="{ 'pt-0': withCheckboxes }">
<perfect-scrollbar ref="scrollContainer" class="p-2.5 flex flex-col max-h-80 relative" :class="{ 'pt-0': withCheckboxes }">
<template v-for="(option, i) in filteredOptions" :key="option[0]">
<div
@click.stop="setValue(option[0])"
@ -143,7 +143,8 @@ export default {
query: '',
fixedWidth: true,
focusedOption: null,
skipQueryCallback: false
skipQueryCallback: false,
nextPage: 1
};
},
mixins: [FixedFlyoutMixin],
@ -270,19 +271,31 @@ export default {
this.$nextTick(() => {
this.setPosition();
this.$refs.search?.focus();
this.$refs.scrollContainer.$el.addEventListener('scroll', this.loadNextPage);
});
}
},
urlParams: {
handler(oldVal, newVal) {
if (!this.compareObjects(oldVal, newVal)) {
this.fetchOptions();
this.reloadItems();
}
},
deep: true
}
},
methods: {
reloadItems() {
this.fetchedOptions = [];
this.nextPage = 1;
this.fetchOptions();
},
loadNextPage() {
const container = this.$refs.scrollContainer.$el;
if (this.nextPage && container.scrollTop + container.clientHeight >= container.scrollHeight) {
this.fetchOptions();
}
},
renderLabel(option) {
if (!option) return false;
@ -354,10 +367,15 @@ export default {
},
fetchOptions() {
if (this.optionsUrl) {
const params = { query: this.query, ...this.urlParams };
const params = { query: this.query, page: this.nextPage, ...this.urlParams };
axios.get(this.optionsUrl, { params })
.then((response) => {
this.fetchedOptions = response.data.data;
if (response.data.paginated) {
this.fetchedOptions = [...this.fetchedOptions, ...response.data.data];
this.nextPage = response.data.next_page;
} else {
this.fetchedOptions = response.data.data;
}
this.$nextTick(() => {
this.setPosition();
});

View file

@ -11,12 +11,13 @@
</div>
<div class="h-full bg-white px-4">
<DataTable :columnDefs="columnDefs"
tableId="StorageLocationsContainer"
:tableId="tableId"
:dataUrl="dataSource"
ref="table"
:reloadingTable="reloadingTable"
:toolbarActions="toolbarActions"
:actionsUrl="actionsUrl"
:actionsMethod="'post'"
:scrollMode="paginationMode"
@assign="assignRow"
@move="moveRow"
@ -123,7 +124,9 @@ export default {
paginationMode() {
return this.withGrid ? 'none' : 'pages';
},
tableId() {
return this.withGrid ? 'StorageLocationsContainerGrid' : 'StorageLocationsContainer';
},
columnDefs() {
let columns = [];

View file

@ -10,12 +10,18 @@
<h4 v-if="selectedPosition" class="modal-title truncate !block">
{{ i18n.t(`storage_locations.show.assign_modal.selected_position_title`, { position: formattedPosition }) }}
</h4>
<h4 v-else-if="selectedRow && selectedRowName" class="modal-title truncate !block">
{{ i18n.t(`storage_locations.show.assign_modal.selected_row_title`) }}
</h4>
<h4 v-else class="modal-title truncate !block">
{{ i18n.t(`storage_locations.show.assign_modal.${assignMode}_title`) }}
</h4>
</div>
<div class="modal-body">
<p class="mb-4">
<p v-if="selectedRow && selectedRowName" class="mb-4">
{{ i18n.t(`storage_locations.show.assign_modal.selected_row_description`, { name: selectedRowName }) }}
</p>
<p v-else class="mb-4">
{{ i18n.t(`storage_locations.show.assign_modal.${assignMode}_description`) }}
</p>
<RowSelector v-if="!selectedRow" @change="this.rowId = $event" class="mb-4"></RowSelector>
@ -56,6 +62,7 @@ export default {
name: 'NewProjectModal',
props: {
selectedRow: Number,
selectedRowName: String,
selectedContainer: Number,
cellId: Number,
selectedPosition: Array,
@ -85,7 +92,8 @@ export default {
return {
rowId: this.selectedRow,
containerId: this.selectedContainer,
position: this.selectedPosition
position: this.selectedPosition,
saving: false
};
},
components: {
@ -95,13 +103,21 @@ export default {
},
methods: {
submit() {
if (this.saving) {
return;
}
this.saving = true;
axios.post(this.actionUrl, {
repository_row_id: this.rowId,
metadata: { position: this.position?.map((pos) => parseInt(pos, 10)) }
}).then(() => {
this.$emit('close');
this.saving = false;
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.errors.join(', '), 'danger');
this.saving = false;
});
}
}

View file

@ -1,29 +1,33 @@
<template>
<div>
<div class="mb-4">
<div class="sci-input-container-v2 left-icon">
<input type="text"
v-model="query"
class="sci-input-field"
ref="input"
autofocus="true"
:placeholder=" i18n.t('storage_locations.index.move_modal.placeholder.find_storage_locations')" />
<i class="sn-icon sn-icon-search"></i>
<div v-if="dataLoaded">
<div v-if="storageLocationsTree.length > 0">
<div class="mb-4">
<div class="sci-input-container-v2 left-icon">
<input type="text"
v-model="query"
class="sci-input-field"
ref="input"
autofocus="true"
:placeholder=" i18n.t('storage_locations.index.move_modal.placeholder.find_storage_locations')" />
<i class="sn-icon sn-icon-search"></i>
</div>
</div>
<div class="max-h-80 overflow-y-auto">
<div class="p-2 flex items-center gap-2 cursor-pointer text-sn-blue hover:bg-sn-super-light-grey"
@click="selectStorageLocation(null)"
:class="{'!bg-sn-super-light-blue': selectedStorageLocationId == null}">
<i class="sn-icon sn-icon-projects"></i>
{{ i18n.t('storage_locations.index.move_modal.search_header') }}
</div>
<MoveTree
:storageLocationsTree="filteredStorageLocationsTree"
:moveMode="moveMode"
:value="selectedStorageLocationId"
@selectStorageLocation="selectStorageLocation" />
</div>
</div>
<div class="max-h-80 overflow-y-auto">
<div class="p-2 flex items-center gap-2 cursor-pointer text-sn-blue hover:bg-sn-super-light-grey"
@click="selectStorageLocation(null)"
:class="{'!bg-sn-super-light-blue': selectedStorageLocationId == null}">
<i class="sn-icon sn-icon-projects"></i>
{{ i18n.t('storage_locations.index.move_modal.search_header') }}
</div>
<MoveTree
:storageLocationsTree="filteredStorageLocationsTree"
:moveMode="moveMode"
:value="selectedStorageLocationId"
@selectStorageLocation="selectStorageLocation" />
</div>
<div v-else class="py-2 text-sn-dark-grey" v-html="i18n.t('storage_locations.index.move_modal.no_results')"></div>
</div>
</template>

View file

@ -52,6 +52,7 @@ export default {
watch: {
selectedRow() {
[[this.selectedColumn]] = this.availableColumns;
this.$emit('change', [this.selectedRow, this.selectedColumn]);
},
selectedColumn() {
this.$emit('change', [this.selectedRow, this.selectedColumn]);
@ -62,15 +63,23 @@ export default {
return available_positions_storage_location_path(this.selectedContainerId);
},
availableRows() {
if (!this.availablePositions) {
return [];
}
return Object.keys(this.availablePositions).map((row) => [row, this.convertNumberToLetter(row)]);
},
availableColumns() {
if (!this.availablePositions) {
return [];
}
return (this.availablePositions[this.selectedRow] || []).map((col) => [col, col]);
}
},
data() {
return {
availablePositions: {},
availablePositions: null,
selectedRow: null,
selectedColumn: null
};

View file

@ -48,14 +48,14 @@ export default {
},
computed: {
repositoriesUrl() {
return list_team_repositories_path(this.teamId, { non_empty: true });
return list_team_repositories_path(this.teamId, { non_empty: true, active: true });
},
rowsUrl() {
if (!this.selectedRepository) {
return null;
}
return rows_list_team_repositories_path(this.teamId);
return rows_list_team_repositories_path(this.teamId, { active: true });
}
},
data() {

View file

@ -49,6 +49,10 @@
{{ i18n.t('general.cancel') }}
</button>
</div>
<div v-if="loading" class="flex absolute top-0 left-0 items-center justify-center w-full flex-grow h-full z-10">
<div class="absolute top-0 left-0 w-full h-full bg-black opacity-20"></div>
<img src="/images/medium/loading.svg" alt="Loading" class="p-4 rounded-xl bg-sn-white relative z-10" />
</div>
</div>
</div>
</div>
@ -78,7 +82,8 @@ export default {
},
data() {
return {
error: null
error: null,
loading: false
};
},
computed: {
@ -99,7 +104,7 @@ export default {
},
uploadFile(file) {
const formData = new FormData();
this.loading = true;
// required payload
formData.append('file', file);
@ -108,9 +113,11 @@ export default {
})
.then(() => {
this.$emit('reloadTable');
this.loading = false;
this.close();
}).catch((error) => {
this.handleError(error.response.data.message);
this.loading = false;
});
}
}

View file

@ -25,9 +25,12 @@
</div>
</div>
<div class="max-h-80 overflow-y-auto">
<div class="p-2 flex items-center gap-2 cursor-pointer text-sn-blue hover:bg-sn-super-light-grey"
@click="selectStorageLocation(null)"
:class="{'!bg-sn-super-light-blue': selectedStorageLocationId == null}">
<div class="p-2 flex items-center gap-2 "
@click="selectStorageLocation(null)"
:class="{
'!bg-sn-super-light-blue': selectedStorageLocationId == null,
'cursor-pointer text-sn-blue hover:bg-sn-super-light-grey': movableToRoot
}">
<i class="sn-icon sn-icon-projects"></i>
{{ i18n.t('storage_locations.index.move_modal.search_header') }}
</div>
@ -63,11 +66,15 @@ export default {
selectedObject: Array,
moveToUrl: String
},
created() {
this.teamId = this.selectedObject.team_id;
},
mixins: [modalMixin, MoveTreeMixin],
data() {
return {
selectedStorageLocationId: null,
storageLocationsTree: [],
teamId: null,
query: '',
moveMode: 'locations'
};

View file

@ -6,15 +6,22 @@ import {
export default {
mounted() {
axios.get(this.storageLocationsTreeUrl).then((response) => {
this.storageLocationsTree = response.data;
axios.get(this.storageLocationsTreeUrl, { params: { team_id: this.teamId } }).then((response) => {
this.storageLocationsTree = response.data.locations;
this.movableToRoot = response.data.movable_to_root;
if (!this.movableToRoot) {
this.selectedStorageLocationId = -1;
}
this.dataLoaded = true;
});
},
data() {
return {
selectedStorageLocationId: null,
movableToRoot: false,
storageLocationsTree: [],
query: ''
query: '',
dataLoaded: false
};
},
computed: {
@ -34,16 +41,20 @@ export default {
},
methods: {
filteredStorageLocationsTreeHelper(storageLocationsTree) {
return storageLocationsTree.map(({ storage_location, children }) => {
return storageLocationsTree.map(({ storage_location, children, can_manage }) => {
if (storage_location.name.toLowerCase().includes(this.query.toLowerCase())) {
return { storage_location, children };
return { storage_location, children, can_manage };
}
const filteredChildren = this.filteredStorageLocationsTreeHelper(children);
return filteredChildren.length ? { storage_location, children: filteredChildren } : null;
return filteredChildren.length ? { storage_location, can_manage, children: filteredChildren } : null;
}).filter(Boolean);
},
selectStorageLocation(storageLocationId) {
if (!this.movableToRoot && storageLocationId === null) {
return;
}
this.selectedStorageLocationId = storageLocationId;
}
}

View file

@ -1,5 +1,5 @@
<template>
<div v-if="params.data.have_reminders">
<div v-if="params.data.has_reminder">
<GeneralDropdown ref="dropdown" position="right" @open="getReminders">
<template v-slot:field>
<i class="sn-icon sn-icon-notifications "></i>

View file

@ -1,69 +0,0 @@
# frozen_string_literal: true
module ReminderRepositoryCellJoinable
extend ActiveSupport::Concern
included do
def self.reminder_repository_cells_scope(relation, user)
relation.joins(
'INNER JOIN repository_columns repository_reminder_columns ON ' \
'repository_reminder_columns.id = repository_cells.repository_column_id'
).joins( # datetime reminders
'LEFT OUTER JOIN "repository_date_time_values" ON '\
'"repository_date_time_values"."id" = "repository_cells"."value_id" AND '\
'"repository_cells"."value_type" = \'RepositoryDateTimeValueBase\' '\
'AND repository_reminder_columns.metadata ->> \'reminder_value\' <> \'\' AND '\
'(repository_date_time_values.data - NOW()) <= '\
'(repository_reminder_columns.metadata ->> \'reminder_value\')::int * ' \
'(repository_reminder_columns.metadata ->> \'reminder_unit\')::int * interval \'1 sec\''
).joins( # stock reminders
'LEFT OUTER JOIN "repository_stock_values" ON '\
'"repository_cells"."value_type" = \'RepositoryStockValue\' AND '\
'"repository_stock_values"."id" = "repository_cells"."value_id" AND '\
'(repository_stock_values.amount <= repository_stock_values.low_stock_threshold OR '\
' repository_stock_values.amount <= 0)'
).joins(
'LEFT OUTER JOIN "hidden_repository_cell_reminders" ON '\
'"repository_cells"."id" = "hidden_repository_cell_reminders"."repository_cell_id" AND '\
'"hidden_repository_cell_reminders"."user_id" = ' + user.id.to_s
).where(
'hidden_repository_cell_reminders.id IS NULL AND '\
'(repository_date_time_values.id IS NOT NULL OR repository_stock_values.id IS NOT NULL)'
)
end
def self.stock_reminder_repository_cells_scope(relation, user)
relation.joins( # stock reminders
'LEFT OUTER JOIN "repository_stock_values" ON ' \
'"repository_cells"."value_type" = \'RepositoryStockValue\' AND ' \
'"repository_stock_values"."id" = "repository_cells"."value_id" AND ' \
'(repository_stock_values.amount <= repository_stock_values.low_stock_threshold OR ' \
'repository_stock_values.amount <= 0)'
).joins(
'LEFT OUTER JOIN "hidden_repository_cell_reminders" ON ' \
'"repository_cells"."id" = "hidden_repository_cell_reminders"."repository_cell_id" AND ' \
'"hidden_repository_cell_reminders"."user_id" = ' + user.id.to_s
).where(
'hidden_repository_cell_reminders.id IS NULL AND repository_stock_values.id IS NOT NULL'
)
end
def self.date_time_reminder_repository_cells_scope(relation, user)
relation.joins( # datetime reminders
'LEFT OUTER JOIN "repository_date_time_values" ON ' \
'"repository_date_time_values"."id" = "repository_cells"."value_id" AND ' \
'"repository_cells"."value_type" = \'RepositoryDateTimeValueBase\' ' \
'AND repository_columns.metadata ->> \'reminder_value\' <> \'\' AND ' \
'(repository_date_time_values.data - NOW()) <= ' \
'(repository_columns.metadata ->> \'reminder_value\')::int * ' \
'(repository_columns.metadata ->> \'reminder_unit\')::int * interval \'1 sec\''
).joins(
'LEFT OUTER JOIN "hidden_repository_cell_reminders" ON ' \
'"repository_cells"."id" = "hidden_repository_cell_reminders"."repository_cell_id" AND ' \
'"hidden_repository_cell_reminders"."user_id" = ' + user.id.to_s
).where(
'hidden_repository_cell_reminders.id IS NULL AND repository_date_time_values.id IS NOT NULL'
)
end
end
end

View file

@ -33,6 +33,7 @@ class Repository < RepositoryBase
before_destroy :refresh_report_references_on_destroy, prepend: true
after_save :assign_globally_shared_inventories, if: -> { saved_change_to_permission_level? && globally_shared? }
after_save :unassign_globally_shared_inventories, if: -> { saved_change_to_permission_level? && !globally_shared? }
after_save :unassign_unshared_items, if: :saved_change_to_permission_level
after_save :unlink_unshared_items, if: -> { saved_change_to_permission_level? && !globally_shared? }
validates :name,
@ -145,6 +146,17 @@ class Repository < RepositoryBase
repository_rows.joins(:my_module_repository_rows).where(my_module_repository_rows: { my_module_id: my_module.id })
end
def unassign_unshared_items
return if shared_read? || shared_write?
MyModuleRepositoryRow.joins(my_module: { experiment: { project: :team } })
.joins(repository_row: :repository)
.where(repository_rows: { repository: self })
.where.not(my_module: { experiment: { projects: { team: team } } })
.where.not(my_module: { experiment: { projects: { team: teams_shared_with } } })
.destroy_all
end
def unlink_unshared_items
repository_rows_ids = repository_rows.select(:id)
rows_to_unlink = RepositoryRow.joins("LEFT JOIN repository_row_connections \

View file

@ -43,6 +43,12 @@ class RepositoryBase < ApplicationRecord
@has_stock_management ||= self.class.stock_management_enabled? && repository_columns.stock_type.exists?
end
def has_reminders?
@has_reminders ||=
self.class.reminders_enabled? &&
(repository_columns.stock_type.exists? || repository_columns.where('"repository_columns"."metadata" ->> \'reminder_value\' <> \'\'').exists?)
end
def has_stock_consumption?
true
end

View file

@ -1,8 +1,6 @@
# frozen_string_literal: true
class RepositoryCell < ApplicationRecord
include ReminderRepositoryCellJoinable
attr_accessor :importing, :to_destroy
belongs_to :repository_row, touch: true
@ -48,7 +46,47 @@ class RepositoryCell < ApplicationRecord
after_touch :update_repository_row_last_modified_by
scope :with_active_reminder, lambda { |user|
reminder_repository_cells_scope(self, user)
from(
"((#{with_active_stock_reminder(user).to_sql}) UNION ALL " \
"(#{with_active_datetime_reminder(user).to_sql})) AS repository_cells"
)
}
scope :with_active_stock_reminder, lambda { |user|
joins( # stock reminders
'LEFT OUTER JOIN "repository_stock_values" ON ' \
'"repository_cells"."value_type" = \'RepositoryStockValue\' AND ' \
'"repository_stock_values"."id" = "repository_cells"."value_id" AND ' \
'(repository_stock_values.amount <= repository_stock_values.low_stock_threshold OR ' \
'repository_stock_values.amount <= 0)'
).joins(
'LEFT OUTER JOIN "hidden_repository_cell_reminders" ON ' \
'"repository_cells"."id" = "hidden_repository_cell_reminders"."repository_cell_id" AND ' \
'"hidden_repository_cell_reminders"."user_id" = ' + user.id.to_s
).where(
'hidden_repository_cell_reminders.id IS NULL AND repository_stock_values.id IS NOT NULL'
)
}
scope :with_active_datetime_reminder, lambda { |user|
joins(
'INNER JOIN repository_columns repository_reminder_columns ON ' \
'repository_reminder_columns.id = repository_cells.repository_column_id'
).joins( # datetime reminders
'LEFT OUTER JOIN "repository_date_time_values" ON ' \
'"repository_date_time_values"."id" = "repository_cells"."value_id" AND ' \
'"repository_cells"."value_type" = \'RepositoryDateTimeValueBase\' ' \
'AND repository_reminder_columns.metadata ->> \'reminder_value\' <> \'\' AND ' \
'(repository_date_time_values.data - NOW()) <= ' \
'(repository_reminder_columns.metadata ->> \'reminder_value\')::int * ' \
'(repository_reminder_columns.metadata ->> \'reminder_unit\')::int * interval \'1 sec\''
).joins(
'LEFT OUTER JOIN "hidden_repository_cell_reminders" ON ' \
'"repository_cells"."id" = "hidden_repository_cell_reminders"."repository_cell_id" AND ' \
'"hidden_repository_cell_reminders"."user_id" = ' + user.id.to_s
).where(
'hidden_repository_cell_reminders.id IS NULL AND repository_date_time_values.id IS NOT NULL'
)
}
def update_repository_row_last_modified_by

View file

@ -5,12 +5,11 @@ class RepositoryRow < ApplicationRecord
include SearchableModel
include SearchableByNameModel
include ArchivableModel
include ReminderRepositoryCellJoinable
ID_PREFIX = 'IT'
include PrefixedIdModel
belongs_to :repository, class_name: 'RepositoryBase'
belongs_to :repository, class_name: 'RepositoryBase', counter_cache: :repository_rows_count
delegate :team, to: :repository
belongs_to :parent, class_name: 'RepositoryRow', optional: true
belongs_to :created_by, class_name: 'User'
@ -117,8 +116,8 @@ class RepositoryRow < ApplicationRecord
scope :active, -> { where(archived: false) }
scope :archived, -> { where(archived: true) }
scope :with_active_reminders, lambda { |user|
reminder_repository_cells_scope(joins(:repository_cells), user)
scope :with_active_reminders, lambda { |repository, user|
left_outer_joins_active_reminders(repository, user).where.not(repository_cells_with_active_reminders: { id: nil })
}
def code
@ -171,6 +170,16 @@ class RepositoryRow < ApplicationRecord
.update_all(created_by_id: new_owner.id)
end
def self.left_outer_joins_active_reminders(repository, user)
repository_cells = RepositoryCell.joins("INNER JOIN repository_columns ON repository_columns.id = repository_cells.repository_column_id AND " \
"repository_columns.repository_id = #{repository.id}")
joins(
"LEFT OUTER JOIN (#{repository_cells.with_active_reminder(user).select(:id, :repository_row_id).to_sql}) " \
"AS repository_cells_with_active_reminders " \
"ON repository_cells_with_active_reminders.repository_row_id = repository_rows.id"
)
end
def editable?
true
end
@ -179,14 +188,6 @@ class RepositoryRow < ApplicationRecord
self[:archived]
end
def has_reminders?(user)
stock_reminders = RepositoryCell.stock_reminder_repository_cells_scope(
repository_cells.joins(:repository_column), user)
date_reminders = RepositoryCell.date_time_reminder_repository_cells_scope(
repository_cells.joins(:repository_column), user)
stock_reminders.any? || date_reminders.any?
end
def archived
row_archived? || repository&.archived?
end

View file

@ -10,6 +10,8 @@ class RepositoryTextValue < ApplicationRecord
has_one :repository_cell, as: :value, dependent: :destroy, touch: true
accepts_nested_attributes_for :repository_cell
before_save -> { self.has_smart_annotation = data.match?(SmartAnnotations::TagToHtml::ALL_REGEX) }, if: -> { data_changed? }
validates :repository_cell, presence: true
validates :data, presence: true, length: { maximum: Constants::TEXT_MAX_LENGTH }

View file

@ -6,6 +6,7 @@ class StorageLocation < ApplicationRecord
ID_PREFIX = 'SL'
include PrefixedIdModel
include Shareable
include SearchableModel
default_scope -> { kept }
@ -20,6 +21,7 @@ class StorageLocation < ApplicationRecord
has_many :repository_rows, through: :storage_location_repository_rows
validates :name, length: { maximum: Constants::NAME_MAX_LENGTH }
validate :parent_same_team, if: -> { parent.present? }
validate :parent_validation, if: -> { parent.present? }
validate :no_grid_options, if: -> { !container }
validate :no_dimensions, if: -> { !with_grid? }
@ -59,13 +61,14 @@ class StorageLocation < ApplicationRecord
storage_location_repository_rows.count.zero?
end
def duplicate!
def duplicate!(user)
ActiveRecord::Base.transaction do
new_storage_location = dup
new_storage_location.name = next_clone_name
new_storage_location.created_by = user
new_storage_location.save!
copy_image(self, new_storage_location)
recursive_duplicate(id, new_storage_location.id)
recursive_duplicate(id, new_storage_location.id, user)
new_storage_location
rescue ActiveRecord::RecordInvalid
false
@ -141,13 +144,14 @@ class StorageLocation < ApplicationRecord
private
def recursive_duplicate(old_parent_id = nil, new_parent_id = nil)
def recursive_duplicate(old_parent_id = nil, new_parent_id = nil, user = nil)
StorageLocation.where(parent_id: old_parent_id).find_each do |child|
new_child = child.dup
new_child.parent_id = new_parent_id
new_child.created_by = user
new_child.save!
copy_image(child, new_child)
recursive_duplicate(child.id, new_child.id)
recursive_duplicate(child.id, new_child.id, user)
end
end
@ -200,6 +204,10 @@ class StorageLocation < ApplicationRecord
errors.add(:metadata, I18n.t('activerecord.errors.models.storage_location.attributes.metadata.invalid')) if metadata['display_type'] || metadata['dimensions']
end
def parent_same_team
errors.add(:parent, I18n.t('activerecord.errors.models.storage_location.attributes.parent_storage_location_team')) if parent.team != team
end
def no_dimensions
errors.add(:metadata, I18n.t('activerecord.errors.models.storage_location.attributes.metadata.invalid')) if !with_grid? && metadata['dimensions']
end

View file

@ -5,6 +5,7 @@ class TeamSharedObject < ApplicationRecord
after_create :assign_shared_inventories, if: -> { shared_object.is_a?(Repository) }
before_destroy :unlink_unshared_items, if: -> { shared_object.is_a?(Repository) }
before_destroy :unassign_unshared_items, if: -> { shared_object.is_a?(Repository) }
before_destroy :unassign_unshared_inventories, if: -> { shared_object.is_a?(Repository) }
belongs_to :team
@ -42,6 +43,16 @@ class TeamSharedObject < ApplicationRecord
end
end
def unassign_unshared_items
return if shared_object.shared_read? || shared_object.shared_write?
MyModuleRepositoryRow.joins(my_module: { experiment: { project: :team } })
.joins(repository_row: :repository)
.where(my_module: { experiment: { projects: { team: team } } })
.where(repository_rows: { repository: shared_object })
.destroy_all
end
def unassign_unshared_inventories
team.repository_sharing_user_assignments.where(assignable: shared_object).find_each(&:destroy!)
end

View file

@ -104,6 +104,10 @@ Canaid::Permissions.register_for(Repository) do
repository.permission_granted?(user, RepositoryPermissions::COLUMNS_CREATE)
end
can :manage_repository_columns do |user, repository|
repository.repository_snapshots.provisioning.none? && can_create_repository_columns?(user, repository)
end
# repository: create/update/delete filters
can :manage_repository_filters do |user, repository|
repository.permission_granted?(user, RepositoryPermissions::FILTERS_MANAGE)
@ -113,3 +117,11 @@ Canaid::Permissions.register_for(Repository) do
RepositoryBase.stock_management_enabled? && can_manage_repository_rows?(user, repository)
end
end
Canaid::Permissions.register_for(RepositoryColumn) do
# repository: update/delete field
# Tested in scope of RepositoryPermissions spec
can :manage_repository_column do |user, repository_column|
repository_column.repository.repository_snapshots.provisioning.none? && can_create_repository_columns?(user, repository_column.repository)
end
end

View file

@ -1,12 +0,0 @@
# frozen_string_literal: true
Canaid::Permissions.register_for(RepositoryColumn) do
# repository: update/delete field
# Tested in scope of RepositoryPermissions spec
can :manage_repository_column do |user, repository_column|
managable = repository_column.repository.repository_snapshots.provisioning.none? &&
can_create_repository_columns?(user, repository_column.repository)
managable
end
end

View file

@ -9,7 +9,7 @@ module Lists
attributes :name, :code, :nr_of_rows, :team, :created_at, :created_by, :archived_on, :archived_by, :urls
def nr_of_rows
object[:row_count]
object[:repository_rows_count]
end
def team

View file

@ -6,7 +6,7 @@ module Lists
include Rails.application.routes.url_helpers
attributes :created_by, :created_on, :position, :row_id, :row_name, :hidden, :position_formatted, :stock,
:have_reminders, :reminders_url, :row_url, :row_code
:has_reminder, :reminders_url, :row_url, :row_code
def row_id
object.repository_row.id
@ -52,8 +52,8 @@ module Lists
@hidden = !can_read_repository?(object.repository_row.repository)
end
def have_reminders
object.repository_row.has_reminders?(scope) unless hidden
def has_reminder
object.repository_row.repository_cells.with_active_reminder(scope).any? unless hidden
end
def reminders_url

View file

@ -9,7 +9,7 @@ module Lists
attributes :id, :code, :name, :container, :description, :owned_by, :created_by,
:created_on, :urls, :metadata, :file_name, :sub_location_count, :is_empty,
:img_url, :sa_description, :name_hash
:img_url, :sa_description, :name_hash, :team_id
def owned_by
object['team_name']

View file

@ -9,7 +9,7 @@ module RepositoryDatatable
def value
@user = scope[:user]
{
view: custom_auto_link(value_object.data, simple_format: true, team: scope[:team]),
view: value_object.has_smart_annotation? ? custom_auto_link(value_object.data, simple_format: true, team: scope[:team]) : escape_input(value_object.data),
edit: value_object.data
}
end

View file

@ -9,11 +9,9 @@ module Lists
'ON repositories.created_by_id = creators.id')
.joins('LEFT OUTER JOIN users AS archivers ' \
'ON repositories.archived_by_id = archivers.id')
.left_outer_joins(:repository_rows)
.joins(:team)
.select('repositories.*')
.select('MAX(teams.name) AS team_name')
.select('COUNT(DISTINCT(repository_rows.*)) AS row_count')
.select('MAX(creators.full_name) AS created_by_user')
.select('MAX(archivers.full_name) AS archived_by_user')
.select(shared_sql_select)
@ -47,7 +45,7 @@ module Lists
created_at: 'repositories.created_at',
archived_on: 'repositories.archived_on',
archived_by: 'archived_by_user',
nr_of_rows: 'row_count',
nr_of_rows: 'repository_rows_count',
code: 'repositories.id',
shared_label: 'shared'
}

View file

@ -44,8 +44,25 @@ module Lists
@records = @records.where(parent_id: @parent_id)
end
@records = @records.where('LOWER(storage_locations.name) ILIKE ?', "%#{@filters[:query].downcase}%") if @filters[:query].present?
@records = @records.where('LOWER(storage_locations.name) ILIKE ?', "%#{@params[:search].downcase}%") if @params[:search].present?
if @filters[:query].present?
@records = @records.where_attributes_like(
[
'storage_locations.name',
'storage_locations.description',
StorageLocation::PREFIXED_ID_SQL
], @filters[:query]
)
end
if @params[:search].present?
@records = @records.where_attributes_like(
[
'storage_locations.name',
'storage_locations.description',
StorageLocation::PREFIXED_ID_SQL
], @params[:search]
)
end
end
private
@ -60,9 +77,9 @@ module Lists
@records = @records.order(id: :asc)
when 'code_DESC'
@records = @records.order(id: :desc)
when 'name_ASC'
when 'name_hash_ASC'
@records = @records.order(name: :asc)
when 'name_DESC'
when 'name_hash_DESC'
@records = @records.order(name: :desc)
when 'sub_location_count_ASC'
@records = @records.order(sub_location_count: :asc)

View file

@ -13,7 +13,7 @@ module Reports::Docx::DrawMyModuleRepository
return false unless repository_data[:rows].any? && can_read_repository?(@user, repository)
table = prepare_row_columns(repository_data, my_module, repository)
table = prepare_row_columns_for_docx(repository_data, my_module, repository)
@docx.p
@docx.p I18n.t('projects.reports.elements.module_repository.name',

View file

@ -4,7 +4,7 @@ module Reports::Docx::RepositoryHelper
include InputSanitizeHelper
include ActionView::Helpers::NumberHelper
def prepare_row_columns(repository_data, my_module, repository)
def prepare_row_columns_for_docx(repository_data, my_module, repository)
result = [repository_data[:headers]]
repository_data[:rows].each do |record|
row = []

View file

@ -13,10 +13,11 @@ class RepositoryDatatableService
PREDEFINED_COLUMNS = %w(row_id row_name added_on added_by archived_on archived_by
assigned relationships updated_on updated_by).freeze
def initialize(repository, params, user, my_module = nil)
def initialize(repository, params, user, my_module = nil, preload_cells: true)
@repository = repository
@user = user
@my_module = my_module
@preload_cells = preload_cells
@params = params
@assigned_view = @params[:assigned].in?(%w(assigned assigned_simple))
@sortable_columns = build_sortable_columns
@ -46,7 +47,7 @@ class RepositoryDatatableService
repository_rows = fetch_rows(search_value)
# filter only rows with reminders if filter param is present
repository_rows = repository_rows.with_active_reminders(@user) if @params[:only_reminders]
repository_rows = repository_rows.with_active_reminders(@repository, @user) if @params[:only_reminders]
# Aliased my_module_repository_rows join for consistent assigned counts
repository_rows =
@ -77,6 +78,18 @@ class RepositoryDatatableService
.group('current_my_module_repository_rows.id')
end
end
if Repository.reminders_enabled?
repository_rows =
if @repository.archived? || @repository.is_a?(RepositorySnapshot)
# don't load reminders for archived repositories or snapshots
repository_rows.select('FALSE AS has_active_stock_reminders, FALSE AS has_active_datetime_reminders')
else
repository_rows.left_outer_joins_active_reminders(@repository, @user)
.select('COUNT(repository_cells_with_active_reminders.id) > 0 AS has_active_reminders')
end
end
repository_rows = repository_rows
.left_outer_joins(my_module_repository_rows: { my_module: :experiment })
.select('COUNT(DISTINCT all_my_module_repository_rows.id) AS "assigned_my_modules_count"')
@ -85,6 +98,8 @@ class RepositoryDatatableService
.select('COALESCE(parent_connections_count, 0) + COALESCE(child_connections_count, 0)
AS "relationships_count"')
repository_rows = repository_rows.preload(Extends::REPOSITORY_ROWS_PRELOAD_RELATIONS)
repository_rows = repository_rows.preload(:repository_columns, repository_cells: { value: @repository.cell_preload_includes }) if @preload_cells
repository_rows = repository_rows.preload(:repository_stock_cell, :repository_stock_value) if @repository.has_stock_management?
sort_rows(order_by_column, repository_rows)
end
@ -101,7 +116,7 @@ class RepositoryDatatableService
.where(my_module_repository_rows: { my_module_id: @my_module })
.count
else
repository_rows.count
@repository.repository_rows_count
end
repository_rows = repository_rows.where(external_id: @params[:external_ids]) if @params[:external_ids]
@ -128,8 +143,13 @@ class RepositoryDatatableService
repository_rows = repository_rows.where(id: advanced_search(repository_rows)) if @params[:advanced_search].present?
repository_rows.left_outer_joins(:created_by, :archived_by, :last_modified_by)
repository_rows.joins('LEFT OUTER JOIN "users" "created_by" ON "created_by"."id" = "repository_rows"."created_by_id"')
.joins('LEFT OUTER JOIN "users" "last_modified_by" ON "last_modified_by"."id" = "repository_rows"."last_modified_by_id"')
.joins('LEFT OUTER JOIN "users" "archived_by" ON "archived_by"."id" = "repository_rows"."archived_by_id"')
.select('repository_rows.*')
.select('MAX("created_by"."full_name") AS created_by_full_name')
.select('MAX("last_modified_by"."full_name") AS last_modified_by_full_name')
.select('MAX("archived_by"."full_name") AS archived_by_full_name')
.select('COUNT("repository_rows"."id") OVER() AS filtered_count')
.group('repository_rows.id')
end
@ -616,7 +636,7 @@ class RepositoryDatatableService
.group('values.value')
.order("values.value #{dir}")
when 'users.full_name'
records.group('users.full_name').order("users.full_name #{dir}")
records.group('created_by.full_name').order("created_by.full_name #{dir}")
when 'consumed_stock'
records.order("#{@sortable_columns[column_id - 1]} #{dir}")
when 'relationships'

View file

@ -18,6 +18,8 @@ class RepositorySnapshotDatatableService < RepositoryDatatableService
order_by_column = { column: order_params[:column].to_i, dir: order_params[:dir] }
repository_rows = fetch_rows(search_value).preload(Extends::REPOSITORY_ROWS_PRELOAD_RELATIONS)
repository_rows = repository_rows.preload(:repository_columns, repository_cells: { value: @repository.cell_preload_includes }) if @preload_cells
repository_rows = repository_rows.preload(:repository_stock_cell, :repository_stock_value) if @repository.has_stock_management?
sort_rows(order_by_column, repository_rows)
end
@ -44,8 +46,9 @@ class RepositorySnapshotDatatableService < RepositoryDatatableService
repository_rows = results
end
repository_rows.left_outer_joins(:created_by)
repository_rows.joins('LEFT OUTER JOIN "users" "created_by" ON "created_by"."id" = "repository_rows"."created_by_id"')
.select('repository_rows.*')
.select('MAX("created_by"."full_name") AS created_by_full_name')
.select('COUNT("repository_rows"."id") OVER() AS filtered_count')
.group('repository_rows.id')
end

View file

@ -2,8 +2,9 @@
module SmartAnnotations
class TagToHtml
REGEX = /\[\#(.*?)~(prj|exp|tsk|rep_item)~([0-9a-zA-Z]+)\]/.freeze
USER_REGEX = /\[@(.*?)~([0-9a-zA-Z]+)\]/.freeze
ALL_REGEX = /\[(@(.*?)|\#(.*?)~(prj|exp|tsk|rep_item))~([0-9a-zA-Z]+)\]/
ITEMS_REGEX = /\[\#(.*?)~(prj|exp|tsk|rep_item)~([0-9a-zA-Z]+)\]/
USER_REGEX = /\[@(.*?)~([0-9a-zA-Z]+)\]/
attr_reader :html
def initialize(user, team, text, preview_repository = false)
@ -18,7 +19,7 @@ module SmartAnnotations
rep_item: RepositoryRow }.freeze
def parse(user, team, text, preview_repository = false)
@html = text.gsub(REGEX) do |el|
@html = text.gsub(ITEMS_REGEX) do |el|
value = extract_values(el)
type = value[:object_type]
begin
@ -49,7 +50,7 @@ module SmartAnnotations
end
def extract_values(element)
match = element.match(REGEX)
match = element.match(ITEMS_REGEX)
{
name: match[1],
object_type: match[2],

View file

@ -2,6 +2,9 @@
module SmartAnnotations
class TagToText
USER_REGEX = /\[@(.*?)~([0-9a-zA-Z]+)\]/
ITEMS_REGEX = /\[\#(.*?)~(prj|exp|tsk|rep_item)~([0-9a-zA-Z]+)\]/
attr_reader :text
def initialize(user, team, text, is_shared_object: false)
@ -11,8 +14,6 @@ module SmartAnnotations
private
USER_REGEX = /\[\@(.*?)~([0-9a-zA-Z]+)\]/
ITEMS_REGEX = /\[\#(.*?)~(prj|exp|tsk|rep_item)~([0-9a-zA-Z]+)\]/
OBJECT_MAPPINGS = { prj: Project,
exp: Experiment,
tsk: MyModule,

View file

@ -4,6 +4,8 @@ require 'caxlsx'
module StorageLocations
class ImportService
class PositionNotValid < StandardError; end
def initialize(storage_location, file, user)
@storage_location = storage_location
@assigned_count = 0
@ -54,6 +56,8 @@ module StorageLocations
{ status: :ok, assigned_count: @assigned_count, unassigned_count: @unassigned_count, updated_count: @updated_count }
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
{ status: :error, message: @error_message }
rescue PositionNotValid
{ status: :error, message: I18n.t('storage_locations.show.import_modal.errors.invalid_position') }
end
private
@ -105,6 +109,8 @@ module StorageLocations
def convert_position_letter_to_number(position)
return unless position
raise PositionNotValid unless position.to_s.match?(/^[A-Z]\d+$/)
column_letter = position[0]
row_number = position[1..]

View file

@ -27,7 +27,7 @@ module Toolbars
private
def unassign_action
return unless can_manage_storage_location?(@storage_location)
return unless can_read_storage_location?(@storage_location)
{
name: 'unassign',
@ -39,7 +39,7 @@ module Toolbars
end
def move_action
return unless @single && can_manage_storage_location?(@storage_location)
return unless @single && can_read_storage_location?(@storage_location)
{
name: 'move',

View file

@ -1,5 +1,4 @@
<% @assigned_repositories.each do |repository| %>
<% readable = @my_module.readable_live_and_snapshot_repositories_list(current_user, current_team).include?(repository) %>
<div class="assigned-repository panel" data-repository-id="<%= repository.id %>">
<a class="assigned-repository-caret collapsed"
role="button"
@ -9,57 +8,40 @@
>
<i class="sn-icon sn-icon-right"></i>
<span class="assigned-repository-title" data-rows-count="<%= repository.assigned_rows_count %>">
<%= readable && repository.name || t('my_modules.repository.private_repository', code: repository.code) %>
<%= repository.name %>
</span>
<% if repository.is_a?(RepositorySnapshot) %>
<span class="snapshot-tag">
<%= t('my_modules.repository.snapshots.simple_view.snapshot_tag') %>
</span>
<% end %>
<% if readable %>
<div class="action-buttons">
<button class="btn btn-light icon-btn full-screen" data-table-url="<%= assigned_repository_full_view_table_path(@my_module, repository) %><%= '?include_stock_consumption=true' if repository.has_stock_consumption? %>">
<i class="sn-icon sn-icon-expand"></i>
</button>
</div>
<% end %>
<div class="action-buttons">
<button class="btn btn-light icon-btn full-screen" data-table-url="<%= assigned_repository_full_view_table_path(@my_module, repository) %><%= '?include_stock_consumption=true' if repository.has_stock_consumption? %>">
<i class="sn-icon sn-icon-expand"></i>
</button>
</div>
</a>
<% if readable %>
<div class="collapse assigned-repository-container readable-repository"
id="assigned-repository-items-container-<%= repository.id %>"
data-repository-url="<%= assigned_repository_simple_view_index_path(@my_module, repository) %>"
data-footer-label="<%= assigned_repository_simple_view_footer_label(repository) %>"
data-name-column-id="<%= assigned_repository_simple_view_name_column_id(repository) %>"
>
<table class="table hidden repository-table repository-dataTable"
data-stock-management="<%= repository.has_stock_management? && repository.has_stock_consumption? %>"
data-stock-consumption-editable="<%= can_update_my_module_stock_consumption?(@my_module) && repository.has_stock_consumption? %>">
<thead>
<tr>
<th class="row-name"><%= t("repositories.table.row_name") %></th>
<% if repository.has_stock_management? && repository.has_stock_consumption? %>
<th class="row-stock" data-columns-visible="false"><%= repository.repository_stock_column.name %></th>
<th class="row-consumption" data-columns-visible="false"><%= t("repositories.table.row_consumption") %></th>
<% end %>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<% else %>
<div class="collapse assigned-repository-container" id="assigned-repository-items-container-<%= repository.id %>">
<table class="table border-solid border-0 border-b border-b-sn-light-grey">
<div class="collapse assigned-repository-container"
id="assigned-repository-items-container-<%= repository.id %>"
data-repository-url="<%= assigned_repository_simple_view_index_path(@my_module, repository) %>"
data-footer-label="<%= assigned_repository_simple_view_footer_label(repository) %>"
data-name-column-id="<%= assigned_repository_simple_view_name_column_id(repository) %>"
>
<table class="table hidden repository-table repository-dataTable"
data-stock-management="<%= repository.has_stock_management? && repository.has_stock_consumption? %>"
data-stock-consumption-editable="<%= can_update_my_module_stock_consumption?(@my_module) && repository.has_stock_consumption? %>">
<thead>
<tr>
<th class="bg-sn-light-grey"><%= t('repositories.table.id') %></th>
<th class="row-name"><%= t("repositories.table.row_name") %></th>
<% if repository.has_stock_management? && repository.has_stock_consumption? %>
<th class="row-stock" data-columns-visible="false"><%= repository.repository_stock_column.name %></th>
<th class="row-consumption" data-columns-visible="false"><%= t("repositories.table.row_consumption") %></th>
<% end %>
</tr>
<% @my_module.repository_rows.where(repository: repository).select(:parent_id, :id).map(&:code).each do |code| %>
<tr>
<td class="!border-t-sn-light-grey"><%= code %></td>
<tr>
<% end %>
</table>
</div>
<% end %>
</thead>
<tbody></tbody>
</table>
</div>
<%= render 'shared/dialog',
id: "snapshot-error-#{repository.id}",
type: "error",

View file

@ -48,6 +48,7 @@
<% if @repository.is_a?(LinkedRepository) %>
<th id="row-external-id"><%= t('repositories.table.external_id') %></th>
<% end %>
<% columns_editable = can_manage_repository_columns?(@repository) && !repository.is_a?(SoftLockedRepository) %>
<% repository.repository_columns.order(:id).each do |column| %>
<th
class="repository-column <%= 'row-stock item-stock' if column.data_type == 'RepositoryStockValue' %>"
@ -55,7 +56,7 @@
data-type="<%= column.data_type %>"
data-edit-column-url="<%= edit_repository_repository_column_path(repository, column) %>"
data-destroy-column-url="<%= repository_columns_destroy_html_path(repository, column) %>"
data-editable-row="<%= can_manage_repository_column?(column) && !repository.is_a?(SoftLockedRepository) %>"
data-editable-row="<%= columns_editable %>"
<% column.metadata.each do |k, v| %>
<%= "data-metadata-#{k}=#{v}" %>
<% end %>

View file

@ -37,7 +37,7 @@
</button>
</span>
<% end %>
<% if @repository.repository_rows.with_active_reminders(current_user).any? %>
<% if @repository.has_reminders? && @repository.repository_rows.with_active_reminders(@repository, current_user).any? %>
<button type="button" data-toggle="tooltip" data-placement="bottom" title="<%= t("repositories.hide_reminders") %>"
class="btn btn-light auto-shrink-button"
id="hideRepositoryReminders"

View file

@ -6,7 +6,7 @@ json.data do
@repository,
@columns_mappings,
@repository.team,
@datatable_params || {})
(@datatable_params || {}).merge(omit_editable: true))
end
json.recordsFiltered @repository_rows.first ? @repository_rows.first.filtered_count : 0
json.recordsFiltered @filtered_rows_count
json.recordsTotal @all_rows_count

View file

@ -6,6 +6,7 @@ json.repository do
json.name @repository.name
json.is_snapshot @repository.is_a?(RepositorySnapshot)
end
json.editable @repository_row.editable?
json.notification @notification
json.update_path update_cell_repository_repository_row_path(@repository, @repository_row)

View file

@ -6,5 +6,5 @@ json.data do
@repository_rows, @repository, @my_module, @datatable_params || {}
)
end
json.recordsFiltered @repository_rows.first ? @repository_rows.first.filtered_count : 0
json.recordsFiltered @filtered_rows_count
json.recordsTotal @all_rows_count

View file

@ -7,5 +7,5 @@ json.data do
@repository_snapshot.team,
@repository_snapshot)
end
json.recordsFiltered @repository_rows.first ? @repository_rows.first.filtered_count : 0
json.recordsFiltered @filtered_rows_count
json.recordsTotal @all_rows_count

View file

@ -57,7 +57,7 @@
</div>
</div>
<!-- Assigned items -->
<% assigned_repositories = @my_module.live_and_snapshot_repositories_list.select { |r| r.team == @my_module.team || r.shared_with?(@my_module.team) } %>
<% assigned_repositories = @my_module.live_and_snapshot_repositories_list %>
<div class="task-section">
<div class="task-section-header">
<a class="task-section-caret" role="button" data-toggle="collapse" href="#assigned-items-container" aria-expanded="true" aria-controls="assigned-items-container">

View file

@ -538,7 +538,7 @@ class Extends
team: [92, 94, 93, 97, 104, 244, 245],
label_templates: [*216..219],
storage_locations: [*309..315],
container_storage_location: [*316..322, 326],
container_storage_locations: [*316..322, 326],
storage_location_repository_rows: [*323..325]
}
@ -696,6 +696,7 @@ class Extends
Repositories_archived_state
StorageLocationsTable_active_state
StorageLocationsContainer_active_state
StorageLocationsContainerGrid_active_state
task_step_states
results_order
repository_export_file_type

View file

@ -265,6 +265,7 @@ en:
not_uniq_repository_row: 'Inventory item already exists'
attributes:
parent_storage_location: "Storage location cannot be parent to itself"
parent_storage_location_team: "Parent storage location and storage location should belongs to the same team"
parent_storage_location_child: "Storage location cannot be moved to it's child"
metadata:
invalid: 'Invalid metadata'
@ -2180,6 +2181,7 @@ en:
errors:
too_long: "Item name is too long (maximum is %{max_length} characters)"
is_empty: "Item name should be filled"
row_locked: The item is locked, snapshot creation is in progress. Please try later.
pagination_edit_overlay_html: "Please <a class=\"repository-save-changes-link\">save your changes</a> before you go to another page"
toolbar_edit_overlay_html: "Please <a class=\"repository-save-changes-link\">save your changes</a> first to use filters"
add_new_record: "New item"
@ -2692,10 +2694,12 @@ en:
button: 'Unassign'
assign_modal:
selected_position_title: 'Assign to position %{position}'
selected_row_title: 'Assign new location'
assign_title: 'Assign position'
move_title: 'Move item'
assign_description: 'Select an item to assign it to a location.'
move_description: 'Select a new location for your item.'
selected_row_description: "Select a location for the item %{name}."
assign_action: 'Assign'
move_action: 'Move'
row: 'Row'
@ -2772,6 +2776,7 @@ en:
title: 'Move %{name}'
description: 'Select where you want to move %{name}.'
search_header: 'Locations'
no_results: "You haven't created any storage locations and boxes yet.<br> Go to <b>Inventories > Locations</b> and create your location structure first to assign items to locations."
success_flash: "You have successfully moved the selected location/box to another location."
error_flash: "An error occurred. The selected location/box has not been moved."
placeholder:

View file

@ -650,8 +650,9 @@ en:
team: "Team"
exports: "Exports"
label_templates: "Label templates"
storage_locations: "Locations"
container_storage_locations: "Boxes"
storage_locations: "Inventory locations"
container_storage_locations: "Inventory location boxes"
storage_location_repository_rows: "Item assignments to location"
subject_name:
repository: "Inventory"
project: "Project"

View file

@ -833,7 +833,7 @@ Rails.application.routes.draw do
end
resources :storage_location_repository_rows, only: %i(index create destroy update) do
collection do
get :actions_toolbar
post :actions_toolbar
end
member do
post :move

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
class AddCounterCacheRepositoryRow < ActiveRecord::Migration[7.0]
def change
change_table :repositories, bulk: true do |t|
t.integer :repository_rows_count, default: 0, null: false
end
Repository.find_each do |repository|
Repository.reset_counters(repository.id, :repository_rows)
end
end
end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddSmartAnnotationFlagToRepositoryTextValue < ActiveRecord::Migration[7.0]
def up
add_column :repository_text_values, :has_smart_annotation, :boolean, null: false, default: false
execute('UPDATE repository_text_values SET has_smart_annotation = TRUE ' \
'WHERE data ~ \'\[(@(.*?)|\#(.*?)~(prj|exp|tsk|rep_item))~([0-9a-zA-Z]+)\]\'')
end
def down
remove_column :repository_text_values, :has_smart_annotation
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2024_07_05_122903) do
ActiveRecord::Schema[7.0].define(version: 2024_10_02_122340) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gist"
enable_extension "pg_trgm"
@ -936,6 +936,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_07_05_122903) do
t.datetime "updated_at", precision: nil
t.bigint "created_by_id", null: false
t.bigint "last_modified_by_id", null: false
t.boolean "has_smart_annotation", default: false, null: false
t.index "trim_html_tags((data)::text) gin_trgm_ops", name: "index_repository_text_values_on_data", using: :gin
end
@ -1550,7 +1551,6 @@ ActiveRecord::Schema[7.0].define(version: 2024_07_05_122903) do
add_foreign_key "tags", "projects"
add_foreign_key "tags", "users", column: "created_by_id"
add_foreign_key "tags", "users", column: "last_modified_by_id"
add_foreign_key "team_shared_objects", "repositories", column: "shared_object_id"
add_foreign_key "team_shared_objects", "teams"
add_foreign_key "teams", "users", column: "created_by_id"
add_foreign_key "teams", "users", column: "last_modified_by_id"