mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 06:35:56 +08:00
Merge branch 'features/storage-locations' into ma_SCI_10859
This commit is contained in:
commit
10290d82d4
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -95,3 +95,7 @@ public/marvin4js-license.cxl
|
|||
|
||||
/app/assets/builds/*
|
||||
!/app/assets/builds/.keep
|
||||
|
||||
# Ignore automatically generated js-routes files.
|
||||
/app/javascript/routes.js
|
||||
/app/javascript/routes.d.ts
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -94,6 +94,7 @@ gem 'graphviz'
|
|||
|
||||
gem 'cssbundling-rails'
|
||||
gem 'jsbundling-rails'
|
||||
gem 'js-routes'
|
||||
|
||||
gem 'tailwindcss-rails', '~> 2.4'
|
||||
|
||||
|
@ -107,6 +108,7 @@ group :development, :test do
|
|||
gem 'awesome_print'
|
||||
gem 'better_errors'
|
||||
gem 'binding_of_caller'
|
||||
gem 'brakeman', require: false
|
||||
gem 'bullet'
|
||||
gem 'byebug'
|
||||
gem 'factory_bot_rails'
|
||||
|
|
|
@ -208,6 +208,8 @@ GEM
|
|||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.16.0)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (6.1.2)
|
||||
racc
|
||||
builder (3.2.4)
|
||||
bullet (7.0.7)
|
||||
activesupport (>= 3.0.0)
|
||||
|
@ -386,6 +388,8 @@ GEM
|
|||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
js-routes (2.2.8)
|
||||
railties (>= 4)
|
||||
jsbundling-rails (1.1.1)
|
||||
railties (>= 6.0.0)
|
||||
json (2.6.3)
|
||||
|
@ -797,6 +801,7 @@ DEPENDENCIES
|
|||
better_errors
|
||||
binding_of_caller
|
||||
bootsnap
|
||||
brakeman
|
||||
bullet
|
||||
byebug
|
||||
canaid!
|
||||
|
@ -826,6 +831,7 @@ DEPENDENCIES
|
|||
image_processing
|
||||
img2zpl!
|
||||
jbuilder
|
||||
js-routes
|
||||
jsbundling-rails
|
||||
json-jwt
|
||||
json_matchers
|
||||
|
|
2
Rakefile
2
Rakefile
|
@ -5,3 +5,5 @@ require File.expand_path('../config/application', __FILE__)
|
|||
|
||||
Rails.application.load_tasks
|
||||
Doorkeeper::Rake.load_tasks
|
||||
# Update js-routes file before javascript build
|
||||
task 'javascript:build' => 'js:routes:typescript'
|
||||
|
|
|
@ -185,7 +185,7 @@ var RepositoryColumns = (function() {
|
|||
disableSearch: true,
|
||||
labelHTML: true,
|
||||
optionLabel: function(option) {
|
||||
return `<div class="column-type-option" data-disabled="${option.params.disabled}">
|
||||
return `<div class="column-type-option" data-e2e="${option.params.data_e2e || ''}" data-disabled="${option.params.disabled}">
|
||||
<span>${option.label}</span>
|
||||
<span class="text-description">${option.params.text_description || ''}</span>
|
||||
</div>`
|
||||
|
@ -284,6 +284,7 @@ var RepositoryColumns = (function() {
|
|||
let editableRow = ($(el).attr('data-editable-row') === 'true') ? 'has-permissions' : '';
|
||||
let editUrl = $(el).attr('data-edit-column-url');
|
||||
let destroyUrl = $(el).attr('data-destroy-column-url');
|
||||
const isDisabled = $(el).attr('data-disabled') === 'true';
|
||||
let thederName;
|
||||
|
||||
if ($(el).find('.modal-tooltiptext').length > 0) {
|
||||
|
@ -315,7 +316,9 @@ var RepositoryColumns = (function() {
|
|||
<span class="vis-controls">
|
||||
<span class="vis sn-icon ${visClass}" title="${visText}" data-e2e="e2e-BT-invItems-manageColumnsModal-${e2eName}-visibility"></span>
|
||||
</span>
|
||||
<div class="text truncate" title="${thederName}" data-e2e="e2e-TX-invItems-manageColumnsModal-${e2eName}-columnName">${thederName}</div>
|
||||
<div class="text truncate" title="${thederName}" data-e2e="e2e-TX-invItems-manageColumnsModal-${e2eName}-columnName">
|
||||
${thederName} ${isDisabled ? `<span data-e2e="e2e-LB-invItems-manageColumnsModal-${e2eName}-disabled"></span>` : ''}
|
||||
</div>
|
||||
<span class="column-type pull-right shrink-0">${
|
||||
getColumnTypeText(el, colId) || `<i class="sn-icon sn-icon-locked-task" data-e2e="e2e-IC-invItems-manageColumnsModal-${e2eName}-locked"></i>`
|
||||
}</span>
|
||||
|
|
|
@ -202,10 +202,14 @@ var MarvinJsEditorApi = (function() {
|
|||
}
|
||||
$(marvinJsModal).modal('hide');
|
||||
|
||||
config.editor.focus();
|
||||
if (config.editor) config.editor.focus();
|
||||
|
||||
config.button.dataset.inProgress = false;
|
||||
|
||||
if (MarvinJsEditor.saveCallback) MarvinJsEditor.saveCallback();
|
||||
if (MarvinJsEditor.saveCallback) {
|
||||
MarvinJsEditor.saveCallback();
|
||||
delete MarvinJsEditor.saveCallback;
|
||||
}
|
||||
},
|
||||
error: function(response) {
|
||||
if (response.status === 403) {
|
||||
|
@ -264,8 +268,8 @@ var MarvinJsEditorApi = (function() {
|
|||
MarvinJsEditor.save(config);
|
||||
} else if (config.mode === 'edit') {
|
||||
config.objectType = 'Asset';
|
||||
MarvinJsEditor.saveCallback = (() => window.location.reload());
|
||||
MarvinJsEditor.update(config);
|
||||
location.reload();
|
||||
} else if (config.mode === 'new-tinymce') {
|
||||
config.objectType = 'TinyMceAsset';
|
||||
MarvinJsEditor.save(config);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
@import "tailwind/buttons";
|
||||
@import "tailwind/modals";
|
||||
@import "tailwind/flyouts";
|
||||
@import "tailwind/radio";
|
||||
@import "tailwind/loader.css";
|
||||
|
||||
@tailwind base;
|
||||
|
@ -69,6 +70,6 @@ html {
|
|||
|
||||
@keyframes shine-lines {
|
||||
0% { background-position: -150px }
|
||||
|
||||
|
||||
40%, 100% { background-position: 320px }
|
||||
}
|
||||
|
|
|
@ -110,13 +110,13 @@
|
|||
}
|
||||
|
||||
.send-comment {
|
||||
bottom: 5px;
|
||||
color: $brand-primary;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
text-align: center;
|
||||
top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// scss-lint:disable SelectorDepth QualifyingElement
|
||||
|
||||
/*
|
||||
:root {
|
||||
--sci-radio-size: 16px;
|
||||
}
|
||||
|
@ -85,3 +85,4 @@ input[type="radio"].sci-radio {
|
|||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
}
|
||||
|
||||
.btn.btn-light {
|
||||
@apply bg-transparent text-sn-blue border-transparent;
|
||||
@apply bg-transparent text-sn-blue border-transparent bg-sn-white;
|
||||
}
|
||||
|
||||
.btn.btn-light.btn-black {
|
||||
|
|
42
app/assets/stylesheets/tailwind/radio.css
Normal file
42
app/assets/stylesheets/tailwind/radio.css
Normal file
|
@ -0,0 +1,42 @@
|
|||
@layer components {
|
||||
|
||||
.sci-radio-container {
|
||||
@apply inline-block h-4 w-4 relative;
|
||||
}
|
||||
|
||||
input[type="radio"].sci-radio {
|
||||
@apply cursor-pointer shrink-0 h-4 w-4 m-0 opacity-0 relative z-[2];
|
||||
}
|
||||
|
||||
input[type="radio"].sci-radio + .sci-radio-label {
|
||||
@apply inline-block shrink-0 h-4 w-4 absolute left-0;
|
||||
}
|
||||
|
||||
input[type="radio"].sci-radio + .sci-radio-label::before {
|
||||
@apply border-[1px] border-solid border-sn-black rounded-full text-white text-center transition-all
|
||||
h-4 w-4 left-0 absolute;
|
||||
content: "";
|
||||
}
|
||||
|
||||
input[type="radio"].sci-radio + .sci-radio-label::after{
|
||||
@apply bg-white rounded-full text-white text-center transition-all
|
||||
absolute w-2.5 h-2.5 top-[3px] left-[3px] ;
|
||||
content: "";
|
||||
}
|
||||
|
||||
input[type="radio"].sci-radio:checked + .sci-radio-label::before {
|
||||
@apply !border-sn-blue;
|
||||
}
|
||||
|
||||
input[type="radio"].sci-radio:checked + .sci-radio-label::after {
|
||||
@apply !bg-sn-science-blue;
|
||||
}
|
||||
|
||||
input[type="radio"].sci-radio:disabled + .sci-radio-label::before {
|
||||
@apply !border-sn-sleepy-grey;
|
||||
}
|
||||
|
||||
input[type="radio"].sci-radio:checked:disabled + .sci-radio-label::after {
|
||||
@apply !bg-sn-sleepy-grey;
|
||||
}
|
||||
}
|
|
@ -103,8 +103,6 @@ module AccessPermissions
|
|||
destroy: true
|
||||
)
|
||||
|
||||
user_assignment.destroy!
|
||||
|
||||
log_activity(:unassign_user_from_project, { user_target: user_assignment.user.id,
|
||||
role: user_assignment.user_role.name })
|
||||
|
||||
|
|
|
@ -10,14 +10,14 @@ class RepositoriesController < ApplicationController
|
|||
include MyModulesHelper
|
||||
|
||||
before_action :load_repository, except: %i(index create create_modal sidebar archive restore actions_toolbar
|
||||
export_modal export_repositories)
|
||||
before_action :load_repositories, only: :index
|
||||
export_modal export_repositories list)
|
||||
before_action :load_repositories, only: %i(index list)
|
||||
before_action :load_repositories_for_archiving, only: :archive
|
||||
before_action :load_repositories_for_restoring, only: :restore
|
||||
before_action :check_view_all_permissions, only: %i(index sidebar)
|
||||
before_action :check_view_all_permissions, only: %i(index sidebar list)
|
||||
before_action :check_view_permissions, except: %i(index create_modal create update destroy parse_sheet
|
||||
import_records sidebar archive restore actions_toolbar
|
||||
export_modal export_repositories)
|
||||
export_modal export_repositories list)
|
||||
before_action :check_manage_permissions, only: %i(rename_modal update)
|
||||
before_action :check_delete_permissions, only: %i(destroy destroy_modal)
|
||||
before_action :check_archive_permissions, only: %i(archive restore)
|
||||
|
@ -44,6 +44,16 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def list
|
||||
results = @repositories
|
||||
results = results.name_like(params[:query]) if params[:query].present?
|
||||
render json: { data: results.map { |r| [r.id, r.name] } }
|
||||
end
|
||||
|
||||
def rows_list
|
||||
render json: { data: @repository.repository_rows.map { |r| [r.id, r.name] } }
|
||||
end
|
||||
|
||||
def sidebar
|
||||
render json: {
|
||||
html: render_to_string(partial: 'repositories/sidebar', locals: {
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StorageLocationRepositoryRowsController < ApplicationController
|
||||
before_action :load_storage_location_repository_row, only: %i(update destroy)
|
||||
before_action :check_storage_locations_enabled, except: :destroy
|
||||
before_action :load_storage_location_repository_row, only: %i(update destroy move)
|
||||
before_action :load_storage_location
|
||||
before_action :load_repository_row
|
||||
before_action :check_read_permissions, only: :index
|
||||
before_action :check_manage_permissions, except: :index
|
||||
before_action :load_repository_row, only: %i(create update destroy move)
|
||||
before_action :check_read_permissions, except: %i(create actions_toolbar)
|
||||
before_action :check_manage_permissions, only: %i(create update destroy)
|
||||
|
||||
def index
|
||||
storage_location_repository_row = Lists::StorageLocationRepositoryRowsService.new(
|
||||
current_team, storage_location_repository_row_params
|
||||
current_team, params
|
||||
).call
|
||||
render json: storage_location_repository_row,
|
||||
each_serializer: Lists::StorageLocationRepositoryRowSerializer,
|
||||
include: %i(repository_row)
|
||||
meta: (pagination_dict(storage_location_repository_row) unless @storage_location.with_grid?)
|
||||
end
|
||||
|
||||
def update
|
||||
|
@ -21,8 +22,7 @@ class StorageLocationRepositoryRowsController < ApplicationController
|
|||
|
||||
if @storage_location_repository_row.save
|
||||
render json: @storage_location_repository_row,
|
||||
serializer: Lists::StorageLocationRepositoryRowSerializer,
|
||||
include: :repository_row
|
||||
serializer: Lists::StorageLocationRepositoryRowSerializer
|
||||
else
|
||||
render json: @storage_location_repository_row.errors, status: :unprocessable_entity
|
||||
end
|
||||
|
@ -38,13 +38,30 @@ class StorageLocationRepositoryRowsController < ApplicationController
|
|||
|
||||
if @storage_location_repository_row.save
|
||||
render json: @storage_location_repository_row,
|
||||
serializer: Lists::StorageLocationRepositoryRowSerializer,
|
||||
include: :repository_row
|
||||
serializer: Lists::StorageLocationRepositoryRowSerializer
|
||||
else
|
||||
render json: @storage_location_repository_row.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def move
|
||||
ActiveRecord::Base.transaction do
|
||||
@storage_location_repository_row.discard
|
||||
@storage_location_repository_row = StorageLocationRepositoryRow.create!(
|
||||
repository_row: @repository_row,
|
||||
storage_location: @storage_location,
|
||||
metadata: storage_location_repository_row_params[:metadata] || {},
|
||||
created_by: current_user
|
||||
)
|
||||
|
||||
render json: @storage_location_repository_row,
|
||||
serializer: Lists::StorageLocationRepositoryRowSerializer
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @storage_location_repository_row.discard
|
||||
render json: {}
|
||||
|
@ -53,8 +70,21 @@ class StorageLocationRepositoryRowsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def actions_toolbar
|
||||
render json: {
|
||||
actions: Toolbars::StorageLocationRepositoryRowsService.new(
|
||||
current_user,
|
||||
items_ids: JSON.parse(params[:items]).map { |i| i['id'] }
|
||||
).actions
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_storage_locations_enabled
|
||||
render_403 unless StorageLocation.storage_locations_enabled?
|
||||
end
|
||||
|
||||
def load_storage_location_repository_row
|
||||
@storage_location_repository_row = StorageLocationRepositoryRow.find(
|
||||
storage_location_repository_row_params[:id]
|
||||
|
@ -80,10 +110,12 @@ class StorageLocationRepositoryRowsController < ApplicationController
|
|||
end
|
||||
|
||||
def check_read_permissions
|
||||
render_403 unless true
|
||||
render_403 unless can_read_storage_location_containers?(current_team)
|
||||
end
|
||||
|
||||
def check_manage_permissions
|
||||
render_403 unless true
|
||||
unless can_manage_storage_location_containers?(current_team) && can_read_repository?(@repository_row.repository)
|
||||
render_403
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StorageLocationsController < ApplicationController
|
||||
before_action :load_storage_location, only: %i(update destroy)
|
||||
before_action :check_read_permissions, only: :index
|
||||
before_action :check_manage_permissions, except: :index
|
||||
before_action :set_breadcrumbs_items, only: :index
|
||||
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)
|
||||
before_action :check_read_permissions, except: %i(index create tree actions_toolbar)
|
||||
before_action :check_create_permissions, only: :create
|
||||
before_action :check_manage_permissions, only: %i(update destroy duplicate move unassign_rows)
|
||||
before_action :set_breadcrumbs_items, only: %i(index show)
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
|
@ -17,14 +19,17 @@ class StorageLocationsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def update
|
||||
@storage_location.image.attach(storage_location_params[:signed_blob_id]) if storage_location_params[:signed_blob_id]
|
||||
@storage_location.image.purge if params[:file_name].blank?
|
||||
@storage_location.image.attach(params[:signed_blob_id]) if params[:signed_blob_id]
|
||||
@storage_location.update(storage_location_params)
|
||||
|
||||
if @storage_location.save
|
||||
render json: @storage_location, serializer: Lists::StorageLocationSerializer
|
||||
else
|
||||
render json: @storage_location.errors, status: :unprocessable_entity
|
||||
render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -33,12 +38,12 @@ class StorageLocationsController < ApplicationController
|
|||
storage_location_params.merge({ team: current_team, created_by: current_user })
|
||||
)
|
||||
|
||||
@storage_location.image.attach(storage_location_params[:signed_blob_id]) if storage_location_params[:signed_blob_id]
|
||||
@storage_location.image.attach(params[:signed_blob_id]) if params[:signed_blob_id]
|
||||
|
||||
if @storage_location.save
|
||||
render json: @storage_location, serializer: Lists::StorageLocationSerializer
|
||||
else
|
||||
render json: @storage_location.errors, status: :unprocessable_entity
|
||||
render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -46,34 +51,103 @@ class StorageLocationsController < ApplicationController
|
|||
if @storage_location.discard
|
||||
render json: {}
|
||||
else
|
||||
render json: { errors: @storage_location.errors.full_messages }, status: :unprocessable_entity
|
||||
render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def duplicate
|
||||
new_storage_location = @storage_location.duplicate!
|
||||
if new_storage_location
|
||||
render json: new_storage_location, serializer: Lists::StorageLocationSerializer
|
||||
else
|
||||
render json: { errors: :failed }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def move
|
||||
storage_location_destination =
|
||||
if move_params[:destination_storage_location_id] == 'root_storage_location'
|
||||
nil
|
||||
else
|
||||
current_team.storage_locations.find(move_params[:destination_storage_location_id])
|
||||
end
|
||||
|
||||
@storage_location.update!(parent: storage_location_destination)
|
||||
|
||||
render json: { message: I18n.t('storage_locations.index.move_modal.success_flash') }
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e.message
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
render json: { error: I18n.t('storage_locations.index.move_modal.error_flash') }, status: :bad_request
|
||||
end
|
||||
|
||||
def tree
|
||||
records = current_team.storage_locations.where(parent: nil, container: [false, params[:container] == 'true'])
|
||||
render json: storage_locations_recursive_builder(records)
|
||||
end
|
||||
|
||||
def available_positions
|
||||
render json: { positions: @storage_location.available_positions }
|
||||
end
|
||||
|
||||
def unassign_rows
|
||||
@storage_location.storage_location_repository_rows.where(id: params[:ids]).discard_all
|
||||
|
||||
render json: { status: :ok }
|
||||
end
|
||||
|
||||
def actions_toolbar
|
||||
render json: {
|
||||
actions: [] # TODO: Add actions
|
||||
actions:
|
||||
Toolbars::StorageLocationsService.new(
|
||||
current_user,
|
||||
storage_location_ids: JSON.parse(params[:items]).map { |i| i['id'] }
|
||||
).actions
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_storage_locations_enabled
|
||||
render_403 unless StorageLocation.storage_locations_enabled?
|
||||
end
|
||||
|
||||
def storage_location_params
|
||||
params.permit(:id, :parent_id, :name, :container, :signed_blob_id, :description,
|
||||
metadata: { dimensions: [], parent_coordinations: [], display_type: :string })
|
||||
params.permit(:id, :parent_id, :name, :container, :description,
|
||||
metadata: [:display_type, dimensions: [], parent_coordinations: []])
|
||||
end
|
||||
|
||||
def move_params
|
||||
params.permit(:id, :destination_storage_location_id)
|
||||
end
|
||||
|
||||
def load_storage_location
|
||||
@storage_location = StorageLocation.where(team: current_team).find(storage_location_params[:id])
|
||||
@storage_location = current_team.storage_locations.find_by(id: storage_location_params[:id])
|
||||
render_404 unless @storage_location
|
||||
end
|
||||
|
||||
def check_read_permissions
|
||||
render_403 unless true
|
||||
if @storage_location.container
|
||||
render_403 unless can_read_storage_location_containers?(current_team)
|
||||
else
|
||||
render_403 unless can_read_storage_locations?(current_team)
|
||||
end
|
||||
end
|
||||
|
||||
def check_create_permissions
|
||||
if storage_location_params[:container]
|
||||
render_403 unless can_create_storage_location_containers?(current_team)
|
||||
else
|
||||
render_403 unless can_create_storage_locations?(current_team)
|
||||
end
|
||||
end
|
||||
|
||||
def check_manage_permissions
|
||||
render_403 unless true
|
||||
if @storage_location.container
|
||||
render_403 unless can_manage_storage_location_containers?(current_team)
|
||||
else
|
||||
render_403 unless can_manage_storage_locations?(current_team)
|
||||
end
|
||||
end
|
||||
|
||||
def set_breadcrumbs_items
|
||||
|
@ -89,8 +163,8 @@ class StorageLocationsController < ApplicationController
|
|||
})
|
||||
|
||||
storage_locations = []
|
||||
if params[:parent_id]
|
||||
location = StorageLocation.where(team: current_team).find_by(id: params[:parent_id])
|
||||
if params[:parent_id] || @storage_location
|
||||
location = (current_team.storage_locations.find_by(id: params[:parent_id]) || @storage_location)
|
||||
if location
|
||||
storage_locations.unshift(breadcrumbs_item(location))
|
||||
while location.parent
|
||||
|
@ -108,4 +182,15 @@ class StorageLocationsController < ApplicationController
|
|||
url: storage_locations_path(parent_id: location.id)
|
||||
}
|
||||
end
|
||||
|
||||
def storage_locations_recursive_builder(storage_locations)
|
||||
storage_locations.map do |storage_location|
|
||||
{
|
||||
storage_location: storage_location,
|
||||
children: storage_locations_recursive_builder(
|
||||
storage_location.storage_locations.where(container: [false, params[:container] == 'true'])
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,8 +17,8 @@ module Users
|
|||
next unless Extends::WHITELISTED_USER_SETTINGS.include?(key.to_s)
|
||||
|
||||
case key.to_s
|
||||
when 'task_step_states'
|
||||
update_task_step_states(data)
|
||||
when 'task_step_states', 'result_states'
|
||||
update_object_states(data, key.to_s)
|
||||
else
|
||||
current_user.settings[key] = data
|
||||
end
|
||||
|
@ -34,18 +34,18 @@ module Users
|
|||
|
||||
private
|
||||
|
||||
def update_task_step_states(task_step_states_data)
|
||||
current_states = current_user.settings.fetch('task_step_states', {})
|
||||
def update_object_states(object_states_data, object_state_key)
|
||||
current_states = current_user.settings.fetch(object_state_key, {})
|
||||
|
||||
task_step_states_data.each do |step_id, collapsed|
|
||||
object_states_data.each do |object_id, collapsed|
|
||||
if collapsed
|
||||
current_states[step_id] = true
|
||||
current_states[object_id] = true
|
||||
else
|
||||
current_states.delete(step_id)
|
||||
current_states.delete(object_id)
|
||||
end
|
||||
end
|
||||
|
||||
current_user.settings['task_step_states'] = current_states
|
||||
current_user.settings[object_state_key] = current_states
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
10
app/javascript/packs/vue/storage_locations_container.js
Normal file
10
app/javascript/packs/vue/storage_locations_container.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||
import PerfectScrollbar from 'vue3-perfect-scrollbar';
|
||||
import StorageLocationsContainer from '../../vue/storage_locations/container.vue';
|
||||
import { mountWithTurbolinks } from './helpers/turbolinks.js';
|
||||
|
||||
const app = createApp();
|
||||
app.component('StorageLocationsContainer', StorageLocationsContainer);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
app.use(PerfectScrollbar);
|
||||
mountWithTurbolinks(app, '#StorageLocationsContainer');
|
|
@ -98,20 +98,19 @@ export default {
|
|||
if (this.query === '') {
|
||||
return this.foldersTree;
|
||||
}
|
||||
return this.foldersTree.map((folder) => (
|
||||
{
|
||||
folder: folder.folder,
|
||||
children: folder.children.filter((child) => (
|
||||
child.folder.name.toLowerCase().includes(this.query.toLowerCase())
|
||||
)),
|
||||
}
|
||||
)).filter((folder) => (
|
||||
folder.folder.name.toLowerCase().includes(this.query.toLowerCase())
|
||||
|| folder.children.length > 0
|
||||
));
|
||||
return this.filteredFoldersTreeHelper(this.foldersTree);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
filteredFoldersTreeHelper(foldersTree) {
|
||||
return foldersTree.map(({ folder, children }) => {
|
||||
if (folder.name.toLowerCase().includes(this.query.toLowerCase())) {
|
||||
return { folder, children };
|
||||
}
|
||||
const filteredChildren = this.filteredFoldersTreeHelper(children);
|
||||
return filteredChildren.length ? { folder, children: filteredChildren } : null;
|
||||
}).filter(Boolean);
|
||||
},
|
||||
selectFolder(folderId) {
|
||||
this.selectedFolderId = folderId;
|
||||
},
|
||||
|
|
|
@ -177,6 +177,7 @@
|
|||
<div v-for="(step, index) in steps" :key="step.id" class="step-block">
|
||||
<div v-if="index > 0 && urls.add_step_url" class="insert-step" @click="addStep(index)" data-e2e="e2e-BT-protocol-templateSteps-insertStep">
|
||||
<i class="sn-icon sn-icon-new-task"></i>
|
||||
<span class="mr-3">{{ i18n.t("protocols.steps.add_step") }}</span>
|
||||
</div>
|
||||
<Step
|
||||
ref="steps"
|
||||
|
@ -201,6 +202,7 @@
|
|||
/>
|
||||
<div v-if="(index === steps.length - 1) && urls.add_step_url" class="insert-step" @click="addStep(index + 1)" data-e2e="e2e-BT-protocol-templateSteps-insertStep">
|
||||
<i class="sn-icon sn-icon-new-task"></i>
|
||||
<span class="mr-3">{{ i18n.t("protocols.steps.add_step") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="steps.length > 0 && urls.add_step_url && inRepository" class="py-5">
|
||||
|
|
|
@ -145,7 +145,19 @@
|
|||
@attachments:viewMode="changeAttachmentsViewMode"
|
||||
@attachment:viewMode="updateAttachmentViewMode"/>
|
||||
</div>
|
||||
<ContentToolbar
|
||||
v-if="orderedElements.length > 2"
|
||||
:insertMenu="insertMenu"
|
||||
@create:table="(...args) => this.createElement('table', ...args)"
|
||||
@create:checklist="createElement('checklist')"
|
||||
@create:text="createElement('text')"
|
||||
@create:file="openLoadFromComputer"
|
||||
@create:wopi_file="openWopiFileModal"
|
||||
@create:ove_file="openOVEditor"
|
||||
@create:marvinjs_file="openMarvinJsModal($refs.marvinJsButton)"
|
||||
></ContentToolbar>
|
||||
</div>
|
||||
|
||||
<deleteStepModal v-if="confirmingDelete" @confirm="deleteStep" @cancel="closeDeleteModal"/>
|
||||
<ReorderableItemsModal v-if="reordering"
|
||||
:title="i18n.t('protocols.steps.modals.reorder_elements.title', { step_position: step.attributes.position + 1 })"
|
||||
|
@ -172,6 +184,7 @@
|
|||
import Attachments from '../shared/content/attachments.vue'
|
||||
import ReorderableItemsModal from '../shared/reorderable_items_modal.vue'
|
||||
import MenuDropdown from '../shared/menu_dropdown.vue'
|
||||
import ContentToolbar from '../shared/content/content_toolbar.vue'
|
||||
|
||||
import UtilsMixin from '../mixins/utils.js'
|
||||
import AttachmentsMixin from '../shared/content/mixins/attachments.js'
|
||||
|
@ -265,7 +278,8 @@
|
|||
Attachments,
|
||||
StorageUsage,
|
||||
ReorderableItemsModal,
|
||||
MenuDropdown
|
||||
MenuDropdown,
|
||||
ContentToolbar
|
||||
},
|
||||
created() {
|
||||
this.loadAttachments();
|
||||
|
@ -359,25 +373,30 @@
|
|||
if (this.urls.update_url) {
|
||||
menu = menu.concat([{
|
||||
text: this.i18n.t('protocols.steps.insert.text'),
|
||||
icon: 'sn-icon sn-icon-result-text',
|
||||
emit: 'create:text',
|
||||
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertText`
|
||||
},{
|
||||
text: this.i18n.t('protocols.steps.insert.attachment'),
|
||||
submenu: this.filesMenu,
|
||||
position: 'left',
|
||||
icon: 'sn-icon sn-icon-file',
|
||||
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertAttachment`
|
||||
},{
|
||||
text: this.i18n.t('protocols.steps.insert.table'),
|
||||
icon: 'sn-icon sn-icon-tables',
|
||||
emit: 'create:table',
|
||||
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertTable`
|
||||
},{
|
||||
text: this.i18n.t('protocols.steps.insert.well_plate'),
|
||||
submenu: this.wellPlateOptions,
|
||||
icon: 'sn-icon sn-icon-tables',
|
||||
position: 'left',
|
||||
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertWellplate`
|
||||
},{
|
||||
text: this.i18n.t('protocols.steps.insert.checklist'),
|
||||
emit: 'create:checklist',
|
||||
icon: 'sn-icon sn-icon-checkllist',
|
||||
data_e2e: `e2e-BT-protocol-step${this.step.id}-insertChecklist`
|
||||
}]);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-content" data-e2e="e2e-MD-protocolVersions">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-protocolVersionsModal-close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block">
|
||||
<h4 class="modal-title truncate !block" data-e2e="e2e-TX-protocolVersionsModal-title">
|
||||
{{ i18n.t('protocols.index.versions.title', { protocol: protocol.name }) }}
|
||||
</h4>
|
||||
</div>
|
||||
|
@ -15,9 +15,9 @@
|
|||
<img src="/images/medium/loading.svg" alt="Loading" class="p-4 rounded-xl bg-sn-white" />
|
||||
</div>
|
||||
<div class="max-h-[400px] overflow-y-auto">
|
||||
<div v-if="draft">
|
||||
<div v-if="draft" data-e2e="e2e-CO-protocolVersionsModal-draft">
|
||||
<div class="flex items-center gap-4">
|
||||
<a :href="draft.urls.show" class="hover:no-underline cursor-pointer shrink-0">
|
||||
<a :href="draft.urls.show" class="hover:no-underline cursor-pointer shrink-0" data-e2e="e2e-TL-protocolVersionsModal-draft-draftLink">
|
||||
<span v-if="draft.previous_number"
|
||||
v-html="i18n.t('protocols.index.versions.draft_html', {
|
||||
parent_version: draft.previous_number
|
||||
|
@ -25,7 +25,7 @@
|
|||
></span>
|
||||
<span v-else v-html="i18n.t('protocols.index.versions.first_draft_html')"></span>
|
||||
</a>
|
||||
<span class="text-xs" v-if="draft.modified_by">
|
||||
<span class="text-xs" v-if="draft.modified_by" data-e2e="e2e-TX-protocolVersionsModal-draft-timestamp">
|
||||
{{
|
||||
i18n.t('protocols.index.versions.draft_full_modification_info', {
|
||||
modified_on: draft.modified_on,
|
||||
|
@ -33,7 +33,7 @@
|
|||
})
|
||||
}}
|
||||
</span>
|
||||
<span class="text-xs" v-else>
|
||||
<span class="text-xs" v-else data-e2e="e2e-TX-protocolVersionsModal-draft-timestamp">
|
||||
{{
|
||||
i18n.t('protocols.index.versions.draft_update_modification_info', {
|
||||
modified_on: draft.modified_on
|
||||
|
@ -41,11 +41,17 @@
|
|||
}}
|
||||
</span>
|
||||
<div class="flex items-center gap-2 ml-auto">
|
||||
<button v-if="draft.urls.publish" class="btn btn-light" :disabled="updating" @click="publishDraft">
|
||||
<button v-if="draft.urls.publish" class="btn btn-light" :disabled="updating" @click="publishDraft" data-e2e="e2e-BT-protocolVersionsModal-draft-publish">
|
||||
<i class="sn-icon sn-icon-Publish"></i>
|
||||
{{ i18n.t('protocols.index.versions.publish') }}
|
||||
</button>
|
||||
<button v-if="draft.urls.destroy" @click="destroyDraft" :disabled="updating" class="btn btn-light icon-btn">
|
||||
<button
|
||||
v-if="draft.urls.destroy"
|
||||
@click="destroyDraft"
|
||||
:disabled="updating"
|
||||
class="btn btn-light icon-btn"
|
||||
data-e2e="e2e-BT-protocolVersionsModal-draft-deleteDraft"
|
||||
>
|
||||
<i class="sn-icon sn-icon-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -60,16 +66,17 @@
|
|||
:singleLine="false"
|
||||
:attributeName="`${i18n.t('Draft')} ${i18n.t('comment')}`"
|
||||
@update="updateComment"
|
||||
:dataE2e="'protocolVersionsModal-draft-revisionNotes'"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="version in publishedVersions" :key="version.number">
|
||||
<div v-for="version in publishedVersions" :key="version.number" :data-e2e="`e2e-CO-protocolVersionsModal-version${version.number}`">
|
||||
<div class="flex items-center gap-4 group min-h-[40px]">
|
||||
<a :href="version.urls.show" class="hover:no-underline cursor-pointer shrink-0">
|
||||
<a :href="version.urls.show" class="hover:no-underline cursor-pointer shrink-0" :data-e2e="`e2e-TL-protocolVersionsModal-version${version.number}-versionLink`">
|
||||
<b>
|
||||
{{ i18n.t('protocols.index.versions.revision', { version: version.number }) }}
|
||||
</b>
|
||||
</a>
|
||||
<span class="text-xs">
|
||||
<span class="text-xs" :data-e2e="`e2e-TX-protocolVersionsModal-version${version.number}-timestamp`">
|
||||
{{
|
||||
i18n.t('protocols.index.versions.revision_publishing_info', {
|
||||
published_on: version.published_on,
|
||||
|
@ -83,11 +90,12 @@
|
|||
:title="i18n.t('protocols.index.versions.save_as_draft')"
|
||||
@click="saveAsDraft(version.urls.save_as_draft)"
|
||||
:disabled="draft || updating"
|
||||
:data-e2e="`e2e-BT-protocolVersionsModal-version${version.number}-saveAsDraft`"
|
||||
>
|
||||
<i class="sn-icon sn-icon-duplicate"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-4" :data-e2e="`e2e-TX-protocolVersionsModal-version${version.number}-revisionNotes`">
|
||||
{{ version.comment }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
@changeStep="changeStep"
|
||||
@importRows="importRecords"
|
||||
@updateAutoMapping="updateAutoMapping"
|
||||
@updateAutoClearing="updateAutoClearing"
|
||||
/>
|
||||
<ExportModal
|
||||
v-else
|
||||
|
@ -49,7 +50,7 @@ export default {
|
|||
return {
|
||||
modalOpened: false,
|
||||
activeStep: 'UploadStep',
|
||||
params: { autoMapping: true },
|
||||
params: { autoMapping: true, autoClearing: false },
|
||||
modalId: null,
|
||||
loading: false
|
||||
};
|
||||
|
@ -62,6 +63,7 @@ export default {
|
|||
this.activeStep = 'UploadStep';
|
||||
this.params.selectedItems = null;
|
||||
this.params.autoMapping = true;
|
||||
this.params.autoClearing = false;
|
||||
this.fetchRepository();
|
||||
},
|
||||
fetchRepository() {
|
||||
|
@ -78,6 +80,11 @@ export default {
|
|||
},
|
||||
updateAutoMapping(value) {
|
||||
this.params.autoMapping = value;
|
||||
this.params.autoClearing = false;
|
||||
},
|
||||
updateAutoClearing() {
|
||||
this.params.autoMapping = false;
|
||||
this.params.autoClearing = true;
|
||||
},
|
||||
generatePreview(selectedItems, updateWithEmptyCells, onlyAddNewItems) {
|
||||
this.params.selectedItems = selectedItems;
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<div class="flex gap-6 items-center my-6">
|
||||
<div class="flex items-center gap-2" :title="i18n.t('repositories.import_records.steps.step2.autoMappingTooltip')">
|
||||
<div class="sci-checkbox-container">
|
||||
<input type="checkbox" class="sci-checkbox" @change="$emit('update-auto-mapping', $event.target.checked)" :checked="params.autoMapping" />
|
||||
<input type="checkbox" class="sci-checkbox" @change="toggleAutoMapping" :checked="params.autoMapping" />
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
{{ i18n.t('repositories.import_records.steps.step2.autoMappingText') }}
|
||||
|
@ -57,6 +57,7 @@
|
|||
:value="this.selectedItems.find((item) => item.index === index)"
|
||||
@selection:changed="handleChange"
|
||||
:autoMapping="params.autoMapping"
|
||||
:autoClearing="params.autoClearing"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -140,7 +141,12 @@ export default {
|
|||
methods: {
|
||||
handleChange(payload) {
|
||||
this.error = null;
|
||||
const { index, key, value } = payload;
|
||||
const {
|
||||
index,
|
||||
key,
|
||||
value,
|
||||
autoMap
|
||||
} = payload;
|
||||
|
||||
const item = this.selectedItems.find((i) => i.index === index);
|
||||
const usedBeforeItem = this.selectedItems.find((i) => i.key === key && i.index !== index);
|
||||
|
@ -152,6 +158,15 @@ export default {
|
|||
|
||||
item.key = key;
|
||||
item.value = value;
|
||||
|
||||
this.$emit('updateAutoMapping', autoMap);
|
||||
},
|
||||
toggleAutoMapping(event) {
|
||||
if (event.target.checked) {
|
||||
this.$emit('updateAutoMapping', true);
|
||||
} else {
|
||||
this.$emit('updateAutoClearing');
|
||||
}
|
||||
},
|
||||
loadAvailableFields() {
|
||||
// Adding alreadySelected attribute for tracking
|
||||
|
|
|
@ -90,6 +90,10 @@ export default {
|
|||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
autoClearing: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
value: Object
|
||||
},
|
||||
data() {
|
||||
|
@ -122,11 +126,10 @@ export default {
|
|||
}
|
||||
},
|
||||
autoMapping(newVal) {
|
||||
if (newVal === true) {
|
||||
this.autoMap();
|
||||
} else {
|
||||
this.clearAutoMap();
|
||||
}
|
||||
if (newVal) this.autoMap();
|
||||
},
|
||||
autoClearing(newVal) {
|
||||
if (newVal) this.clearAutoMap();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -151,19 +154,31 @@ export default {
|
|||
return this.systemColumns.includes(column);
|
||||
},
|
||||
autoMap() {
|
||||
this.changeSelected(null);
|
||||
this.changeAutoSelected(null);
|
||||
Object.entries(this.params.import_data.available_fields).forEach(([key, value]) => {
|
||||
if (this.item === value) {
|
||||
this.changeSelected(key);
|
||||
this.changeAutoSelected(key);
|
||||
}
|
||||
});
|
||||
},
|
||||
clearAutoMap() {
|
||||
this.changeSelected('do_not_import');
|
||||
},
|
||||
changeSelected(e) {
|
||||
updateSelectedColumnType(e, autoMap) {
|
||||
const value = this.params.import_data.available_fields[e];
|
||||
this.selectedColumnType = { index: this.index, key: e, value };
|
||||
this.selectedColumnType = {
|
||||
index: this.index,
|
||||
key: e,
|
||||
value,
|
||||
autoMap
|
||||
};
|
||||
},
|
||||
changeAutoSelected(e) {
|
||||
this.updateSelectedColumnType(e, true);
|
||||
this.$emit('selection:changed', this.selectedColumnType);
|
||||
},
|
||||
changeSelected(e) {
|
||||
this.updateSelectedColumnType(e, false);
|
||||
this.$emit('selection:changed', this.selectedColumnType);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog flex" role="document" :class="{'!w-[900px]': showingInfo}">
|
||||
<div class="modal-dialog flex" role="document" :class="{'!w-[900px]': showingInfo}" data-e2e="e2e-MD-invInventoryImport-upload">
|
||||
<div v-if="showingInfo" class="w-[300px] shrink-0 h-full bg-sn-super-light-grey p-6 rounded-s text-sn-dark-grey">
|
||||
<h3 class="my-0 mb-4">{{ this.i18n.t('repositories.import_records.info_sidebar.title') }}</h3>
|
||||
<h3 class="my-0 mb-4" data-e2e="e2e-TX-invInventoryImport-uploadModal-help-title">{{ this.i18n.t('repositories.import_records.info_sidebar.title') }}</h3>
|
||||
<div v-for="i in 4" :key="i" class="flex gap-3 mb-4">
|
||||
<span class="btn btn-secondary icon-btn !text-sn-black !pointer-events-none">
|
||||
<i class="sn-icon"
|
||||
:class="i18n.t(`repositories.import_records.info_sidebar.elements.element${i - 1}.icon`)"
|
||||
:data-e2e="`e2e-IC-invInventoryImport-uploadModal-help-icon${i}`"
|
||||
></i>
|
||||
</span>
|
||||
<div>
|
||||
<div class="font-bold mb-2">{{ i18n.t(`repositories.import_records.info_sidebar.elements.element${i - 1}.label`) }}</div>
|
||||
<div>{{ i18n.t(`repositories.import_records.info_sidebar.elements.element${i - 1}.subtext`) }}</div>
|
||||
<div
|
||||
class="font-bold mb-2"
|
||||
:data-e2e="`e2e-TX-invInventoryImport-uploadModal-help-title${i}`"
|
||||
>{{ i18n.t(`repositories.import_records.info_sidebar.elements.element${i - 1}.label`) }}</div>
|
||||
<div
|
||||
:data-e2e="`e2e-TX-invInventoryImport-uploadModal-help-text${i}`"
|
||||
>{{ i18n.t(`repositories.import_records.info_sidebar.elements.element${i - 1}.subtext`) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 mb-4 items-center">
|
||||
|
@ -25,36 +31,41 @@
|
|||
</div>
|
||||
<div class="modal-content grow flex flex-col" :class="{'!rounded-s-none': showingInfo}">
|
||||
<div class="modal-header gap-4">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-newInventoryModal-close">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-invInventoryImport-uploadModal-close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<button class="btn btn-light btn-sm mr-auto" @click="showingInfo = !showingInfo">
|
||||
<button class="btn btn-light btn-sm mr-auto" @click="showingInfo = !showingInfo" data-e2e="e2e-BT-invInventoryImport-uploadModal-help">
|
||||
<i class="sn-icon sn-icon-help-s"></i>
|
||||
{{ i18n.t('repositories.import_records.steps.step1.helpText') }}
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block !mr-0" id="edit-project-modal-label" data-e2e="e2e-TX-newInventoryModal-title">
|
||||
<h4 class="modal-title truncate !block !mr-0" id="edit-project-modal-label" data-e2e="e2e-TX-invInventoryImport-uploadModal-title">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body flex flex-col grow">
|
||||
<p class="text-sn-dark-grey">
|
||||
<p class="text-sn-dark-grey" data-e2e="e2e-TX-invInventoryImport-uploadModal-description">
|
||||
{{ this.i18n.t('repositories.import_records.steps.step1.subtitle') }}
|
||||
</p>
|
||||
<h3 class="my-0 text-sn-dark-grey mb-3">
|
||||
<h3 class="my-0 text-sn-dark-grey mb-3" data-e2e="e2e-TX-invInventoryImport-uploadModal-exportSubtitle">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.exportTitle') }}
|
||||
</h3>
|
||||
<div class="flex gap-4 mb-6">
|
||||
<button class="btn btn-secondary btn-sm" @click="$emit('changeStep', 'ExportModal')">
|
||||
<button class="btn btn-secondary btn-sm" @click="$emit('changeStep', 'ExportModal')" data-e2e="e2e-BT-invInventoryImport-uploadModal-exportAll">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
{{ i18n.t('repositories.import_records.steps.step1.exportFullInvBtnText') }}
|
||||
</button>
|
||||
<a :href="params.attributes.urls.export_empty_repository" target="_blank" class="btn btn-secondary btn-sm">
|
||||
<a
|
||||
:href="params.attributes.urls.export_empty_repository"
|
||||
target="_blank"
|
||||
class="btn btn-secondary btn-sm"
|
||||
data-e2e="e2e-BT-invInventoryImport-uploadModal-downloadTemplate"
|
||||
>
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
{{ i18n.t('repositories.import_records.steps.step1.exportEmptyInvBtnText') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h3 class="my-0 text-sn-dark-grey mb-3">
|
||||
<h3 class="my-0 text-sn-dark-grey mb-3" data-e2e="e2e-TX-invInventoryImport-uploadModal-importSubtitle">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.importTitle') }}
|
||||
</h3>
|
||||
<DragAndDropUpload
|
||||
|
@ -64,6 +75,7 @@
|
|||
@file:error:clear="this.error = null"
|
||||
:supportingText="`${i18n.t('repositories.import_records.steps.step1.dragAndDropSupportingText')}`"
|
||||
:supportedFormats="['xlsx', 'csv', 'xls', 'txt', 'tsv']"
|
||||
:dataE2e="'invInventoryImport-uploadModal-dragDrop'"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
@ -75,7 +87,7 @@
|
|||
<i class="sn-icon sn-icon-check"></i>
|
||||
<div class="my-auto">{{ exportInventoryMessage }}</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" @click="close" aria-label="Close">
|
||||
<button class="btn btn-secondary" @click="close" aria-label="Close" data-e2e="e2e-BT-invInventoryImport-uploadModal-cancel">
|
||||
{{ i18n.t('repositories.import_records.steps.step1.cancelBtnText') }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -312,6 +312,11 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="!repository?.is_snapshot" id="divider" class="bg-sn-light-grey flex px-8 items-center self-stretch h-px "></div>
|
||||
<!-- Locations -->
|
||||
<section v-if="!repository?.is_snapshot" id="locations-section" ref="locationsSectionRef">
|
||||
<Locations :repositoryRow="repositoryRow" :repository="repository" />
|
||||
</section>
|
||||
<div v-if="!repository?.is_snapshot" id="divider" class="bg-sn-light-grey flex px-8 items-center self-stretch h-px "></div>
|
||||
|
||||
<!-- QR -->
|
||||
|
@ -367,6 +372,7 @@ import ScrollSpy from './repository_values/ScrollSpy.vue';
|
|||
import CustomColumns from './customColumns.vue';
|
||||
import RepositoryItemSidebarTitle from './Title.vue';
|
||||
import UnlinkModal from './unlink_modal.vue';
|
||||
import Locations from './locations.vue';
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
|
||||
const items = [
|
||||
|
@ -405,6 +411,14 @@ const items = [
|
|||
{
|
||||
id: 'highlight-item-5',
|
||||
textId: 'text-item-5',
|
||||
labelAlias: 'locations_label',
|
||||
label: 'locations-label',
|
||||
sectionId: 'locations-section',
|
||||
showInSnapshot: false
|
||||
},
|
||||
{
|
||||
id: 'highlight-item-6',
|
||||
textId: 'text-item-6',
|
||||
labelAlias: 'QR_label',
|
||||
label: 'QR-label',
|
||||
sectionId: 'qr-section',
|
||||
|
@ -416,6 +430,7 @@ export default {
|
|||
name: 'RepositoryItemSidebar',
|
||||
components: {
|
||||
CustomColumns,
|
||||
Locations,
|
||||
'repository-item-sidebar-title': RepositoryItemSidebarTitle,
|
||||
'inline-edit': InlineEdit,
|
||||
'scroll-spy': ScrollSpy,
|
||||
|
@ -433,6 +448,7 @@ export default {
|
|||
repository: null,
|
||||
defaultColumns: null,
|
||||
customColumns: null,
|
||||
repositoryRow: null,
|
||||
parentsCount: 0,
|
||||
childrenCount: 0,
|
||||
parents: null,
|
||||
|
@ -591,6 +607,7 @@ export default {
|
|||
{ params: { my_module_id: this.myModuleId } }
|
||||
).then((response) => {
|
||||
const result = response.data;
|
||||
this.repositoryRow = result;
|
||||
this.repositoryRowId = result.id;
|
||||
this.repository = result.repository;
|
||||
this.optionsPath = result.options_path;
|
||||
|
|
55
app/javascript/vue/repository_item_sidebar/locations.vue
Normal file
55
app/javascript/vue/repository_item_sidebar/locations.vue
Normal file
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<div v-if="repositoryRow">
|
||||
<div class="flex items-center gap-4">
|
||||
<h4>{{ i18n.t('repositories.locations.title', { count: repositoryRow.storage_locations.locations.length }) }}</h4>
|
||||
<button v-if="repositoryRow.permissions.can_manage && repositoryRow.storage_locations.enabled" class="btn btn-light">
|
||||
{{ i18n.t('repositories.locations.assign') }}
|
||||
</button>
|
||||
</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 items-center gap-2 mb-3">
|
||||
{{ i18n.t('repositories.locations.container') }}:
|
||||
<a :href="containerUrl(location.id)">{{ location.name }}</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<div v-for="(position) in location.positions" :key="position.id">
|
||||
<div v-if="position.metadata.position" class="flex items-center font-sm gap-1 uppercase bg-sn-grey-300 rounded pl-1.5 pr-2">
|
||||
{{ formatPosition(position.metadata.position) }}
|
||||
<i v-if="repositoryRow.permissions.can_manage" class="sn-icon sn-icon-unlink-italic-s cursor-pointer"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
storage_location_path
|
||||
} from '../../routes.js';
|
||||
|
||||
export default {
|
||||
name: 'RepositoryItemLocations',
|
||||
props: {
|
||||
repositoryRow: Object,
|
||||
repository: Object
|
||||
},
|
||||
methods: {
|
||||
containerUrl(id) {
|
||||
return storage_location_path(id);
|
||||
},
|
||||
formatPosition(position) {
|
||||
if (position) {
|
||||
return `${this.numberToLetter(position[0])}${position[1]}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
numberToLetter(number) {
|
||||
return String.fromCharCode(97 + number);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -131,6 +131,16 @@
|
|||
@attachments:viewMode="changeAttachmentsViewMode"
|
||||
@attachment:viewMode="updateAttachmentViewMode"/>
|
||||
</div>
|
||||
<ContentToolbar
|
||||
v-if="orderedElements.length > 2"
|
||||
:insertMenu="insertMenu"
|
||||
@create:table="(...args) => this.createElement('table', ...args)"
|
||||
@create:text="createElement('text')"
|
||||
@create:file="openLoadFromComputer"
|
||||
@create:wopi_file="openWopiFileModal"
|
||||
@create:ove_file="openOVEditor"
|
||||
@create:marvinjs_file="openMarvinJsModal($refs.marvinJsButton)"
|
||||
></ContentToolbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -144,6 +154,7 @@ import Attachments from '../shared/content/attachments.vue';
|
|||
import InlineEdit from '../shared/inline_edit.vue';
|
||||
import MenuDropdown from '../shared/menu_dropdown.vue';
|
||||
import deleteResultModal from './delete_result.vue';
|
||||
import ContentToolbar from '../shared/content/content_toolbar';
|
||||
|
||||
import AttachmentsMixin from '../shared/content/mixins/attachments.js';
|
||||
import WopiFileModal from '../shared/content/attachments/mixins/wopi_file_modal.js';
|
||||
|
@ -158,6 +169,9 @@ export default {
|
|||
resultToReload: { type: Number, required: false },
|
||||
activeDragResult: {
|
||||
required: false
|
||||
},
|
||||
userSettingsUrl: {
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -192,7 +206,8 @@ export default {
|
|||
InlineEdit,
|
||||
MenuDropdown,
|
||||
deleteResultModal,
|
||||
StorageUsage
|
||||
StorageUsage,
|
||||
ContentToolbar
|
||||
},
|
||||
watch: {
|
||||
resultToReload() {
|
||||
|
@ -215,6 +230,17 @@ export default {
|
|||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const resultId = `#resultBody${this.result.id}`;
|
||||
this.isCollapsed = this.result.attributes.collapsed;
|
||||
if (this.isCollapsed) {
|
||||
$(resultId).collapse('hide');
|
||||
} else {
|
||||
$(resultId).collapse('show');
|
||||
}
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
reorderableElements() {
|
||||
return this.orderedElements.map((e) => ({ id: e.id, attributes: e.attributes.orderable }));
|
||||
|
@ -261,16 +287,20 @@ export default {
|
|||
if (this.urls.update_url) {
|
||||
menu = menu.concat([{
|
||||
text: this.i18n.t('my_modules.results.insert.text'),
|
||||
icon: 'sn-icon sn-icon-result-text',
|
||||
emit: 'create:text'
|
||||
}, {
|
||||
text: this.i18n.t('my_modules.results.insert.attachment'),
|
||||
submenu: this.filesMenu,
|
||||
icon: 'sn-icon sn-icon-file',
|
||||
position: 'left'
|
||||
}, {
|
||||
text: this.i18n.t('my_modules.results.insert.table'),
|
||||
icon: 'sn-icon sn-icon-tables',
|
||||
emit: 'create:table'
|
||||
}, {
|
||||
text: this.i18n.t('my_modules.results.insert.well_plate'),
|
||||
icon: 'sn-icon sn-icon-tables',
|
||||
submenu: this.wellPlateOptions,
|
||||
position: 'left'
|
||||
}]);
|
||||
|
@ -321,6 +351,13 @@ export default {
|
|||
toggleCollapsed() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
this.result.attributes.collapsed = this.isCollapsed;
|
||||
|
||||
const settings = {
|
||||
key: 'result_states',
|
||||
data: { [this.result.id]: this.isCollapsed }
|
||||
};
|
||||
|
||||
axios.put(this.userSettingsUrl, { settings: [settings] });
|
||||
},
|
||||
dragEnter(e) {
|
||||
if (!this.urls.upload_attachment_url) return;
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
:result="result"
|
||||
:resultToReload="resultToReload"
|
||||
:activeDragResult="activeDragResult"
|
||||
:userSettingsUrl="userSettingsUrl"
|
||||
@result:elements:loaded="resultToReload = null"
|
||||
@result:move_element="reloadResult"
|
||||
@result:attachments:loaded="resultToReload = null"
|
||||
|
@ -64,7 +65,8 @@ export default {
|
|||
canCreate: { type: String, required: true },
|
||||
archived: { type: String, required: true },
|
||||
active_url: { type: String, required: true },
|
||||
archived_url: { type: String, required: true }
|
||||
archived_url: { type: String, required: true },
|
||||
userSettingsUrl: { type: String, required: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -74,10 +76,12 @@ export default {
|
|||
resultToReload: null,
|
||||
nextPageUrl: null,
|
||||
loadingPage: false,
|
||||
activeDragResult: null
|
||||
activeDragResult: null,
|
||||
userSettingsUrl: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.userSettingsUrl = document.querySelector('meta[name="user-settings-url"]').getAttribute('content');
|
||||
window.addEventListener('scroll', this.loadResults, false);
|
||||
window.addEventListener('scroll', this.initStackableHeaders, false);
|
||||
this.nextPageUrl = this.url;
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
<template>
|
||||
<div>
|
||||
<button class="ml-2 btn"
|
||||
id="share-button"
|
||||
type="button"
|
||||
data-e2e="e2e-BT-tasks-shareTask"
|
||||
:class="shareClass"
|
||||
:title="shareValue"
|
||||
@click="openModal">
|
||||
<span class="sn-icon sn-icon-shared"></span>
|
||||
<span class="text-sm">
|
||||
{{ shareValue }}
|
||||
</span>
|
||||
</button>
|
||||
<div :title="shareTitle">
|
||||
<button class="btn"
|
||||
id="share-button"
|
||||
type="button"
|
||||
data-e2e="e2e-BT-tasks-shareTask"
|
||||
:class="[shareClass, {'disabled': !enabled}]"
|
||||
@click="openModal">
|
||||
<span class="sn-icon sn-icon-shared"></span>
|
||||
<span class="text-sm">
|
||||
{{ shareValue }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div ref="modal">
|
||||
<shareModalContainer :shared="share"
|
||||
:open="visibleShareModal"
|
||||
|
@ -43,6 +44,10 @@ export default {
|
|||
canShare: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -60,6 +65,9 @@ export default {
|
|||
},
|
||||
shareValue() {
|
||||
return this.i18n.t(this.share ? 'my_modules.shareable_links.shared' : 'my_modules.shareable_links.share');
|
||||
},
|
||||
shareTitle() {
|
||||
return this.enabled ? this.shareValue : this.i18n.t('my_modules.shareable_links.disabled');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<span @mouseenter="fetchLocalAppInfo()">
|
||||
<span>
|
||||
<!-- multiple options -->
|
||||
<MenuDropdown
|
||||
v-if="multipleOpenOptions.length > 1"
|
||||
|
@ -124,6 +124,9 @@ export default {
|
|||
required: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchLocalAppInfo();
|
||||
},
|
||||
computed: {
|
||||
multipleOpenOptions() {
|
||||
const options = [];
|
||||
|
|
43
app/javascript/vue/shared/content/content_toolbar.vue
Normal file
43
app/javascript/vue/shared/content/content_toolbar.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div class="px-4 py-2 bg-sn-super-light-blue flex gap-4 mt-10 mb-4 rounded">
|
||||
<span class="font-bold shrink-0 leading-10">
|
||||
{{ i18n.t('protocols.steps.insert.button') }}:
|
||||
</span>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<template v-for="item in insertMenu">
|
||||
<button v-if="!item.submenu" @click="$emit(item.emit)" class="btn btn-light">
|
||||
<i :class="item.icon"></i>
|
||||
{{ item.text }}
|
||||
</button>
|
||||
<MenuDropdown
|
||||
:listItems="item.submenu"
|
||||
:btnText="item.text"
|
||||
:btnClasses="'btn btn-light'"
|
||||
:position="'right'"
|
||||
:caret="true"
|
||||
:btnIcon="item.icon"
|
||||
@dtEvent="handleEvents"
|
||||
></MenuDropdown>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import MenuDropdown from '../menu_dropdown.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuDropdown
|
||||
},
|
||||
name: 'stepToolbar',
|
||||
props: {
|
||||
insertMenu: Array
|
||||
},
|
||||
methods: {
|
||||
handleEvents(event, option) {
|
||||
this.$emit(event, option.params);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
|
@ -15,7 +15,7 @@
|
|||
:allowBlank="true"
|
||||
:autofocus="editingName"
|
||||
:attributeName="`${i18n.t('Text')} ${i18n.t('name')}`"
|
||||
:dataE2e="`${dataE2e}-stepText${element.id}`"
|
||||
:dataE2e="`${dataE2e}-stepText${element.id}-title`"
|
||||
@editingEnabled="enableNameEdit"
|
||||
@editingDisabled="disableNameEdit"
|
||||
@update="updateName"
|
||||
|
@ -36,7 +36,7 @@
|
|||
</div>
|
||||
<div class="flex rounded min-h-[2.25rem] mb-4 relative group/text_container content__text-body"
|
||||
:class="{ 'edit': inEditMode, 'component__element--locked': !element.attributes.orderable.urls.update_url }"
|
||||
:data-e2e="`e2e-IF-${dataE2e}-stepText${element.id}`"
|
||||
:data-e2e="`e2e-IF-${dataE2e}-stepText${element.id}-content`"
|
||||
@keyup.enter="enableEditMode($event)"
|
||||
tabindex="0">
|
||||
<Tinymce
|
||||
|
|
|
@ -364,7 +364,7 @@ export default {
|
|||
}
|
||||
},
|
||||
handleScroll() {
|
||||
if (this.scrollMode === 'pages') return;
|
||||
if (this.scrollMode === 'pages' || this.scrollMode === 'none') return;
|
||||
|
||||
let target = null;
|
||||
if (this.currentViewRender === 'cards') {
|
||||
|
@ -506,15 +506,18 @@ export default {
|
|||
this.rowData = [];
|
||||
}
|
||||
|
||||
if (this.scrollMode === 'pages') {
|
||||
if (this.scrollMode === 'pages' || this.scrollMode === 'none') {
|
||||
if (this.gridApi) this.gridApi.setRowData(this.formatData(response.data.data));
|
||||
this.rowData = this.formatData(response.data.data);
|
||||
} else {
|
||||
this.handleInfiniteScroll(response);
|
||||
}
|
||||
this.totalPage = response.data.meta.total_pages;
|
||||
this.totalEntries = response.data.meta.total_count;
|
||||
this.$emit('tableReloaded');
|
||||
|
||||
if (this.scrollMode !== 'none') {
|
||||
this.totalPage = response.data.meta.total_pages;
|
||||
this.totalEntries = response.data.meta.total_count;
|
||||
}
|
||||
this.$emit('tableReloaded', this.rowData);
|
||||
this.dataLoading = false;
|
||||
this.restoreSelection();
|
||||
|
||||
|
@ -577,8 +580,11 @@ export default {
|
|||
this.gridApi.forEachNode((node) => {
|
||||
if (this.selectedRows.find((row) => row.id === node.data.id)) {
|
||||
node.setSelected(true);
|
||||
} else {
|
||||
node.setSelected(false);
|
||||
}
|
||||
});
|
||||
this.$emit('selectionChanged', this.selectedRows);
|
||||
}
|
||||
},
|
||||
setSelectedRows(e) {
|
||||
|
@ -591,6 +597,7 @@ export default {
|
|||
} else {
|
||||
this.selectedRows = this.selectedRows.filter((row) => row.id !== e.data.id);
|
||||
}
|
||||
this.$emit('selectionChanged', this.selectedRows);
|
||||
},
|
||||
emitAction(action) {
|
||||
this.$emit(action.name, action, this.selectedRows);
|
||||
|
@ -602,6 +609,7 @@ export default {
|
|||
clickCell(e) {
|
||||
if (e.column.colId !== 'rowMenu' && e.column.userProvidedColDef.notSelectable !== true) {
|
||||
e.node.setSelected(true);
|
||||
this.$emit('selectionChanged', this.selectedRows);
|
||||
}
|
||||
},
|
||||
applyFilters(filters) {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
@dragover.prevent
|
||||
@click="handleImportClick"
|
||||
class="flex h-full w-full p-6 rounded border border-sn-light-grey bg-sn-super-light-blue cursor-pointer"
|
||||
:data-e2e="`e2e-CO-${dataE2e}`"
|
||||
>
|
||||
<div id="centered-content" class="flex flex-col gap-4 items-center h-fit w-fit m-auto">
|
||||
<!-- icon -->
|
||||
|
@ -19,7 +20,7 @@
|
|||
{{ i18n.t('repositories.import_records.dragAndDropUpload.importText.firstPart') }}
|
||||
</span> {{ i18n.t('repositories.import_records.dragAndDropUpload.importText.secondPart') }}
|
||||
</div>
|
||||
<div class="text-sn-grey">
|
||||
<div class="text-sn-grey text-center">
|
||||
{{ supportingText }}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -43,6 +44,10 @@ export default {
|
|||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
dataE2e: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['file:dropped', 'file:error'],
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
<div class="buttons">
|
||||
<template v-if="isWindows">
|
||||
<a :href="getWindowsHref"
|
||||
class="btn btn-primary new-project-btn"
|
||||
class="w-full btn btn-primary new-project-btn"
|
||||
:title="i18n.t('users.settings.account.addons.desktop_app.windows_button')"
|
||||
role="button"
|
||||
target="_blank">
|
||||
<span class="hidden-xs">{{ i18n.t('users.settings.account.addons.desktop_app.windows_button') }}</span>
|
||||
<span :class="{'hidden-xs' : !isCompact }">
|
||||
{{ this.isCompact ? i18n.t('general.download') : i18n.t('users.settings.account.addons.desktop_app.windows_button') }}
|
||||
</span>
|
||||
</a>
|
||||
<div v-if="showButtonLabel" class="text-xs pt-2 pb-6" style="color: var(--sn-sleepy-grey);">
|
||||
{{ i18n.t('users.settings.account.addons.desktop_app.version', { version: this.responseData[0]['version']}) }}
|
||||
|
@ -15,18 +17,20 @@
|
|||
|
||||
<template v-else-if="isMac">
|
||||
<a :href="getMacHref"
|
||||
class="btn btn-primary new-project-btn"
|
||||
class="w-full btn btn-primary new-project-btn"
|
||||
:title="i18n.t('users.settings.account.addons.desktop_app.macos_button')"
|
||||
role="button"
|
||||
target="_blank">
|
||||
<span class="hidden-xs">{{ i18n.t('users.settings.account.addons.desktop_app.macos_button') }}</span>
|
||||
<span :class="{'hidden-xs' : !isCompact }">
|
||||
{{ this.isCompact ? i18n.t('general.download') : i18n.t('users.settings.account.addons.desktop_app.macos_button') }}
|
||||
</span>
|
||||
</a>
|
||||
<div v-if="showButtonLabel" class="text-xs pt-2 pb-6" style="color: var(--sn-sleepy-grey);">
|
||||
{{ i18n.t('users.settings.account.addons.desktop_app.version', { version: this.responseData[1]['version']}) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<template v-else-if="!isCompact">
|
||||
<div class="flex">
|
||||
<div>
|
||||
<a :href="getWindowsHref"
|
||||
|
@ -60,7 +64,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<a v-if="!isUpdateVersionModal" :href="'https://knowledgebase.scinote.net/en/knowledge/how-to-use-scinote-edit'"
|
||||
<a v-if="!isUpdateVersionModal && !isCompact" :href="'https://knowledgebase.scinote.net/en/knowledge/how-to-use-scinote-edit'"
|
||||
:title="i18n.t('users.settings.account.addons.more_info')"
|
||||
class="text-sn-blue"
|
||||
target="_blank">
|
||||
|
@ -75,7 +79,8 @@ export default {
|
|||
name: 'ScinoteEditDownload',
|
||||
props: {
|
||||
data: { type: String, required: true },
|
||||
isUpdateVersionModal: { type: Boolean, required: false }
|
||||
isUpdateVersionModal: { type: Boolean, required: false },
|
||||
isCompact: { type: Boolean, required: false, default: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -91,7 +96,7 @@ export default {
|
|||
return /Mac OS/.test(this.userAgent);
|
||||
},
|
||||
showButtonLabel() {
|
||||
return this.responseData && this.responseData.length > 0 && !this.isUpdateVersionModal;
|
||||
return this.responseData && this.responseData.length > 0 && !this.isUpdateVersionModal && !this.isCompact;
|
||||
},
|
||||
getWindowsHref() {
|
||||
return this.responseData && this.responseData.length > 0 ? this.responseData[0].url : '#';
|
||||
|
|
222
app/javascript/vue/storage_locations/container.vue
Normal file
222
app/javascript/vue/storage_locations/container.vue
Normal file
|
@ -0,0 +1,222 @@
|
|||
<template>
|
||||
<div class="grid w-full h-full gap-6" :class="{ 'grid-cols-2': withGrid }">
|
||||
<div v-if="withGrid">
|
||||
<div class="py-4">
|
||||
<div class="h-11">
|
||||
<button class="btn btn-primary" @click="assignRow">
|
||||
<i class="sn-icon sn-icon-new-task"></i>
|
||||
{{ i18n.t('storage_locations.show.toolbar.assign') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Grid
|
||||
:gridSize="gridSize"
|
||||
:assignedItems="assignedItems"
|
||||
:selectedItems="selectedItems"
|
||||
@assign="assignRowToPosition"
|
||||
@select="selectRow"
|
||||
/>
|
||||
</div>
|
||||
<div class="h-full bg-white p-4">
|
||||
<DataTable :columnDefs="columnDefs"
|
||||
tableId="StorageLocationsContainer"
|
||||
:dataUrl="dataSource"
|
||||
ref="table"
|
||||
:reloadingTable="reloadingTable"
|
||||
:toolbarActions="toolbarActions"
|
||||
:actionsUrl="actionsUrl"
|
||||
:scrollMode="paginationMode"
|
||||
@assign="assignRow"
|
||||
@move="moveRow"
|
||||
@unassign="unassignRows"
|
||||
@tableReloaded="handleTableReload"
|
||||
@selectionChanged="selectedItems = $event"
|
||||
/>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<AssignModal
|
||||
v-if="openAssignModal"
|
||||
:assignMode="assignMode"
|
||||
:selectedContainer="assignToContainer"
|
||||
:selectedPosition="assignToPosition"
|
||||
:selectedRow="rowIdToMove"
|
||||
:cellId="cellIdToUnassign"
|
||||
:withGrid="withGrid"
|
||||
@close="openAssignModal = false; this.reloadingTable = true"
|
||||
></AssignModal>
|
||||
<ConfirmationModal
|
||||
:title="i18n.t('storage_locations.show.unassign_modal.title')"
|
||||
:description="storageLocationUnassignDescription"
|
||||
confirmClass="btn btn-danger"
|
||||
:confirmText="i18n.t('storage_locations.show.unassign_modal.button')"
|
||||
ref="unassignStorageLocationModal"
|
||||
></ConfirmationModal>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global HelperModule */
|
||||
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
import DataTable from '../shared/datatable/table.vue';
|
||||
import Grid from './grid.vue';
|
||||
import AssignModal from './modals/assign.vue';
|
||||
import ConfirmationModal from '../shared/confirmation_modal.vue';
|
||||
|
||||
export default {
|
||||
name: 'StorageLocationsContainer',
|
||||
components: {
|
||||
DataTable,
|
||||
Grid,
|
||||
AssignModal,
|
||||
ConfirmationModal
|
||||
},
|
||||
props: {
|
||||
dataSource: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
actionsUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
withGrid: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
containerId: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
gridSize: Array
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reloadingTable: false,
|
||||
openEditModal: false,
|
||||
editModalMode: null,
|
||||
editStorageLocation: null,
|
||||
objectToMove: null,
|
||||
moveToUrl: null,
|
||||
assignedItems: [],
|
||||
selectedItems: [],
|
||||
openAssignModal: false,
|
||||
assignToPosition: null,
|
||||
assignToContainer: null,
|
||||
rowIdToMove: null,
|
||||
cellIdToUnassign: null,
|
||||
assignMode: 'assign',
|
||||
storageLocationUnassignDescription: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
paginationMode() {
|
||||
return this.withGrid ? 'none' : 'pages';
|
||||
},
|
||||
|
||||
columnDefs() {
|
||||
const columns = [{
|
||||
field: 'position_formatted',
|
||||
headerName: this.i18n.t('storage_locations.show.table.position'),
|
||||
sortable: true,
|
||||
notSelectable: true,
|
||||
cellRenderer: this.nameRenderer
|
||||
},
|
||||
{
|
||||
field: 'reminders',
|
||||
headerName: this.i18n.t('storage_locations.show.table.reminders'),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'row_id',
|
||||
headerName: this.i18n.t('storage_locations.show.table.row_id'),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'row_name',
|
||||
headerName: this.i18n.t('storage_locations.show.table.row_name'),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
field: 'stock',
|
||||
headerName: this.i18n.t('storage_locations.show.table.stock'),
|
||||
sortable: true
|
||||
}];
|
||||
|
||||
return columns;
|
||||
},
|
||||
toolbarActions() {
|
||||
const left = [];
|
||||
|
||||
if (!this.withGrid) {
|
||||
left.push({
|
||||
name: 'assign',
|
||||
icon: 'sn-icon sn-icon-new-task',
|
||||
label: this.i18n.t('storage_locations.show.toolbar.assign'),
|
||||
type: 'emit',
|
||||
buttonStyle: 'btn btn-primary'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
left,
|
||||
right: []
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTableReload(items) {
|
||||
this.reloadingTable = false;
|
||||
this.assignedItems = items;
|
||||
},
|
||||
selectRow(row) {
|
||||
if (this.$refs.table.selectedRows.includes(row)) {
|
||||
this.$refs.table.selectedRows = this.$refs.table.selectedRows.filter((r) => r !== row);
|
||||
} else {
|
||||
this.$refs.table.selectedRows.push(row);
|
||||
}
|
||||
this.$refs.table.restoreSelection();
|
||||
},
|
||||
assignRow() {
|
||||
this.openAssignModal = true;
|
||||
this.rowIdToMove = null;
|
||||
this.assignToContainer = this.containerId;
|
||||
this.assignToPosition = null;
|
||||
this.cellIdToUnassign = null;
|
||||
this.assignMode = 'assign';
|
||||
},
|
||||
assignRowToPosition(position) {
|
||||
this.openAssignModal = true;
|
||||
this.rowIdToMove = null;
|
||||
this.assignToContainer = this.containerId;
|
||||
this.assignToPosition = position;
|
||||
this.cellIdToUnassign = null;
|
||||
this.assignMode = 'assign';
|
||||
},
|
||||
moveRow(_event, data) {
|
||||
this.openAssignModal = true;
|
||||
this.rowIdToMove = data[0].row_id;
|
||||
this.assignToContainer = null;
|
||||
this.assignToPosition = null;
|
||||
this.cellIdToUnassign = data[0].id;
|
||||
this.assignMode = 'move';
|
||||
},
|
||||
async unassignRows(event, rows) {
|
||||
this.storageLocationUnassignDescription = this.i18n.t(
|
||||
'storage_locations.show.unassign_modal.description',
|
||||
{ items: rows.length }
|
||||
);
|
||||
const ok = await this.$refs.unassignStorageLocationModal.show();
|
||||
if (ok) {
|
||||
axios.post(event.path).then(() => {
|
||||
this.reloadingTable = true;
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
139
app/javascript/vue/storage_locations/grid.vue
Normal file
139
app/javascript/vue/storage_locations/grid.vue
Normal file
|
@ -0,0 +1,139 @@
|
|||
<template>
|
||||
<div class="grid grid-cols-[1.5rem_auto] grid-rows-[1.5rem_auto] overflow-hidden">
|
||||
<div class="z-10 bg-sn-super-light-grey"></div>
|
||||
<div ref="columnsContainer" class="overflow-x-hidden overflow-y-scroll">
|
||||
<div :style="{'width': `${columnsList.length * 54}px`}">
|
||||
<div v-for="column in columnsList" :key="column" class="uppercase float-left flex items-center justify-center w-[54px] ">
|
||||
<span>{{ column }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="rowContainer" class="overflow-y-hidden overflow-x-scroll max-h-[70vh]">
|
||||
<div v-for="row in rowsList" :key="row" class="uppercase flex items-center justify-center h-[54px]">
|
||||
<span>{{ row }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="cellsContainer" class="overflow-scroll max-h-[70vh]">
|
||||
<div class="grid" :style="{
|
||||
'grid-template-columns': `repeat(${columnsList.length}, 1fr)`,
|
||||
'width': `${columnsList.length * 54}px`
|
||||
}">
|
||||
<div v-for="cell in cellsList" :key="cell.row + cell.column" class="cell">
|
||||
<div class="w-[54px] h-[54px] uppercase items-center flex justify-center p-1
|
||||
border border-solid !border-transparent !border-b-sn-grey !border-r-sn-grey"
|
||||
:class="{ '!border-t-sn-grey': cell.row === 0, '!border-l-sn-grey': cell.column === 0 }"
|
||||
>
|
||||
<div
|
||||
class="h-full w-full rounded-full items-center flex justify-center"
|
||||
@click="assignRow(cell)"
|
||||
:class="{
|
||||
'bg-sn-background-green': cellIsOccupied(cell),
|
||||
'bg-sn-grey-100': cellIsHidden(cell),
|
||||
'bg-white': cellIsAvailable(cell),
|
||||
'bg-white border-sn-science-blue border-solid border-[1px]': cellIsSelected(cell),
|
||||
'cursor-pointer': !cellIsHidden(cell)
|
||||
}"
|
||||
>
|
||||
<template v-if="cellIsHidden(cell)">
|
||||
<i class="sn-icon sn-icon-locked-task"></i>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ rowsList[cell.row] }}{{ columnsList[cell.column] }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StorageLocationsGrid',
|
||||
props: {
|
||||
gridSize: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
assignedItems: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedItems: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.cellsContainer.addEventListener('scroll', this.handleScroll);
|
||||
window.addEventListener('resize', this.handleScroll);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.$refs.cellsContainer.removeEventListener('scroll', this.handleScroll);
|
||||
window.removeEventListener('resize', this.handleScroll);
|
||||
},
|
||||
computed: {
|
||||
columnsList() {
|
||||
return Array.from({ length: this.gridSize[1] }, (v, i) => i + 1);
|
||||
},
|
||||
rowsList() {
|
||||
return Array.from({ length: this.gridSize[0] }, (v, i) => String.fromCharCode(97 + i));
|
||||
},
|
||||
cellsList() {
|
||||
const cells = [];
|
||||
for (let i = 0; i < this.gridSize[0]; i++) {
|
||||
for (let j = 0; j < this.gridSize[1]; j++) {
|
||||
cells.push({ row: i, column: j });
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cellObject(cell) {
|
||||
return this.assignedItems.find((item) => item.position[0] === cell.row + 1 && item.position[1] === cell.column + 1);
|
||||
},
|
||||
cellIsOccupied(cell) {
|
||||
return this.cellObject(cell) && !this.cellObject(cell)?.hidden;
|
||||
},
|
||||
cellIsHidden(cell) {
|
||||
return this.cellObject(cell)?.hidden;
|
||||
},
|
||||
cellIsSelected(cell) {
|
||||
return this.selectedItems.some((item) => item.position[0] === cell.row + 1 && item.position[1] === cell.column + 1);
|
||||
},
|
||||
cellIsAvailable(cell) {
|
||||
return !this.cellIsOccupied(cell) && !this.cellIsHidden(cell);
|
||||
},
|
||||
assignRow(cell) {
|
||||
if (this.cellIsOccupied(cell)) {
|
||||
this.$emit('select', this.cellObject(cell));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.cellIsHidden(cell)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('assign', [cell.row + 1, cell.column + 1]);
|
||||
},
|
||||
handleScroll() {
|
||||
this.$refs.columnsContainer.scrollLeft = this.$refs.cellsContainer.scrollLeft;
|
||||
this.$refs.rowContainer.scrollTop = this.$refs.cellsContainer.scrollTop;
|
||||
|
||||
if (this.$refs.rowContainer.scrollTop > 0) {
|
||||
this.$refs.columnsContainer.style.boxShadow = '0px 0px 20px 0px rgba(16, 24, 40, 0.20)';
|
||||
} else {
|
||||
this.$refs.columnsContainer.style.boxShadow = 'none';
|
||||
}
|
||||
|
||||
if (this.$refs.columnsContainer.scrollLeft > 0) {
|
||||
this.$refs.rowContainer.style.boxShadow = '0px 0px 20px 0px rgba(16, 24, 40, 0.20)';
|
||||
} else {
|
||||
this.$refs.rowContainer.style.boxShadow = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
99
app/javascript/vue/storage_locations/modals/assign.vue
Normal file
99
app/javascript/vue/storage_locations/modals/assign.vue
Normal file
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form @submit.prevent="submit">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block">
|
||||
{{ i18n.t(`storage_locations.show.assign_modal.${assignMode}_title`) }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p 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>
|
||||
<ContainerSelector v-if="!selectedContainer" @change="this.containerId = $event"></ContainerSelector>
|
||||
<PositionSelector
|
||||
v-if="containerId && !selectedPosition && withGrid"
|
||||
:key="containerId"
|
||||
:selectedContainerId="containerId"
|
||||
@change="this.position = $event"></PositionSelector>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{{ i18n.t(`storage_locations.show.assign_modal.${assignMode}_action`) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global HelperModule */
|
||||
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import modalMixin from '../../shared/modal_mixin';
|
||||
import RowSelector from './assign/row_selector.vue';
|
||||
import ContainerSelector from './assign/container_selector.vue';
|
||||
import PositionSelector from './assign/position_selector.vue';
|
||||
import {
|
||||
storage_location_storage_location_repository_rows_path,
|
||||
move_storage_location_storage_location_repository_row_path,
|
||||
|
||||
} from '../../../routes.js';
|
||||
|
||||
export default {
|
||||
name: 'NewProjectModal',
|
||||
props: {
|
||||
selectedRow: Number,
|
||||
selectedContainer: Number,
|
||||
cellId: Number,
|
||||
selectedPosition: Array,
|
||||
withGrid: Boolean,
|
||||
assignMode: String
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
computed: {
|
||||
createUrl() {
|
||||
return storage_location_storage_location_repository_rows_path({
|
||||
storage_location_id: this.containerId
|
||||
});
|
||||
},
|
||||
moveUrl() {
|
||||
return move_storage_location_storage_location_repository_row_path(this.containerId, this.cellId);
|
||||
},
|
||||
actionUrl() {
|
||||
return this.assignMode === 'assign' ? this.createUrl : this.moveUrl;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rowId: this.selectedRow,
|
||||
containerId: this.selectedContainer,
|
||||
position: this.selectedPosition
|
||||
};
|
||||
},
|
||||
components: {
|
||||
RowSelector,
|
||||
ContainerSelector,
|
||||
PositionSelector
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
axios.post(this.actionUrl, {
|
||||
repository_row_id: this.rowId,
|
||||
metadata: { position: this.position?.map((pos) => parseInt(pos, 10)) }
|
||||
}).then(() => {
|
||||
this.$emit('close');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,43 @@
|
|||
<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>
|
||||
</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" :value="selectedStorageLocationId" @selectStorageLocation="selectStorageLocation" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MoveTreeMixin from '../move_tree_mixin';
|
||||
|
||||
export default {
|
||||
name: 'ContainerSelector',
|
||||
mixins: [MoveTreeMixin],
|
||||
data() {
|
||||
return {
|
||||
container: true
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
selectedStorageLocationId() {
|
||||
this.$emit('change', this.selectedStorageLocationId);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<div class="">
|
||||
<div class="sci-label">{{ i18n.t(`storage_locations.show.assign_modal.row`) }}</div>
|
||||
<SelectDropdown
|
||||
:options="availableRows"
|
||||
:value="selectedRow"
|
||||
@change="selectedRow = $event"
|
||||
></SelectDropdown>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sci-label">{{ i18n.t(`storage_locations.show.assign_modal.column`) }}</div>
|
||||
<SelectDropdown
|
||||
:disabled="!selectedRow"
|
||||
:options="availableColumns"
|
||||
:value="selectedColumn"
|
||||
@change="selectedColumn= $event"
|
||||
></SelectDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SelectDropdown from '../../../shared/select_dropdown.vue';
|
||||
import axios from '../../../../packs/custom_axios.js';
|
||||
import {
|
||||
available_positions_storage_location_path,
|
||||
} from '../../../../routes.js';
|
||||
|
||||
export default {
|
||||
name: 'PositionSelector',
|
||||
components: {
|
||||
SelectDropdown
|
||||
},
|
||||
props: {
|
||||
selectedContainerId: Number
|
||||
},
|
||||
created() {
|
||||
axios.get(this.positionsUrl)
|
||||
.then((response) => {
|
||||
this.availablePositions = response.data.positions;
|
||||
this.$nextTick(() => {
|
||||
[[this.selectedRow]] = this.availableRows;
|
||||
this.$nextTick(() => {
|
||||
[[this.selectedColumn]] = this.availableColumns;
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
selectedRow() {
|
||||
[[this.selectedColumn]] = this.availableColumns;
|
||||
},
|
||||
selectedColumn() {
|
||||
this.$emit('change', [this.selectedRow, this.selectedColumn]);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
positionsUrl() {
|
||||
return available_positions_storage_location_path(this.selectedContainerId);
|
||||
},
|
||||
availableRows() {
|
||||
return Object.keys(this.availablePositions).map((row) => [row, row]);
|
||||
},
|
||||
availableColumns() {
|
||||
return (this.availablePositions[this.selectedRow] || []).map((col) => [col, col]);
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
availablePositions: {},
|
||||
selectedRow: null,
|
||||
selectedColumn: null
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<div class="sci-label">{{ i18n.t(`storage_locations.show.assign_modal.inventory`) }}</div>
|
||||
<SelectDropdown
|
||||
:optionsUrl="repositoriesUrl"
|
||||
placeholder="Select inventory"
|
||||
:searchable="true"
|
||||
@change="selectedRepository = $event"
|
||||
></SelectDropdown>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sci-label">{{ i18n.t(`storage_locations.show.assign_modal.item`) }}</div>
|
||||
<SelectDropdown
|
||||
:disabled="!selectedRepository"
|
||||
:optionsUrl="rowsUrl"
|
||||
:urlParams="{ repository_id: selectedRepository }"
|
||||
placeholder="Select item"
|
||||
:searchable="true"
|
||||
@change="selectedRow= $event"
|
||||
></SelectDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SelectDropdown from '../../../shared/select_dropdown.vue';
|
||||
import {
|
||||
list_team_repositories_path,
|
||||
rows_list_team_repositories_path
|
||||
} from '../../../../routes.js';
|
||||
|
||||
export default {
|
||||
name: 'RowSelector',
|
||||
components: {
|
||||
SelectDropdown
|
||||
},
|
||||
created() {
|
||||
this.teamId = document.body.dataset.currentTeamId;
|
||||
},
|
||||
watch: {
|
||||
selectedRepository() {
|
||||
this.selectedRow = null;
|
||||
},
|
||||
selectedRow() {
|
||||
this.$emit('change', this.selectedRow);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
repositoriesUrl() {
|
||||
return list_team_repositories_path(this.teamId);
|
||||
},
|
||||
rowsUrl() {
|
||||
if (!this.selectedRepository) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return rows_list_team_repositories_path(this.teamId);
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedRepository: null,
|
||||
selectedRow: null,
|
||||
teamId: null
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
83
app/javascript/vue/storage_locations/modals/move.vue
Normal file
83
app/javascript/vue/storage_locations/modals/move.vue
Normal file
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form @submit.prevent="submit">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block">
|
||||
{{ i18n.t('storage_locations.index.move_modal.title', { name: this.selectedObject.name }) }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-4">{{ i18n.t('storage_locations.index.move_modal.description', { name: this.selectedObject.name }) }}</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>
|
||||
</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" :value="selectedStorageLocationId" @selectStorageLocation="selectStorageLocation" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{{ i18n.t('general.move') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global HelperModule */
|
||||
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import modalMixin from '../../shared/modal_mixin';
|
||||
import MoveTreeMixin from './move_tree_mixin';
|
||||
|
||||
export default {
|
||||
name: 'NewProjectModal',
|
||||
props: {
|
||||
selectedObject: Array,
|
||||
moveToUrl: String
|
||||
},
|
||||
mixins: [modalMixin, MoveTreeMixin],
|
||||
data() {
|
||||
return {
|
||||
selectedStorageLocationId: null,
|
||||
storageLocationsTree: [],
|
||||
query: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
axios.post(this.moveToUrl, {
|
||||
destination_storage_location_id: this.selectedStorageLocationId || 'root_storage_location'
|
||||
}).then((response) => {
|
||||
this.$emit('move');
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
45
app/javascript/vue/storage_locations/modals/move_tree.vue
Normal file
45
app/javascript/vue/storage_locations/modals/move_tree.vue
Normal file
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<div class="pl-6" v-if="storageLocationsTree.length" v-for="storageLocationTree in storageLocationsTree"
|
||||
:key="storageLocationTree.storage_location.id">
|
||||
<div class="flex items-center">
|
||||
<i v-if="storageLocationTree.children.length > 0"
|
||||
:class="{'sn-icon-up': opendedStorageLocations[storageLocationTree.storage_location.id],
|
||||
'sn-icon-down': !opendedStorageLocations[storageLocationTree.storage_location.id]}"
|
||||
@click="opendedStorageLocations[storageLocationTree.storage_location.id] = !opendedStorageLocations[storageLocationTree.storage_location.id]"
|
||||
class="sn-icon p-2 pr-1 cursor-pointer"></i>
|
||||
<i v-else class="sn-icon sn-icon-up p-2 pr-1 opacity-0"></i>
|
||||
<div @click="$emit('selectStorageLocation', storageLocationTree.storage_location.id)"
|
||||
class="cursor-pointer flex items-center pl-1 flex-1 gap-2
|
||||
text-sn-blue hover:bg-sn-super-light-grey"
|
||||
:class="{'!bg-sn-super-light-blue': storageLocationTree.storage_location.id == value}">
|
||||
<i v-if="storageLocationTree.storage_location.container" class="sn-icon sn-icon-item"></i>
|
||||
<div class="flex-1 truncate p-2 pl-0" :title="storageLocationTree.storage_location.name">
|
||||
{{ storageLocationTree.storage_location.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MoveTree v-if="opendedStorageLocations[storageLocationTree.storage_location.id]"
|
||||
:storageLocationsTree="storageLocationTree.children"
|
||||
:value="value"
|
||||
@selectStorageLocation="$emit('selectStorageLocation', $event)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MoveTree',
|
||||
emits: ['selectStorageLocation'],
|
||||
props: {
|
||||
storageLocationsTree: Array,
|
||||
value: Number
|
||||
},
|
||||
components: {
|
||||
MoveTree: () => import('./move_tree.vue')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
opendedStorageLocations: {}
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,50 @@
|
|||
import axios from '../../../packs/custom_axios.js';
|
||||
import MoveTree from './move_tree.vue';
|
||||
import {
|
||||
tree_storage_locations_path
|
||||
} from '../../../routes.js';
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
axios.get(this.storageLocationsTreeUrl).then((response) => {
|
||||
this.storageLocationsTree = response.data;
|
||||
});
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedStorageLocationId: null,
|
||||
storageLocationsTree: [],
|
||||
query: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
storageLocationsTreeUrl() {
|
||||
return tree_storage_locations_path({ format: 'json', container: this.container });
|
||||
},
|
||||
filteredStorageLocationsTree() {
|
||||
if (this.query === '') {
|
||||
return this.storageLocationsTree;
|
||||
}
|
||||
|
||||
return this.filteredStorageLocationsTreeHelper(this.storageLocationsTree);
|
||||
}
|
||||
},
|
||||
components: {
|
||||
MoveTree
|
||||
},
|
||||
methods: {
|
||||
filteredStorageLocationsTreeHelper(storageLocationsTree) {
|
||||
return storageLocationsTree.map(({ storage_location, children }) => {
|
||||
if (storage_location.name.toLowerCase().includes(this.query.toLowerCase())) {
|
||||
return { storage_location, children };
|
||||
}
|
||||
|
||||
const filteredChildren = this.filteredStorageLocationsTreeHelper(children);
|
||||
return filteredChildren.length ? { storage_location, children: filteredChildren } : null;
|
||||
}).filter(Boolean);
|
||||
},
|
||||
selectStorageLocation(storageLocationId) {
|
||||
this.selectedStorageLocationId = storageLocationId;
|
||||
}
|
||||
}
|
||||
};
|
223
app/javascript/vue/storage_locations/modals/new_edit.vue
Normal file
223
app/javascript/vue/storage_locations/modals/new_edit.vue
Normal file
|
@ -0,0 +1,223 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form @submit.prevent="submit">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block" >
|
||||
{{ i18n.t(`storage_locations.index.edit_modal.title_${mode}_${editModalMode}`) }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p v-if="mode == 'create'" class="mb-6">{{ i18n.t(`storage_locations.index.edit_modal.description_create_${editModalMode}`) }}</p>
|
||||
<div class="mb-6">
|
||||
<label class="sci-label">
|
||||
{{ i18n.t(`storage_locations.index.edit_modal.name_label_${editModalMode}`) }}
|
||||
</label>
|
||||
<div class="sci-input-container-v2">
|
||||
<input
|
||||
type="text"
|
||||
v-model="object.name"
|
||||
:placeholder="i18n.t(`storage_locations.index.edit_modal.name_placeholder`)"
|
||||
>
|
||||
</div>
|
||||
<span v-if="this.errors.name" class="text-sn-coral text-xs">{{ this.errors.name }}</span>
|
||||
</div>
|
||||
<div v-if="editModalMode == 'container'" class="mb-6">
|
||||
<label class="sci-label">
|
||||
{{ i18n.t(`storage_locations.index.edit_modal.dimensions_label`) }}
|
||||
</label>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="sci-radio-container">
|
||||
<input type="radio" class="sci-radio" :disabled="object.code" v-model="object.metadata.display_type" name="display_type" value="no_grid" >
|
||||
<span class="sci-radio-label"></span>
|
||||
</div>
|
||||
<span>{{ i18n.t('storage_locations.index.edit_modal.no_grid') }}</span>
|
||||
<i class="sn-icon sn-icon-info text-sn-grey" :title="i18n.t('storage_locations.index.edit_modal.no_grid_tooltip')"></i>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="sci-radio-container">
|
||||
<input type="radio" class="sci-radio" :disabled="object.code" v-model="object.metadata.display_type" name="display_type" value="grid" >
|
||||
<span class="sci-radio-label"></span>
|
||||
</div>
|
||||
<span>{{ i18n.t('storage_locations.index.edit_modal.grid') }}</span>
|
||||
<div class="sci-input-container-v2 !w-28">
|
||||
<input type="number" :disabled="object.code" v-model="object.metadata.dimensions[0]" min="1" max="24">
|
||||
</div>
|
||||
<i class="sn-icon sn-icon-close-small"></i>
|
||||
<div class="sci-input-container-v2 !w-28">
|
||||
<input type="number" :disabled="object.code" v-model="object.metadata.dimensions[1]" min="1" max="24">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="sci-label">
|
||||
{{ i18n.t(`storage_locations.index.edit_modal.image_label_${editModalMode}`) }}
|
||||
</label>
|
||||
<DragAndDropUpload
|
||||
v-if="!attachedImage && !object.file_name"
|
||||
class="h-60"
|
||||
@file:dropped="addFile"
|
||||
@file:error="handleError"
|
||||
@file:error:clear="this.imageError = null"
|
||||
:supportingText="`${i18n.t('storage_locations.index.edit_modal.drag_and_drop_supporting_text')}`"
|
||||
:supportedFormats="['jpg', 'png', 'jpeg']"
|
||||
/>
|
||||
<div v-else class="border border-sn-light-grey rounded flex items-center p-2 gap-2">
|
||||
<i class="sn-icon sn-icon-result-image text-sn-grey"></i>
|
||||
<span class="text-sn-blue">{{ object.file_name || attachedImage?.name }}</span>
|
||||
<i class="sn-icon sn-icon-close text-sn-blue ml-auto cursor-pointer" @click="removeImage"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="sci-label">
|
||||
{{ i18n.t(`storage_locations.index.edit_modal.description_label`) }}
|
||||
</label>
|
||||
<div class="sci-input-container-v2 h-32">
|
||||
<textarea
|
||||
ref="description"
|
||||
v-model="object.description"
|
||||
:placeholder="i18n.t(`storage_locations.index.edit_modal.description_placeholder`)"
|
||||
></textarea>
|
||||
</div>
|
||||
<span v-if="this.errors.description" class="text-sn-coral text-xs">{{ this.errors.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" :disabled="!validObject" type="submit">
|
||||
{{ mode == 'create' ? i18n.t('general.create') : i18n.t('general.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global HelperModule SmartAnnotation ActiveStorage GLOBAL_CONSTANTS */
|
||||
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import modalMixin from '../../shared/modal_mixin';
|
||||
import DragAndDropUpload from '../../shared/drag_and_drop_upload.vue';
|
||||
|
||||
export default {
|
||||
name: 'EditLocationModal',
|
||||
props: {
|
||||
createUrl: String,
|
||||
editModalMode: String,
|
||||
directUploadUrl: String,
|
||||
editStorageLocation: Object
|
||||
},
|
||||
components: {
|
||||
DragAndDropUpload
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return {
|
||||
object: {
|
||||
metadata: {
|
||||
dimensions: [9, 9],
|
||||
display_type: 'grid'
|
||||
}
|
||||
},
|
||||
attachedImage: null,
|
||||
imageError: false,
|
||||
errors: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
mode() {
|
||||
return this.editStorageLocation ? 'edit' : 'create';
|
||||
},
|
||||
validObject() {
|
||||
this.errors = {};
|
||||
|
||||
if (!this.object.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.object.name.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH) {
|
||||
this.errors.name = this.i18n.t('storage_locations.index.edit_modal.errors.max_length', { max_length: GLOBAL_CONSTANTS.NAME_MAX_LENGTH });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.object.description && this.object.description.length > GLOBAL_CONSTANTS.TEXT_MAX_LENGTH) {
|
||||
this.errors.description = this.i18n.t('storage_locations.index.edit_modal.errors.max_length', { max_length: GLOBAL_CONSTANTS.NAME_MAX_LENGTH });
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.editStorageLocation) {
|
||||
this.object = this.editStorageLocation;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
SmartAnnotation.init($(this.$refs.description), false);
|
||||
$(this.$refs.modal).on('hidden.bs.modal', this.handleAtWhoModalClose);
|
||||
|
||||
this.object.container = this.editModalMode === 'container';
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
if (this.attachedImage) {
|
||||
this.uploadImage();
|
||||
} else {
|
||||
this.saveLocation();
|
||||
}
|
||||
},
|
||||
saveLocation() {
|
||||
if (this.object.code) {
|
||||
axios.put(this.object.urls.update, this.object)
|
||||
.then(() => {
|
||||
this.$emit('tableReloaded');
|
||||
HelperModule.flashAlertMsg(this.i18n.t(`storage_locations.index.edit_modal.success_message.edit_${this.editModalMode}`, { name: this.object.name }), 'success');
|
||||
this.close();
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
} else {
|
||||
axios.post(this.createUrl, this.object)
|
||||
.then(() => {
|
||||
this.$emit('tableReloaded');
|
||||
HelperModule.flashAlertMsg(this.i18n.t(`storage_locations.index.edit_modal.success_message.create_${this.editModalMode}`, { name: this.object.name }), 'success');
|
||||
this.close();
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
},
|
||||
handleError() {
|
||||
},
|
||||
addFile(file) {
|
||||
this.attachedImage = file;
|
||||
},
|
||||
removeImage() {
|
||||
this.attachedImage = null;
|
||||
this.object.file_name = null;
|
||||
},
|
||||
uploadImage() {
|
||||
const upload = new ActiveStorage.DirectUpload(this.attachedImage, this.directUploadUrl);
|
||||
|
||||
upload.create((error, blob) => {
|
||||
if (error) {
|
||||
// Handle the error
|
||||
} else {
|
||||
this.object.signed_blob_id = blob.signed_id;
|
||||
this.saveLocation();
|
||||
}
|
||||
});
|
||||
},
|
||||
handleAtWhoModalClose() {
|
||||
$('.atwho-view.old').css('display', 'none');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -6,28 +6,54 @@
|
|||
:reloadingTable="reloadingTable"
|
||||
:toolbarActions="toolbarActions"
|
||||
:actionsUrl="actionsUrl"
|
||||
@archive="archive"
|
||||
@restore="restore"
|
||||
@delete="deleteRepository"
|
||||
@update="update"
|
||||
:filters="filters"
|
||||
@create_location="openCreateLocationModal"
|
||||
@create_container="openCreateContainerModal"
|
||||
@edit="edit"
|
||||
@duplicate="duplicate"
|
||||
@export="exportRepositories"
|
||||
@share="share"
|
||||
@create="newRepository = true"
|
||||
@tableReloaded="reloadingTable = false"
|
||||
@move="move"
|
||||
@delete="deleteStorageLocation"
|
||||
/>
|
||||
<Teleport to="body">
|
||||
<EditModal v-if="openEditModal"
|
||||
@close="openEditModal = false"
|
||||
@tableReloaded="reloadingTable = true"
|
||||
:createUrl="createUrl"
|
||||
:editModalMode="editModalMode"
|
||||
:directUploadUrl="directUploadUrl"
|
||||
:editStorageLocation="editStorageLocation"
|
||||
/>
|
||||
<MoveModal v-if="objectToMove" :moveToUrl="moveToUrl"
|
||||
:selectedObject="objectToMove"
|
||||
@close="objectToMove = null" @move="updateTable()" />
|
||||
<ConfirmationModal
|
||||
:title="storageLocationDeleteTitle"
|
||||
:description="storageLocationDeleteDescription"
|
||||
confirmClass="btn btn-danger"
|
||||
:confirmText="i18n.t('general.delete')"
|
||||
ref="deleteStorageLocationModal"
|
||||
></ConfirmationModal>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global */
|
||||
/* global HelperModule */
|
||||
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
import DataTable from '../shared/datatable/table.vue';
|
||||
import EditModal from './modals/new_edit.vue';
|
||||
import MoveModal from './modals/move.vue';
|
||||
import ConfirmationModal from '../shared/confirmation_modal.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepositoriesTable',
|
||||
components: {
|
||||
DataTable
|
||||
DataTable,
|
||||
EditModal,
|
||||
MoveModal,
|
||||
ConfirmationModal
|
||||
},
|
||||
props: {
|
||||
dataSource: {
|
||||
|
@ -38,13 +64,26 @@ export default {
|
|||
type: String,
|
||||
required: true
|
||||
},
|
||||
createUrl: {
|
||||
createLocationUrl: {
|
||||
type: String
|
||||
},
|
||||
createLocationInstanceUrl: {
|
||||
type: String
|
||||
},
|
||||
directUploadUrl: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
reloadingTable: false
|
||||
reloadingTable: false,
|
||||
openEditModal: false,
|
||||
editModalMode: null,
|
||||
editStorageLocation: null,
|
||||
objectToMove: null,
|
||||
moveToUrl: null,
|
||||
storageLocationDeleteTitle: '',
|
||||
storageLocationDeleteDescription: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -102,21 +141,24 @@ export default {
|
|||
},
|
||||
toolbarActions() {
|
||||
const left = [];
|
||||
if (this.createUrl) {
|
||||
if (this.createLocationUrl) {
|
||||
left.push({
|
||||
name: 'create_location',
|
||||
icon: 'sn-icon sn-icon-new-task',
|
||||
label: this.i18n.t('storage_locations.index.new_location'),
|
||||
type: 'emit',
|
||||
path: this.createUrl,
|
||||
path: this.createLocationUrl,
|
||||
buttonStyle: 'btn btn-primary'
|
||||
});
|
||||
}
|
||||
|
||||
if (this.createLocationInstanceUrl) {
|
||||
left.push({
|
||||
name: 'create_box',
|
||||
name: 'create_container',
|
||||
icon: 'sn-icon sn-icon-item',
|
||||
label: this.i18n.t('storage_locations.index.new_box'),
|
||||
label: this.i18n.t('storage_locations.index.new_container'),
|
||||
type: 'emit',
|
||||
path: this.createUrl,
|
||||
path: this.createLocationInstanceUrl,
|
||||
buttonStyle: 'btn btn-secondary'
|
||||
});
|
||||
}
|
||||
|
@ -124,19 +166,98 @@ export default {
|
|||
left,
|
||||
right: []
|
||||
};
|
||||
},
|
||||
filters() {
|
||||
const filters = [
|
||||
{
|
||||
key: 'query',
|
||||
type: 'Text'
|
||||
},
|
||||
{
|
||||
key: 'search_tree',
|
||||
type: 'Checkbox',
|
||||
label: this.i18n.t('storage_locations.index.filters_modal.search_tree')
|
||||
}
|
||||
];
|
||||
|
||||
return filters;
|
||||
},
|
||||
createUrl() {
|
||||
return this.editModalMode === 'location' ? this.createLocationUrl : this.createLocationInstanceUrl;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openCreateLocationModal() {
|
||||
this.openEditModal = true;
|
||||
this.editModalMode = 'location';
|
||||
this.editStorageLocation = null;
|
||||
},
|
||||
openCreateContainerModal() {
|
||||
this.openEditModal = true;
|
||||
this.editModalMode = 'container';
|
||||
this.editStorageLocation = null;
|
||||
},
|
||||
edit(action, params) {
|
||||
this.openEditModal = true;
|
||||
this.editModalMode = params[0].container ? 'container' : 'location';
|
||||
[this.editStorageLocation] = params;
|
||||
},
|
||||
duplicate(action) {
|
||||
axios.post(action.path)
|
||||
.then(() => {
|
||||
this.reloadingTable = true;
|
||||
HelperModule.flashAlertMsg(this.i18n.t('storage_locations.index.duplicate.success_message'), 'success');
|
||||
})
|
||||
.catch(() => {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
|
||||
});
|
||||
},
|
||||
// Renderers
|
||||
nameRenderer(params) {
|
||||
const {
|
||||
name,
|
||||
urls
|
||||
} = params.data;
|
||||
let containerIcon = '';
|
||||
if (params.data.container) {
|
||||
containerIcon = '<i class="sn-icon sn-icon-item"></i>';
|
||||
}
|
||||
return `<a class="hover:no-underline flex items-center gap-1"
|
||||
title="${name}" href="${urls.show}">
|
||||
${containerIcon}
|
||||
<span class="truncate">${name}</span>
|
||||
</a>`;
|
||||
},
|
||||
updateTable() {
|
||||
this.reloadingTable = true;
|
||||
this.objectToMove = null;
|
||||
},
|
||||
move(event, rows) {
|
||||
[this.objectToMove] = rows;
|
||||
this.moveToUrl = event.path;
|
||||
},
|
||||
async deleteStorageLocation(event, rows) {
|
||||
const storageLocationType = rows[0].container ? this.i18n.t('storage_locations.container') : this.i18n.t('storage_locations.location');
|
||||
const description = `
|
||||
<p>${this.i18n.t('storage_locations.index.delete_modal.description_1_html',
|
||||
{ name: rows[0].name, type: storageLocationType, num_of_items: event.number_of_items })}</p>
|
||||
<p>${this.i18n.t('storage_locations.index.delete_modal.description_2_html')}</p>`;
|
||||
|
||||
this.storageLocationDeleteDescription = description;
|
||||
this.storageLocationDeleteTitle = this.i18n.t('storage_locations.index.delete_modal.title', { type: storageLocationType });
|
||||
const ok = await this.$refs.deleteStorageLocationModal.show();
|
||||
if (ok) {
|
||||
axios.delete(event.path).then((_) => {
|
||||
this.reloadingTable = true;
|
||||
HelperModule.flashAlertMsg(this.i18n.t('storage_locations.index.delete_modal.success_message',
|
||||
{
|
||||
type: storageLocationType[0].toUpperCase() + storageLocationType.slice(1),
|
||||
name: rows[0].name
|
||||
}), 'success');
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,7 +4,9 @@ class CleanupUserSettingsJob < ApplicationJob
|
|||
queue_as :default
|
||||
|
||||
def perform(record_type, record_id)
|
||||
raise ArgumentError, 'Invalid record_type' unless %w(task_step_states results_order).include?(record_type)
|
||||
unless %w(task_step_states results_order result_states).include?(record_type)
|
||||
raise ArgumentError, 'Invalid record_type'
|
||||
end
|
||||
|
||||
sanitized_record_id = record_id.to_i.to_s
|
||||
raise ArgumentError, 'Invalid record_id' unless sanitized_record_id == record_id.to_s
|
||||
|
|
|
@ -7,12 +7,18 @@ module Cloneable
|
|||
raise NotImplementedError, "Cloneable model must implement the '.parent' method!" unless respond_to?(:parent)
|
||||
|
||||
clone_label = I18n.t('general.clone_label')
|
||||
last_clone_number =
|
||||
parent.public_send(self.class.table_name)
|
||||
.select("substring(#{self.class.table_name}.name, '(?:^#{clone_label} )(\\d+)')::int AS clone_number")
|
||||
.where('name ~ ?', "^#{clone_label} \\d+ - #{Regexp.escape(name)}$")
|
||||
.order(clone_number: :asc)
|
||||
.last&.clone_number
|
||||
|
||||
records = if parent
|
||||
parent.public_send(self.class.table_name)
|
||||
else
|
||||
self.class.where(parent_id: nil, team: team)
|
||||
end
|
||||
|
||||
last_clone_number = records
|
||||
.select("substring(#{self.class.table_name}.name, '(?:^#{clone_label} )(\\d+)')::int AS clone_number")
|
||||
.where('name ~ ?', "^#{clone_label} \\d+ - #{Regexp.escape(name)}$")
|
||||
.order(clone_number: :asc)
|
||||
.last&.clone_number
|
||||
|
||||
"#{clone_label} #{(last_clone_number || 0) + 1} - #{name}".truncate(Constants::NAME_MAX_LENGTH)
|
||||
end
|
||||
|
|
|
@ -29,18 +29,19 @@ module TinyMceImages
|
|||
)[0]
|
||||
next unless tm_asset_to_update
|
||||
|
||||
tm_asset = tm_asset.image.representation(resize_to_limit: Constants::LARGE_PIC_FORMAT).processed
|
||||
variant = tm_asset.image.variant(resize_to_limit: Constants::LARGE_PIC_FORMAT)
|
||||
resized_asset = ActiveStorage::Variant.new(variant.blob, variant.variation).processed
|
||||
|
||||
width_attr = tm_asset_to_update.attributes['width']
|
||||
height_attr = tm_asset_to_update.attributes['height']
|
||||
|
||||
if width_attr && height_attr && (width_attr.value.to_i >= Constants::LARGE_PIC_FORMAT[0] ||
|
||||
height_attr.value.to_i >= Constants::LARGE_PIC_FORMAT[1])
|
||||
width_attr.value = tm_asset.image.blob.metadata['width'].to_s
|
||||
height_attr.value = tm_asset.image.blob.metadata['height'].to_s
|
||||
width_attr.value = resized_asset.image.blob.metadata['width'].to_s
|
||||
height_attr.value = resized_asset.image.blob.metadata['height'].to_s
|
||||
end
|
||||
|
||||
tm_asset_to_update.attributes['src'].value = convert_to_base64(tm_asset.image)
|
||||
tm_asset_to_update.attributes['src'].value = convert_to_base64(resized_asset)
|
||||
description = html_description.css('body').inner_html.to_s
|
||||
end
|
||||
description
|
||||
|
|
|
@ -71,6 +71,7 @@ class MyModuleRepositoryRow < ApplicationRecord
|
|||
team_id: my_module.experiment.project.team.id
|
||||
}
|
||||
)
|
||||
stock_value.last_modified_by_id = last_modified_by_id
|
||||
stock_value.save!
|
||||
save!
|
||||
end
|
||||
|
|
|
@ -52,7 +52,16 @@ class RepositoryCell < ApplicationRecord
|
|||
}
|
||||
|
||||
def update_repository_row_last_modified_by
|
||||
repository_row.update!(last_modified_by_id: value.last_modified_by_id)
|
||||
# RepositoryStockConsumptionValue currently don't store last_modified_by
|
||||
# so this would fail. Should probably be refactored to unify the behaviour (23.7.2024)
|
||||
if value.last_modified_by_id
|
||||
repository_row.update!(last_modified_by_id: value.last_modified_by_id)
|
||||
else
|
||||
Rails.logger.warn(
|
||||
"Missing last_modified_by_id for #{value.class} with id #{value.id}, " \
|
||||
"skipping update of last_modified_by on RepositoryRow with id #{repository_row_id}."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def self.create_with_value!(row, column, data, user)
|
||||
|
|
|
@ -175,6 +175,16 @@ class RepositoryRow < ApplicationRecord
|
|||
self[:archived]
|
||||
end
|
||||
|
||||
def grouped_storage_locations
|
||||
storage_location_repository_rows.joins(:storage_location).group(:storage_location_id).select(
|
||||
"storage_location_id as id,
|
||||
MAX(storage_locations.name) as name,
|
||||
jsonb_agg(jsonb_build_object(
|
||||
'id', storage_location_repository_rows.id, 'metadata',
|
||||
storage_location_repository_rows.metadata)
|
||||
) as positions").as_json
|
||||
end
|
||||
|
||||
def archived
|
||||
row_archived? || repository&.archived?
|
||||
end
|
||||
|
|
|
@ -37,6 +37,9 @@ class Result < ApplicationRecord
|
|||
accepts_nested_attributes_for :tables
|
||||
|
||||
before_save :ensure_default_name
|
||||
after_discard do
|
||||
CleanupUserSettingsJob.perform_later('result_states', id)
|
||||
end
|
||||
|
||||
def self.search(user,
|
||||
include_archived,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StorageLocation < ApplicationRecord
|
||||
include Cloneable
|
||||
include Discard::Model
|
||||
ID_PREFIX = 'SL'
|
||||
include PrefixedIdModel
|
||||
|
@ -18,9 +19,110 @@ class StorageLocation < ApplicationRecord
|
|||
has_many :repository_rows, through: :storage_location_repository_row
|
||||
|
||||
validates :name, length: { maximum: Constants::NAME_MAX_LENGTH }
|
||||
validate :parent_validation, if: -> { parent.present? }
|
||||
|
||||
after_discard do
|
||||
StorageLocation.where(parent_id: id).find_each(&:discard)
|
||||
storage_location_repository_rows.each(&:discard)
|
||||
end
|
||||
|
||||
def duplicate!
|
||||
ActiveRecord::Base.transaction do
|
||||
new_storage_location = dup
|
||||
new_storage_location.name = next_clone_name
|
||||
new_storage_location.save!
|
||||
copy_image(self, new_storage_location)
|
||||
recursive_duplicate(id, new_storage_location.id)
|
||||
new_storage_location
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def with_grid?
|
||||
metadata['display_type'] == 'grid'
|
||||
end
|
||||
|
||||
def grid_size
|
||||
metadata['dimensions'] if with_grid?
|
||||
end
|
||||
|
||||
def available_positions
|
||||
return unless with_grid?
|
||||
|
||||
occupied_positions = storage_location_repository_rows.pluck(:metadata).map { |metadata| metadata['position'] }
|
||||
|
||||
rows = {}
|
||||
|
||||
grid_size[0].times do |row|
|
||||
rows_cells = []
|
||||
grid_size[1].times.filter_map do |col|
|
||||
rows_cells.push(col + 1) if occupied_positions.exclude?([row + 1, col + 1])
|
||||
end
|
||||
rows[row + 1] = rows_cells unless rows_cells.empty?
|
||||
end
|
||||
|
||||
rows
|
||||
end
|
||||
|
||||
def self.storage_locations_enabled?
|
||||
ApplicationSettings.instance.values['storage_locations_enabled']
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def recursive_duplicate(old_parent_id = nil, new_parent_id = 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.save!
|
||||
copy_image(child, new_child)
|
||||
recursive_duplicate(child.id, new_child.id)
|
||||
end
|
||||
end
|
||||
|
||||
def copy_image(old_storage_location, new_storage_location)
|
||||
return unless old_storage_location.image.attached?
|
||||
|
||||
old_blob = old_storage_location.image.blob
|
||||
old_blob.open do |tmp_file|
|
||||
to_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: tmp_file,
|
||||
filename: old_blob.filename,
|
||||
metadata: old_blob.metadata
|
||||
)
|
||||
new_storage_location.image.attach(to_blob)
|
||||
end
|
||||
end
|
||||
|
||||
def self.inner_storage_locations(team, storage_location = nil)
|
||||
entry_point_condition = storage_location ? 'parent_id = ?' : 'parent_id IS NULL'
|
||||
|
||||
inner_storage_locations_sql =
|
||||
"WITH RECURSIVE inner_storage_locations(id, selected_storage_locations_ids) AS (
|
||||
SELECT id, ARRAY[id]
|
||||
FROM storage_locations
|
||||
WHERE team_id = ? AND #{entry_point_condition}
|
||||
UNION ALL
|
||||
SELECT storage_locations.id, selected_storage_locations_ids || storage_locations.id
|
||||
FROM inner_storage_locations
|
||||
JOIN storage_locations ON storage_locations.parent_id = inner_storage_locations.id
|
||||
WHERE NOT storage_locations.id = ANY(selected_storage_locations_ids)
|
||||
)
|
||||
SELECT id FROM inner_storage_locations ORDER BY selected_storage_locations_ids".gsub(/\n|\t/, ' ').squeeze(' ')
|
||||
|
||||
if storage_location.present?
|
||||
where("storage_locations.id IN (#{inner_storage_locations_sql})", team.id, storage_location.id)
|
||||
else
|
||||
where("storage_locations.id IN (#{inner_storage_locations_sql})", team.id)
|
||||
end
|
||||
end
|
||||
|
||||
def parent_validation
|
||||
if parent.id == id
|
||||
errors.add(:parent, I18n.t('activerecord.errors.models.storage_location.attributes.parent_storage_location'))
|
||||
elsif StorageLocation.inner_storage_locations(team, self).exists?(id: parent_id)
|
||||
errors.add(:parent, I18n.t('activerecord.errors.models.project_folder.attributes.parent_storage_location_child'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -72,6 +72,7 @@ class Team < ApplicationRecord
|
|||
source_type: 'RepositoryBase',
|
||||
dependent: :destroy
|
||||
has_many :shareable_links, inverse_of: :team, dependent: :destroy
|
||||
has_many :storage_locations, dependent: :destroy
|
||||
|
||||
attr_accessor :without_templates
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ Canaid::Permissions.register_for(MyModule) do
|
|||
end
|
||||
|
||||
can :share_my_module do |user, my_module|
|
||||
my_module.permission_granted?(user, MyModulePermissions::SHARE)
|
||||
my_module.team.shareable_links_enabled? && my_module.permission_granted?(user, MyModulePermissions::SHARE)
|
||||
end
|
||||
|
||||
can :restore_my_module do |user, my_module|
|
||||
|
|
|
@ -43,8 +43,28 @@ Canaid::Permissions.register_for(Team) do
|
|||
within_limits && team.permission_granted?(user, TeamPermissions::INVENTORIES_CREATE)
|
||||
end
|
||||
|
||||
can :read_storage_locations do |user, team|
|
||||
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATIONS_READ)
|
||||
end
|
||||
|
||||
can :create_storage_locations do |user, team|
|
||||
true # TODO: Add permission check
|
||||
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATIONS_CREATE)
|
||||
end
|
||||
|
||||
can :manage_storage_locations do |user, team|
|
||||
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATIONS_MANAGE)
|
||||
end
|
||||
|
||||
can :read_storage_location_containers do |user, team|
|
||||
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATION_CONTAINERS_READ)
|
||||
end
|
||||
|
||||
can :create_storage_location_containers do |user, team|
|
||||
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATION_CONTAINERS_CREATE)
|
||||
end
|
||||
|
||||
can :manage_storage_location_containers do |user, team|
|
||||
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATION_CONTAINERS_MANAGE)
|
||||
end
|
||||
|
||||
can :create_reports do |user, team|
|
||||
|
|
|
@ -2,20 +2,42 @@
|
|||
|
||||
module Lists
|
||||
class StorageLocationRepositoryRowSerializer < ActiveModel::Serializer
|
||||
attributes :created_by, :created_on, :position
|
||||
include Canaid::Helpers::PermissionsHelper
|
||||
|
||||
belongs_to :repository_row, serializer: RepositoryRowSerializer
|
||||
attributes :created_by, :created_on, :position, :row_id, :row_name, :hidden, :position_formatted, :stock
|
||||
|
||||
def row_id
|
||||
object.repository_row.id unless hidden
|
||||
end
|
||||
|
||||
def row_name
|
||||
object.repository_row.name unless hidden
|
||||
end
|
||||
|
||||
def created_by
|
||||
object.created_by.full_name
|
||||
object.created_by.full_name unless hidden
|
||||
end
|
||||
|
||||
def created_on
|
||||
I18n.l(object.created_at, format: :full)
|
||||
I18n.l(object.created_at, format: :full) unless hidden
|
||||
end
|
||||
|
||||
def position
|
||||
object.metadata['position']
|
||||
end
|
||||
|
||||
def position_formatted
|
||||
"#{('A'..'Z').to_a[position[0] - 1]}#{position[1]}" if position
|
||||
end
|
||||
|
||||
def stock
|
||||
if object.repository_row.repository.has_stock_management? && !hidden
|
||||
object.repository_row.repository_cells.find_by(value_type: 'RepositoryStockValue')&.value&.formatted
|
||||
end
|
||||
end
|
||||
|
||||
def hidden
|
||||
!can_read_repository?(object.repository_row.repository)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,13 +4,24 @@ module Lists
|
|||
class StorageLocationSerializer < ActiveModel::Serializer
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
attributes :id, :code, :name, :container, :description, :owned_by, :created_by, :created_on, :urls,
|
||||
:sub_location_count
|
||||
attributes :id, :code, :name, :container, :description, :owned_by, :created_by,
|
||||
:created_on, :urls, :metadata, :file_name, :sub_location_count
|
||||
|
||||
def owned_by
|
||||
object.team.name
|
||||
end
|
||||
|
||||
def metadata
|
||||
{
|
||||
display_type: object.metadata['display_type'],
|
||||
dimensions: object.metadata['dimensions'] || []
|
||||
}
|
||||
end
|
||||
|
||||
def file_name
|
||||
object.image.filename if object.image.attached?
|
||||
end
|
||||
|
||||
def created_by
|
||||
object.created_by.full_name
|
||||
end
|
||||
|
@ -28,8 +39,14 @@ module Lists
|
|||
end
|
||||
|
||||
def urls
|
||||
show_url = if @object.container
|
||||
storage_location_path(@object)
|
||||
else
|
||||
storage_locations_path(parent_id: object.id)
|
||||
end
|
||||
{
|
||||
show: storage_locations_path(parent_id: object.id)
|
||||
show: show_url,
|
||||
update: storage_location_path(@object)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,12 @@ class ResultSerializer < ActiveModel::Serializer
|
|||
attributes :name, :id, :urls, :updated_at, :created_at_formatted, :updated_at_formatted, :user,
|
||||
:my_module_id, :attachments_manageble, :marvinjs_enabled, :marvinjs_context, :type,
|
||||
:wopi_enabled, :wopi_context, :created_at, :created_by, :archived, :assets_order,
|
||||
:open_vector_editor_context, :comments_count, :assets_view_mode, :storage_limit
|
||||
:open_vector_editor_context, :comments_count, :assets_view_mode, :storage_limit, :collapsed
|
||||
|
||||
def collapsed
|
||||
result_states = current_user.settings.fetch('result_states', {})
|
||||
result_states[object.id.to_s] == true
|
||||
end
|
||||
|
||||
def marvinjs_enabled
|
||||
MarvinJsService.enabled?
|
||||
|
|
|
@ -5,6 +5,7 @@ module Lists
|
|||
def initialize(team, params)
|
||||
@team = team
|
||||
@parent_id = params[:parent_id]
|
||||
@filters = params[:filters] || {}
|
||||
@params = params
|
||||
end
|
||||
|
||||
|
@ -13,10 +14,22 @@ module Lists
|
|||
StorageLocation.joins('LEFT JOIN storage_locations AS sub_locations ' \
|
||||
'ON storage_locations.id = sub_locations.parent_id')
|
||||
.select('storage_locations.*, COUNT(sub_locations.id) AS sub_location_count')
|
||||
.where(team: @team, parent_id: @parent_id)
|
||||
.where(team: @team)
|
||||
.group(:id)
|
||||
end
|
||||
|
||||
def filter_records; end
|
||||
def filter_records
|
||||
if @filters[:search_tree].present?
|
||||
if @parent_id.present?
|
||||
storage_location = @records.find_by(id: @parent_id)
|
||||
@records = @records.where(id: StorageLocation.inner_storage_locations(@team, storage_location))
|
||||
end
|
||||
else
|
||||
@records = @records.where(parent_id: @parent_id)
|
||||
end
|
||||
|
||||
@records = @records.where('LOWER(name) ILIKE ?', "%#{@filters[:query].downcase}%") if @filters[:query].present?
|
||||
@records = @records.where('LOWER(name) ILIKE ?', "%#{@params[:search].downcase}%") if @params[:search].present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Toolbars
|
||||
class StorageLocationRepositoryRowsService
|
||||
attr_reader :current_user
|
||||
|
||||
include Canaid::Helpers::PermissionsHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
def initialize(current_user, items_ids: [])
|
||||
@current_user = current_user
|
||||
@assigned_rows = StorageLocationRepositoryRow.where(id: items_ids)
|
||||
@storage_location = @assigned_rows.first&.storage_location
|
||||
|
||||
@single = @assigned_rows.length == 1
|
||||
end
|
||||
|
||||
def actions
|
||||
return [] if @assigned_rows.none?
|
||||
|
||||
[
|
||||
unassign_action,
|
||||
move_action
|
||||
].compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unassign_action
|
||||
{
|
||||
name: 'unassign',
|
||||
label: I18n.t('storage_locations.show.toolbar.unassign'),
|
||||
icon: 'sn-icon sn-icon-close',
|
||||
path: unassign_rows_storage_location_path(@storage_location, ids: @assigned_rows.pluck(:id)),
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
|
||||
def move_action
|
||||
return unless @single
|
||||
|
||||
{
|
||||
name: 'move',
|
||||
label: I18n.t('storage_locations.show.toolbar.move'),
|
||||
icon: 'sn-icon sn-icon-move',
|
||||
path: move_storage_location_storage_location_repository_row_path(
|
||||
@storage_location, @assigned_rows.first
|
||||
),
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
94
app/services/toolbars/storage_locations_service.rb
Normal file
94
app/services/toolbars/storage_locations_service.rb
Normal file
|
@ -0,0 +1,94 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Toolbars
|
||||
class StorageLocationsService
|
||||
attr_reader :current_user
|
||||
|
||||
include Canaid::Helpers::PermissionsHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
def initialize(current_user, storage_location_ids: [])
|
||||
@current_user = current_user
|
||||
@storage_locations = StorageLocation.where(id: storage_location_ids)
|
||||
|
||||
@single = @storage_locations.length == 1
|
||||
end
|
||||
|
||||
def actions
|
||||
return [] if @storage_locations.none?
|
||||
|
||||
[
|
||||
edit_action,
|
||||
move_action,
|
||||
duplicate_action,
|
||||
delete_action
|
||||
].compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def edit_action
|
||||
return unless @single
|
||||
|
||||
return unless can_manage_storage_locations?(current_user.current_team)
|
||||
|
||||
{
|
||||
name: 'edit',
|
||||
label: I18n.t('storage_locations.index.toolbar.edit'),
|
||||
icon: 'sn-icon sn-icon-edit',
|
||||
path: storage_location_path(@storage_locations.first),
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
|
||||
def move_action
|
||||
return unless @single
|
||||
|
||||
return unless can_manage_storage_locations?(current_user.current_team)
|
||||
|
||||
{
|
||||
name: 'move',
|
||||
label: I18n.t("storage_locations.index.toolbar.move"),
|
||||
icon: 'sn-icon sn-icon-move',
|
||||
path: move_storage_location_path(@storage_locations.first),
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
|
||||
def duplicate_action
|
||||
return unless @single
|
||||
|
||||
return unless can_manage_storage_locations?(current_user.current_team)
|
||||
|
||||
{
|
||||
name: 'duplicate',
|
||||
label: I18n.t('storage_locations.index.toolbar.duplicate'),
|
||||
icon: 'sn-icon sn-icon-duplicate',
|
||||
path: duplicate_storage_location_path(@storage_locations.first),
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
|
||||
def delete_action
|
||||
return unless @single
|
||||
|
||||
return unless can_manage_storage_locations?(current_user.current_team)
|
||||
|
||||
storage_location = @storage_locations.first
|
||||
|
||||
number_of_items = storage_location.storage_location_repository_rows.count +
|
||||
StorageLocation.inner_storage_locations(current_user.current_team, storage_location)
|
||||
.where(container: true)
|
||||
.joins(:storage_location_repository_rows)
|
||||
.count
|
||||
{
|
||||
name: 'delete',
|
||||
label: I18n.t('storage_locations.index.toolbar.delete'),
|
||||
icon: 'sn-icon sn-icon-delete',
|
||||
number_of_items: number_of_items,
|
||||
path: storage_location_path(storage_location),
|
||||
type: :emit
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -213,12 +213,7 @@ module RepositoryImportParser
|
|||
def handle_nil_cell_value(repository_cell)
|
||||
return unless repository_cell.present? && @should_overwrite_with_empty_cells
|
||||
|
||||
if @preview
|
||||
repository_cell.to_destroy = true
|
||||
@updated = true
|
||||
else
|
||||
repository_cell.value.destroy!
|
||||
end
|
||||
@updated = erase_cell!(repository_cell, preview: @preview)
|
||||
|
||||
repository_cell
|
||||
end
|
||||
|
@ -241,6 +236,12 @@ module RepositoryImportParser
|
|||
when RepositoryStatusValue
|
||||
repository_status_item_id = cell_value[:repository_status_item_id]
|
||||
repository_cell.value.update_data!(repository_status_item_id, @user, preview: @preview)
|
||||
when RepositoryTimeValue
|
||||
repository_cell.value.update_data!(
|
||||
repository_cell.value.data.change(hour: cell_value.data.hour, min: cell_value.data.min),
|
||||
@user,
|
||||
preview: @preview
|
||||
)
|
||||
else
|
||||
sanitized_cell_value_data = sanitize_cell_value_data(cell_value.data)
|
||||
repository_cell.value.update_data!(sanitized_cell_value_data, @user, preview: @preview)
|
||||
|
@ -296,5 +297,25 @@ module RepositoryImportParser
|
|||
# all rows minus header
|
||||
@rows.count - 1
|
||||
end
|
||||
|
||||
def erase_cell!(repository_cell, preview: false)
|
||||
case repository_cell.value
|
||||
when RepositoryStockValue
|
||||
return false if repository_cell.value.amount.zero?
|
||||
|
||||
repository_cell.value.update_data!(
|
||||
{
|
||||
amount: 0,
|
||||
unit_item_id: repository_cell.value.repository_stock_unit_item_id
|
||||
},
|
||||
@user,
|
||||
preview: preview
|
||||
)
|
||||
else
|
||||
preview ? repository_cell.to_destroy = true : repository_cell.value.destroy!
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
19
app/views/design_elements/_radio.html.erb
Normal file
19
app/views/design_elements/_radio.html.erb
Normal file
|
@ -0,0 +1,19 @@
|
|||
<h1>Radio</h1>
|
||||
<div class="flex flex-items gap-8 p-6">
|
||||
<div class="sci-radio-container">
|
||||
<input type="radio" name="test_1" class="sci-radio">
|
||||
<span class="sci-radio-label">
|
||||
</div>
|
||||
<div class="sci-radio-container">
|
||||
<input type="radio" name="test_1" class="sci-radio" checked>
|
||||
<span class="sci-radio-label">
|
||||
</div>
|
||||
<div class="sci-radio-container">
|
||||
<input type="radio" name="test_2" class="sci-radio" disabled>
|
||||
<span class="sci-radio-label">
|
||||
</div>
|
||||
<div class="sci-radio-container">
|
||||
<input type="radio" name="test_2" class="sci-radio" checked disabled>
|
||||
<span class="sci-radio-label">
|
||||
</div>
|
||||
</div>
|
|
@ -10,6 +10,8 @@
|
|||
end
|
||||
%>
|
||||
|
||||
<%= render partial: 'radio' %>
|
||||
|
||||
<%= render partial: 'select' %>
|
||||
|
||||
<%= render partial: 'modals' %>
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
<div id="taskSecondaryMenu" class="sticky-header-element bg-sn-white border-b border-solid border-0 border-sn-sleepy-grey rounded-t px-4 py-2 pb-[16px] top-0 sticky flex items-center flex-wrap z-[106]">
|
||||
<div
|
||||
id="taskSecondaryMenu"
|
||||
class="sticky-header-element bg-sn-white border-b border-solid border-0 border-sn-sleepy-grey rounded-t px-4 py-2 pb-[16px] top-0 sticky flex items-center flex-wrap z-[106]"
|
||||
data-e2e="e2e-CO-taskTopToolbar"
|
||||
>
|
||||
<div class="flex items-center gap-4 mr-auto">
|
||||
<% if can_read_experiment?(@my_module.experiment) %>
|
||||
<a class="p-3 border-b-4 border-transparent hover:no-underline uppercase text-bold capitalize <%= is_module_protocols? ? "text-sn-blue" : "text-sn-grey" %>"
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
<div class="task-sharing-and-flows flex items-center gap-2 pl-3">
|
||||
<%= render partial: 'my_modules/status_flow/task_flow_button', locals: { my_module: @my_module } if @my_module.my_module_status_flow %>
|
||||
<%= javascript_include_tag("my_modules/status_flow") %>
|
||||
<% if current_team.shareable_links_enabled? %>
|
||||
<div id="share-task-container" data-behaviour="vue">
|
||||
<share-task-container
|
||||
shareable-link-url="<%= my_module_shareable_link_path(@my_module) %>"
|
||||
:enabled="<%= current_team.shareable_links_enabled? %>"
|
||||
:shared="<%= @my_module.shared? %>"
|
||||
:can-share="<%= can_share_my_module?(@my_module) %>" />
|
||||
</div>
|
||||
|
||||
<%= javascript_include_tag 'vue_share_task_container' %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -37,7 +37,8 @@
|
|||
<th id="assigned" data-unmanageable="true"><%= t("repositories.table.assigned") %></th>
|
||||
<th id="row-id"><%= t("repositories.table.id") %></th>
|
||||
<th id="row-name"><%= t("repositories.table.row_name") %></th>
|
||||
<th id="relationship" title="<%= t("repositories.table.relationships") %>"><%= t("repositories.table.relationships") %></th>
|
||||
<th id="relationship" data-disabled="<%= !Repository.repository_row_connections_enabled? %>"
|
||||
title="<%= t("repositories.table.relationships") %>"><%= t("repositories.table.relationships") %></th>
|
||||
<th id="added-on" ><%= t("repositories.table.added_on") %></th>
|
||||
<th id="added-by" ><%= t("repositories.table.added_by") %></th>
|
||||
<th id="updated-on" ><%= t("repositories.table.updated_on") %></th>
|
||||
|
|
|
@ -98,6 +98,14 @@
|
|||
selected>
|
||||
<%= t('libraries.manange_modal_column.select.repository_stock_value') %>
|
||||
</option>
|
||||
<% elsif !RepositoryBase.stock_management_enabled? %>
|
||||
<option value="RepositoryStockValue" class="disabled-option"
|
||||
data-params="<%= {
|
||||
optionClass: 'disabled-option',
|
||||
data_e2e: 'e2e-LB-invItems-manageColumnsModal-stock-dec_clm-disabled'
|
||||
}.to_json %>">
|
||||
<%= t('libraries.manange_modal_column.select.repository_stock_value') %>
|
||||
</option>
|
||||
<% end %>
|
||||
<option data-delimiter=true></option>
|
||||
|
||||
|
|
|
@ -37,6 +37,11 @@ json.actions do
|
|||
end
|
||||
end
|
||||
|
||||
json.storage_locations do
|
||||
json.locations @repository_row.grouped_storage_locations
|
||||
json.enabled StorageLocation.storage_locations_enabled?
|
||||
end
|
||||
|
||||
json.default_columns do
|
||||
json.name @repository_row.name
|
||||
json.code @repository_row.code
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<div class="comment-input-container !pr-0 grid grid-flow-row">
|
||||
<textarea class="comment-input-field smart-text-area textarea-sm"
|
||||
placeholder="<%= t('comments.placeholder') %>"></textarea>
|
||||
<i class="sn-icon sn-icon-send send-comment !contents before:ml-auto"></i>
|
||||
<i class="sn-icon sn-icon-send send-comment"></i>
|
||||
</div>
|
||||
<div class="error-container"></div>
|
||||
<div class="update-buttons sci-btn-group">
|
||||
|
|
|
@ -5,17 +5,17 @@
|
|||
</a>
|
||||
</div>
|
||||
<% left_menu_elements.each_with_index do |item, index| %>
|
||||
<%= link_to item[:url], title: item[:name], class:"sci--layout--menu-item", data: { e2e: "e2e-BT-leftMenu-#{item[:name].downcase}", active: item[:submenu].blank? && item[:active], disabled: current_user.teams.blank?, submenu: item[:submenu].any? } do %>
|
||||
<%= link_to item[:url], title: item[:title] || item[:name], class:"sci--layout--menu-item", data: { e2e: "e2e-BT-leftMenu-#{item[:name].downcase}", active: item[:submenu].blank? && item[:active], disabled: current_user.teams.blank?, submenu: item[:submenu].any? } do %>
|
||||
<i class="sn-icon <%= item[:icon] %>"></i>
|
||||
<%= item[:name] %>
|
||||
<%= item[:name].html_safe %>
|
||||
<i class="sn-icon <%= item[:active] ? 'sn-icon-down' : 'sn-icon-right' %> show-submenu"></i>
|
||||
<% end %>
|
||||
<% if item[:submenu].any? %>
|
||||
<div class="sci--layout--menu-submenu" data-id="<%= index %>" data-collapsed="<%= !item[:active] %>" style="--submenu-items:<%= item[:submenu].length %>">
|
||||
<% item[:submenu].each do |subitem| %>
|
||||
<%= link_to subitem[:url], title: subitem[:name], class:"sci--layout--menu-item", data: { e2e: "e2e-BT-leftMenu-#{item[:name].downcase}-#{subitem[:name].downcase}", active: subitem[:active], disabled: current_user.teams.blank? } do %>
|
||||
<%= link_to subitem[:url], title: subitem[:title] || subitem[:name], class:"sci--layout--menu-item", data: { e2e: "e2e-BT-leftMenu-#{item[:name].downcase}-#{subitem[:name].downcase}", active: subitem[:active], disabled: current_user.teams.blank? } do %>
|
||||
<i class="sn-icon sn-icon-dot-small"></i>
|
||||
<%= subitem[:name] %>
|
||||
<%= subitem[:name].html_safe %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -2,18 +2,20 @@
|
|||
<% provide(:container_class, "no-second-nav-container") %>
|
||||
|
||||
<% if current_team %>
|
||||
<div class="content-pane flexible">
|
||||
<div class="content-pane with-grey-background flexible">
|
||||
<div class="content-header">
|
||||
<div class="title-row">
|
||||
<h1><%= t('storage_locations.index.head_title') %></h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-body" data-e2e="e2e-CO-storage_-ocations">
|
||||
<div class="content-body " data-e2e="e2e-CO-storage_-ocations">
|
||||
<div id="storageLocationsTable" class="fixed-content-body">
|
||||
<storage-locations
|
||||
actions-url="<%= actions_toolbar_storage_locations_path(current_team) %>"
|
||||
data-source="<%= storage_locations_path(format: :json, parent_id: params[:parent_id]) %>"
|
||||
create-url="<%= storage_locations_path if can_create_storage_locations?(current_team) %>"
|
||||
direct-upload-url="<%= rails_direct_uploads_url %>"
|
||||
create-location-url="<%= storage_locations_path(parent_id: params[:parent_id]) if can_create_storage_locations?(current_team) %>"
|
||||
create-location-instance-url="<%= storage_locations_path(parent_id: params[:parent_id]) if can_create_storage_location_containers?(current_team) %>"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
25
app/views/storage_locations/show.html.erb
Normal file
25
app/views/storage_locations/show.html.erb
Normal file
|
@ -0,0 +1,25 @@
|
|||
<% provide(:head_title, @storage_location.name) %>
|
||||
<% provide(:container_class, "no-second-nav-container") %>
|
||||
|
||||
<% if current_team %>
|
||||
<div class="content-pane flexible with-grey-background">
|
||||
<div class="content-header">
|
||||
<div class="title-row">
|
||||
<h1><%= @storage_location.name %></h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-body" data-e2e="e2e-CO-storage_locations_container">
|
||||
<div id="StorageLocationsContainer" class="fixed-content-body">
|
||||
<storage-locations-container
|
||||
actions-url="<%= actions_toolbar_storage_location_storage_location_repository_rows_path(@storage_location) %>"
|
||||
data-source="<%= storage_location_storage_location_repository_rows_path(@storage_location) %>"
|
||||
:with-grid="<%= @storage_location.with_grid? %>"
|
||||
:grid-size="<%= @storage_location.grid_size.to_json %>"
|
||||
:container-id="<%= @storage_location.id %>"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= javascript_include_tag 'vue_storage_locations_container' %>
|
||||
<% end %>
|
|
@ -105,4 +105,8 @@ Rails.application.configure do
|
|||
config.x.new_team_on_signup = false
|
||||
end
|
||||
config.hosts << "dev.scinote.test"
|
||||
|
||||
# Automatically update js-routes file
|
||||
# when routes.rb is changed
|
||||
config.middleware.use(JsRoutes::Middleware)
|
||||
end
|
||||
|
|
|
@ -636,6 +636,8 @@ class Extends
|
|||
preferences/index
|
||||
addons/index
|
||||
search/index
|
||||
storage_locations/index
|
||||
storage_locations/show
|
||||
)
|
||||
|
||||
DEFAULT_USER_NOTIFICATION_SETTINGS = {
|
||||
|
@ -673,6 +675,7 @@ class Extends
|
|||
repository_export_file_type
|
||||
navigator_collapsed
|
||||
navigator_width
|
||||
result_states
|
||||
).freeze
|
||||
end
|
||||
|
||||
|
|
|
@ -13,7 +13,13 @@ module PermissionExtends
|
|||
REPORTS_CREATE
|
||||
LABEL_TEMPLATES_READ
|
||||
LABEL_TEMPLATES_MANAGE
|
||||
).each { |permission| const_set(permission, "team_#{permission.underscore}") }
|
||||
STORAGE_LOCATIONS_CREATE
|
||||
STORAGE_LOCATIONS_MANAGE
|
||||
STORAGE_LOCATIONS_READ
|
||||
STORAGE_LOCATION_CONTAINERS_CREATE
|
||||
STORAGE_LOCATION_CONTAINERS_MANAGE
|
||||
STORAGE_LOCATION_CONTAINERS_READ
|
||||
).each { |permission| const_set(permission, "team_#{permission.parameterize}") }
|
||||
end
|
||||
|
||||
module ProtocolPermissions
|
||||
|
@ -24,7 +30,7 @@ module PermissionExtends
|
|||
MANAGE
|
||||
USERS_MANAGE
|
||||
MANAGE_DRAFT
|
||||
).each { |permission| const_set(permission, "protocol_#{permission.underscore}") }
|
||||
).each { |permission| const_set(permission, "protocol_#{permission.parameterize}") }
|
||||
end
|
||||
|
||||
module ReportPermissions
|
||||
|
@ -33,7 +39,7 @@ module PermissionExtends
|
|||
READ
|
||||
MANAGE
|
||||
USERS_MANAGE
|
||||
).each { |permission| const_set(permission, "report_#{permission.underscore}") }
|
||||
).each { |permission| const_set(permission, "report_#{permission.parameterize}") }
|
||||
end
|
||||
|
||||
module ProjectPermissions
|
||||
|
@ -51,7 +57,7 @@ module PermissionExtends
|
|||
COMMENTS_MANAGE_OWN
|
||||
TAGS_MANAGE
|
||||
EXPERIMENTS_CREATE
|
||||
).each { |permission| const_set(permission, "project_#{permission.underscore}") }
|
||||
).each { |permission| const_set(permission, "project_#{permission.parameterize}") }
|
||||
end
|
||||
|
||||
module ExperimentPermissions
|
||||
|
@ -65,7 +71,7 @@ module PermissionExtends
|
|||
USERS_MANAGE
|
||||
READ_CANVAS
|
||||
ACTIVITIES_READ
|
||||
).each { |permission| const_set(permission, "experiment_#{permission.underscore}") }
|
||||
).each { |permission| const_set(permission, "experiment_#{permission.parameterize}") }
|
||||
end
|
||||
|
||||
module MyModulePermissions
|
||||
|
@ -107,7 +113,7 @@ module PermissionExtends
|
|||
USERS_MANAGE
|
||||
DESIGNATED_USERS_MANAGE
|
||||
STOCK_CONSUMPTION_UPDATE
|
||||
).each { |permission| const_set(permission, "task_#{permission.underscore}") }
|
||||
).each { |permission| const_set(permission, "task_#{permission.parameterize}") }
|
||||
end
|
||||
|
||||
module RepositoryPermissions
|
||||
|
@ -127,7 +133,7 @@ module PermissionExtends
|
|||
COLUMNS_DELETE
|
||||
USERS_MANAGE
|
||||
FILTERS_MANAGE
|
||||
).each { |permission| const_set(permission, "inventory_#{permission.underscore}") }
|
||||
).each { |permission| const_set(permission, "inventory_#{permission.parameterize}") }
|
||||
end
|
||||
|
||||
module PredefinedRoles
|
||||
|
@ -147,6 +153,12 @@ module PermissionExtends
|
|||
TeamPermissions::REPORTS_CREATE,
|
||||
TeamPermissions::LABEL_TEMPLATES_READ,
|
||||
TeamPermissions::LABEL_TEMPLATES_MANAGE,
|
||||
TeamPermissions::STORAGE_LOCATIONS_READ,
|
||||
TeamPermissions::STORAGE_LOCATIONS_CREATE,
|
||||
TeamPermissions::STORAGE_LOCATIONS_MANAGE,
|
||||
TeamPermissions::STORAGE_LOCATION_CONTAINERS_READ,
|
||||
TeamPermissions::STORAGE_LOCATION_CONTAINERS_CREATE,
|
||||
TeamPermissions::STORAGE_LOCATION_CONTAINERS_MANAGE,
|
||||
ProtocolPermissions::READ,
|
||||
ProtocolPermissions::READ_ARCHIVED,
|
||||
ProtocolPermissions::MANAGE_DRAFT,
|
||||
|
@ -241,6 +253,8 @@ module PermissionExtends
|
|||
|
||||
VIEWER_PERMISSIONS = [
|
||||
TeamPermissions::LABEL_TEMPLATES_READ,
|
||||
TeamPermissions::STORAGE_LOCATIONS_READ,
|
||||
TeamPermissions::STORAGE_LOCATION_CONTAINERS_READ,
|
||||
ProtocolPermissions::READ,
|
||||
ProtocolPermissions::READ_ARCHIVED,
|
||||
ReportPermissions::READ,
|
||||
|
|
7
config/initializers/js_routes.rb
Normal file
7
config/initializers/js_routes.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
JsRoutes.setup do |c|
|
||||
# Setup your JS module system:
|
||||
# ESM, CJS, AMD, UMD or nil
|
||||
# c.module_type = "ESM"
|
||||
end
|
|
@ -264,6 +264,9 @@ en:
|
|||
storage_location:
|
||||
missing_position: 'Missing position metadata'
|
||||
not_uniq_position: 'Position already taken'
|
||||
attributes:
|
||||
parent_storage_location: "Storage location cannot be parent to itself"
|
||||
parent_storage_location_child: "Storage location cannot be moved to it's child"
|
||||
storage:
|
||||
limit_reached: "Storage limit has been reached."
|
||||
helpers:
|
||||
|
@ -1197,6 +1200,7 @@ en:
|
|||
shareable_links:
|
||||
share: "Share"
|
||||
shared: "Shared"
|
||||
disabled: "Sharing is disabled for your team.\nAsk your team admin to enable sharing in the team settings."
|
||||
notes:
|
||||
title: "Notes"
|
||||
no_description: "No task description"
|
||||
|
@ -2612,7 +2616,12 @@ en:
|
|||
custom_columns_label: 'Custom columns'
|
||||
relationships_label: 'Relationships'
|
||||
assigned_label: 'Assigned'
|
||||
locations_label: 'Locations'
|
||||
QR_label: 'QR'
|
||||
locations:
|
||||
title: 'Locations (%{count})'
|
||||
container: 'Box'
|
||||
assign: 'Assign new location'
|
||||
repository_stock_values:
|
||||
manage_modal:
|
||||
title: "Stock %{item}"
|
||||
|
@ -2668,10 +2677,45 @@ en:
|
|||
errors:
|
||||
my_module_references_missing: 'Task references are not set'
|
||||
storage_locations:
|
||||
container: 'box'
|
||||
location: 'location'
|
||||
show:
|
||||
table:
|
||||
position: "Position"
|
||||
reminders: "Reminders"
|
||||
row_id: "Item ID"
|
||||
row_name: "Name"
|
||||
stock: "Stock"
|
||||
toolbar:
|
||||
assign: 'Assign item'
|
||||
unassign: 'Unassign'
|
||||
move: 'Move'
|
||||
unassign_modal:
|
||||
title: 'Unassign location'
|
||||
description: 'Are you sure you want to remove %{items} item(s) from their current storage location?'
|
||||
button: 'Unassign'
|
||||
assign_modal:
|
||||
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.'
|
||||
assign_action: 'Assign'
|
||||
move_action: 'Move'
|
||||
row: 'Row'
|
||||
column: 'Column'
|
||||
inventory: 'Inventory'
|
||||
item: 'Item'
|
||||
index:
|
||||
head_title: "Locations"
|
||||
new_location: "New location"
|
||||
new_box: "New box"
|
||||
new_container: "New box"
|
||||
duplicate:
|
||||
success_message: "Location was successfully duplicated."
|
||||
toolbar:
|
||||
edit: 'Edit'
|
||||
move: 'Move'
|
||||
duplicate: 'Duplicate'
|
||||
delete: 'Delete'
|
||||
table:
|
||||
name: "Location name"
|
||||
id: "ID"
|
||||
|
@ -2682,6 +2726,48 @@ en:
|
|||
owned_by: "Owned by"
|
||||
created_on: "Created on"
|
||||
description: "Description"
|
||||
filters_modal:
|
||||
search_tree: "Look inside locations"
|
||||
edit_modal:
|
||||
title_create_location: "Create new location"
|
||||
title_create_container: "Create new box"
|
||||
title_edit_location: "Edit location"
|
||||
title_edit_container: "Edit box"
|
||||
description_create_location: "Fill in the fields and create a new location."
|
||||
description_create_container: "Fill in the fields to create a new box. Defining the box dimensions allows you to control the number of available spaces for placing inventory items."
|
||||
name_label_location: "Location name"
|
||||
image_label_location: "Image of location"
|
||||
name_label_container: "Box name"
|
||||
image_label_container: "Image of box"
|
||||
drag_and_drop_supporting_text: ".png or .jpg file"
|
||||
description_label: "Description"
|
||||
name_placeholder: "Big freezer"
|
||||
description_placeholder: "Keep everyone on the same page. You can also use smart annotations."
|
||||
dimensions_label: "Dimensions (rows x columns)"
|
||||
no_grid: "No grid"
|
||||
grid: "Grid"
|
||||
no_grid_tooltip: "You can assign unlimited items to the “No-grid” box but they do not have assigned position."
|
||||
success_message:
|
||||
create_location: "Location %{name} was successfully created."
|
||||
create_container: "Box %{name} was successfully created."
|
||||
edit_location: "Location %{name} was successfully updated."
|
||||
edit_container: "Box %{name} was successfully updated."
|
||||
errors:
|
||||
max_length: "is too long (maximum is %{max_length} characters)"
|
||||
move_modal:
|
||||
title: 'Move %{name}'
|
||||
description: 'Select where you want to move %{name}.'
|
||||
search_header: '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:
|
||||
find_storage_locations: 'Find location'
|
||||
delete_modal:
|
||||
title: 'Delete a %{type}'
|
||||
description_1_html: "You're about to delete <b>%{name}</b>. This action will delete the %{type}. <b>%{num_of_items}</b> items inside will lose their assigned positions."
|
||||
description_2_html: '<b>Are you sure you want to delete it?</b>'
|
||||
success_message: "%{type} %{name} successfully deleted."
|
||||
|
||||
libraries:
|
||||
manange_modal_column_index:
|
||||
title: "Manage columns"
|
||||
|
@ -3590,6 +3676,7 @@ en:
|
|||
expand_label: "Expand All"
|
||||
collapse_label: "Collapse All"
|
||||
new_step: "New step"
|
||||
add_step: "Add step"
|
||||
new_step_title: "Create new step"
|
||||
subtitle: "Protocol Steps"
|
||||
no_steps: "Protocol has no steps."
|
||||
|
|
|
@ -194,6 +194,8 @@ Rails.application.routes.draw do
|
|||
get 'create_modal', to: 'repositories#create_modal',
|
||||
defaults: { format: 'json' }
|
||||
get 'actions_toolbar'
|
||||
get :list
|
||||
get :rows_list
|
||||
end
|
||||
member do
|
||||
get :export_empty_repository
|
||||
|
@ -807,11 +809,25 @@ Rails.application.routes.draw do
|
|||
|
||||
resources :connected_devices, controller: 'users/connected_devices', only: %i(destroy)
|
||||
|
||||
resources :storage_locations, only: %i(index create destroy update) do
|
||||
resources :storage_locations, only: %i(index create destroy update show) do
|
||||
collection do
|
||||
get :actions_toolbar
|
||||
get :tree
|
||||
end
|
||||
member do
|
||||
post :move
|
||||
post :duplicate
|
||||
post :unassign_rows
|
||||
get :available_positions
|
||||
end
|
||||
resources :storage_location_repository_rows, only: %i(index create destroy update) do
|
||||
collection do
|
||||
get :actions_toolbar
|
||||
end
|
||||
member do
|
||||
post :move
|
||||
end
|
||||
end
|
||||
resources :storage_location_repository_rows, only: %i(index create destroy update)
|
||||
end
|
||||
|
||||
get 'search' => 'search#index'
|
||||
|
|
|
@ -66,7 +66,8 @@ const entryList = {
|
|||
vue_legacy_access_modal: './app/javascript/packs/vue/legacy/access_modal.js',
|
||||
vue_legacy_repository_menu_dropdown: './app/javascript/packs/vue/legacy/repository_menu_dropdown.js',
|
||||
vue_dashboard_new_task: './app/javascript/packs/vue/dashboard_new_task.js',
|
||||
vue_storage_locations_table: './app/javascript/packs/vue/storage_locations_table.js'
|
||||
vue_storage_locations_table: './app/javascript/packs/vue/storage_locations_table.js',
|
||||
vue_storage_locations_container: './app/javascript/packs/vue/storage_locations_container.js'
|
||||
};
|
||||
|
||||
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddStorageLocationPermissions < ActiveRecord::Migration[7.0]
|
||||
STORAGE_LOCATIONS_MANAGE_PERMISSION = [
|
||||
TeamPermissions::STORAGE_LOCATIONS_CREATE,
|
||||
TeamPermissions::STORAGE_LOCATIONS_MANAGE,
|
||||
TeamPermissions::STORAGE_LOCATION_CONTAINERS_CREATE,
|
||||
TeamPermissions::STORAGE_LOCATION_CONTAINERS_MANAGE
|
||||
].freeze
|
||||
|
||||
STORAGE_LOCATIONS_READ_PERMISSION = [
|
||||
TeamPermissions::STORAGE_LOCATIONS_READ,
|
||||
TeamPermissions::STORAGE_LOCATION_CONTAINERS_READ
|
||||
].freeze
|
||||
|
||||
def up
|
||||
@owner_role = UserRole.find_predefined_owner_role
|
||||
@normal_user_role = UserRole.find_predefined_normal_user_role
|
||||
@viewer_user_role = UserRole.find_predefined_viewer_role
|
||||
|
||||
@owner_role.permissions = @owner_role.permissions | STORAGE_LOCATIONS_MANAGE_PERMISSION |
|
||||
STORAGE_LOCATIONS_READ_PERMISSION
|
||||
@normal_user_role.permissions = @normal_user_role.permissions | STORAGE_LOCATIONS_MANAGE_PERMISSION |
|
||||
STORAGE_LOCATIONS_READ_PERMISSION
|
||||
@viewer_user_role.permissions = @viewer_user_role.permissions | STORAGE_LOCATIONS_READ_PERMISSION
|
||||
|
||||
@owner_role.save(validate: false)
|
||||
@normal_user_role.save(validate: false)
|
||||
@viewer_user_role.save(validate: false)
|
||||
end
|
||||
|
||||
def down
|
||||
@owner_role = UserRole.find_predefined_owner_role
|
||||
@normal_user_role = UserRole.find_predefined_normal_user_role
|
||||
@viewer_user_role = UserRole.find_predefined_viewer_role
|
||||
|
||||
@owner_role.permissions = @owner_role.permissions - STORAGE_LOCATIONS_MANAGE_PERMISSION -
|
||||
STORAGE_LOCATIONS_READ_PERMISSION
|
||||
@normal_user_role.permissions = @normal_user_role.permissions - STORAGE_LOCATIONS_MANAGE_PERMISSION -
|
||||
STORAGE_LOCATIONS_READ_PERMISSION
|
||||
@viewer_user_role.permissions = @viewer_user_role.permissions - STORAGE_LOCATIONS_READ_PERMISSION
|
||||
|
||||
@owner_role.save(validate: false)
|
||||
@normal_user_role.save(validate: false)
|
||||
@viewer_user_role.save(validate: false)
|
||||
end
|
||||
end
|
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.eot
vendored
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.eot
vendored
Binary file not shown.
|
@ -195,4 +195,8 @@
|
|||
<glyph unicode="" glyph-name="task-mini" data-tags="task-mini" d="M339.2 140.8c-23.894 0-43.725 7.885-59.494 23.654-15.804 15.805-23.706 35.651-23.706 59.546v448c0 23.894 7.902 43.742 23.706 59.546 15.77 15.77 35.601 23.654 59.494 23.654h249.6l179.2-179.2v-352c0-23.895-7.885-43.74-23.654-59.546-15.805-15.77-35.651-23.654-59.546-23.654h-345.6zM563.2 550.4v153.6h-224c-8.534 0-15.991-3.209-22.374-9.626-6.417-6.383-9.626-13.841-9.626-22.374v-448c0-8.535 3.209-15.99 9.626-22.374 6.383-6.415 13.841-9.626 22.374-9.626h345.6c8.535 0 15.99 3.21 22.374 9.626 6.415 6.385 9.626 13.839 9.626 22.374v326.4h-153.6zM652.984 468.48l-171.313-228.413-123.272 123.269 36.204 36.209 81.528-81.531 135.892 181.187 40.96-30.72z" />
|
||||
<glyph unicode="" glyph-name="flag" data-tags="flag" d="M256 128v661.333h306.871l17.067-85.333h230.729v-341.333h-221.538l-17.067 85.333h-273.395v-320h-42.667zM625.067 405.333h142.933v256h-224l-17.067 85.333h-228.267v-256h309.333l17.067-85.333z" />
|
||||
<glyph unicode="" glyph-name="undo" data-tags="undo" d="M315.893 192v42.667h309.506c44.25 0 81.954 15.181 113.109 45.538 31.142 30.357 46.72 67.413 46.72 111.177s-15.578 80.687-46.72 110.775c-31.155 30.084-68.86 45.129-113.109 45.129h-330.338l126.517-126.528-30.197-30.199-178.048 178.061 178.048 178.048 30.197-30.197-126.517-126.517h330.338c55.851 0 103.561-19.257 143.134-57.771 39.573-38.507 59.362-85.44 59.362-140.8 0-55.351-19.789-102.421-59.362-141.205s-87.283-58.176-143.134-58.176h-309.506z" />
|
||||
<glyph unicode="" glyph-name="upgrade" data-tags="upgrade" d="M418.808 393.519l33.233 110.114-87.468 64.162h109.292l33.476 111.261 33.476-111.261h109.295l-87.714-64.162 33.229-110.114-88.286 68.348-88.533-68.348zM294.008 44.309v281.186c-27.023 26.586-48 57.711-62.933 93.376s-22.4 73.818-22.4 114.462c0 83.255 28.964 153.847 86.892 211.775s128.521 86.892 211.775 86.892c83.255 0 153.847-28.964 211.776-86.892s86.891-128.521 86.891-211.775c0-40.644-7.467-78.797-22.4-114.462s-35.913-66.79-62.933-93.376v-281.186l-213.333 63.996-213.333-63.996zM507.341 277.333c71.113 0 131.554 24.887 181.333 74.667s74.667 110.221 74.667 181.333c0 71.111-24.887 131.555-74.667 181.333s-110.221 74.667-181.333 74.667c-71.113 0-131.555-24.889-181.333-74.667s-74.667-110.222-74.667-181.333c0-71.113 24.889-131.554 74.667-181.333s110.22-74.667 181.333-74.667zM336.675 104.781l170.666 47.834 170.667-47.834v184.777c-23.795-17.502-50.227-31.027-79.3-40.572-29.077-9.545-59.529-14.319-91.366-14.319-31.834 0-62.289 4.774-91.364 14.319-29.073 9.545-55.507 23.070-79.302 40.572v-184.777z" />
|
||||
<glyph unicode="" glyph-name="test-tube" data-tags="test-tube" d="M610.718 768l241.361-241.361-30.17-30.17-30.17 30.17-362.039-362.035c-49.988-49.988-131.033-49.988-181.020 0-49.987 49.984-49.987 131.029 0 181.018l362.038 362.039-30.17 30.17 30.17 30.17zM640.887 677.49l-235.11-235.11h241.36l114.432 114.43-120.683 120.68z" />
|
||||
<glyph unicode="" glyph-name="storage" data-tags="storage" d="M239.59 106.667c-19.637 0-36.034 6.579-49.19 19.733s-19.733 29.551-19.733 49.19v544.82c0 19.637 6.578 36.034 19.733 49.189s29.552 19.733 49.19 19.733h544.82c19.639 0 36.036-6.578 49.19-19.733s19.733-29.552 19.733-49.189v-544.82c0-19.639-6.579-36.036-19.733-49.19s-29.551-19.733-49.19-19.733h-544.82zM213.333 576.082h597.333v144.329c0 6.563-2.735 12.58-8.205 18.051s-11.486 8.205-18.052 8.205h-544.82c-6.564 0-12.581-2.735-18.051-8.205s-8.205-11.488-8.205-18.051v-144.329zM213.333 362.586h597.333v170.83h-597.333v-170.83zM239.59 149.333h544.82c6.566 0 12.582 2.735 18.052 8.205s8.205 11.486 8.205 18.052v144.329h-597.333v-144.329c0-6.566 2.735-12.582 8.205-18.052s11.488-8.205 18.051-8.205zM426.667 640v55.959h170.667v-55.959h-170.667zM426.667 426.667v55.795h170.667v-55.795h-170.667zM426.667 213.333v55.629h170.667v-55.629h-170.667z" />
|
||||
<glyph unicode="" glyph-name="outbound" data-tags="outbound" d="M264.043 222.357l-29.376 29.376 430.11 430.933h-396.467v42.667h469.333v-469.333h-42.667v396.469l-430.933-430.112z" />
|
||||
</font></defs></svg>
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 128 KiB |
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.ttf
vendored
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.ttf
vendored
Binary file not shown.
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.woff
vendored
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.woff
vendored
Binary file not shown.
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.woff2
vendored
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.woff2
vendored
Binary file not shown.
24
vendor/assets/stylesheets/sn-icon-font.css
vendored
24
vendor/assets/stylesheets/sn-icon-font.css
vendored
|
@ -1,11 +1,11 @@
|
|||
@font-face {
|
||||
font-family: 'SN-icon-font';
|
||||
src: url('fonts/SN-icon-font.eot?m1g5fz');
|
||||
src: url('fonts/SN-icon-font.eot?m1g5fz#iefix') format('embedded-opentype'),
|
||||
url('fonts/SN-icon-font.woff2?m1g5fz') format('woff2'),
|
||||
url('fonts/SN-icon-font.ttf?m1g5fz') format('truetype'),
|
||||
url('fonts/SN-icon-font.woff?m1g5fz') format('woff'),
|
||||
url('fonts/SN-icon-font.svg?m1g5fz#SN-icon-font') format('svg');
|
||||
src: url('fonts/SN-icon-font.eot?9ywu8k');
|
||||
src: url('fonts/SN-icon-font.eot?9ywu8k#iefix') format('embedded-opentype'),
|
||||
url('fonts/SN-icon-font.woff2?9ywu8k') format('woff2'),
|
||||
url('fonts/SN-icon-font.ttf?9ywu8k') format('truetype'),
|
||||
url('fonts/SN-icon-font.woff?9ywu8k') format('woff'),
|
||||
url('fonts/SN-icon-font.svg?9ywu8k#SN-icon-font') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
|
@ -533,3 +533,15 @@
|
|||
.sn-icon-undo:before {
|
||||
content: "\e9a8";
|
||||
}
|
||||
.sn-icon-upgrade:before {
|
||||
content: "\e9a9";
|
||||
}
|
||||
.sn-icon-test-tube:before {
|
||||
content: "\e9aa";
|
||||
}
|
||||
.sn-icon-storage:before {
|
||||
content: "\e9ab";
|
||||
}
|
||||
.sn-icon-outbound:before {
|
||||
content: "\e9ac";
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue