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 develop
This commit is contained in:
commit
2039a65e9b
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -95,3 +95,7 @@ public/marvin4js-license.cxl
|
||||||
|
|
||||||
/app/assets/builds/*
|
/app/assets/builds/*
|
||||||
!/app/assets/builds/.keep
|
!/app/assets/builds/.keep
|
||||||
|
|
||||||
|
# Ignore automatically generated js-routes files.
|
||||||
|
/app/javascript/routes.js
|
||||||
|
/app/javascript/routes.d.ts
|
||||||
|
|
|
@ -13,6 +13,7 @@ before_install:
|
||||||
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 > docker-compose
|
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 > docker-compose
|
||||||
- chmod +x docker-compose
|
- chmod +x docker-compose
|
||||||
- sudo mv docker-compose /usr/local/bin
|
- sudo mv docker-compose /usr/local/bin
|
||||||
|
- sudo chown --recursive 1000 .
|
||||||
- make docker-ci
|
- make docker-ci
|
||||||
script:
|
script:
|
||||||
- make tests-ci
|
- make tests-ci
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM ruby:3.2.2-bookworm
|
FROM ruby:3.2.5-bookworm
|
||||||
MAINTAINER SciNote <info@scinote.net>
|
MAINTAINER SciNote <info@scinote.net>
|
||||||
|
|
||||||
# additional dependecies
|
# additional dependecies
|
||||||
|
@ -20,7 +20,8 @@ RUN apt-get update -qq && \
|
||||||
fonts-wqy-microhei \
|
fonts-wqy-microhei \
|
||||||
fonts-wqy-zenhei \
|
fonts-wqy-zenhei \
|
||||||
libfile-mimeinfo-perl \
|
libfile-mimeinfo-perl \
|
||||||
chromium-driver \
|
chromium \
|
||||||
|
chromium-sandbox \
|
||||||
yarnpkg && \
|
yarnpkg && \
|
||||||
ln -s /usr/lib/x86_64-linux-gnu/libvips.so.42 /usr/lib/x86_64-linux-gnu/libvips.so && \
|
ln -s /usr/lib/x86_64-linux-gnu/libvips.so.42 /usr/lib/x86_64-linux-gnu/libvips.so && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
@ -35,6 +36,10 @@ ENV BUNDLE_PATH /usr/local/bundle/
|
||||||
ENV APP_HOME /usr/src/app
|
ENV APP_HOME /usr/src/app
|
||||||
ENV PATH $APP_HOME/bin:$PATH
|
ENV PATH $APP_HOME/bin:$PATH
|
||||||
RUN mkdir $APP_HOME
|
RUN mkdir $APP_HOME
|
||||||
|
RUN adduser --uid 1000 scinote
|
||||||
|
RUN chown scinote:scinote $APP_HOME
|
||||||
|
USER scinote
|
||||||
|
ENV CHROMIUM_PATH /usr/bin/chromium
|
||||||
WORKDIR $APP_HOME
|
WORKDIR $APP_HOME
|
||||||
|
|
||||||
CMD rails s -b 0.0.0.0
|
CMD rails s -b 0.0.0.0
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Building stage
|
# Building stage
|
||||||
FROM ruby:3.2.2-bookworm AS builder
|
FROM ruby:3.2.5-bookworm AS builder
|
||||||
|
|
||||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||||
RUN \
|
RUN \
|
||||||
|
@ -23,7 +23,7 @@ COPY . $APP_HOME
|
||||||
RUN rm -f $APP_HOME/config/application.yml $APP_HOME/production.env
|
RUN rm -f $APP_HOME/config/application.yml $APP_HOME/production.env
|
||||||
WORKDIR $APP_HOME
|
WORKDIR $APP_HOME
|
||||||
RUN \
|
RUN \
|
||||||
--mount=target=$APP_HOME/tmp/bundle,type=cache \
|
--mount=target=/usr/src/app/tmp/bundle,type=cache \
|
||||||
bundle config set without 'development test' && \
|
bundle config set without 'development test' && \
|
||||||
bundle config set path '/usr/src/app/tmp/bundle' && \
|
bundle config set path '/usr/src/app/tmp/bundle' && \
|
||||||
bundle install --jobs `nproc` && \
|
bundle install --jobs `nproc` && \
|
||||||
|
@ -34,14 +34,14 @@ RUN \
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
--mount=type=cache,target=/usr/local/share/.cache/yarn/v6,sharing=locked \
|
--mount=type=cache,target=/usr/local/share/.cache/yarn/v6,sharing=locked \
|
||||||
--mount=type=cache,target=$APP_HOME/node_modules,sharing=locked \
|
--mount=type=cache,target=/usr/src/app/node_modules,sharing=locked \
|
||||||
DATABASE_URL=postgresql://postgres@db/scinote_production \
|
DATABASE_URL=postgresql://postgres@db/scinote_production \
|
||||||
SECRET_KEY_BASE=dummy \
|
SECRET_KEY_BASE=dummy \
|
||||||
DEFACE_ENABLED=true \
|
DEFACE_ENABLED=true \
|
||||||
bash -c "rake assets:precompile && rake deface:precompile"
|
bash -c "rake assets:precompile && rake deface:precompile && rm -rf ./tmp/cache"
|
||||||
|
|
||||||
# Final stage
|
# Final stage
|
||||||
FROM ruby:3.2.2-bookworm AS runner
|
FROM ruby:3.2.5-bookworm AS runner
|
||||||
MAINTAINER SciNote <info@scinote.net>
|
MAINTAINER SciNote <info@scinote.net>
|
||||||
|
|
||||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||||
|
@ -76,6 +76,7 @@ RUN \
|
||||||
libvips42 \
|
libvips42 \
|
||||||
graphviz \
|
graphviz \
|
||||||
chromium \
|
chromium \
|
||||||
|
chromium-sandbox \
|
||||||
libfile-mimeinfo-perl \
|
libfile-mimeinfo-perl \
|
||||||
yarnpkg && \
|
yarnpkg && \
|
||||||
/usr/share/nodejs/yarn/bin/yarn add puppeteer@npm:puppeteer-core@^22.15.0 && \
|
/usr/share/nodejs/yarn/bin/yarn add puppeteer@npm:puppeteer-core@^22.15.0 && \
|
||||||
|
@ -98,8 +99,12 @@ ENV GEM_HOME=$APP_HOME/vendor/bundle/ruby/3.2.0
|
||||||
ENV PATH=$GEM_HOME/bin:$PATH
|
ENV PATH=$GEM_HOME/bin:$PATH
|
||||||
ENV BUNDLE_APP_CONFIG=.bundle
|
ENV BUNDLE_APP_CONFIG=.bundle
|
||||||
|
|
||||||
COPY --from=builder $APP_HOME $APP_HOME
|
RUN adduser --uid 1000 scinote
|
||||||
|
USER scinote
|
||||||
|
|
||||||
|
COPY --from=builder --chown=scinote:scinote $APP_HOME $APP_HOME
|
||||||
|
|
||||||
|
ENV CHROMIUM_PATH /usr/bin/chromium
|
||||||
WORKDIR $APP_HOME
|
WORKDIR $APP_HOME
|
||||||
|
|
||||||
CMD rails s -b 0.0.0.0
|
CMD rails s -b 0.0.0.0
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
source 'http://rubygems.org'
|
source 'http://rubygems.org'
|
||||||
|
|
||||||
ruby '3.2.2'
|
ruby '~> 3.2.2'
|
||||||
|
|
||||||
gem 'activerecord-session_store'
|
gem 'activerecord-session_store'
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
|
@ -94,6 +94,7 @@ gem 'graphviz'
|
||||||
|
|
||||||
gem 'cssbundling-rails'
|
gem 'cssbundling-rails'
|
||||||
gem 'jsbundling-rails'
|
gem 'jsbundling-rails'
|
||||||
|
gem 'js-routes'
|
||||||
|
|
||||||
gem 'tailwindcss-rails', '~> 2.4'
|
gem 'tailwindcss-rails', '~> 2.4'
|
||||||
|
|
||||||
|
|
|
@ -388,6 +388,8 @@ GEM
|
||||||
rails-dom-testing (>= 1, < 3)
|
rails-dom-testing (>= 1, < 3)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
thor (>= 0.14, < 2.0)
|
thor (>= 0.14, < 2.0)
|
||||||
|
js-routes (2.2.8)
|
||||||
|
railties (>= 4)
|
||||||
jsbundling-rails (1.1.1)
|
jsbundling-rails (1.1.1)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
json (2.6.3)
|
json (2.6.3)
|
||||||
|
@ -828,6 +830,7 @@ DEPENDENCIES
|
||||||
image_processing
|
image_processing
|
||||||
img2zpl!
|
img2zpl!
|
||||||
jbuilder
|
jbuilder
|
||||||
|
js-routes
|
||||||
jsbundling-rails
|
jsbundling-rails
|
||||||
json-jwt
|
json-jwt
|
||||||
json_matchers
|
json_matchers
|
||||||
|
@ -898,4 +901,4 @@ RUBY VERSION
|
||||||
ruby 3.2.2p53
|
ruby 3.2.2p53
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.4.10
|
2.5.11
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -22,13 +22,13 @@ heroku:
|
||||||
@echo "Set environment variables, DATABASE_URL, RAILS_SERVE_STATIC_FILES, RAKE_ENV, RAILS_ENV, SECRET_KEY_BASE"
|
@echo "Set environment variables, DATABASE_URL, RAILS_SERVE_STATIC_FILES, RAKE_ENV, RAILS_ENV, SECRET_KEY_BASE"
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
@docker-compose build
|
@docker-compose --progress plain build
|
||||||
|
|
||||||
docker-ci:
|
docker-ci:
|
||||||
@docker-compose --progress plain build web
|
@docker-compose --progress plain build web
|
||||||
|
|
||||||
docker-production:
|
docker-production:
|
||||||
@docker-compose -f docker-compose.production.yml build --build-arg BUILD_TIMESTAMP=$(BUILD_TIMESTAMP)
|
@docker-compose --progress plain -f docker-compose.production.yml build --build-arg BUILD_TIMESTAMP=$(BUILD_TIMESTAMP)
|
||||||
|
|
||||||
config-production:
|
config-production:
|
||||||
ifeq (production.env,$(wildcard production.env))
|
ifeq (production.env,$(wildcard production.env))
|
||||||
|
|
2
Rakefile
2
Rakefile
|
@ -5,3 +5,5 @@ require File.expand_path('../config/application', __FILE__)
|
||||||
|
|
||||||
Rails.application.load_tasks
|
Rails.application.load_tasks
|
||||||
Doorkeeper::Rake.load_tasks
|
Doorkeeper::Rake.load_tasks
|
||||||
|
# Update js-routes file before javascript build
|
||||||
|
task 'javascript:build' => 'js:routes:typescript'
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
@import "tailwind/buttons";
|
@import "tailwind/buttons";
|
||||||
@import "tailwind/modals";
|
@import "tailwind/modals";
|
||||||
@import "tailwind/flyouts";
|
@import "tailwind/flyouts";
|
||||||
|
@import "tailwind/radio";
|
||||||
@import "tailwind/loader.css";
|
@import "tailwind/loader.css";
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
|
@ -69,6 +70,6 @@ html {
|
||||||
|
|
||||||
@keyframes shine-lines {
|
@keyframes shine-lines {
|
||||||
0% { background-position: -150px }
|
0% { background-position: -150px }
|
||||||
|
|
||||||
40%, 100% { background-position: 320px }
|
40%, 100% { background-position: 320px }
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,5 +26,11 @@
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin: 0 16px 10px;
|
margin: 0 16px 10px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminders-view-mode {
|
||||||
|
.row-reminders-footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// scss-lint:disable SelectorDepth QualifyingElement
|
// scss-lint:disable SelectorDepth QualifyingElement
|
||||||
|
/*
|
||||||
:root {
|
:root {
|
||||||
--sci-radio-size: 16px;
|
--sci-radio-size: 16px;
|
||||||
}
|
}
|
||||||
|
@ -85,3 +85,4 @@ input[type="radio"].sci-radio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,18 +10,17 @@ class RepositoriesController < ApplicationController
|
||||||
include MyModulesHelper
|
include MyModulesHelper
|
||||||
|
|
||||||
before_action :load_repository, except: %i(index create create_modal sidebar archive restore actions_toolbar
|
before_action :load_repository, except: %i(index create create_modal sidebar archive restore actions_toolbar
|
||||||
export_modal export_repositories)
|
export_modal export_repositories list)
|
||||||
before_action :load_repositories, only: :index
|
before_action :load_repositories, only: %i(index list)
|
||||||
before_action :load_repositories_for_archiving, only: :archive
|
before_action :load_repositories_for_archiving, only: :archive
|
||||||
before_action :load_repositories_for_restoring, only: :restore
|
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
|
before_action :check_view_permissions, except: %i(index create_modal create update destroy parse_sheet
|
||||||
import_records sidebar archive restore actions_toolbar
|
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_manage_permissions, only: %i(rename_modal update)
|
||||||
before_action :check_delete_permissions, only: %i(destroy destroy_modal)
|
before_action :check_delete_permissions, only: %i(destroy destroy_modal)
|
||||||
before_action :check_archive_permissions, only: %i(archive restore)
|
before_action :check_archive_permissions, only: %i(archive restore)
|
||||||
before_action :check_share_permissions, only: :share_modal
|
|
||||||
before_action :check_create_permissions, only: %i(create_modal create)
|
before_action :check_create_permissions, only: %i(create_modal create)
|
||||||
before_action :check_copy_permissions, only: %i(copy_modal copy)
|
before_action :check_copy_permissions, only: %i(copy_modal copy)
|
||||||
before_action :set_inline_name_editing, only: %i(show)
|
before_action :set_inline_name_editing, only: %i(show)
|
||||||
|
@ -44,6 +43,16 @@ class RepositoriesController < ApplicationController
|
||||||
end
|
end
|
||||||
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
|
def sidebar
|
||||||
render json: {
|
render json: {
|
||||||
html: render_to_string(partial: 'repositories/sidebar', locals: {
|
html: render_to_string(partial: 'repositories/sidebar', locals: {
|
||||||
|
@ -101,15 +110,6 @@ class RepositoriesController < ApplicationController
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def share_modal
|
|
||||||
render json: { html: render_to_string(partial: 'share_repository_modal', formats: :html) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def shareable_teams
|
|
||||||
teams = current_user.teams.order(:name) - [@repository.team]
|
|
||||||
render json: teams, each_serializer: ShareableTeamSerializer, repository: @repository
|
|
||||||
end
|
|
||||||
|
|
||||||
def hide_reminders
|
def hide_reminders
|
||||||
# synchronously hide currently visible reminders
|
# synchronously hide currently visible reminders
|
||||||
if params[:visible_reminder_repository_row_ids].present?
|
if params[:visible_reminder_repository_row_ids].present?
|
||||||
|
@ -522,10 +522,6 @@ class RepositoriesController < ApplicationController
|
||||||
render_403 unless can_delete_repository?(@repository)
|
render_403 unless can_delete_repository?(@repository)
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_share_permissions
|
|
||||||
render_403 unless can_share_repository?(@repository)
|
|
||||||
end
|
|
||||||
|
|
||||||
def repository_params
|
def repository_params
|
||||||
params.require(:repository).permit(:name)
|
params.require(:repository).permit(:name)
|
||||||
end
|
end
|
||||||
|
|
|
@ -328,7 +328,7 @@ class RepositoryRowsController < ApplicationController
|
||||||
def active_reminder_repository_cells
|
def active_reminder_repository_cells
|
||||||
reminder_cells = @repository_row.repository_cells.with_active_reminder(current_user).distinct
|
reminder_cells = @repository_row.repository_cells.with_active_reminder(current_user).distinct
|
||||||
render json: {
|
render json: {
|
||||||
html: render_to_string(partial: 'shared/repository_row_reminder', locals: {
|
html: render_to_string(partial: 'shared/repository_row_reminder', formats: :html, locals: {
|
||||||
reminders: reminder_cells
|
reminders: reminder_cells
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
142
app/controllers/storage_location_repository_rows_controller.rb
Normal file
142
app/controllers/storage_location_repository_rows_controller.rb
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class StorageLocationRepositoryRowsController < ApplicationController
|
||||||
|
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, 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, params
|
||||||
|
).call
|
||||||
|
render json: storage_location_repository_row,
|
||||||
|
each_serializer: Lists::StorageLocationRepositoryRowSerializer,
|
||||||
|
meta: (pagination_dict(storage_location_repository_row) unless @storage_location.with_grid?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@storage_location_repository_row = StorageLocationRepositoryRow.new(
|
||||||
|
repository_row: @repository_row,
|
||||||
|
storage_location: @storage_location,
|
||||||
|
metadata: storage_location_repository_row_params[:metadata] || {},
|
||||||
|
created_by: current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
if @storage_location_repository_row.save
|
||||||
|
log_activity(:storage_location_repository_row_created)
|
||||||
|
render json: @storage_location_repository_row,
|
||||||
|
serializer: Lists::StorageLocationRepositoryRowSerializer
|
||||||
|
else
|
||||||
|
render json: @storage_location_repository_row.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@storage_location_repository_row.update(storage_location_repository_row_params)
|
||||||
|
|
||||||
|
if @storage_location_repository_row.save
|
||||||
|
log_activity(:storage_location_repository_row_moved)
|
||||||
|
render json: @storage_location_repository_row,
|
||||||
|
serializer: Lists::StorageLocationRepositoryRowSerializer
|
||||||
|
else
|
||||||
|
render json: @storage_location_repository_row.errors, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
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
|
||||||
|
)
|
||||||
|
log_activity(:storage_location_repository_row_moved)
|
||||||
|
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
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
if @storage_location_repository_row.discard
|
||||||
|
log_activity(:storage_location_repository_row_deleted)
|
||||||
|
render json: {}
|
||||||
|
else
|
||||||
|
render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def actions_toolbar
|
||||||
|
render json: {
|
||||||
|
actions: Toolbars::StorageLocationRepositoryRowsService.new(
|
||||||
|
current_user,
|
||||||
|
items_ids: JSON.parse(params[:items]).pluck('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]
|
||||||
|
)
|
||||||
|
render_404 unless @storage_location_repository_row
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_storage_location
|
||||||
|
@storage_location = StorageLocation.viewable_by_user(current_user).find(
|
||||||
|
storage_location_repository_row_params[:storage_location_id]
|
||||||
|
)
|
||||||
|
render_404 unless @storage_location
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_repository_row
|
||||||
|
@repository_row = RepositoryRow.find(storage_location_repository_row_params[:repository_row_id])
|
||||||
|
render_404 unless @repository_row
|
||||||
|
end
|
||||||
|
|
||||||
|
def storage_location_repository_row_params
|
||||||
|
params.permit(:id, :storage_location_id, :repository_row_id,
|
||||||
|
metadata: { position: [] })
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_read_permissions
|
||||||
|
render_403 unless can_read_storage_location?(@storage_location)
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_manage_permissions
|
||||||
|
render_403 unless can_manage_storage_location?(@storage_location)
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_activity(type_of, message_items = {})
|
||||||
|
Activities::CreateActivityService
|
||||||
|
.call(activity_type: type_of,
|
||||||
|
owner: current_user,
|
||||||
|
team: @storage_location.team,
|
||||||
|
subject: @storage_location_repository_row.repository_row,
|
||||||
|
message_items: {
|
||||||
|
storage_location: @storage_location_repository_row.storage_location_id,
|
||||||
|
repository_row: @storage_location_repository_row.repository_row_id,
|
||||||
|
position: @storage_location_repository_row.human_readable_position,
|
||||||
|
user: current_user.id
|
||||||
|
}.merge(message_items))
|
||||||
|
end
|
||||||
|
end
|
259
app/controllers/storage_locations_controller.rb
Normal file
259
app/controllers/storage_locations_controller.rb
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class StorageLocationsController < ApplicationController
|
||||||
|
before_action :check_storage_locations_enabled, except: :unassign_rows
|
||||||
|
before_action :load_storage_location, only: %i(update destroy duplicate move show available_positions unassign_rows export_container import_container)
|
||||||
|
before_action :check_read_permissions, except: %i(index create tree actions_toolbar)
|
||||||
|
before_action :check_create_permissions, only: :create
|
||||||
|
before_action :check_manage_permissions, only: %i(update destroy duplicate move unassign_rows import_container)
|
||||||
|
before_action :set_breadcrumbs_items, only: %i(index show)
|
||||||
|
|
||||||
|
def index
|
||||||
|
respond_to do |format|
|
||||||
|
format.html
|
||||||
|
format.json do
|
||||||
|
storage_locations = Lists::StorageLocationsService.new(current_user, current_team, params).call
|
||||||
|
render json: storage_locations, each_serializer: Lists::StorageLocationSerializer,
|
||||||
|
user: current_user, meta: pagination_dict(storage_locations)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@storage_location = StorageLocation.new(
|
||||||
|
storage_location_params.merge({ created_by: current_user })
|
||||||
|
)
|
||||||
|
|
||||||
|
@storage_location.team = @storage_location.root_storage_location.team || current_team
|
||||||
|
|
||||||
|
@storage_location.image.attach(params[:signed_blob_id]) if params[:signed_blob_id]
|
||||||
|
|
||||||
|
if @storage_location.save
|
||||||
|
log_activity('storage_location_created')
|
||||||
|
render json: @storage_location, serializer: Lists::StorageLocationSerializer
|
||||||
|
else
|
||||||
|
render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@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
|
||||||
|
log_activity('storage_location_edited')
|
||||||
|
render json: @storage_location, serializer: Lists::StorageLocationSerializer
|
||||||
|
else
|
||||||
|
render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
if @storage_location.discard
|
||||||
|
log_activity('storage_location_deleted')
|
||||||
|
render json: {}
|
||||||
|
else
|
||||||
|
render json: { error: @storage_location.errors.full_messages }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def duplicate
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
new_storage_location = @storage_location.duplicate!
|
||||||
|
if new_storage_location
|
||||||
|
@storage_location = new_storage_location
|
||||||
|
log_activity('storage_location_created')
|
||||||
|
render json: @storage_location, serializer: Lists::StorageLocationSerializer
|
||||||
|
else
|
||||||
|
render json: { errors: :failed }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def move
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
original_storage_location = @storage_location.parent
|
||||||
|
destination_storage_location =
|
||||||
|
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: destination_storage_location)
|
||||||
|
|
||||||
|
log_activity('storage_location_moved', { storage_location_original: original_storage_location.id, storage_location_destination: destination_storage_location.id })
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@storage_location_repository_rows = @storage_location.storage_location_repository_rows.where(id: params[:ids])
|
||||||
|
@storage_location_repository_rows.each(&:discard)
|
||||||
|
log_unassign_activities
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: { status: :ok }
|
||||||
|
end
|
||||||
|
|
||||||
|
def export_container
|
||||||
|
xlsx = StorageLocations::ExportService.new(@storage_location, current_user).to_xlsx
|
||||||
|
|
||||||
|
send_data(
|
||||||
|
xlsx,
|
||||||
|
filename: "#{@storage_location.name.gsub(/\s/, '_')}_export_#{Date.current}.xlsx",
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_container
|
||||||
|
result = StorageLocations::ImportService.new(@storage_location, params[:file], current_user).import_items
|
||||||
|
if result[:status] == :ok
|
||||||
|
render json: result
|
||||||
|
else
|
||||||
|
render json: result, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def actions_toolbar
|
||||||
|
render json: {
|
||||||
|
actions:
|
||||||
|
Toolbars::StorageLocationsService.new(
|
||||||
|
current_user,
|
||||||
|
storage_location_ids: JSON.parse(params[:items]).pluck('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, :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.viewable_by_user(current_user).find_by(id: storage_location_params[:id])
|
||||||
|
render_404 unless @storage_location
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_read_permissions
|
||||||
|
render_403 unless can_read_storage_location?(@storage_location)
|
||||||
|
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 can_manage_storage_location?(@storage_location)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_breadcrumbs_items
|
||||||
|
@breadcrumbs_items = []
|
||||||
|
|
||||||
|
@breadcrumbs_items.push({
|
||||||
|
label: t('breadcrumbs.inventories')
|
||||||
|
})
|
||||||
|
|
||||||
|
@breadcrumbs_items.push({
|
||||||
|
label: t('breadcrumbs.locations'),
|
||||||
|
url: storage_locations_path
|
||||||
|
})
|
||||||
|
|
||||||
|
storage_locations = []
|
||||||
|
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
|
||||||
|
location = location.parent
|
||||||
|
storage_locations.unshift(breadcrumbs_item(location))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@breadcrumbs_items += storage_locations
|
||||||
|
end
|
||||||
|
|
||||||
|
def breadcrumbs_item(location)
|
||||||
|
{
|
||||||
|
label: location.name,
|
||||||
|
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
|
||||||
|
|
||||||
|
def log_activity(type_of, message_items = {})
|
||||||
|
Activities::CreateActivityService
|
||||||
|
.call(activity_type: "#{'container_' if @storage_location.container}#{type_of}",
|
||||||
|
owner: current_user,
|
||||||
|
team: @storage_location.team,
|
||||||
|
subject: @storage_location,
|
||||||
|
message_items: {
|
||||||
|
storage_location: @storage_location.id,
|
||||||
|
user: current_user.id
|
||||||
|
}.merge(message_items))
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_unassign_activities
|
||||||
|
@storage_location_repository_rows.each do |storage_location_repository_row|
|
||||||
|
Activities::CreateActivityService
|
||||||
|
.call(activity_type: :storage_location_repository_row_deleted,
|
||||||
|
owner: current_user,
|
||||||
|
team: @storage_location.team,
|
||||||
|
subject: storage_location_repository_row.repository_row,
|
||||||
|
message_items: {
|
||||||
|
storage_location: storage_location_repository_row.storage_location_id,
|
||||||
|
repository_row: storage_location_repository_row.repository_row_id,
|
||||||
|
position: storage_location_repository_row.human_readable_position,
|
||||||
|
user: current_user.id
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
165
app/controllers/team_shared_objects_controller.rb
Normal file
165
app/controllers/team_shared_objects_controller.rb
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TeamSharedObjectsController < ApplicationController
|
||||||
|
before_action :load_vars
|
||||||
|
before_action :check_sharing_permissions
|
||||||
|
|
||||||
|
def update
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@activities_to_log = []
|
||||||
|
|
||||||
|
# Global share
|
||||||
|
if @model.globally_shareable?
|
||||||
|
permission_level =
|
||||||
|
if params[:select_all_teams]
|
||||||
|
params[:select_all_write_permission] ? :shared_write : :shared_read
|
||||||
|
else
|
||||||
|
:not_shared
|
||||||
|
end
|
||||||
|
|
||||||
|
@model.permission_level = permission_level
|
||||||
|
|
||||||
|
if @model.permission_level_changed?
|
||||||
|
@model.save!
|
||||||
|
@model.team_shared_objects.each(&:destroy!) unless permission_level == :not_shared
|
||||||
|
case @model
|
||||||
|
when Repository
|
||||||
|
setup_repository_global_share_activity
|
||||||
|
end
|
||||||
|
|
||||||
|
log_activities and next
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Share to specific teams
|
||||||
|
params[:team_share_params].each do |t|
|
||||||
|
@model.update!(permission_level: :not_shared) if @model.globally_shareable?
|
||||||
|
|
||||||
|
team_shared_object = @model.team_shared_objects.find_or_initialize_by(team_id: t['id'])
|
||||||
|
|
||||||
|
new_record = team_shared_object.new_record?
|
||||||
|
team_shared_object.update!(
|
||||||
|
permission_level: t['private_shared_with_write'] ? :shared_write : :shared_read
|
||||||
|
)
|
||||||
|
|
||||||
|
setup_team_share_activity(team_shared_object, new_record) if team_shared_object.saved_changes?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unshare
|
||||||
|
@model.team_shared_objects.where.not(
|
||||||
|
team_id: params[:team_share_params].filter { |t| t['private_shared_with'] }.pluck('id')
|
||||||
|
).each do |team_shared_object|
|
||||||
|
team_shared_object.destroy!
|
||||||
|
setup_team_share_activity(team_shared_object, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
log_activities
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def shareable_teams
|
||||||
|
teams = current_user.teams.order(:name) - [@model.team]
|
||||||
|
render json: teams, each_serializer: ShareableTeamSerializer, model: @model
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_vars
|
||||||
|
case params[:object_type]
|
||||||
|
when 'Repository'
|
||||||
|
@model = Repository.viewable_by_user(current_user).find_by(id: params[:object_id])
|
||||||
|
when 'StorageLocation'
|
||||||
|
@model = StorageLocation.viewable_by_user(current_user).find_by(id: params[:object_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
render_404 unless @model
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_params
|
||||||
|
params.permit(:team_id, :object_type, :object_id, :target_team_id, :permission_level)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_params
|
||||||
|
params.permit(:team_id, :id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_params
|
||||||
|
params.permit(permission_changes: {}, share_team_ids: [], write_permissions: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_sharing_permissions
|
||||||
|
object_name = @model.is_a?(RepositoryBase) ? 'repository' : @model.model_name.param_key
|
||||||
|
render_403 unless public_send("can_share_#{object_name}?", @model)
|
||||||
|
render_403 if !@model.shareable_write? && update_params[:write_permissions].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def share_all_params
|
||||||
|
{
|
||||||
|
shared_with_all: params[:select_all_teams].present?,
|
||||||
|
shared_permissions_level: params[:select_all_write_permission].present? ? 'shared_write' : 'shared_read'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_team_share_activity(team_shared_object, new_record)
|
||||||
|
type =
|
||||||
|
case @model
|
||||||
|
when Repository
|
||||||
|
if team_shared_object.destroyed?
|
||||||
|
:unshare_inventory
|
||||||
|
elsif new_record
|
||||||
|
:share_inventory
|
||||||
|
else
|
||||||
|
:update_share_inventory
|
||||||
|
end
|
||||||
|
when StorageLocation
|
||||||
|
if team_shared_object.destroyed?
|
||||||
|
"#{'container_' if @model.container?}storage_location_unshared"
|
||||||
|
elsif new_record
|
||||||
|
"#{'container_' if @model.container?}storage_location_shared"
|
||||||
|
else
|
||||||
|
"#{'container_' if @model.container?}storage_location_sharing_updated"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@activities_to_log << {
|
||||||
|
type: type,
|
||||||
|
message_items: {
|
||||||
|
@model.model_name.param_key.to_sym => team_shared_object.shared_object.id,
|
||||||
|
team: team_shared_object.team.id,
|
||||||
|
permission_level: Extends::SHARED_INVENTORIES_PL_MAPPINGS[team_shared_object.permission_level.to_sym]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def setup_repository_global_share_activity
|
||||||
|
message_items = {
|
||||||
|
repository: @model.id,
|
||||||
|
team: @model.team.id,
|
||||||
|
permission_level: Extends::SHARED_INVENTORIES_PL_MAPPINGS[@model.permission_level.to_sym]
|
||||||
|
}
|
||||||
|
|
||||||
|
activity_params =
|
||||||
|
if @model.saved_changes['permission_level'][0] == 'not_shared'
|
||||||
|
{ type: :share_inventory_with_all, message_items: message_items }
|
||||||
|
elsif @model.saved_changes['permission_level'][1] == 'not_shared'
|
||||||
|
{ type: :unshare_inventory_with_all, message_items: message_items }
|
||||||
|
else
|
||||||
|
{ type: :update_share_with_all_permission_level, message_items: message_items }
|
||||||
|
end
|
||||||
|
|
||||||
|
@activities_to_log << activity_params
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_activities
|
||||||
|
@activities_to_log.each do |activity_params|
|
||||||
|
Activities::CreateActivityService
|
||||||
|
.call(activity_type: activity_params[:type],
|
||||||
|
owner: current_user,
|
||||||
|
team: @model.team,
|
||||||
|
subject: @model,
|
||||||
|
message_items: {
|
||||||
|
user: current_user.id
|
||||||
|
}.merge(activity_params[:message_items]))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -17,8 +17,8 @@ module Users
|
||||||
next unless Extends::WHITELISTED_USER_SETTINGS.include?(key.to_s)
|
next unless Extends::WHITELISTED_USER_SETTINGS.include?(key.to_s)
|
||||||
|
|
||||||
case key.to_s
|
case key.to_s
|
||||||
when 'task_step_states'
|
when 'task_step_states', 'result_states'
|
||||||
update_task_step_states(data)
|
update_object_states(data, key.to_s)
|
||||||
else
|
else
|
||||||
current_user.settings[key] = data
|
current_user.settings[key] = data
|
||||||
end
|
end
|
||||||
|
@ -34,18 +34,18 @@ module Users
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def update_task_step_states(task_step_states_data)
|
def update_object_states(object_states_data, object_state_key)
|
||||||
current_states = current_user.settings.fetch('task_step_states', {})
|
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
|
if collapsed
|
||||||
current_states[step_id] = true
|
current_states[object_id] = true
|
||||||
else
|
else
|
||||||
current_states.delete(step_id)
|
current_states.delete(object_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
current_user.settings['task_step_states'] = current_states
|
current_user.settings[object_state_key] = current_states
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
11
app/helpers/active_storage_helper.rb
Normal file
11
app/helpers/active_storage_helper.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActiveStorageHelper
|
||||||
|
def image_preview_format(blob)
|
||||||
|
if ['image/jpeg', 'image/jpg'].include?(blob&.content_type)
|
||||||
|
:jpeg
|
||||||
|
else
|
||||||
|
:png
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -108,6 +108,8 @@ module GlobalActivitiesHelper
|
||||||
else
|
else
|
||||||
project_folder_path(obj, team: obj.team.id)
|
project_folder_path(obj, team: obj.team.id)
|
||||||
end
|
end
|
||||||
|
when StorageLocation
|
||||||
|
path = storage_location_path(obj)
|
||||||
else
|
else
|
||||||
return current_value
|
return current_value
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,8 +19,16 @@ module LeftMenuBarHelper
|
||||||
url: repositories_path,
|
url: repositories_path,
|
||||||
name: t('left_menu_bar.repositories'),
|
name: t('left_menu_bar.repositories'),
|
||||||
icon: 'sn-icon-inventory',
|
icon: 'sn-icon-inventory',
|
||||||
active: repositories_are_selected?,
|
active: repositories_are_selected? || storage_locations_are_selected?,
|
||||||
submenu: []
|
submenu: [{
|
||||||
|
url: repositories_path,
|
||||||
|
name: t('left_menu_bar.items'),
|
||||||
|
active: repositories_are_selected?
|
||||||
|
}, {
|
||||||
|
url: storage_locations_path,
|
||||||
|
name: t('left_menu_bar.locations'),
|
||||||
|
active: storage_locations_are_selected?
|
||||||
|
}]
|
||||||
}, {
|
}, {
|
||||||
url: "#",
|
url: "#",
|
||||||
name: t('left_menu_bar.templates'),
|
name: t('left_menu_bar.templates'),
|
||||||
|
@ -63,6 +71,10 @@ module LeftMenuBarHelper
|
||||||
controller_name == 'repositories'
|
controller_name == 'repositories'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def storage_locations_are_selected?
|
||||||
|
controller_name == 'storage_locations'
|
||||||
|
end
|
||||||
|
|
||||||
def protocols_are_selected?
|
def protocols_are_selected?
|
||||||
controller_name == 'protocols'
|
controller_name == 'protocols'
|
||||||
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');
|
10
app/javascript/packs/vue/storage_locations_table.js
Normal file
10
app/javascript/packs/vue/storage_locations_table.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||||
|
import PerfectScrollbar from 'vue3-perfect-scrollbar';
|
||||||
|
import StorageLocations from '../../vue/storage_locations/table.vue';
|
||||||
|
import { mountWithTurbolinks } from './helpers/turbolinks.js';
|
||||||
|
|
||||||
|
const app = createApp();
|
||||||
|
app.component('StorageLocations', StorageLocations);
|
||||||
|
app.config.globalProperties.i18n = window.I18n;
|
||||||
|
app.use(PerfectScrollbar);
|
||||||
|
mountWithTurbolinks(app, '#storageLocationsTable');
|
|
@ -98,20 +98,19 @@ export default {
|
||||||
if (this.query === '') {
|
if (this.query === '') {
|
||||||
return this.foldersTree;
|
return this.foldersTree;
|
||||||
}
|
}
|
||||||
return this.foldersTree.map((folder) => (
|
return this.filteredFoldersTreeHelper(this.foldersTree);
|
||||||
{
|
|
||||||
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
|
|
||||||
));
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
selectFolder(folderId) {
|
||||||
this.selectedFolderId = folderId;
|
this.selectedFolderId = folderId;
|
||||||
},
|
},
|
||||||
|
|
|
@ -50,9 +50,10 @@
|
||||||
:repository="duplicateRepository"
|
:repository="duplicateRepository"
|
||||||
@close="duplicateRepository = null"
|
@close="duplicateRepository = null"
|
||||||
@duplicate="updateTable" />
|
@duplicate="updateTable" />
|
||||||
<ShareRepositoryModal
|
<ShareObjectModal
|
||||||
v-if="shareRepository"
|
v-if="shareRepository"
|
||||||
:repository="shareRepository"
|
:object="shareRepository"
|
||||||
|
:globalShareEnabled="true"
|
||||||
@close="shareRepository = null"
|
@close="shareRepository = null"
|
||||||
@share="updateTable" />
|
@share="updateTable" />
|
||||||
</template>
|
</template>
|
||||||
|
@ -66,7 +67,7 @@ import ExportRepositoryModal from './modals/export.vue';
|
||||||
import NewRepositoryModal from './modals/new.vue';
|
import NewRepositoryModal from './modals/new.vue';
|
||||||
import EditRepositoryModal from './modals/edit.vue';
|
import EditRepositoryModal from './modals/edit.vue';
|
||||||
import DuplicateRepositoryModal from './modals/duplicate.vue';
|
import DuplicateRepositoryModal from './modals/duplicate.vue';
|
||||||
import ShareRepositoryModal from './modals/share.vue';
|
import ShareObjectModal from '../shared/share_modal.vue';
|
||||||
import DataTable from '../shared/datatable/table.vue';
|
import DataTable from '../shared/datatable/table.vue';
|
||||||
import NameRenderer from './renderers/name.vue';
|
import NameRenderer from './renderers/name.vue';
|
||||||
|
|
||||||
|
@ -79,8 +80,8 @@ export default {
|
||||||
NewRepositoryModal,
|
NewRepositoryModal,
|
||||||
EditRepositoryModal,
|
EditRepositoryModal,
|
||||||
DuplicateRepositoryModal,
|
DuplicateRepositoryModal,
|
||||||
ShareRepositoryModal,
|
NameRenderer,
|
||||||
NameRenderer
|
ShareObjectModal
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
dataSource: {
|
dataSource: {
|
||||||
|
|
|
@ -312,6 +312,11 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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" data-e2e="e2e-CO-itemCard-locations">
|
||||||
|
<Locations :repositoryRow="repositoryRow" :repository="repository" @reloadRow="reload" />
|
||||||
|
</section>
|
||||||
<div v-if="!repository?.is_snapshot" id="divider" class="bg-sn-light-grey flex px-8 items-center self-stretch h-px "></div>
|
<div v-if="!repository?.is_snapshot" id="divider" class="bg-sn-light-grey flex px-8 items-center self-stretch h-px "></div>
|
||||||
|
|
||||||
<!-- QR -->
|
<!-- QR -->
|
||||||
|
@ -367,6 +372,7 @@ import ScrollSpy from './repository_values/ScrollSpy.vue';
|
||||||
import CustomColumns from './customColumns.vue';
|
import CustomColumns from './customColumns.vue';
|
||||||
import RepositoryItemSidebarTitle from './Title.vue';
|
import RepositoryItemSidebarTitle from './Title.vue';
|
||||||
import UnlinkModal from './unlink_modal.vue';
|
import UnlinkModal from './unlink_modal.vue';
|
||||||
|
import Locations from './locations.vue';
|
||||||
import axios from '../../packs/custom_axios.js';
|
import axios from '../../packs/custom_axios.js';
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
|
@ -405,6 +411,14 @@ const items = [
|
||||||
{
|
{
|
||||||
id: 'highlight-item-5',
|
id: 'highlight-item-5',
|
||||||
textId: 'text-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',
|
labelAlias: 'QR_label',
|
||||||
label: 'QR-label',
|
label: 'QR-label',
|
||||||
sectionId: 'qr-section',
|
sectionId: 'qr-section',
|
||||||
|
@ -416,6 +430,7 @@ export default {
|
||||||
name: 'RepositoryItemSidebar',
|
name: 'RepositoryItemSidebar',
|
||||||
components: {
|
components: {
|
||||||
CustomColumns,
|
CustomColumns,
|
||||||
|
Locations,
|
||||||
'repository-item-sidebar-title': RepositoryItemSidebarTitle,
|
'repository-item-sidebar-title': RepositoryItemSidebarTitle,
|
||||||
'inline-edit': InlineEdit,
|
'inline-edit': InlineEdit,
|
||||||
'scroll-spy': ScrollSpy,
|
'scroll-spy': ScrollSpy,
|
||||||
|
@ -433,6 +448,7 @@ export default {
|
||||||
repository: null,
|
repository: null,
|
||||||
defaultColumns: null,
|
defaultColumns: null,
|
||||||
customColumns: null,
|
customColumns: null,
|
||||||
|
repositoryRow: null,
|
||||||
parentsCount: 0,
|
parentsCount: 0,
|
||||||
childrenCount: 0,
|
childrenCount: 0,
|
||||||
parents: null,
|
parents: null,
|
||||||
|
@ -591,6 +607,7 @@ export default {
|
||||||
{ params: { my_module_id: this.myModuleId } }
|
{ params: { my_module_id: this.myModuleId } }
|
||||||
).then((response) => {
|
).then((response) => {
|
||||||
const result = response.data;
|
const result = response.data;
|
||||||
|
this.repositoryRow = result;
|
||||||
this.repositoryRowId = result.id;
|
this.repositoryRowId = result.id;
|
||||||
this.repository = result.repository;
|
this.repository = result.repository;
|
||||||
this.optionsPath = result.options_path;
|
this.optionsPath = result.options_path;
|
||||||
|
|
77
app/javascript/vue/repository_item_sidebar/locations.vue
Normal file
77
app/javascript/vue/repository_item_sidebar/locations.vue
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="repositoryRow">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<h4 data-e2e="e2e-TX-itemCard-locations-title">{{ i18n.t('repositories.locations.title', { count: repositoryRow.storage_locations.locations.length }) }}</h4>
|
||||||
|
<button v-if="repositoryRow.permissions.can_manage && repositoryRow.storage_locations.enabled"
|
||||||
|
@click="openAssignModal = true" class="btn btn-light ml-auto" data-e2e="e2e-BT-itemCard-assignLocation">
|
||||||
|
{{ 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 v-if="location.readable" :href="containerUrl(location.id)">{{ location.name }}</a>
|
||||||
|
<span v-else>{{ location.name }}</span>
|
||||||
|
<span v-if="location.metadata.display_type !== 'grid'">
|
||||||
|
({{ location.positions.length }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="location.metadata.display_type === 'grid'" 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>
|
||||||
|
<Teleport to="body">
|
||||||
|
<AssignModal
|
||||||
|
v-if="openAssignModal"
|
||||||
|
assignMode="assign"
|
||||||
|
:selectedRow="repositoryRow.id"
|
||||||
|
@close="openAssignModal = false; $emit('reloadRow')"
|
||||||
|
></AssignModal>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AssignModal from '../storage_locations/modals/assign.vue';
|
||||||
|
import {
|
||||||
|
storage_location_path
|
||||||
|
} from '../../routes.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RepositoryItemLocations',
|
||||||
|
props: {
|
||||||
|
repositoryRow: Object,
|
||||||
|
repository: Object
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
AssignModal
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
openAssignModal: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
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(96 + number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -169,6 +169,9 @@ export default {
|
||||||
resultToReload: { type: Number, required: false },
|
resultToReload: { type: Number, required: false },
|
||||||
activeDragResult: {
|
activeDragResult: {
|
||||||
required: false
|
required: false
|
||||||
|
},
|
||||||
|
userSettingsUrl: {
|
||||||
|
required: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -227,6 +230,17 @@ export default {
|
||||||
deep: true
|
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: {
|
computed: {
|
||||||
reorderableElements() {
|
reorderableElements() {
|
||||||
return this.orderedElements.map((e) => ({ id: e.id, attributes: e.attributes.orderable }));
|
return this.orderedElements.map((e) => ({ id: e.id, attributes: e.attributes.orderable }));
|
||||||
|
@ -337,6 +351,13 @@ export default {
|
||||||
toggleCollapsed() {
|
toggleCollapsed() {
|
||||||
this.isCollapsed = !this.isCollapsed;
|
this.isCollapsed = !this.isCollapsed;
|
||||||
this.result.attributes.collapsed = 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) {
|
dragEnter(e) {
|
||||||
if (!this.urls.upload_attachment_url) return;
|
if (!this.urls.upload_attachment_url) return;
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
:result="result"
|
:result="result"
|
||||||
:resultToReload="resultToReload"
|
:resultToReload="resultToReload"
|
||||||
:activeDragResult="activeDragResult"
|
:activeDragResult="activeDragResult"
|
||||||
|
:userSettingsUrl="userSettingsUrl"
|
||||||
@result:elements:loaded="resultToReload = null"
|
@result:elements:loaded="resultToReload = null"
|
||||||
@result:move_element="reloadResult"
|
@result:move_element="reloadResult"
|
||||||
@result:attachments:loaded="resultToReload = null"
|
@result:attachments:loaded="resultToReload = null"
|
||||||
|
@ -64,7 +65,8 @@ export default {
|
||||||
canCreate: { type: String, required: true },
|
canCreate: { type: String, required: true },
|
||||||
archived: { type: String, required: true },
|
archived: { type: String, required: true },
|
||||||
active_url: { 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() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -74,10 +76,12 @@ export default {
|
||||||
resultToReload: null,
|
resultToReload: null,
|
||||||
nextPageUrl: null,
|
nextPageUrl: null,
|
||||||
loadingPage: false,
|
loadingPage: false,
|
||||||
activeDragResult: null
|
activeDragResult: null,
|
||||||
|
userSettingsUrl: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.userSettingsUrl = document.querySelector('meta[name="user-settings-url"]').getAttribute('content');
|
||||||
window.addEventListener('scroll', this.loadResults, false);
|
window.addEventListener('scroll', this.loadResults, false);
|
||||||
window.addEventListener('scroll', this.initStackableHeaders, false);
|
window.addEventListener('scroll', this.initStackableHeaders, false);
|
||||||
this.nextPageUrl = this.url;
|
this.nextPageUrl = this.url;
|
||||||
|
|
|
@ -364,7 +364,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleScroll() {
|
handleScroll() {
|
||||||
if (this.scrollMode === 'pages') return;
|
if (this.scrollMode === 'pages' || this.scrollMode === 'none') return;
|
||||||
|
|
||||||
let target = null;
|
let target = null;
|
||||||
if (this.currentViewRender === 'cards') {
|
if (this.currentViewRender === 'cards') {
|
||||||
|
@ -506,15 +506,18 @@ export default {
|
||||||
this.rowData = [];
|
this.rowData = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.scrollMode === 'pages') {
|
if (this.scrollMode === 'pages' || this.scrollMode === 'none') {
|
||||||
if (this.gridApi) this.gridApi.setRowData(this.formatData(response.data.data));
|
if (this.gridApi) this.gridApi.setRowData(this.formatData(response.data.data));
|
||||||
this.rowData = this.formatData(response.data.data);
|
this.rowData = this.formatData(response.data.data);
|
||||||
} else {
|
} else {
|
||||||
this.handleInfiniteScroll(response);
|
this.handleInfiniteScroll(response);
|
||||||
}
|
}
|
||||||
this.totalPage = response.data.meta.total_pages;
|
|
||||||
this.totalEntries = response.data.meta.total_count;
|
if (this.scrollMode !== 'none') {
|
||||||
this.$emit('tableReloaded');
|
this.totalPage = response.data.meta.total_pages;
|
||||||
|
this.totalEntries = response.data.meta.total_count;
|
||||||
|
}
|
||||||
|
this.$emit('tableReloaded', this.rowData);
|
||||||
this.dataLoading = false;
|
this.dataLoading = false;
|
||||||
this.restoreSelection();
|
this.restoreSelection();
|
||||||
|
|
||||||
|
@ -577,8 +580,11 @@ export default {
|
||||||
this.gridApi.forEachNode((node) => {
|
this.gridApi.forEachNode((node) => {
|
||||||
if (this.selectedRows.find((row) => row.id === node.data.id)) {
|
if (this.selectedRows.find((row) => row.id === node.data.id)) {
|
||||||
node.setSelected(true);
|
node.setSelected(true);
|
||||||
|
} else {
|
||||||
|
node.setSelected(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.$emit('selectionChanged', this.selectedRows);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setSelectedRows(e) {
|
setSelectedRows(e) {
|
||||||
|
@ -591,6 +597,7 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
this.selectedRows = this.selectedRows.filter((row) => row.id !== e.data.id);
|
this.selectedRows = this.selectedRows.filter((row) => row.id !== e.data.id);
|
||||||
}
|
}
|
||||||
|
this.$emit('selectionChanged', this.selectedRows);
|
||||||
},
|
},
|
||||||
emitAction(action) {
|
emitAction(action) {
|
||||||
this.$emit(action.name, action, this.selectedRows);
|
this.$emit(action.name, action, this.selectedRows);
|
||||||
|
@ -602,6 +609,7 @@ export default {
|
||||||
clickCell(e) {
|
clickCell(e) {
|
||||||
if (e.column.colId !== 'rowMenu' && e.column.userProvidedColDef.notSelectable !== true) {
|
if (e.column.colId !== 'rowMenu' && e.column.userProvidedColDef.notSelectable !== true) {
|
||||||
e.node.setSelected(true);
|
e.node.setSelected(true);
|
||||||
|
this.$emit('selectionChanged', this.selectedRows);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
applyFilters(filters) {
|
applyFilters(filters) {
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
{{ i18n.t('repositories.import_records.dragAndDropUpload.importText.firstPart') }}
|
{{ i18n.t('repositories.import_records.dragAndDropUpload.importText.firstPart') }}
|
||||||
</span> {{ i18n.t('repositories.import_records.dragAndDropUpload.importText.secondPart') }}
|
</span> {{ i18n.t('repositories.import_records.dragAndDropUpload.importText.secondPart') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sn-grey">
|
<div class="text-sn-grey text-center">
|
||||||
{{ supportingText }}
|
{{ supportingText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,6 +32,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
/* global GLOBAL_CONSTANTS I18n */
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DragAndDropUpload',
|
name: 'DragAndDropUpload',
|
||||||
|
@ -69,7 +70,9 @@ export default {
|
||||||
// check if it's a correct file type
|
// check if it's a correct file type
|
||||||
const fileExtension = file.name.split('.')[1];
|
const fileExtension = file.name.split('.')[1];
|
||||||
if (!this.supportedFormats.includes(fileExtension)) {
|
if (!this.supportedFormats.includes(fileExtension)) {
|
||||||
const error = I18n.t('repositories.import_records.dragAndDropUpload.wrongFileTypeError');
|
const error = I18n.t('repositories.import_records.dragAndDropUpload.wrongFileTypeError', {
|
||||||
|
extensions: this.supportedFormats.join(', ')
|
||||||
|
});
|
||||||
this.$emit('file:error', error);
|
this.$emit('file:error', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,29 +7,29 @@
|
||||||
<i class="sn-icon sn-icon-close"></i>
|
<i class="sn-icon sn-icon-close"></i>
|
||||||
</button>
|
</button>
|
||||||
<h4 class="modal-title truncate !block">
|
<h4 class="modal-title truncate !block">
|
||||||
{{ i18n.t('repositories.index.modal_share.title', {name: repository.name }) }}
|
{{ i18n.t('modal_share.title', {object_name: object.name }) }}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
{{ i18n.t("repositories.index.modal_share.share_with_team") }}
|
{{ i18n.t("modal_share.share_with_team") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
{{ i18n.t("repositories.index.modal_share.can_edit") }}
|
{{ i18n.t("modal_share.can_edit") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2 flex items-center h-9 gap-1">
|
<div v-if="globalShareEnabled" class="col-span-2 flex items-center h-9 gap-1">
|
||||||
<span class="sci-checkbox-container">
|
<span class="sci-checkbox-container">
|
||||||
<input type="checkbox" class="sci-checkbox" v-model="sharedWithAllRead" />
|
<input type="checkbox" class="sci-checkbox" v-model="sharedWithAllRead" />
|
||||||
<span class="sci-checkbox-label"></span>
|
<span class="sci-checkbox-label"></span>
|
||||||
</span>
|
</span>
|
||||||
{{ i18n.t("repositories.index.modal_share.all_teams") }}
|
{{ i18n.t("modal_share.all_teams") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center items-center">
|
<div v-if="globalShareEnabled" class="flex justify-center items-center">
|
||||||
<span v-if="sharedWithAllRead" class="sci-toggle-checkbox-container">
|
<span v-if="sharedWithAllRead" class="sci-toggle-checkbox-container">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
class="sci-toggle-checkbox"
|
class="sci-toggle-checkbox"
|
||||||
:disabled="!repository.shareable_write"
|
:disabled="!object.shareable_write"
|
||||||
v-model="sharedWithAllWrite" />
|
v-model="sharedWithAllWrite" />
|
||||||
<span class="sci-toggle-checkbox-label"></span>
|
<span class="sci-toggle-checkbox-label"></span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -48,8 +48,7 @@
|
||||||
class="sci-toggle-checkbox-container">
|
class="sci-toggle-checkbox-container">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
class="sci-toggle-checkbox"
|
class="sci-toggle-checkbox"
|
||||||
@change="permission_changes[team.id] = true"
|
:disabled="!object.shareable_write"
|
||||||
:disabled="!repository.shareable_write"
|
|
||||||
v-model="team.attributes.private_shared_with_write" />
|
v-model="team.attributes.private_shared_with_write" />
|
||||||
<span class="sci-toggle-checkbox-label"></span>
|
<span class="sci-toggle-checkbox-label"></span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -60,7 +59,7 @@
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||||
<button class="btn btn-primary" @click="submit" type="submit">
|
<button class="btn btn-primary" @click="submit" type="submit">
|
||||||
{{ i18n.t('repositories.index.modal_share.submit') }}
|
{{ i18n.t('modal_share.submit') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -71,19 +70,20 @@
|
||||||
<script>
|
<script>
|
||||||
/* global HelperModule */
|
/* global HelperModule */
|
||||||
|
|
||||||
import axios from '../../../packs/custom_axios.js';
|
import axios from '../../packs/custom_axios.js';
|
||||||
import modalMixin from '../../shared/modal_mixin';
|
import modalMixin from './modal_mixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ShareRepositoryModal',
|
name: 'ShareObjectModal',
|
||||||
props: {
|
props: {
|
||||||
repository: Object
|
object: Object,
|
||||||
|
globalShareEnabled: { type: Boolean, default: false }
|
||||||
},
|
},
|
||||||
mixins: [modalMixin],
|
mixins: [modalMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
sharedWithAllRead: this.repository.shared_read || this.repository.shared_write,
|
sharedWithAllRead: this.object.shared_read || this.object.shared_write,
|
||||||
sharedWithAllWrite: this.repository.shared_write,
|
sharedWithAllWrite: this.object.shared_write,
|
||||||
shareableTeams: [],
|
shareableTeams: [],
|
||||||
permission_changes: {}
|
permission_changes: {}
|
||||||
};
|
};
|
||||||
|
@ -93,7 +93,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getTeams() {
|
getTeams() {
|
||||||
axios.get(this.repository.urls.shareable_teams).then((response) => {
|
axios.get(this.object.urls.shareable_teams).then((response) => {
|
||||||
this.shareableTeams = response.data.data;
|
this.shareableTeams = response.data.data;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -101,14 +101,12 @@ export default {
|
||||||
const data = {
|
const data = {
|
||||||
select_all_teams: this.sharedWithAllRead,
|
select_all_teams: this.sharedWithAllRead,
|
||||||
select_all_write_permission: this.sharedWithAllWrite,
|
select_all_write_permission: this.sharedWithAllWrite,
|
||||||
share_team_ids: this.shareableTeams.filter((team) => team.attributes.private_shared_with).map((team) => team.id),
|
team_share_params: this.shareableTeams.map((team) => { return { id: team.id, ...team.attributes } })
|
||||||
write_permissions: this.shareableTeams.filter((team) => team.attributes.private_shared_with_write).map((team) => team.id),
|
|
||||||
permission_changes: this.permission_changes
|
|
||||||
};
|
};
|
||||||
axios.post(this.repository.urls.share, data).then(() => {
|
axios.post(this.object.urls.share, data).then(() => {
|
||||||
HelperModule.flashAlertMsg(this.i18n.t(
|
HelperModule.flashAlertMsg(this.i18n.t(
|
||||||
'repositories.index.modal_share.success_message',
|
'modal_share.success_message',
|
||||||
{ inventory_name: this.repository.name }
|
{ object_name: this.object.name }
|
||||||
), 'success');
|
), 'success');
|
||||||
this.$emit('share');
|
this.$emit('share');
|
||||||
});
|
});
|
249
app/javascript/vue/storage_locations/container.vue
Normal file
249
app/javascript/vue/storage_locations/container.vue
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
<template>
|
||||||
|
<div class="grid w-full h-full gap-6" :class="{ 'grid-cols-[auto_1fr]': withGrid }">
|
||||||
|
<div v-if="withGrid" class="max-w-[40vw]">
|
||||||
|
<Grid
|
||||||
|
:gridSize="gridSize"
|
||||||
|
:assignedItems="assignedItems"
|
||||||
|
:selectedItems="selectedItems"
|
||||||
|
@assign="assignRowToPosition"
|
||||||
|
@select="selectRow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="h-full bg-white px-4">
|
||||||
|
<DataTable :columnDefs="columnDefs"
|
||||||
|
tableId="StorageLocationsContainer"
|
||||||
|
:dataUrl="dataSource"
|
||||||
|
ref="table"
|
||||||
|
:reloadingTable="reloadingTable"
|
||||||
|
:toolbarActions="toolbarActions"
|
||||||
|
:actionsUrl="actionsUrl"
|
||||||
|
:scrollMode="paginationMode"
|
||||||
|
@assign="assignRow"
|
||||||
|
@move="moveRow"
|
||||||
|
@import="openImportModal = true"
|
||||||
|
@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"
|
||||||
|
@close="openAssignModal = false; this.reloadingTable = true"
|
||||||
|
></AssignModal>
|
||||||
|
<ImportModal
|
||||||
|
v-if="openImportModal"
|
||||||
|
:containerId="containerId"
|
||||||
|
@close="openImportModal = false"
|
||||||
|
@reloadTable="reloadingTable = true"
|
||||||
|
></ImportModal>
|
||||||
|
<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 ImportModal from './modals/import.vue';
|
||||||
|
import ConfirmationModal from '../shared/confirmation_modal.vue';
|
||||||
|
import RemindersRender from './renderers/reminders.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'StorageLocationsContainer',
|
||||||
|
components: {
|
||||||
|
DataTable,
|
||||||
|
Grid,
|
||||||
|
AssignModal,
|
||||||
|
ConfirmationModal,
|
||||||
|
RemindersRender,
|
||||||
|
ImportModal
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
canManage: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
openImportModal: 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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'reminders',
|
||||||
|
headerName: this.i18n.t('storage_locations.show.table.reminders'),
|
||||||
|
sortable: true,
|
||||||
|
cellRenderer: RemindersRender
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
cellRenderer: this.rowNameRenderer
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'stock',
|
||||||
|
headerName: this.i18n.t('storage_locations.show.table.stock'),
|
||||||
|
sortable: true
|
||||||
|
}];
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
},
|
||||||
|
toolbarActions() {
|
||||||
|
const left = [];
|
||||||
|
|
||||||
|
if (this.canManage) {
|
||||||
|
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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
left.push({
|
||||||
|
name: 'import',
|
||||||
|
icon: 'sn-icon sn-icon-import',
|
||||||
|
label: this.i18n.t('storage_locations.show.import_modal.import_button'),
|
||||||
|
type: 'emit',
|
||||||
|
buttonStyle: 'btn btn-light'
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
left,
|
||||||
|
right: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
rowNameRenderer(params) {
|
||||||
|
const { row_name: rowName, hidden } = params.data;
|
||||||
|
if (hidden) {
|
||||||
|
return `
|
||||||
|
<span class="text-sn-grey-700">
|
||||||
|
<i class="sn-icon sn-icon-locked-task"></i> ${this.i18n.t('storage_locations.show.hidden')}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return rowName;
|
||||||
|
},
|
||||||
|
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>
|
150
app/javascript/vue/storage_locations/grid.vue
Normal file
150
app/javascript/vue/storage_locations/grid.vue
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
<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">
|
||||||
|
<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 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-hidden max-h-[70vh] relative">
|
||||||
|
<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>
|
||||||
|
/* global PerfectScrollbar */
|
||||||
|
export default {
|
||||||
|
name: 'StorageLocationsGrid',
|
||||||
|
props: {
|
||||||
|
gridSize: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
assignedItems: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
selectedItems: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
scrollBar: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$refs.cellsContainer.addEventListener('scroll', this.handleScroll);
|
||||||
|
window.addEventListener('resize', this.handleScroll);
|
||||||
|
this.scrollBar = new PerfectScrollbar(this.$refs.cellsContainer, { wheelSpeed: 0.5, minScrollbarLength: 20 });
|
||||||
|
},
|
||||||
|
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.cellsContainer.scrollLeft > this.$refs.columnsContainer.scrollLeft) {
|
||||||
|
this.$refs.cellsContainer.scrollLeft = this.$refs.columnsContainer.scrollLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
98
app/javascript/vue/storage_locations/modals/assign.vue
Normal file
98
app/javascript/vue/storage_locations/modals/assign.vue
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
<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"
|
||||||
|
: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,
|
||||||
|
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,48 @@
|
||||||
|
<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"
|
||||||
|
:moveMode="moveMode"
|
||||||
|
:value="selectedStorageLocationId"
|
||||||
|
@selectStorageLocation="selectStorageLocation" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MoveTreeMixin from '../move_tree_mixin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ContainerSelector',
|
||||||
|
mixins: [MoveTreeMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
container: true,
|
||||||
|
moveMode: 'containers'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
selectedStorageLocationId() {
|
||||||
|
this.$emit('change', this.selectedStorageLocationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="availablePositions" 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;
|
||||||
|
if (this.availablePositions) {
|
||||||
|
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>
|
118
app/javascript/vue/storage_locations/modals/import.vue
Normal file
118
app/javascript/vue/storage_locations/modals/import.vue
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
<template>
|
||||||
|
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<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" id="edit-project-modal-label">
|
||||||
|
{{ i18n.t('storage_locations.show.import_modal.title') }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body flex flex-col grow">
|
||||||
|
<p>
|
||||||
|
{{ i18n.t('storage_locations.show.import_modal.description') }}
|
||||||
|
</p>
|
||||||
|
<h3 class="my-0 text-sn-dark-grey mb-3">
|
||||||
|
{{ i18n.t('storage_locations.show.import_modal.export') }}
|
||||||
|
</h3>
|
||||||
|
<div class="flex gap-4 mb-6">
|
||||||
|
<a
|
||||||
|
:href="exportUrl"
|
||||||
|
target="_blank"
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
>
|
||||||
|
<i class="sn-icon sn-icon-export"></i>
|
||||||
|
{{ i18n.t('storage_locations.show.import_modal.export_button') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="my-0 text-sn-dark-grey mb-3">
|
||||||
|
{{ i18n.t('storage_locations.show.import_modal.import') }}
|
||||||
|
</h3>
|
||||||
|
<DragAndDropUpload
|
||||||
|
class="h-60"
|
||||||
|
@file:dropped="uploadFile"
|
||||||
|
@file:error="handleError"
|
||||||
|
@file:error:clear="this.error = null"
|
||||||
|
:supportingText="`${i18n.t('storage_locations.show.import_modal.drag_n_drop')}`"
|
||||||
|
:supportedFormats="['xlsx']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div v-if="error" class="flex flex-row gap-2 my-auto mr-auto text-sn-delete-red">
|
||||||
|
<i class="sn-icon sn-icon-alert-warning"></i>
|
||||||
|
<div class="my-auto">{{ error }}</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" @click="close" aria-label="Close">
|
||||||
|
{{ i18n.t('general.cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DragAndDropUpload from '../../shared/drag_and_drop_upload.vue';
|
||||||
|
import modalMixin from '../../shared/modal_mixin';
|
||||||
|
import axios from '../../../packs/custom_axios';
|
||||||
|
import {
|
||||||
|
export_container_storage_location_path,
|
||||||
|
import_container_storage_location_path
|
||||||
|
} from '../../../routes.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ImportContainer',
|
||||||
|
emits: ['uploadFile', 'close'],
|
||||||
|
components: {
|
||||||
|
DragAndDropUpload
|
||||||
|
},
|
||||||
|
mixins: [modalMixin],
|
||||||
|
props: {
|
||||||
|
containerId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
exportUrl() {
|
||||||
|
return export_container_storage_location_path({
|
||||||
|
id: this.containerId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
importUrl() {
|
||||||
|
return import_container_storage_location_path({
|
||||||
|
id: this.containerId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleError(error) {
|
||||||
|
this.error = error;
|
||||||
|
},
|
||||||
|
uploadFile(file) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// required payload
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
axios.post(this.importUrl, formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$emit('reloadTable');
|
||||||
|
this.close();
|
||||||
|
}).catch((error) => {
|
||||||
|
this.handleError(error.response.data.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
88
app/javascript/vue/storage_locations/modals/move.vue
Normal file
88
app/javascript/vue/storage_locations/modals/move.vue
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<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"
|
||||||
|
:moveMode="moveMode"
|
||||||
|
: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: '',
|
||||||
|
moveMode: 'locations'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
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>
|
58
app/javascript/vue/storage_locations/modals/move_tree.vue
Normal file
58
app/javascript/vue/storage_locations/modals/move_tree.vue
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<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="selectStorageLocation(storageLocationTree)"
|
||||||
|
class="flex items-center pl-1 flex-1 gap-2"
|
||||||
|
:class="{
|
||||||
|
'!bg-sn-super-light-blue': storageLocationTree.storage_location.id == value,
|
||||||
|
'text-sn-blue cursor-pointer hover:bg-sn-super-light-grey': (
|
||||||
|
moveMode === 'locations' || storageLocationTree.storage_location.container
|
||||||
|
)
|
||||||
|
}">
|
||||||
|
<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"
|
||||||
|
:moveMode="moveMode"
|
||||||
|
@selectStorageLocation="$emit('selectStorageLocation', $event)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'MoveTree',
|
||||||
|
emits: ['selectStorageLocation'],
|
||||||
|
props: {
|
||||||
|
storageLocationsTree: Array,
|
||||||
|
value: Number,
|
||||||
|
moveMode: String
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MoveTree: () => import('./move_tree.vue')
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
opendedStorageLocations: {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectStorageLocation(storageLocationTree) {
|
||||||
|
if (this.moveMode === 'locations' || storageLocationTree.storage_location.container) {
|
||||||
|
this.$emit('selectStorageLocation', storageLocationTree.storage_location.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
232
app/javascript/vue/storage_locations/modals/new_edit.vue
Normal file
232
app/javascript/vue/storage_locations/modals/new_edit.vue
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
<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'" :title="warningBoxNotEmpty" 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="!canChangeGrid" 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="!canChangeGrid" 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="!canChangeGrid" 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="!canChangeGrid" 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';
|
||||||
|
},
|
||||||
|
canChangeGrid() {
|
||||||
|
return !this.object.code || this.object.is_empty;
|
||||||
|
},
|
||||||
|
warningBoxNotEmpty() {
|
||||||
|
if (this.canChangeGrid) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return this.i18n.t('storage_locations.index.edit_modal.warning_box_not_empty');
|
||||||
|
},
|
||||||
|
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>
|
42
app/javascript/vue/storage_locations/renderers/reminders.vue
Normal file
42
app/javascript/vue/storage_locations/renderers/reminders.vue
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="params.data.have_reminders">
|
||||||
|
<GeneralDropdown ref="dropdown" position="right" @open="getReminders">
|
||||||
|
<template v-slot:field>
|
||||||
|
<i class="sn-icon sn-icon-notifications "></i>
|
||||||
|
</template>
|
||||||
|
<template v-slot:flyout>
|
||||||
|
<ul v-html="reminders.html" class="list-none p-0 reminders-view-mode"></ul>
|
||||||
|
</template>
|
||||||
|
</GeneralDropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from '../../../packs/custom_axios.js';
|
||||||
|
import GeneralDropdown from '../../shared/general_dropdown.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RemindersRenderer',
|
||||||
|
props: {
|
||||||
|
params: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
reminders: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
GeneralDropdown
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getReminders() {
|
||||||
|
axios.get(this.params.data.reminders_url)
|
||||||
|
.then((response) => {
|
||||||
|
this.reminders = response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
280
app/javascript/vue/storage_locations/table.vue
Normal file
280
app/javascript/vue/storage_locations/table.vue
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-full">
|
||||||
|
<DataTable :columnDefs="columnDefs"
|
||||||
|
tableId="StorageLocationsTable"
|
||||||
|
:dataUrl="dataSource"
|
||||||
|
:reloadingTable="reloadingTable"
|
||||||
|
:toolbarActions="toolbarActions"
|
||||||
|
:actionsUrl="actionsUrl"
|
||||||
|
:filters="filters"
|
||||||
|
@create_location="openCreateLocationModal"
|
||||||
|
@create_container="openCreateContainerModal"
|
||||||
|
@edit="edit"
|
||||||
|
@duplicate="duplicate"
|
||||||
|
@tableReloaded="reloadingTable = false"
|
||||||
|
@move="move"
|
||||||
|
@delete="deleteStorageLocation"
|
||||||
|
@share="share"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
<ShareObjectModal
|
||||||
|
v-if="shareStorageLocation"
|
||||||
|
:object="shareStorageLocation"
|
||||||
|
@close="shareStorageLocation = null"
|
||||||
|
@share="updateTable" />
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* 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';
|
||||||
|
import ShareObjectModal from '../shared/share_modal.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RepositoriesTable',
|
||||||
|
components: {
|
||||||
|
DataTable,
|
||||||
|
EditModal,
|
||||||
|
MoveModal,
|
||||||
|
ConfirmationModal,
|
||||||
|
ShareObjectModal
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
dataSource: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
actionsUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
createLocationUrl: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
createLocationInstanceUrl: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
directUploadUrl: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
reloadingTable: false,
|
||||||
|
openEditModal: false,
|
||||||
|
editModalMode: null,
|
||||||
|
editStorageLocation: null,
|
||||||
|
objectToMove: null,
|
||||||
|
moveToUrl: null,
|
||||||
|
shareStorageLocation: null,
|
||||||
|
storageLocationDeleteTitle: '',
|
||||||
|
storageLocationDeleteDescription: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
columnDefs() {
|
||||||
|
const columns = [{
|
||||||
|
field: 'name',
|
||||||
|
headerName: this.i18n.t('storage_locations.index.table.name'),
|
||||||
|
sortable: true,
|
||||||
|
notSelectable: true,
|
||||||
|
cellRenderer: this.nameRenderer
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'code',
|
||||||
|
headerName: this.i18n.t('storage_locations.index.table.id'),
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sub_location_count',
|
||||||
|
headerName: this.i18n.t('storage_locations.index.table.sub_locations'),
|
||||||
|
width: 250,
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'items',
|
||||||
|
headerName: this.i18n.t('storage_locations.index.table.items'),
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'shared',
|
||||||
|
headerName: this.i18n.t('storage_locations.index.table.shared'),
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'owned_by',
|
||||||
|
headerName: this.i18n.t('storage_locations.index.table.owned_by'),
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'created_on',
|
||||||
|
headerName: this.i18n.t('storage_locations.index.table.created_on'),
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
headerName: this.i18n.t('storage_locations.index.table.description'),
|
||||||
|
sortable: true
|
||||||
|
}];
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
},
|
||||||
|
toolbarActions() {
|
||||||
|
const left = [];
|
||||||
|
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.createLocationUrl,
|
||||||
|
buttonStyle: 'btn btn-primary'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.createLocationInstanceUrl) {
|
||||||
|
left.push({
|
||||||
|
name: 'create_container',
|
||||||
|
icon: 'sn-icon sn-icon-item',
|
||||||
|
label: this.i18n.t('storage_locations.index.new_container'),
|
||||||
|
type: 'emit',
|
||||||
|
path: this.createLocationInstanceUrl,
|
||||||
|
buttonStyle: 'btn btn-secondary'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
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,
|
||||||
|
shared,
|
||||||
|
ishared
|
||||||
|
} = params.data;
|
||||||
|
let containerIcon = '';
|
||||||
|
if (params.data.container) {
|
||||||
|
containerIcon = '<i class="sn-icon sn-icon-item"></i>';
|
||||||
|
}
|
||||||
|
let sharedIcon = '';
|
||||||
|
if (shared || ishared) {
|
||||||
|
sharedIcon = '<i class="fas fa-users"></i>';
|
||||||
|
}
|
||||||
|
return `<a class="hover:no-underline flex items-center gap-1"
|
||||||
|
title="${name}" href="${urls.show}">
|
||||||
|
${sharedIcon}${containerIcon}
|
||||||
|
<span class="truncate">${name}</span>
|
||||||
|
</a>`;
|
||||||
|
},
|
||||||
|
updateTable() {
|
||||||
|
this.reloadingTable = true;
|
||||||
|
this.objectToMove = null;
|
||||||
|
this.shareStorageLocation = 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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
share(_event, rows) {
|
||||||
|
const [storageLocation] = rows;
|
||||||
|
this.shareStorageLocation = storageLocation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
# Provides asynchronous generation of image previews for ActiveStorage::Blob records.
|
# Provides asynchronous generation of image previews for ActiveStorage::Blob records.
|
||||||
class ActiveStorage::PreviewJob < ActiveStorage::BaseJob
|
class ActiveStorage::PreviewJob < ActiveStorage::BaseJob
|
||||||
|
include ActiveStorageHelper
|
||||||
|
|
||||||
queue_as :assets
|
queue_as :assets
|
||||||
|
|
||||||
discard_on StandardError do |job, error|
|
discard_on StandardError do |job, error|
|
||||||
|
@ -18,11 +20,11 @@ class ActiveStorage::PreviewJob < ActiveStorage::BaseJob
|
||||||
|
|
||||||
def perform(blob_id)
|
def perform(blob_id)
|
||||||
blob = ActiveStorage::Blob.find(blob_id)
|
blob = ActiveStorage::Blob.find(blob_id)
|
||||||
preview = blob.representation(resize_to_limit: Constants::MEDIUM_PIC_FORMAT).processed
|
preview = blob.representation(resize_to_limit: Constants::MEDIUM_PIC_FORMAT, format: image_preview_format(blob)).processed
|
||||||
Rails.logger.info "Preview for the Blod with id: #{blob.id} - successfully generated.\n" \
|
Rails.logger.info "Preview for the Blod with id: #{blob.id} - successfully generated.\n" \
|
||||||
"Transformations applied: #{preview.variation.transformations}"
|
"Transformations applied: #{preview.variation.transformations}"
|
||||||
|
|
||||||
preview = blob.representation(resize_to_limit: Constants::LARGE_PIC_FORMAT).processed
|
preview = blob.representation(resize_to_limit: Constants::LARGE_PIC_FORMAT, format: image_preview_format(blob)).processed
|
||||||
Rails.logger.info "Preview for the Blod with id: #{blob.id} - successfully generated.\n" \
|
Rails.logger.info "Preview for the Blod with id: #{blob.id} - successfully generated.\n" \
|
||||||
"Transformations applied: #{preview.variation.transformations}"
|
"Transformations applied: #{preview.variation.transformations}"
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,9 @@ class CleanupUserSettingsJob < ApplicationJob
|
||||||
queue_as :default
|
queue_as :default
|
||||||
|
|
||||||
def perform(record_type, record_id)
|
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
|
sanitized_record_id = record_id.to_i.to_s
|
||||||
raise ArgumentError, 'Invalid record_id' unless sanitized_record_id == record_id.to_s
|
raise ArgumentError, 'Invalid record_id' unless sanitized_record_id == record_id.to_s
|
||||||
|
|
|
@ -187,6 +187,9 @@ class Activity < ApplicationRecord
|
||||||
when Asset
|
when Asset
|
||||||
breadcrumbs[:asset] = subject.blob.filename.to_s
|
breadcrumbs[:asset] = subject.blob.filename.to_s
|
||||||
generate_breadcrumb(subject.result || subject.step || subject.repository_cell.repository_row.repository)
|
generate_breadcrumb(subject.result || subject.step || subject.repository_cell.repository_row.repository)
|
||||||
|
when StorageLocation
|
||||||
|
breadcrumbs[:storage_location] = subject.name
|
||||||
|
generate_breadcrumb(subject.team)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ class Asset < ApplicationRecord
|
||||||
include WopiUtil
|
include WopiUtil
|
||||||
include ActiveStorageFileUtil
|
include ActiveStorageFileUtil
|
||||||
include ActiveStorageConcerns
|
include ActiveStorageConcerns
|
||||||
|
include ActiveStorageHelper
|
||||||
|
|
||||||
require 'tempfile'
|
require 'tempfile'
|
||||||
# Lock duration set to 30 minutes
|
# Lock duration set to 30 minutes
|
||||||
|
@ -105,11 +106,11 @@ class Asset < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def medium_preview
|
def medium_preview
|
||||||
preview_attachment.representation(resize_to_limit: Constants::MEDIUM_PIC_FORMAT)
|
preview_attachment.representation(resize_to_limit: Constants::MEDIUM_PIC_FORMAT, format: image_preview_format(blob))
|
||||||
end
|
end
|
||||||
|
|
||||||
def large_preview
|
def large_preview
|
||||||
preview_attachment.representation(resize_to_limit: Constants::LARGE_PIC_FORMAT)
|
preview_attachment.representation(resize_to_limit: Constants::LARGE_PIC_FORMAT, format: image_preview_format(blob))
|
||||||
end
|
end
|
||||||
|
|
||||||
def file_name
|
def file_name
|
||||||
|
|
|
@ -7,12 +7,18 @@ module Cloneable
|
||||||
raise NotImplementedError, "Cloneable model must implement the '.parent' method!" unless respond_to?(:parent)
|
raise NotImplementedError, "Cloneable model must implement the '.parent' method!" unless respond_to?(:parent)
|
||||||
|
|
||||||
clone_label = I18n.t('general.clone_label')
|
clone_label = I18n.t('general.clone_label')
|
||||||
last_clone_number =
|
|
||||||
parent.public_send(self.class.table_name)
|
records = if parent
|
||||||
.select("substring(#{self.class.table_name}.name, '(?:^#{clone_label} )(\\d+)')::int AS clone_number")
|
parent.public_send(self.class.table_name)
|
||||||
.where('name ~ ?', "^#{clone_label} \\d+ - #{Regexp.escape(name)}$")
|
else
|
||||||
.order(clone_number: :asc)
|
self.class.where(parent_id: nil, team: team)
|
||||||
.last&.clone_number
|
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)
|
"#{clone_label} #{(last_clone_number || 0) + 1} - #{name}".truncate(Constants::NAME_MAX_LENGTH)
|
||||||
end
|
end
|
||||||
|
|
98
app/models/concerns/shareable.rb
Normal file
98
app/models/concerns/shareable.rb
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Shareable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
has_many :team_shared_objects, as: :shared_object, dependent: :destroy
|
||||||
|
has_many :teams_shared_with, through: :team_shared_objects, source: :team, dependent: :destroy
|
||||||
|
|
||||||
|
if column_names.include? 'permission_level'
|
||||||
|
enum permission_level: Extends::SHARED_OBJECTS_PERMISSION_LEVELS
|
||||||
|
define_method :globally_shareable? do
|
||||||
|
true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# If model does not include the permission_level column for global sharing,
|
||||||
|
# all related methods should just return false
|
||||||
|
Extends::SHARED_OBJECTS_PERMISSION_LEVELS.each do |level|
|
||||||
|
define_method "#{level[0]}?" do
|
||||||
|
level[0] == :not_shared
|
||||||
|
end
|
||||||
|
|
||||||
|
define_method :globally_shareable? do
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scope :viewable_by_user, lambda { |user, teams = user.current_team|
|
||||||
|
readable = readable_by_user(user).left_outer_joins(:team_shared_objects)
|
||||||
|
readable
|
||||||
|
.where(team: teams)
|
||||||
|
.or(readable.where(team_shared_objects: { team: teams }))
|
||||||
|
.or(readable
|
||||||
|
.where(
|
||||||
|
if column_names.include?('permission_level')
|
||||||
|
{
|
||||||
|
permission_level: [
|
||||||
|
Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:shared_read],
|
||||||
|
Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:shared_write]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
).where.not(team: teams))
|
||||||
|
.distinct
|
||||||
|
}
|
||||||
|
rescue ActiveRecord::NoDatabaseError,
|
||||||
|
ActiveRecord::ConnectionNotEstablished,
|
||||||
|
ActiveRecord::StatementInvalid,
|
||||||
|
PG::ConnectionBad
|
||||||
|
|
||||||
|
Rails.logger.info('Not connected to database, skipping sharable model initialization.')
|
||||||
|
end
|
||||||
|
|
||||||
|
def shareable_write?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def private_shared_with?(team)
|
||||||
|
team_shared_objects.where(team: team).any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def private_shared_with_write?(team)
|
||||||
|
team_shared_objects.where(team: team, permission_level: :shared_write).any?
|
||||||
|
end
|
||||||
|
|
||||||
|
def i_shared?(team)
|
||||||
|
shared_with_anybody? && self.team == team
|
||||||
|
end
|
||||||
|
|
||||||
|
def globally_shared?
|
||||||
|
shared_read? || shared_write?
|
||||||
|
end
|
||||||
|
|
||||||
|
def shared_with_anybody?
|
||||||
|
(!not_shared? || team_shared_objects.any?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def shared_with?(team)
|
||||||
|
return false if self.team == team
|
||||||
|
|
||||||
|
!not_shared? || private_shared_with?(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
def shared_with_write?(team)
|
||||||
|
return false if self.team == team
|
||||||
|
|
||||||
|
shared_write? || private_shared_with_write?(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
def shared_with_read?(team)
|
||||||
|
return false if self.team == team
|
||||||
|
|
||||||
|
shared_read? || team_shared_objects.where(team: team, permission_level: :shared_read).any?
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,12 +7,11 @@ class Repository < RepositoryBase
|
||||||
include PermissionCheckableModel
|
include PermissionCheckableModel
|
||||||
include RepositoryImportParser
|
include RepositoryImportParser
|
||||||
include ArchivableModel
|
include ArchivableModel
|
||||||
|
include Shareable
|
||||||
|
|
||||||
ID_PREFIX = 'IN'
|
ID_PREFIX = 'IN'
|
||||||
include PrefixedIdModel
|
include PrefixedIdModel
|
||||||
|
|
||||||
enum permission_level: Extends::SHARED_OBJECTS_PERMISSION_LEVELS
|
|
||||||
|
|
||||||
belongs_to :archived_by,
|
belongs_to :archived_by,
|
||||||
foreign_key: :archived_by_id,
|
foreign_key: :archived_by_id,
|
||||||
class_name: 'User',
|
class_name: 'User',
|
||||||
|
@ -23,8 +22,6 @@ class Repository < RepositoryBase
|
||||||
class_name: 'User',
|
class_name: 'User',
|
||||||
inverse_of: :restored_repositories,
|
inverse_of: :restored_repositories,
|
||||||
optional: true
|
optional: true
|
||||||
has_many :team_shared_objects, as: :shared_object, dependent: :destroy
|
|
||||||
has_many :teams_shared_with, through: :team_shared_objects, source: :team, dependent: :destroy
|
|
||||||
has_many :repository_snapshots,
|
has_many :repository_snapshots,
|
||||||
class_name: 'RepositorySnapshot',
|
class_name: 'RepositorySnapshot',
|
||||||
foreign_key: :parent_id,
|
foreign_key: :parent_id,
|
||||||
|
@ -48,17 +45,6 @@ class Repository < RepositoryBase
|
||||||
scope :archived, -> { where(archived: true) }
|
scope :archived, -> { where(archived: true) }
|
||||||
scope :globally_shared, -> { where(permission_level: %i(shared_read shared_write)) }
|
scope :globally_shared, -> { where(permission_level: %i(shared_read shared_write)) }
|
||||||
|
|
||||||
scope :viewable_by_user, lambda { |user, teams = user.current_team|
|
|
||||||
readable_repositories = readable_by_user(user).left_outer_joins(:team_shared_objects)
|
|
||||||
readable_repositories
|
|
||||||
.where(team: teams)
|
|
||||||
.or(readable_repositories.where(team_shared_objects: { team: teams }))
|
|
||||||
.or(readable_repositories
|
|
||||||
.where(permission_level: [Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:shared_read], Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:shared_write]])
|
|
||||||
.where.not(team: teams))
|
|
||||||
.distinct
|
|
||||||
}
|
|
||||||
|
|
||||||
scope :assigned_to_project, lambda { |project|
|
scope :assigned_to_project, lambda { |project|
|
||||||
joins(repository_rows: { my_module_repository_rows: { my_module: { experiment: :project } } })
|
joins(repository_rows: { my_module_repository_rows: { my_module: { experiment: :project } } })
|
||||||
.where(repository_rows: { my_module_repository_rows: { my_module: { experiments: { project: project } } } })
|
.where(repository_rows: { my_module_repository_rows: { my_module: { experiments: { project: project } } } })
|
||||||
|
@ -80,10 +66,6 @@ class Repository < RepositoryBase
|
||||||
teams.blank? ? self : where(team: teams)
|
teams.blank? ? self : where(team: teams)
|
||||||
end
|
end
|
||||||
|
|
||||||
def shareable_write?
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
def permission_parent
|
def permission_parent
|
||||||
team
|
team
|
||||||
end
|
end
|
||||||
|
@ -111,44 +93,6 @@ class Repository < RepositoryBase
|
||||||
['repository_rows.name', RepositoryRow::PREFIXED_ID_SQL, 'users.full_name']
|
['repository_rows.name', RepositoryRow::PREFIXED_ID_SQL, 'users.full_name']
|
||||||
end
|
end
|
||||||
|
|
||||||
def i_shared?(team)
|
|
||||||
shared_with_anybody? && self.team == team
|
|
||||||
end
|
|
||||||
|
|
||||||
def globally_shared?
|
|
||||||
shared_read? || shared_write?
|
|
||||||
end
|
|
||||||
|
|
||||||
def shared_with_anybody?
|
|
||||||
(!not_shared? || team_shared_objects.any?)
|
|
||||||
end
|
|
||||||
|
|
||||||
def shared_with?(team)
|
|
||||||
return false if self.team == team
|
|
||||||
|
|
||||||
!not_shared? || private_shared_with?(team)
|
|
||||||
end
|
|
||||||
|
|
||||||
def shared_with_write?(team)
|
|
||||||
return false if self.team == team
|
|
||||||
|
|
||||||
shared_write? || private_shared_with_write?(team)
|
|
||||||
end
|
|
||||||
|
|
||||||
def shared_with_read?(team)
|
|
||||||
return false if self.team == team
|
|
||||||
|
|
||||||
shared_read? || team_shared_objects.where(team: team, permission_level: :shared_read).any?
|
|
||||||
end
|
|
||||||
|
|
||||||
def private_shared_with?(team)
|
|
||||||
team_shared_objects.where(team: team).any?
|
|
||||||
end
|
|
||||||
|
|
||||||
def private_shared_with_write?(team)
|
|
||||||
team_shared_objects.where(team: team, permission_level: :shared_write).any?
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.name_like(query)
|
def self.name_like(query)
|
||||||
where('repositories.name ILIKE ?', "%#{query}%")
|
where('repositories.name ILIKE ?', "%#{query}%")
|
||||||
end
|
end
|
||||||
|
|
|
@ -98,6 +98,13 @@ class RepositoryRow < ApplicationRecord
|
||||||
class_name: 'RepositoryRow',
|
class_name: 'RepositoryRow',
|
||||||
source: :parent,
|
source: :parent,
|
||||||
dependent: :destroy
|
dependent: :destroy
|
||||||
|
has_many :discarded_storage_location_repository_rows,
|
||||||
|
-> { discarded },
|
||||||
|
class_name: 'StorageLocationRepositoryRow',
|
||||||
|
inverse_of: :repository_row,
|
||||||
|
dependent: :destroy
|
||||||
|
has_many :storage_location_repository_rows, inverse_of: :repository_row, dependent: :destroy
|
||||||
|
has_many :storage_locations, through: :storage_location_repository_rows
|
||||||
|
|
||||||
auto_strip_attributes :name, nullify: false
|
auto_strip_attributes :name, nullify: false
|
||||||
validates :name,
|
validates :name,
|
||||||
|
@ -172,6 +179,14 @@ class RepositoryRow < ApplicationRecord
|
||||||
self[:archived]
|
self[:archived]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_reminders?(user)
|
||||||
|
stock_reminders = RepositoryCell.stock_reminder_repository_cells_scope(
|
||||||
|
repository_cells.joins(:repository_column), user)
|
||||||
|
date_reminders = RepositoryCell.date_time_reminder_repository_cells_scope(
|
||||||
|
repository_cells.joins(:repository_column), user)
|
||||||
|
stock_reminders.any? || date_reminders.any?
|
||||||
|
end
|
||||||
|
|
||||||
def archived
|
def archived
|
||||||
row_archived? || repository&.archived?
|
row_archived? || repository&.archived?
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,6 +37,9 @@ class Result < ApplicationRecord
|
||||||
accepts_nested_attributes_for :tables
|
accepts_nested_attributes_for :tables
|
||||||
|
|
||||||
before_save :ensure_default_name
|
before_save :ensure_default_name
|
||||||
|
after_discard do
|
||||||
|
CleanupUserSettingsJob.perform_later('result_states', id)
|
||||||
|
end
|
||||||
|
|
||||||
def self.search(user,
|
def self.search(user,
|
||||||
include_archived,
|
include_archived,
|
||||||
|
|
159
app/models/storage_location.rb
Normal file
159
app/models/storage_location.rb
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class StorageLocation < ApplicationRecord
|
||||||
|
include Cloneable
|
||||||
|
include Discard::Model
|
||||||
|
ID_PREFIX = 'SL'
|
||||||
|
include PrefixedIdModel
|
||||||
|
include Shareable
|
||||||
|
|
||||||
|
default_scope -> { kept }
|
||||||
|
|
||||||
|
has_one_attached :image
|
||||||
|
|
||||||
|
belongs_to :team
|
||||||
|
belongs_to :parent, class_name: 'StorageLocation', optional: true
|
||||||
|
belongs_to :created_by, class_name: 'User'
|
||||||
|
|
||||||
|
has_many :storage_location_repository_rows, inverse_of: :storage_location, dependent: :destroy
|
||||||
|
has_many :storage_locations, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
|
||||||
|
has_many :repository_rows, through: :storage_location_repository_rows
|
||||||
|
|
||||||
|
validates :name, length: { maximum: Constants::NAME_MAX_LENGTH }
|
||||||
|
validate :parent_validation, if: -> { parent.present? }
|
||||||
|
|
||||||
|
scope :readable_by_user, (lambda do |user, team = user.current_team|
|
||||||
|
next StorageLocation.none unless team.permission_granted?(user, TeamPermissions::STORAGE_LOCATIONS_READ)
|
||||||
|
|
||||||
|
where(team: team)
|
||||||
|
end)
|
||||||
|
|
||||||
|
after_discard do
|
||||||
|
StorageLocation.where(parent_id: id).find_each(&:discard)
|
||||||
|
storage_location_repository_rows.each(&:discard)
|
||||||
|
end
|
||||||
|
|
||||||
|
def shared_with?(team)
|
||||||
|
return false if self.team == team
|
||||||
|
|
||||||
|
(root? ? self : root_storage_location).private_shared_with?(team)
|
||||||
|
end
|
||||||
|
|
||||||
|
def root?
|
||||||
|
parent_id.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def root_storage_location
|
||||||
|
return self if root?
|
||||||
|
|
||||||
|
storage_location = self
|
||||||
|
|
||||||
|
storage_location = storage_location.parent while storage_location.parent_id
|
||||||
|
|
||||||
|
storage_location
|
||||||
|
end
|
||||||
|
|
||||||
|
def empty?
|
||||||
|
storage_location_repository_rows.count.zero?
|
||||||
|
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
|
37
app/models/storage_location_repository_row.rb
Normal file
37
app/models/storage_location_repository_row.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class StorageLocationRepositoryRow < ApplicationRecord
|
||||||
|
include Discard::Model
|
||||||
|
|
||||||
|
default_scope -> { kept }
|
||||||
|
|
||||||
|
belongs_to :storage_location, inverse_of: :storage_location_repository_rows
|
||||||
|
belongs_to :repository_row, inverse_of: :storage_location_repository_rows
|
||||||
|
belongs_to :created_by, class_name: 'User'
|
||||||
|
|
||||||
|
with_options if: -> { storage_location.container && storage_location.metadata['type'] == 'grid' } do
|
||||||
|
validate :position_must_be_present
|
||||||
|
validate :ensure_uniq_position
|
||||||
|
end
|
||||||
|
|
||||||
|
def human_readable_position
|
||||||
|
return unless metadata['position']
|
||||||
|
|
||||||
|
column_letter = ('A'..'Z').to_a[metadata['position'][0] - 1]
|
||||||
|
row_number = metadata['position'][1]
|
||||||
|
|
||||||
|
"#{column_letter}#{row_number}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def position_must_be_present
|
||||||
|
errors.add(:base, I18n.t('activerecord.errors.models.storage_location.missing_position')) if metadata['position'].blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_uniq_position
|
||||||
|
if StorageLocationRepositoryRow.where(storage_location: storage_location)
|
||||||
|
.where('metadata @> ?', { position: metadata['position'] }.to_json)
|
||||||
|
.where.not(id: id).exists?
|
||||||
|
errors.add(:base, I18n.t('activerecord.errors.models.storage_location.not_uniq_position'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -72,6 +72,7 @@ class Team < ApplicationRecord
|
||||||
source_type: 'RepositoryBase',
|
source_type: 'RepositoryBase',
|
||||||
dependent: :destroy
|
dependent: :destroy
|
||||||
has_many :shareable_links, inverse_of: :team, dependent: :destroy
|
has_many :shareable_links, inverse_of: :team, dependent: :destroy
|
||||||
|
has_many :storage_locations, dependent: :destroy
|
||||||
|
|
||||||
attr_accessor :without_templates
|
attr_accessor :without_templates
|
||||||
|
|
||||||
|
|
39
app/permissions/storage_location.rb
Normal file
39
app/permissions/storage_location.rb
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Canaid::Permissions.register_for(StorageLocation) do
|
||||||
|
can :read_storage_location do |user, storage_location|
|
||||||
|
root_storage_location = storage_location.root_storage_location
|
||||||
|
|
||||||
|
next true if root_storage_location.shared_with?(user.current_team)
|
||||||
|
|
||||||
|
user.current_team == root_storage_location.team && root_storage_location.team.permission_granted?(
|
||||||
|
user,
|
||||||
|
if root_storage_location.container?
|
||||||
|
TeamPermissions::STORAGE_LOCATION_CONTAINERS_READ
|
||||||
|
else
|
||||||
|
TeamPermissions::STORAGE_LOCATIONS_READ
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
can :manage_storage_location do |user, storage_location|
|
||||||
|
root_storage_location = storage_location.root_storage_location
|
||||||
|
|
||||||
|
next true if root_storage_location.shared_with_write?(user.current_team)
|
||||||
|
|
||||||
|
user.current_team == root_storage_location.team && root_storage_location.team.permission_granted?(
|
||||||
|
user,
|
||||||
|
if root_storage_location.container?
|
||||||
|
TeamPermissions::STORAGE_LOCATION_CONTAINERS_MANAGE
|
||||||
|
else
|
||||||
|
TeamPermissions::STORAGE_LOCATIONS_MANAGE
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
can :share_storage_location do |user, storage_location|
|
||||||
|
user.current_team == storage_location.team &&
|
||||||
|
storage_location.root? &&
|
||||||
|
can_manage_storage_location?(user, storage_location)
|
||||||
|
end
|
||||||
|
end
|
|
@ -43,6 +43,14 @@ Canaid::Permissions.register_for(Team) do
|
||||||
within_limits && team.permission_granted?(user, TeamPermissions::INVENTORIES_CREATE)
|
within_limits && team.permission_granted?(user, TeamPermissions::INVENTORIES_CREATE)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
can :create_storage_locations do |user, team|
|
||||||
|
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATIONS_CREATE)
|
||||||
|
end
|
||||||
|
|
||||||
|
can :create_storage_location_containers do |user, team|
|
||||||
|
team.permission_granted?(user, TeamPermissions::STORAGE_LOCATION_CONTAINERS_CREATE)
|
||||||
|
end
|
||||||
|
|
||||||
can :create_reports do |user, team|
|
can :create_reports do |user, team|
|
||||||
team.permission_granted?(user, TeamPermissions::REPORTS_CREATE)
|
team.permission_granted?(user, TeamPermissions::REPORTS_CREATE)
|
||||||
end
|
end
|
||||||
|
|
42
app/serializers/concerns/shareable_serializer.rb
Normal file
42
app/serializers/concerns/shareable_serializer.rb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ShareableSerializer
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
attributes :shared, :shared_label, :ishared, :shared_read, :shared_write, :shareable_write
|
||||||
|
end
|
||||||
|
|
||||||
|
def shared
|
||||||
|
object.shared_with?(current_user.current_team)
|
||||||
|
end
|
||||||
|
|
||||||
|
def shared_label
|
||||||
|
case object[:shared]
|
||||||
|
when 1
|
||||||
|
I18n.t('libraries.index.shared')
|
||||||
|
when 2
|
||||||
|
I18n.t('libraries.index.shared_for_editing')
|
||||||
|
when 3
|
||||||
|
I18n.t('libraries.index.shared_for_viewing')
|
||||||
|
when 4
|
||||||
|
I18n.t('libraries.index.not_shared')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ishared
|
||||||
|
object.i_shared?(current_user.current_team)
|
||||||
|
end
|
||||||
|
|
||||||
|
def shared_read
|
||||||
|
object.shared_read?
|
||||||
|
end
|
||||||
|
|
||||||
|
def shared_write
|
||||||
|
object.shared_write?
|
||||||
|
end
|
||||||
|
|
||||||
|
def shareable_write
|
||||||
|
object.shareable_write?
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,36 +4,14 @@ module Lists
|
||||||
class RepositorySerializer < ActiveModel::Serializer
|
class RepositorySerializer < ActiveModel::Serializer
|
||||||
include Canaid::Helpers::PermissionsHelper
|
include Canaid::Helpers::PermissionsHelper
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
|
include ShareableSerializer
|
||||||
|
|
||||||
attributes :name, :code, :nr_of_rows, :shared, :shared_label, :ishared,
|
attributes :name, :code, :nr_of_rows, :team, :created_at, :created_by, :archived_on, :archived_by, :urls
|
||||||
:team, :created_at, :created_by, :archived_on, :archived_by,
|
|
||||||
:urls, :shared_read, :shared_write, :shareable_write
|
|
||||||
|
|
||||||
def nr_of_rows
|
def nr_of_rows
|
||||||
object[:row_count]
|
object[:row_count]
|
||||||
end
|
end
|
||||||
|
|
||||||
def shared
|
|
||||||
object.shared_with?(current_user.current_team)
|
|
||||||
end
|
|
||||||
|
|
||||||
def shared_label
|
|
||||||
case object[:shared]
|
|
||||||
when 1
|
|
||||||
I18n.t('libraries.index.shared')
|
|
||||||
when 2
|
|
||||||
I18n.t('libraries.index.shared_for_editing')
|
|
||||||
when 3
|
|
||||||
I18n.t('libraries.index.shared_for_viewing')
|
|
||||||
when 4
|
|
||||||
I18n.t('libraries.index.not_shared')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def ishared
|
|
||||||
object.i_shared?(current_user.current_team)
|
|
||||||
end
|
|
||||||
|
|
||||||
def team
|
def team
|
||||||
object[:team_name]
|
object[:team_name]
|
||||||
end
|
end
|
||||||
|
@ -54,25 +32,15 @@ module Lists
|
||||||
object[:archived_by_user]
|
object[:archived_by_user]
|
||||||
end
|
end
|
||||||
|
|
||||||
def shared_read
|
|
||||||
object.shared_read?
|
|
||||||
end
|
|
||||||
|
|
||||||
def shared_write
|
|
||||||
object.shared_write?
|
|
||||||
end
|
|
||||||
|
|
||||||
def shareable_write
|
|
||||||
object.shareable_write?
|
|
||||||
end
|
|
||||||
|
|
||||||
def urls
|
def urls
|
||||||
{
|
{
|
||||||
show: repository_path(object),
|
show: repository_path(object),
|
||||||
update: team_repository_path(current_user.current_team, id: object, format: :json),
|
update: team_repository_path(current_user.current_team, id: object, format: :json),
|
||||||
shareable_teams: shareable_teams_team_repository_path(current_user.current_team, object),
|
|
||||||
duplicate: team_repository_copy_path(current_user.current_team, repository_id: object, format: :json),
|
duplicate: team_repository_copy_path(current_user.current_team, repository_id: object, format: :json),
|
||||||
share: team_repository_team_repositories_path(current_user.current_team, object)
|
shareable_teams: shareable_teams_team_shared_objects_path(
|
||||||
|
current_user.current_team, object_id: object.id, object_type: 'Repository'
|
||||||
|
),
|
||||||
|
share: team_shared_objects_path(current_user.current_team, object_id: object.id, object_type: 'Repository')
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Lists
|
||||||
|
class StorageLocationRepositoryRowSerializer < ActiveModel::Serializer
|
||||||
|
include Canaid::Helpers::PermissionsHelper
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
|
attributes :created_by, :created_on, :position, :row_id, :row_name, :hidden, :position_formatted, :stock,
|
||||||
|
:have_reminders, :reminders_url
|
||||||
|
|
||||||
|
def row_id
|
||||||
|
object.repository_row.code
|
||||||
|
end
|
||||||
|
|
||||||
|
def row_name
|
||||||
|
object.repository_row.name unless hidden
|
||||||
|
end
|
||||||
|
|
||||||
|
def created_by
|
||||||
|
object.created_by.full_name unless hidden
|
||||||
|
end
|
||||||
|
|
||||||
|
def created_on
|
||||||
|
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
|
||||||
|
|
||||||
|
def have_reminders
|
||||||
|
object.repository_row.has_reminders?(scope)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reminders_url
|
||||||
|
row = object.repository_row
|
||||||
|
active_reminder_repository_cells_repository_repository_row_url(row.repository, row)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
62
app/serializers/lists/storage_location_serializer.rb
Normal file
62
app/serializers/lists/storage_location_serializer.rb
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Lists
|
||||||
|
class StorageLocationSerializer < ActiveModel::Serializer
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
include ShareableSerializer
|
||||||
|
|
||||||
|
attributes :id, :code, :name, :container, :description, :owned_by, :created_by,
|
||||||
|
:created_on, :urls, :metadata, :file_name, :sub_location_count, :is_empty
|
||||||
|
|
||||||
|
def owned_by
|
||||||
|
object.team.name
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_empty
|
||||||
|
object.empty?
|
||||||
|
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
|
||||||
|
|
||||||
|
def created_on
|
||||||
|
I18n.l(object.created_at, format: :full)
|
||||||
|
end
|
||||||
|
|
||||||
|
def sub_location_count
|
||||||
|
if object.respond_to?(:sub_location_count)
|
||||||
|
object.sub_location_count
|
||||||
|
else
|
||||||
|
StorageLocation.where(parent_id: object.id).count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def urls
|
||||||
|
show_url = if @object.container
|
||||||
|
storage_location_path(@object)
|
||||||
|
else
|
||||||
|
storage_locations_path(parent_id: object.id)
|
||||||
|
end
|
||||||
|
{
|
||||||
|
show: show_url,
|
||||||
|
update: storage_location_path(@object),
|
||||||
|
shareable_teams: shareable_teams_team_shared_objects_path(
|
||||||
|
current_user.current_team, object_id: object.id, object_type: object.class.name
|
||||||
|
),
|
||||||
|
share: team_shared_objects_path(current_user.current_team, object_id: object.id, object_type: object.class.name)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -13,7 +13,8 @@ class ResultSerializer < ActiveModel::Serializer
|
||||||
:open_vector_editor_context, :comments_count, :assets_view_mode, :storage_limit, :collapsed
|
:open_vector_editor_context, :comments_count, :assets_view_mode, :storage_limit, :collapsed
|
||||||
|
|
||||||
def collapsed
|
def collapsed
|
||||||
false
|
result_states = current_user.settings.fetch('result_states', {})
|
||||||
|
result_states[object.id.to_s] == true
|
||||||
end
|
end
|
||||||
|
|
||||||
def marvinjs_enabled
|
def marvinjs_enabled
|
||||||
|
|
|
@ -6,16 +6,16 @@ class ShareableTeamSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :name, :private_shared_with, :private_shared_with_write
|
attributes :id, :name, :private_shared_with, :private_shared_with_write
|
||||||
|
|
||||||
def private_shared_with
|
def private_shared_with
|
||||||
repository.private_shared_with?(object)
|
model.private_shared_with?(object)
|
||||||
end
|
end
|
||||||
|
|
||||||
def private_shared_with_write
|
def private_shared_with_write
|
||||||
repository.private_shared_with_write?(object)
|
model.private_shared_with_write?(object)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def repository
|
def model
|
||||||
scope[:repository] || @instance_options[:repository]
|
scope[:model] || @instance_options[:model]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -61,7 +61,11 @@ module Activities
|
||||||
end
|
end
|
||||||
|
|
||||||
if id
|
if id
|
||||||
obj = const.find id
|
obj = if const.respond_to?(:with_discarded)
|
||||||
|
const.with_discarded.find id
|
||||||
|
else
|
||||||
|
const.find id
|
||||||
|
end
|
||||||
@activity.message_items[k] = { type: const.to_s, value: obj.public_send(getter_method).to_s, id: id }
|
@activity.message_items[k] = { type: const.to_s, value: obj.public_send(getter_method).to_s, id: id }
|
||||||
@activity.message_items[k][:value_for] = getter_method
|
@activity.message_items[k][:value_for] = getter_method
|
||||||
@activity.message_items[k][:value_type] = value_type unless value_type.nil?
|
@activity.message_items[k][:value_type] = value_type unless value_type.nil?
|
||||||
|
|
|
@ -29,7 +29,7 @@ module Lists
|
||||||
end
|
end
|
||||||
|
|
||||||
def paginate_records
|
def paginate_records
|
||||||
@records = @records.page(@params[:page]).per(@params[:per_page])
|
@records = @records.page(@params[:page]).per(@params[:per_page]) if @params[:page].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def sort_direction(order_params)
|
def sort_direction(order_params)
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Lists
|
||||||
|
class StorageLocationRepositoryRowsService < BaseService
|
||||||
|
def initialize(team, params)
|
||||||
|
@team = team
|
||||||
|
@storage_location_id = params[:storage_location_id]
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_records
|
||||||
|
@records = StorageLocationRepositoryRow.includes(:repository_row).where(storage_location_id: @storage_location_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_records; end
|
||||||
|
end
|
||||||
|
end
|
36
app/services/lists/storage_locations_service.rb
Normal file
36
app/services/lists/storage_locations_service.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Lists
|
||||||
|
class StorageLocationsService < BaseService
|
||||||
|
def initialize(user, team, params)
|
||||||
|
@user = user
|
||||||
|
@team = team
|
||||||
|
@parent_id = params[:parent_id]
|
||||||
|
@filters = params[:filters] || {}
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_records
|
||||||
|
@records =
|
||||||
|
StorageLocation.joins('LEFT JOIN storage_locations AS sub_locations ' \
|
||||||
|
'ON storage_locations.id = sub_locations.parent_id')
|
||||||
|
.viewable_by_user(@user, @team)
|
||||||
|
.select('storage_locations.*, COUNT(sub_locations.id) AS sub_location_count')
|
||||||
|
.group(:id)
|
||||||
|
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
|
|
@ -93,7 +93,7 @@ module ModelExporters
|
||||||
element_json = element.as_json
|
element_json = element.as_json
|
||||||
case element.orderable_type
|
case element.orderable_type
|
||||||
when 'ResultText'
|
when 'ResultText'
|
||||||
element_json['step_text'] = element.orderable.as_json
|
element_json['result_text'] = element.orderable.as_json
|
||||||
when 'ResultTable'
|
when 'ResultTable'
|
||||||
element_json['table'] = table(element.orderable.table)
|
element_json['table'] = table(element.orderable.table)
|
||||||
end
|
end
|
||||||
|
|
|
@ -47,10 +47,6 @@ module ModelExporters
|
||||||
user_assignments: team.user_assignments.map do |ua|
|
user_assignments: team.user_assignments.map do |ua|
|
||||||
user_assignment(ua)
|
user_assignment(ua)
|
||||||
end,
|
end,
|
||||||
notifications: Notification
|
|
||||||
.includes(:user_notifications)
|
|
||||||
.where('user_notifications.user_id': team.users)
|
|
||||||
.map { |n| notification(n) },
|
|
||||||
repositories: team.repositories.map { |r| repository(r) },
|
repositories: team.repositories.map { |r| repository(r) },
|
||||||
tiny_mce_assets: team.tiny_mce_assets.map { |tma| tiny_mce_asset_data(tma) },
|
tiny_mce_assets: team.tiny_mce_assets.map { |tma| tiny_mce_asset_data(tma) },
|
||||||
protocols: team.protocols.where(my_module: nil).map do |pr|
|
protocols: team.protocols.where(my_module: nil).map do |pr|
|
||||||
|
@ -68,15 +64,6 @@ module ModelExporters
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def notification(notification)
|
|
||||||
notification_json = notification.as_json
|
|
||||||
notification_json['type_of'] = Extends::NOTIFICATIONS_TYPES
|
|
||||||
.key(notification
|
|
||||||
.read_attribute('type_of'))
|
|
||||||
.to_s
|
|
||||||
notification_json
|
|
||||||
end
|
|
||||||
|
|
||||||
def label_templates(templates)
|
def label_templates(templates)
|
||||||
templates.where.not(type: 'FluicsLabelTemplate').map do |template|
|
templates.where.not(type: 'FluicsLabelTemplate').map do |template|
|
||||||
template_json = template.as_json
|
template_json = template.as_json
|
||||||
|
@ -97,7 +84,6 @@ module ModelExporters
|
||||||
copy_files([user], :avatar, File.join(@dir_to_export, 'avatars'))
|
copy_files([user], :avatar, File.join(@dir_to_export, 'avatars'))
|
||||||
{
|
{
|
||||||
user: user_json,
|
user: user_json,
|
||||||
user_notifications: user.user_notifications,
|
|
||||||
user_identities: user.user_identities,
|
user_identities: user.user_identities,
|
||||||
repository_table_states:
|
repository_table_states:
|
||||||
user.repository_table_states.where(repository: @team.repositories)
|
user.repository_table_states.where(repository: @team.repositories)
|
||||||
|
|
|
@ -11,9 +11,9 @@ module Reports
|
||||||
def self.image_prepare(asset)
|
def self.image_prepare(asset)
|
||||||
if asset.class == Asset
|
if asset.class == Asset
|
||||||
if asset.inline?
|
if asset.inline?
|
||||||
asset.preview_attachment.representation(resize_to_limit: Constants::MEDIUM_PIC_FORMAT, format: :png)
|
asset.medium_preview
|
||||||
else
|
else
|
||||||
asset.preview_attachment.representation(resize_to_limit: Constants::LARGE_PIC_FORMAT, format: :png)
|
asset.large_preview
|
||||||
end
|
end
|
||||||
elsif asset.class == TinyMceAsset
|
elsif asset.class == TinyMceAsset
|
||||||
asset.image.representation(format: :png)
|
asset.image.representation(format: :png)
|
||||||
|
|
43
app/services/storage_locations/export_service.rb
Normal file
43
app/services/storage_locations/export_service.rb
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'caxlsx'
|
||||||
|
|
||||||
|
module StorageLocations
|
||||||
|
class ExportService
|
||||||
|
include Canaid::Helpers::PermissionsHelper
|
||||||
|
def initialize(storage_location, user)
|
||||||
|
@storage_location = storage_location
|
||||||
|
@user = user
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_xlsx
|
||||||
|
package = Axlsx::Package.new
|
||||||
|
workbook = package.workbook
|
||||||
|
|
||||||
|
workbook.add_worksheet(name: 'Box Export') do |sheet|
|
||||||
|
sheet.add_row ['Box position', 'Item ID', 'Item name']
|
||||||
|
|
||||||
|
@storage_location.storage_location_repository_rows.each do |storage_location_item|
|
||||||
|
row = storage_location_item.repository_row
|
||||||
|
row_name = row.name if can_read_repository?(@user, row.repository)
|
||||||
|
sheet.add_row [format_position(storage_location_item), storage_location_item.repository_row_id, row_name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
package.to_stream.read
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def format_position(item)
|
||||||
|
position = item.metadata['position']
|
||||||
|
|
||||||
|
return unless position
|
||||||
|
|
||||||
|
column_letter = ('A'..'Z').to_a[position[0] - 1]
|
||||||
|
row_number = position[1]
|
||||||
|
|
||||||
|
"#{column_letter}#{row_number}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
102
app/services/storage_locations/import_service.rb
Normal file
102
app/services/storage_locations/import_service.rb
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'caxlsx'
|
||||||
|
|
||||||
|
module StorageLocations
|
||||||
|
class ImportService
|
||||||
|
def initialize(storage_location, file, user)
|
||||||
|
@storage_location = storage_location
|
||||||
|
@file = file
|
||||||
|
@user = user
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_items
|
||||||
|
sheet = SpreadsheetParser.open_spreadsheet(@file)
|
||||||
|
incoming_items = SpreadsheetParser.spreadsheet_enumerator(sheet).reject { |r| r.all?(&:blank?) }
|
||||||
|
|
||||||
|
# Check if the file has proper headers
|
||||||
|
header = SpreadsheetParser.parse_row(incoming_items[0], sheet)
|
||||||
|
return { status: :error, message: I18n.t('storage_locations.show.import_modal.errors.invalid_structure') } unless header[0] == 'Box position' && header[1] == 'Item ID'
|
||||||
|
|
||||||
|
# Remove first row
|
||||||
|
incoming_items.shift
|
||||||
|
|
||||||
|
incoming_items.map! { |r| SpreadsheetParser.parse_row(r, sheet) }
|
||||||
|
|
||||||
|
# Check duplicate positions in the file
|
||||||
|
if @storage_location.with_grid? && incoming_items.pluck(0).uniq.length != incoming_items.length
|
||||||
|
return { status: :error, message: I18n.t('storage_locations.show.import_modal.errors.invalid_position') }
|
||||||
|
end
|
||||||
|
|
||||||
|
existing_items = @storage_location.storage_location_repository_rows.map do |item|
|
||||||
|
[convert_position_number_to_letter(item), item.repository_row_id, item.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
items_to_unassign = []
|
||||||
|
|
||||||
|
existing_items.each do |existing_item|
|
||||||
|
if incoming_items.any? { |r| r[0] == existing_item[0] && r[1].to_i == existing_item[1] }
|
||||||
|
incoming_items.reject! { |r| r[0] == existing_item[0] && r[1].to_i == existing_item[1] }
|
||||||
|
else
|
||||||
|
items_to_unassign << existing_item[2]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
error_message = ''
|
||||||
|
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
@storage_location.storage_location_repository_rows.where(id: items_to_unassign).discard_all
|
||||||
|
|
||||||
|
incoming_items.each do |row|
|
||||||
|
if @storage_location.with_grid?
|
||||||
|
position = convert_position_letter_to_number(row[0])
|
||||||
|
|
||||||
|
unless position[0].to_i <= @storage_location.grid_size[0].to_i && position[1].to_i <= @storage_location.grid_size[1].to_i
|
||||||
|
error_message = I18n.t('storage_locations.show.import_modal.errors.invalid_position')
|
||||||
|
raise ActiveRecord::RecordInvalid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
repository_row = RepositoryRow.find_by(id: row[1])
|
||||||
|
|
||||||
|
unless repository_row
|
||||||
|
error_message = I18n.t('storage_locations.show.import_modal.errors.invalid_item', row_id: row[1].to_i)
|
||||||
|
raise ActiveRecord::RecordNotFound
|
||||||
|
end
|
||||||
|
|
||||||
|
@storage_location.storage_location_repository_rows.create!(
|
||||||
|
repository_row: repository_row,
|
||||||
|
metadata: { position: position },
|
||||||
|
created_by: @user
|
||||||
|
)
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
return { status: :error, message: error_message }
|
||||||
|
end
|
||||||
|
|
||||||
|
{ status: :ok }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def convert_position_letter_to_number(position)
|
||||||
|
return unless position
|
||||||
|
|
||||||
|
column_letter = position[0]
|
||||||
|
row_number = position[1]
|
||||||
|
|
||||||
|
[column_letter.ord - 64, row_number]
|
||||||
|
end
|
||||||
|
|
||||||
|
def convert_position_number_to_letter(item)
|
||||||
|
position = item.metadata['position']
|
||||||
|
|
||||||
|
return unless position
|
||||||
|
|
||||||
|
column_letter = ('A'..'Z').to_a[position[0] - 1]
|
||||||
|
row_number = position[1]
|
||||||
|
|
||||||
|
"#{column_letter}#{row_number}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -71,7 +71,6 @@ class TeamImporter
|
||||||
# Find new id of the first admin in the team
|
# Find new id of the first admin in the team
|
||||||
@admin_id = @user_mappings[team_json['default_admin_id']]
|
@admin_id = @user_mappings[team_json['default_admin_id']]
|
||||||
|
|
||||||
create_notifications(team_json['notifications'])
|
|
||||||
create_protocol_keywords(team_json['protocol_keywords'], team)
|
create_protocol_keywords(team_json['protocol_keywords'], team)
|
||||||
create_protocols(team_json['protocols'], nil, team)
|
create_protocols(team_json['protocols'], nil, team)
|
||||||
create_project_folders(team_json['project_folders'], team)
|
create_project_folders(team_json['project_folders'], team)
|
||||||
|
@ -83,17 +82,6 @@ class TeamImporter
|
||||||
# Second run, we needed it because of some models should be created
|
# Second run, we needed it because of some models should be created
|
||||||
|
|
||||||
team_json['users'].each do |user_json|
|
team_json['users'].each do |user_json|
|
||||||
user_json['user_notifications'].each do |user_notification_json|
|
|
||||||
user_notification = UserNotification.new(user_notification_json)
|
|
||||||
user_notification.id = nil
|
|
||||||
user_notification.user_id = find_user(user_notification.user_id)
|
|
||||||
user_notification.notification_id =
|
|
||||||
@notification_mappings[user_notification.notification_id]
|
|
||||||
next if user_notification.notification_id.blank?
|
|
||||||
|
|
||||||
user_notification.save!
|
|
||||||
end
|
|
||||||
|
|
||||||
user_json['repository_table_states'].each do |rep_tbl_state_json|
|
user_json['repository_table_states'].each do |rep_tbl_state_json|
|
||||||
rep_tbl_state = RepositoryTableState.new(rep_tbl_state_json)
|
rep_tbl_state = RepositoryTableState.new(rep_tbl_state_json)
|
||||||
rep_tbl_state.id = nil
|
rep_tbl_state.id = nil
|
||||||
|
@ -423,21 +411,6 @@ class TeamImporter
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_notifications(notifications_json)
|
|
||||||
puts 'Creating notifications...'
|
|
||||||
notifications_json.each do |notification_json|
|
|
||||||
notification = Notification.new(notification_json)
|
|
||||||
next if notification.type_of.blank?
|
|
||||||
|
|
||||||
orig_notification_id = notification.id
|
|
||||||
notification.id = nil
|
|
||||||
notification.generator_user_id = find_user(notification.generator_user_id)
|
|
||||||
notification.save!
|
|
||||||
@notification_mappings[orig_notification_id] = notification.id
|
|
||||||
@notification_counter += 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_repositories(repositories_json, team, snapshots = false)
|
def create_repositories(repositories_json, team, snapshots = false)
|
||||||
puts 'Creating repositories...'
|
puts 'Creating repositories...'
|
||||||
repositories_json.each do |repository_json|
|
repositories_json.each do |repository_json|
|
||||||
|
@ -756,7 +729,7 @@ class TeamImporter
|
||||||
def create_protocols(protocols_json, my_module = nil, team = nil,
|
def create_protocols(protocols_json, my_module = nil, team = nil,
|
||||||
user_id = nil)
|
user_id = nil)
|
||||||
|
|
||||||
sorted_protocols = protocols_json.sort_by { |p| p['id'] }
|
sorted_protocols = protocols_json.sort_by { |p| p['protocol']['id'] }
|
||||||
|
|
||||||
puts 'Creating protocols...'
|
puts 'Creating protocols...'
|
||||||
sorted_protocols.each do |protocol_json|
|
sorted_protocols.each do |protocol_json|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
# 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
|
||||||
|
return unless can_manage_storage_location?(@storage_location)
|
||||||
|
|
||||||
|
{
|
||||||
|
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 && can_manage_storage_location?(@storage_location)
|
||||||
|
|
||||||
|
{
|
||||||
|
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
|
98
app/services/toolbars/storage_locations_service.rb
Normal file
98
app/services/toolbars/storage_locations_service.rb
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
# 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,
|
||||||
|
share_action
|
||||||
|
].compact
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def edit_action
|
||||||
|
return unless @single && can_manage_storage_location?(@storage_locations.first)
|
||||||
|
|
||||||
|
{
|
||||||
|
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 && can_manage_storage_location?(@storage_locations.first)
|
||||||
|
|
||||||
|
{
|
||||||
|
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 && can_manage_storage_location?(@storage_locations.first)
|
||||||
|
|
||||||
|
{
|
||||||
|
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 && can_manage_storage_location?(@storage_locations.first)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def share_action
|
||||||
|
return unless @single && can_share_storage_location?(@storage_locations.first)
|
||||||
|
|
||||||
|
{
|
||||||
|
name: :share,
|
||||||
|
label: I18n.t('storage_locations.index.share'),
|
||||||
|
icon: 'sn-icon sn-icon-shared',
|
||||||
|
type: :emit
|
||||||
|
}
|
||||||
|
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
|
end
|
||||||
%>
|
%>
|
||||||
|
|
||||||
|
<%= render partial: 'radio' %>
|
||||||
|
|
||||||
<%= render partial: 'select' %>
|
<%= render partial: 'select' %>
|
||||||
|
|
||||||
<%= render partial: 'modals' %>
|
<%= render partial: 'modals' %>
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<%= render partial: "global_activities/references/team",
|
||||||
|
locals: { team: team, subject: team, breadcrumbs: breadcrumbs, values: values, type_of: type_of } %>
|
||||||
|
<div class="ga-breadcrumb">
|
||||||
|
<span class="sn-icon sn-icon-storage"></span>
|
||||||
|
<% if subject %>
|
||||||
|
<%= route_to_other_team(storage_location_path(subject.id, team: subject.team.id),
|
||||||
|
team,
|
||||||
|
subject.name&.truncate(Constants::NAME_TRUNCATION_LENGTH),
|
||||||
|
title: subject.name) %>
|
||||||
|
<% else %>
|
||||||
|
<span title="<%= breadcrumbs['storage_location'] %>">
|
||||||
|
<%= breadcrumbs['storage_location']&.truncate(Constants::NAME_TRUNCATION_LENGTH) %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
|
@ -2,7 +2,7 @@
|
||||||
.print-footer {
|
.print-footer {
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,23 @@ json.actions do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
json.storage_locations do
|
||||||
|
json.locations(
|
||||||
|
@repository_row.storage_locations.distinct.map do |storage_location|
|
||||||
|
readable = can_read_storage_location?(storage_location)
|
||||||
|
|
||||||
|
{
|
||||||
|
id: storage_location.id,
|
||||||
|
readable: readable,
|
||||||
|
name: readable ? storage_location.name : storage_location.code,
|
||||||
|
metadata: storage_location.metadata,
|
||||||
|
positions: readable ? storage_location.storage_location_repository_rows.where(repository_row: @repository_row) : []
|
||||||
|
}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
json.enabled StorageLocation.storage_locations_enabled?
|
||||||
|
end
|
||||||
|
|
||||||
json.default_columns do
|
json.default_columns do
|
||||||
json.name @repository_row.name
|
json.name @repository_row.name
|
||||||
json.code @repository_row.code
|
json.code @repository_row.code
|
||||||
|
|
25
app/views/storage_locations/index.html.erb
Normal file
25
app/views/storage_locations/index.html.erb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<% provide(:head_title, t("storage_locations.index.head_title")) %>
|
||||||
|
<% provide(:container_class, "no-second-nav-container") %>
|
||||||
|
|
||||||
|
<% if current_team %>
|
||||||
|
<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-storageLocations">
|
||||||
|
<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]) %>"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= javascript_include_tag 'vue_storage_locations_table' %>
|
||||||
|
<% end %>
|
26
app/views/storage_locations/show.html.erb
Normal file
26
app/views/storage_locations/show.html.erb
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<% 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-storageLocations-box">
|
||||||
|
<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) %>"
|
||||||
|
:can-manage="<%= can_manage_storage_location?(@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 %>
|
|
@ -1,2 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
env -i /usr/bin/chromium $@
|
|
|
@ -105,4 +105,8 @@ Rails.application.configure do
|
||||||
config.x.new_team_on_signup = false
|
config.x.new_team_on_signup = false
|
||||||
end
|
end
|
||||||
config.hosts << "dev.scinote.test"
|
config.hosts << "dev.scinote.test"
|
||||||
|
|
||||||
|
# Automatically update js-routes file
|
||||||
|
# when routes.rb is changed
|
||||||
|
config.middleware.use(JsRoutes::Middleware)
|
||||||
end
|
end
|
||||||
|
|
|
@ -188,7 +188,7 @@ class Extends
|
||||||
|
|
||||||
ACTIVITY_SUBJECT_TYPES = %w(
|
ACTIVITY_SUBJECT_TYPES = %w(
|
||||||
Team RepositoryBase Project Experiment MyModule Result Protocol Report RepositoryRow
|
Team RepositoryBase Project Experiment MyModule Result Protocol Report RepositoryRow
|
||||||
ProjectFolder Asset Step LabelTemplate
|
ProjectFolder Asset Step LabelTemplate StorageLocation StorageLocationRepositoryRow
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
SEARCHABLE_ACTIVITY_SUBJECT_TYPES = %w(
|
SEARCHABLE_ACTIVITY_SUBJECT_TYPES = %w(
|
||||||
|
@ -205,7 +205,8 @@ class Extends
|
||||||
my_module: %i(results protocols),
|
my_module: %i(results protocols),
|
||||||
result: [:assets],
|
result: [:assets],
|
||||||
protocol: [:steps],
|
protocol: [:steps],
|
||||||
step: [:assets]
|
step: [:assets],
|
||||||
|
storage_location: [:storage_location_repository_rows]
|
||||||
}
|
}
|
||||||
|
|
||||||
ACTIVITY_MESSAGE_ITEMS_TYPES =
|
ACTIVITY_MESSAGE_ITEMS_TYPES =
|
||||||
|
@ -495,7 +496,24 @@ class Extends
|
||||||
task_step_asset_renamed: 305,
|
task_step_asset_renamed: 305,
|
||||||
result_asset_renamed: 306,
|
result_asset_renamed: 306,
|
||||||
protocol_step_asset_renamed: 307,
|
protocol_step_asset_renamed: 307,
|
||||||
inventory_items_added_or_updated_with_import: 308
|
inventory_items_added_or_updated_with_import: 308,
|
||||||
|
storage_location_created: 309,
|
||||||
|
storage_location_deleted: 310,
|
||||||
|
storage_location_edited: 311,
|
||||||
|
storage_location_moved: 312,
|
||||||
|
storage_location_shared: 313,
|
||||||
|
storage_location_unshared: 314,
|
||||||
|
storage_location_sharing_updated: 315,
|
||||||
|
container_storage_location_created: 316,
|
||||||
|
container_storage_location_deleted: 317,
|
||||||
|
container_storage_location_edited: 318,
|
||||||
|
container_storage_location_moved: 319,
|
||||||
|
container_storage_location_shared: 320,
|
||||||
|
container_storage_location_unshared: 321,
|
||||||
|
container_storage_location_sharing_updated: 322,
|
||||||
|
storage_location_repository_row_created: 323,
|
||||||
|
storage_location_repository_row_deleted: 324,
|
||||||
|
storage_location_repository_row_moved: 325
|
||||||
}
|
}
|
||||||
|
|
||||||
ACTIVITY_GROUPS = {
|
ACTIVITY_GROUPS = {
|
||||||
|
@ -515,7 +533,10 @@ class Extends
|
||||||
190, 191, *204..215, 220, 223, 227, 228, 229, *230..235,
|
190, 191, *204..215, 220, 223, 227, 228, 229, *230..235,
|
||||||
*237..240, *253..256, *279..283, 300, 304, 307],
|
*237..240, *253..256, *279..283, 300, 304, 307],
|
||||||
team: [92, 94, 93, 97, 104, 244, 245],
|
team: [92, 94, 93, 97, 104, 244, 245],
|
||||||
label_templates: [*216..219]
|
label_templates: [*216..219],
|
||||||
|
storage_locations: [*309..315],
|
||||||
|
container_storage_location: [*316..322],
|
||||||
|
storage_location_repository_rows: [*323..325]
|
||||||
}
|
}
|
||||||
|
|
||||||
TOP_LEVEL_ASSIGNABLES = %w(Project Team Protocol Repository).freeze
|
TOP_LEVEL_ASSIGNABLES = %w(Project Team Protocol Repository).freeze
|
||||||
|
@ -636,6 +657,8 @@ class Extends
|
||||||
preferences/index
|
preferences/index
|
||||||
addons/index
|
addons/index
|
||||||
search/index
|
search/index
|
||||||
|
storage_locations/index
|
||||||
|
storage_locations/show
|
||||||
)
|
)
|
||||||
|
|
||||||
DEFAULT_USER_NOTIFICATION_SETTINGS = {
|
DEFAULT_USER_NOTIFICATION_SETTINGS = {
|
||||||
|
@ -673,6 +696,7 @@ class Extends
|
||||||
repository_export_file_type
|
repository_export_file_type
|
||||||
navigator_collapsed
|
navigator_collapsed
|
||||||
navigator_width
|
navigator_width
|
||||||
|
result_states
|
||||||
).freeze
|
).freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,13 @@ module PermissionExtends
|
||||||
REPORTS_CREATE
|
REPORTS_CREATE
|
||||||
LABEL_TEMPLATES_READ
|
LABEL_TEMPLATES_READ
|
||||||
LABEL_TEMPLATES_MANAGE
|
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
|
end
|
||||||
|
|
||||||
module ProtocolPermissions
|
module ProtocolPermissions
|
||||||
|
@ -24,7 +30,7 @@ module PermissionExtends
|
||||||
MANAGE
|
MANAGE
|
||||||
USERS_MANAGE
|
USERS_MANAGE
|
||||||
MANAGE_DRAFT
|
MANAGE_DRAFT
|
||||||
).each { |permission| const_set(permission, "protocol_#{permission.underscore}") }
|
).each { |permission| const_set(permission, "protocol_#{permission.parameterize}") }
|
||||||
end
|
end
|
||||||
|
|
||||||
module ReportPermissions
|
module ReportPermissions
|
||||||
|
@ -33,7 +39,7 @@ module PermissionExtends
|
||||||
READ
|
READ
|
||||||
MANAGE
|
MANAGE
|
||||||
USERS_MANAGE
|
USERS_MANAGE
|
||||||
).each { |permission| const_set(permission, "report_#{permission.underscore}") }
|
).each { |permission| const_set(permission, "report_#{permission.parameterize}") }
|
||||||
end
|
end
|
||||||
|
|
||||||
module ProjectPermissions
|
module ProjectPermissions
|
||||||
|
@ -51,7 +57,7 @@ module PermissionExtends
|
||||||
COMMENTS_MANAGE_OWN
|
COMMENTS_MANAGE_OWN
|
||||||
TAGS_MANAGE
|
TAGS_MANAGE
|
||||||
EXPERIMENTS_CREATE
|
EXPERIMENTS_CREATE
|
||||||
).each { |permission| const_set(permission, "project_#{permission.underscore}") }
|
).each { |permission| const_set(permission, "project_#{permission.parameterize}") }
|
||||||
end
|
end
|
||||||
|
|
||||||
module ExperimentPermissions
|
module ExperimentPermissions
|
||||||
|
@ -65,7 +71,7 @@ module PermissionExtends
|
||||||
USERS_MANAGE
|
USERS_MANAGE
|
||||||
READ_CANVAS
|
READ_CANVAS
|
||||||
ACTIVITIES_READ
|
ACTIVITIES_READ
|
||||||
).each { |permission| const_set(permission, "experiment_#{permission.underscore}") }
|
).each { |permission| const_set(permission, "experiment_#{permission.parameterize}") }
|
||||||
end
|
end
|
||||||
|
|
||||||
module MyModulePermissions
|
module MyModulePermissions
|
||||||
|
@ -107,7 +113,7 @@ module PermissionExtends
|
||||||
USERS_MANAGE
|
USERS_MANAGE
|
||||||
DESIGNATED_USERS_MANAGE
|
DESIGNATED_USERS_MANAGE
|
||||||
STOCK_CONSUMPTION_UPDATE
|
STOCK_CONSUMPTION_UPDATE
|
||||||
).each { |permission| const_set(permission, "task_#{permission.underscore}") }
|
).each { |permission| const_set(permission, "task_#{permission.parameterize}") }
|
||||||
end
|
end
|
||||||
|
|
||||||
module RepositoryPermissions
|
module RepositoryPermissions
|
||||||
|
@ -127,7 +133,7 @@ module PermissionExtends
|
||||||
COLUMNS_DELETE
|
COLUMNS_DELETE
|
||||||
USERS_MANAGE
|
USERS_MANAGE
|
||||||
FILTERS_MANAGE
|
FILTERS_MANAGE
|
||||||
).each { |permission| const_set(permission, "inventory_#{permission.underscore}") }
|
).each { |permission| const_set(permission, "inventory_#{permission.parameterize}") }
|
||||||
end
|
end
|
||||||
|
|
||||||
module PredefinedRoles
|
module PredefinedRoles
|
||||||
|
@ -147,6 +153,12 @@ module PermissionExtends
|
||||||
TeamPermissions::REPORTS_CREATE,
|
TeamPermissions::REPORTS_CREATE,
|
||||||
TeamPermissions::LABEL_TEMPLATES_READ,
|
TeamPermissions::LABEL_TEMPLATES_READ,
|
||||||
TeamPermissions::LABEL_TEMPLATES_MANAGE,
|
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,
|
||||||
ProtocolPermissions::READ_ARCHIVED,
|
ProtocolPermissions::READ_ARCHIVED,
|
||||||
ProtocolPermissions::MANAGE_DRAFT,
|
ProtocolPermissions::MANAGE_DRAFT,
|
||||||
|
@ -241,6 +253,8 @@ module PermissionExtends
|
||||||
|
|
||||||
VIEWER_PERMISSIONS = [
|
VIEWER_PERMISSIONS = [
|
||||||
TeamPermissions::LABEL_TEMPLATES_READ,
|
TeamPermissions::LABEL_TEMPLATES_READ,
|
||||||
|
TeamPermissions::STORAGE_LOCATIONS_READ,
|
||||||
|
TeamPermissions::STORAGE_LOCATION_CONTAINERS_READ,
|
||||||
ProtocolPermissions::READ,
|
ProtocolPermissions::READ,
|
||||||
ProtocolPermissions::READ_ARCHIVED,
|
ProtocolPermissions::READ_ARCHIVED,
|
||||||
ReportPermissions::READ,
|
ReportPermissions::READ,
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
Grover.configure do |config|
|
Grover.configure do |config|
|
||||||
config.options = {
|
config.options = {
|
||||||
cache: false,
|
cache: false,
|
||||||
executable_path: './bin/chromium',
|
executable_path: ENV['CHROMIUM_PATH'] || '/usr/bin/chromium',
|
||||||
launch_args: %w(--disable-gpu --no-sandbox),
|
launch_args: %w(--disable-dev-shm-usage --disable-gpu --no-sandbox),
|
||||||
timeout: Constants::GROVER_TIMEOUT_MS
|
timeout: Constants::GROVER_TIMEOUT_MS
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
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
|
|
@ -261,6 +261,12 @@ en:
|
||||||
attributes:
|
attributes:
|
||||||
text: Text is too long
|
text: Text is too long
|
||||||
position: "Position has already been taken by another item in the checklist"
|
position: "Position has already been taken by another item in the checklist"
|
||||||
|
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:
|
storage:
|
||||||
limit_reached: "Storage limit has been reached."
|
limit_reached: "Storage limit has been reached."
|
||||||
helpers:
|
helpers:
|
||||||
|
@ -342,6 +348,8 @@ en:
|
||||||
templates: "Templates"
|
templates: "Templates"
|
||||||
protocol: "Protocol"
|
protocol: "Protocol"
|
||||||
label: "Label"
|
label: "Label"
|
||||||
|
items: "Items"
|
||||||
|
locations: "Locations"
|
||||||
reports: "Reports"
|
reports: "Reports"
|
||||||
settings: "Settings"
|
settings: "Settings"
|
||||||
activities: "Activities"
|
activities: "Activities"
|
||||||
|
@ -2004,14 +2012,6 @@ en:
|
||||||
name_placeholder: "My inventory"
|
name_placeholder: "My inventory"
|
||||||
submit: "Create"
|
submit: "Create"
|
||||||
success_flash_html: "Inventory <strong>%{name}</strong> successfully created."
|
success_flash_html: "Inventory <strong>%{name}</strong> successfully created."
|
||||||
modal_share:
|
|
||||||
title: "Share Inventory"
|
|
||||||
submit: "Save sharing options"
|
|
||||||
share_with_team: "Share with Team"
|
|
||||||
can_edit: "Can Edit"
|
|
||||||
all_teams: "All teams (current & new)"
|
|
||||||
all_teams_tooltip: "This will disable individual team settings"
|
|
||||||
success_message: "Selected sharing options for the Inventory %{inventory_name} have been saved."
|
|
||||||
export:
|
export:
|
||||||
notification:
|
notification:
|
||||||
error:
|
error:
|
||||||
|
@ -2341,7 +2341,7 @@ en:
|
||||||
linkTo: 'https://knowledgebase.scinote.net/en/knowledge/how-to-add-items-to-an-inventory'
|
linkTo: 'https://knowledgebase.scinote.net/en/knowledge/how-to-add-items-to-an-inventory'
|
||||||
dragAndDropUpload:
|
dragAndDropUpload:
|
||||||
notSingleFileError: 'Single file import only. Please import one file at a time.'
|
notSingleFileError: 'Single file import only. Please import one file at a time.'
|
||||||
wrongFileTypeError: 'The file has invalid extension (.csv, .xlsx, .txt or .tsv.)'
|
wrongFileTypeError: 'The file has invalid extension (%{extensions}).'
|
||||||
emptyFileError: 'You have uploaded empty file. There is not much to import.'
|
emptyFileError: 'You have uploaded empty file. There is not much to import.'
|
||||||
fileTooLargeError: 'File too large. Max file size limit is'
|
fileTooLargeError: 'File too large. Max file size limit is'
|
||||||
importText:
|
importText:
|
||||||
|
@ -2608,7 +2608,12 @@ en:
|
||||||
custom_columns_label: 'Custom columns'
|
custom_columns_label: 'Custom columns'
|
||||||
relationships_label: 'Relationships'
|
relationships_label: 'Relationships'
|
||||||
assigned_label: 'Assigned'
|
assigned_label: 'Assigned'
|
||||||
|
locations_label: 'Locations'
|
||||||
QR_label: 'QR'
|
QR_label: 'QR'
|
||||||
|
locations:
|
||||||
|
title: 'Locations (%{count})'
|
||||||
|
container: 'Box'
|
||||||
|
assign: 'Assign new location'
|
||||||
repository_stock_values:
|
repository_stock_values:
|
||||||
manage_modal:
|
manage_modal:
|
||||||
title: "Stock %{item}"
|
title: "Stock %{item}"
|
||||||
|
@ -2663,6 +2668,113 @@ en:
|
||||||
repository_ledger_records:
|
repository_ledger_records:
|
||||||
errors:
|
errors:
|
||||||
my_module_references_missing: 'Task references are not set'
|
my_module_references_missing: 'Task references are not set'
|
||||||
|
storage_locations:
|
||||||
|
container: 'box'
|
||||||
|
location: 'location'
|
||||||
|
show:
|
||||||
|
hidden: "Private item"
|
||||||
|
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'
|
||||||
|
import_modal:
|
||||||
|
import_button: 'Import items'
|
||||||
|
title: "Import items to a box"
|
||||||
|
description: "Import items to a box allows for assigning items to a box and updating positions within a grid box. First, export the current box data to download a file listing the items already in the box. Then, edit the exported file to add or update the items you want to place in the box. When importing the file, ensure it includes the ‘Position’ and ‘Item ID’ columns for a successful import."
|
||||||
|
export: "Export"
|
||||||
|
export_button: "Export current box"
|
||||||
|
import: "Import"
|
||||||
|
drag_n_drop: ".xlsx file"
|
||||||
|
errors:
|
||||||
|
invalid_structure: "The imported file content doesn't meet criteria."
|
||||||
|
invalid_position: "Positions in the file must match with the box."
|
||||||
|
invalid_item: "Item ID %{row_id} doesn't exist."
|
||||||
|
index:
|
||||||
|
head_title: "Locations"
|
||||||
|
new_location: "New location"
|
||||||
|
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"
|
||||||
|
sub_locations: "Sub-locations"
|
||||||
|
items: "Items"
|
||||||
|
free_spaces: "Free spaces"
|
||||||
|
shared: "Shared"
|
||||||
|
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"
|
||||||
|
warning_box_not_empty: "Box dimensions can be updated only when the box is empty."
|
||||||
|
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."
|
||||||
|
share: "Share"
|
||||||
|
|
||||||
libraries:
|
libraries:
|
||||||
manange_modal_column_index:
|
manange_modal_column_index:
|
||||||
title: "Manage columns"
|
title: "Manage columns"
|
||||||
|
@ -4307,6 +4419,7 @@ en:
|
||||||
labels: "Label"
|
labels: "Label"
|
||||||
teams: "All Teams"
|
teams: "All Teams"
|
||||||
addons: "Add-ons"
|
addons: "Add-ons"
|
||||||
|
locations: "Locations"
|
||||||
label_printer: "Label printer"
|
label_printer: "Label printer"
|
||||||
fluics_printer: "Fluics printer"
|
fluics_printer: "Fluics printer"
|
||||||
|
|
||||||
|
@ -4418,6 +4531,15 @@ en:
|
||||||
active_state: "Active state"
|
active_state: "Active state"
|
||||||
archived_state: "Archived state"
|
archived_state: "Archived state"
|
||||||
|
|
||||||
|
modal_share:
|
||||||
|
title: "Share %{object_name}"
|
||||||
|
submit: "Save sharing options"
|
||||||
|
share_with_team: "Share with Team"
|
||||||
|
can_edit: "Can Edit"
|
||||||
|
all_teams: "All teams (current & new)"
|
||||||
|
all_teams_tooltip: "This will disable individual team settings"
|
||||||
|
success_message: "Selected sharing options for the %{object_name} have been saved."
|
||||||
|
|
||||||
errors:
|
errors:
|
||||||
general: "Something went wrong."
|
general: "Something went wrong."
|
||||||
general_text_too_long: 'Text is too long'
|
general_text_too_long: 'Text is too long'
|
||||||
|
|
|
@ -322,6 +322,23 @@ en:
|
||||||
protocol_step_asset_renamed_html: "%{user} renamed file %{old_name} to %{new_name} on protocol’s step <strong>%{step}</strong> in Protocol repository."
|
protocol_step_asset_renamed_html: "%{user} renamed file %{old_name} to %{new_name} on protocol’s step <strong>%{step}</strong> in Protocol repository."
|
||||||
result_asset_renamed_html: "%{user} renamed file %{old_name} to %{new_name} on result <strong>%{result}</strong> on task <strong>%{my_module}</strong>."
|
result_asset_renamed_html: "%{user} renamed file %{old_name} to %{new_name} on result <strong>%{result}</strong> on task <strong>%{my_module}</strong>."
|
||||||
item_added_with_import_html: "%{user} edited %{num_of_items} inventory item(s) in %{repository}."
|
item_added_with_import_html: "%{user} edited %{num_of_items} inventory item(s) in %{repository}."
|
||||||
|
storage_location_created_html: "%{user} created location %{storage_location}."
|
||||||
|
storage_location_deleted_html: "%{user} deleted location %{storage_location}."
|
||||||
|
storage_location_edited_html: "%{user} edited location %{storage_location}."
|
||||||
|
storage_location_moved_html: "%{user} moved location %{storage_location} from %{storage_location_original} to %{storage_location_destination}."
|
||||||
|
storage_location_shared_html: "%{user} shared location %{storage_location} with team %{team} with %{permission_level} permission."
|
||||||
|
storage_location_unshared_html: "%{user} unshared location %{storage_location} with team %{team}."
|
||||||
|
storage_location_sharing_updated_html: "%{user} changed permission of shared location %{storage_location} with team %{team} to %{permission_level}."
|
||||||
|
container_storage_location_created_html: "%{user} created box %{storage_location}."
|
||||||
|
container_storage_location_deleted_html: "%{user} deleted box %{storage_location}."
|
||||||
|
container_storage_location_edited_html: "%{user} edited box %{storage_location}."
|
||||||
|
container_storage_location_moved_html: "%{user} moved box %{storage_location} from %{storage_location_original} to %{storage_location_destination}."
|
||||||
|
container_storage_location_shared_html: "%{user} shared box %{storage_location} with team %{team} with %{permission_level} permission."
|
||||||
|
container_storage_location_unshared_html: "%{user} unshared box %{storage_location} with team %{team}."
|
||||||
|
container_storage_location_sharing_updated_html: "%{user} changed permission of shared box %{storage_location} with team %{team} to %{permission_level}."
|
||||||
|
storage_location_repository_row_created_html: "%{user} assigned %{repository_row} to box %{storage_location} %{position}."
|
||||||
|
storage_location_repository_row_deleted_html: "%{user} unassigned %{repository_row} from box %{storage_location} %{position}."
|
||||||
|
storage_location_repository_row_moved_html: "%{user} moved item %{repository_row} from box %{storage_location_original} %{positions} to box %{storage_location_destination} %{positions}."
|
||||||
activity_name:
|
activity_name:
|
||||||
create_project: "Project created"
|
create_project: "Project created"
|
||||||
rename_project: "Project renamed"
|
rename_project: "Project renamed"
|
||||||
|
@ -601,6 +618,23 @@ en:
|
||||||
task_step_file_duplicated: "File attachment on Task step duplicated"
|
task_step_file_duplicated: "File attachment on Task step duplicated"
|
||||||
result_file_duplicated: "File attachment on Task result duplicated"
|
result_file_duplicated: "File attachment on Task result duplicated"
|
||||||
protocol_step_file_duplicated: "File attachment on Protocol step duplicated"
|
protocol_step_file_duplicated: "File attachment on Protocol step duplicated"
|
||||||
|
storage_location_created: "Location created"
|
||||||
|
storage_location_deleted: "Location deleted"
|
||||||
|
storage_location_edited: "Location edited"
|
||||||
|
storage_location_moved: "Location moved"
|
||||||
|
storage_location_shared: "Location shared"
|
||||||
|
storage_location_unshared: "Location unshared"
|
||||||
|
storage_location_sharing_updated: "Location sharing permission updated"
|
||||||
|
container_storage_location_created: "Box created"
|
||||||
|
container_storage_location_deleted: "Box deleted"
|
||||||
|
container_storage_location_edited: "Box edited"
|
||||||
|
container_storage_location_moved: "Box moved"
|
||||||
|
container_storage_location_shared: "Box shared"
|
||||||
|
container_storage_location_unshared: "Box unshared"
|
||||||
|
container_storage_location_sharing_updated: "Box sharing permission updated"
|
||||||
|
storage_location_repository_row_created: "Inventory item location assigned"
|
||||||
|
storage_location_repository_row_deleted: "Inventory item location unassigned"
|
||||||
|
storage_location_repository_row_moved: "Inventory item location moved"
|
||||||
activity_group:
|
activity_group:
|
||||||
projects: "Projects"
|
projects: "Projects"
|
||||||
task_results: "Task results"
|
task_results: "Task results"
|
||||||
|
@ -614,6 +648,8 @@ en:
|
||||||
team: "Team"
|
team: "Team"
|
||||||
exports: "Exports"
|
exports: "Exports"
|
||||||
label_templates: "Label templates"
|
label_templates: "Label templates"
|
||||||
|
storage_locations: "Locations"
|
||||||
|
container_storage_locations: "Boxes"
|
||||||
subject_name:
|
subject_name:
|
||||||
repository: "Inventory"
|
repository: "Inventory"
|
||||||
project: "Project"
|
project: "Project"
|
||||||
|
@ -623,3 +659,4 @@ en:
|
||||||
protocol: "Protocol"
|
protocol: "Protocol"
|
||||||
step: "Step"
|
step: "Step"
|
||||||
report: "Report"
|
report: "Report"
|
||||||
|
storage_location: "Location"
|
||||||
|
|
|
@ -166,6 +166,7 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
resources :user_notifications, only: :index do
|
resources :user_notifications, only: :index do
|
||||||
collection do
|
collection do
|
||||||
|
get :filter_groups
|
||||||
get :unseen_counter
|
get :unseen_counter
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -194,6 +195,8 @@ Rails.application.routes.draw do
|
||||||
get 'create_modal', to: 'repositories#create_modal',
|
get 'create_modal', to: 'repositories#create_modal',
|
||||||
defaults: { format: 'json' }
|
defaults: { format: 'json' }
|
||||||
get 'actions_toolbar'
|
get 'actions_toolbar'
|
||||||
|
get :list
|
||||||
|
get :rows_list
|
||||||
end
|
end
|
||||||
member do
|
member do
|
||||||
get :export_empty_repository
|
get :export_empty_repository
|
||||||
|
@ -253,6 +256,13 @@ Rails.application.routes.draw do
|
||||||
via: [:get, :post, :put, :patch]
|
via: [:get, :post, :put, :patch]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :team_shared_objects, only: [] do
|
||||||
|
collection do
|
||||||
|
post 'update'
|
||||||
|
get 'shareable_teams'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
resources :reports, only: [:index, :new, :create, :update] do
|
resources :reports, only: [:index, :new, :create, :update] do
|
||||||
member do
|
member do
|
||||||
get :document_preview
|
get :document_preview
|
||||||
|
@ -807,6 +817,30 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
resources :connected_devices, controller: 'users/connected_devices', only: %i(destroy)
|
resources :connected_devices, controller: 'users/connected_devices', only: %i(destroy)
|
||||||
|
|
||||||
|
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
|
||||||
|
get :shareable_teams
|
||||||
|
get :export_container
|
||||||
|
post :import_container
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
get 'search' => 'search#index'
|
get 'search' => 'search#index'
|
||||||
get 'search/new' => 'search#new', as: :new_search
|
get 'search/new' => 'search#new', as: :new_search
|
||||||
resource :search, only: [], controller: :search do
|
resource :search, only: [], controller: :search do
|
||||||
|
|
|
@ -65,7 +65,9 @@ const entryList = {
|
||||||
vue_legacy_tags_modal: './app/javascript/packs/vue/legacy/tags_modal.js',
|
vue_legacy_tags_modal: './app/javascript/packs/vue/legacy/tags_modal.js',
|
||||||
vue_legacy_access_modal: './app/javascript/packs/vue/legacy/access_modal.js',
|
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_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_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_container: './app/javascript/packs/vue/storage_locations_container.js'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949
|
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949
|
||||||
|
|
38
db/migrate/20240705122903_add_storage_locations.rb
Normal file
38
db/migrate/20240705122903_add_storage_locations.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddStorageLocations < ActiveRecord::Migration[7.0]
|
||||||
|
include DatabaseHelper
|
||||||
|
|
||||||
|
def up
|
||||||
|
create_table :storage_locations do |t|
|
||||||
|
t.string :name
|
||||||
|
t.string :description
|
||||||
|
t.references :parent, index: true, foreign_key: { to_table: :storage_locations }
|
||||||
|
t.references :team, index: true, foreign_key: { to_table: :teams }
|
||||||
|
t.references :created_by, foreign_key: { to_table: :users }
|
||||||
|
t.boolean :container, default: false, null: false, index: true
|
||||||
|
t.jsonb :metadata, null: false, default: {}
|
||||||
|
t.datetime :discarded_at, index: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :storage_location_repository_rows do |t|
|
||||||
|
t.references :repository_row, index: true, foreign_key: { to_table: :repository_rows }
|
||||||
|
t.references :storage_location, index: true, foreign_key: { to_table: :storage_locations }
|
||||||
|
t.references :created_by, foreign_key: { to_table: :users }
|
||||||
|
t.jsonb :metadata, null: false, default: {}
|
||||||
|
t.datetime :discarded_at, index: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_gin_index_without_tags :storage_locations, :name
|
||||||
|
add_gin_index_without_tags :storage_locations, :description
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_table :storage_location_repository_rows
|
||||||
|
drop_table :storage_locations
|
||||||
|
end
|
||||||
|
end
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RemoveForeignKeyConstraintFromTeamSharedObjects < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
remove_foreign_key :team_shared_objects, :repositories, column: :shared_object_id
|
||||||
|
end
|
||||||
|
end
|
49
db/schema.rb
49
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2024_06_26_113515) do
|
ActiveRecord::Schema[7.0].define(version: 2024_07_05_122903) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "btree_gist"
|
enable_extension "btree_gist"
|
||||||
enable_extension "pg_trgm"
|
enable_extension "pg_trgm"
|
||||||
|
@ -721,8 +721,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_06_26_113515) do
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "type"
|
t.string "type"
|
||||||
t.datetime "start_time_dup"
|
t.datetime "start_time_dup", precision: nil
|
||||||
t.datetime "end_time_dup"
|
t.datetime "end_time_dup", precision: nil
|
||||||
t.index "((end_time)::date)", name: "index_repository_date_time_range_values_on_end_time_as_date", where: "((type)::text = 'RepositoryDateRangeValue'::text)"
|
t.index "((end_time)::date)", name: "index_repository_date_time_range_values_on_end_time_as_date", where: "((type)::text = 'RepositoryDateRangeValue'::text)"
|
||||||
t.index "((end_time)::time without time zone)", name: "index_repository_date_time_range_values_on_end_time_as_time", where: "((type)::text = 'RepositoryTimeRangeValue'::text)"
|
t.index "((end_time)::time without time zone)", name: "index_repository_date_time_range_values_on_end_time_as_time", where: "((type)::text = 'RepositoryTimeRangeValue'::text)"
|
||||||
t.index "((start_time)::date)", name: "index_repository_date_time_range_values_on_start_time_as_date", where: "((type)::text = 'RepositoryDateRangeValue'::text)"
|
t.index "((start_time)::date)", name: "index_repository_date_time_range_values_on_start_time_as_date", where: "((type)::text = 'RepositoryDateRangeValue'::text)"
|
||||||
|
@ -1083,6 +1083,40 @@ ActiveRecord::Schema[7.0].define(version: 2024_06_26_113515) do
|
||||||
t.index ["user_id"], name: "index_steps_on_user_id"
|
t.index ["user_id"], name: "index_steps_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "storage_location_repository_rows", force: :cascade do |t|
|
||||||
|
t.bigint "repository_row_id"
|
||||||
|
t.bigint "storage_location_id"
|
||||||
|
t.bigint "created_by_id"
|
||||||
|
t.jsonb "metadata", default: {}, null: false
|
||||||
|
t.datetime "discarded_at"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["created_by_id"], name: "index_storage_location_repository_rows_on_created_by_id"
|
||||||
|
t.index ["discarded_at"], name: "index_storage_location_repository_rows_on_discarded_at"
|
||||||
|
t.index ["repository_row_id"], name: "index_storage_location_repository_rows_on_repository_row_id"
|
||||||
|
t.index ["storage_location_id"], name: "index_storage_location_repository_rows_on_storage_location_id"
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "storage_locations", force: :cascade do |t|
|
||||||
|
t.string "name"
|
||||||
|
t.string "description"
|
||||||
|
t.bigint "parent_id"
|
||||||
|
t.bigint "team_id"
|
||||||
|
t.bigint "created_by_id"
|
||||||
|
t.boolean "container", default: false, null: false
|
||||||
|
t.jsonb "metadata", default: {}, null: false
|
||||||
|
t.datetime "discarded_at"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index "trim_html_tags((description)::text) gin_trgm_ops", name: "index_storage_locations_on_description", using: :gin
|
||||||
|
t.index "trim_html_tags((name)::text) gin_trgm_ops", name: "index_storage_locations_on_name", using: :gin
|
||||||
|
t.index ["container"], name: "index_storage_locations_on_container"
|
||||||
|
t.index ["created_by_id"], name: "index_storage_locations_on_created_by_id"
|
||||||
|
t.index ["discarded_at"], name: "index_storage_locations_on_discarded_at"
|
||||||
|
t.index ["parent_id"], name: "index_storage_locations_on_parent_id"
|
||||||
|
t.index ["team_id"], name: "index_storage_locations_on_team_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "tables", force: :cascade do |t|
|
create_table "tables", force: :cascade do |t|
|
||||||
t.binary "contents", null: false
|
t.binary "contents", null: false
|
||||||
t.datetime "created_at", precision: nil, null: false
|
t.datetime "created_at", precision: nil, null: false
|
||||||
|
@ -1278,6 +1312,9 @@ ActiveRecord::Schema[7.0].define(version: 2024_06_26_113515) do
|
||||||
t.integer "failed_attempts", default: 0, null: false
|
t.integer "failed_attempts", default: 0, null: false
|
||||||
t.datetime "locked_at", precision: nil
|
t.datetime "locked_at", precision: nil
|
||||||
t.string "unlock_token"
|
t.string "unlock_token"
|
||||||
|
t.string "api_key"
|
||||||
|
t.datetime "api_key_expires_at", precision: nil
|
||||||
|
t.datetime "api_key_created_at", precision: nil
|
||||||
t.index "trim_html_tags((full_name)::text) gin_trgm_ops", name: "index_users_on_full_name", using: :gin
|
t.index "trim_html_tags((full_name)::text) gin_trgm_ops", name: "index_users_on_full_name", using: :gin
|
||||||
t.index ["authentication_token"], name: "index_users_on_authentication_token", unique: true
|
t.index ["authentication_token"], name: "index_users_on_authentication_token", unique: true
|
||||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||||
|
@ -1502,6 +1539,12 @@ ActiveRecord::Schema[7.0].define(version: 2024_06_26_113515) do
|
||||||
add_foreign_key "steps", "protocols"
|
add_foreign_key "steps", "protocols"
|
||||||
add_foreign_key "steps", "users"
|
add_foreign_key "steps", "users"
|
||||||
add_foreign_key "steps", "users", column: "last_modified_by_id"
|
add_foreign_key "steps", "users", column: "last_modified_by_id"
|
||||||
|
add_foreign_key "storage_location_repository_rows", "repository_rows"
|
||||||
|
add_foreign_key "storage_location_repository_rows", "storage_locations"
|
||||||
|
add_foreign_key "storage_location_repository_rows", "users", column: "created_by_id"
|
||||||
|
add_foreign_key "storage_locations", "storage_locations", column: "parent_id"
|
||||||
|
add_foreign_key "storage_locations", "teams"
|
||||||
|
add_foreign_key "storage_locations", "users", column: "created_by_id"
|
||||||
add_foreign_key "tables", "users", column: "created_by_id"
|
add_foreign_key "tables", "users", column: "created_by_id"
|
||||||
add_foreign_key "tables", "users", column: "last_modified_by_id"
|
add_foreign_key "tables", "users", column: "last_modified_by_id"
|
||||||
add_foreign_key "tags", "projects"
|
add_foreign_key "tags", "projects"
|
||||||
|
|
|
@ -19,6 +19,7 @@ services:
|
||||||
container_name: scinote_web_development
|
container_name: scinote_web_development
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
|
user: scinote
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
ports:
|
ports:
|
||||||
|
@ -43,6 +44,7 @@ services:
|
||||||
container_name: scinote_webpack_development
|
container_name: scinote_webpack_development
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
|
user: scinote
|
||||||
command: >
|
command: >
|
||||||
bash -c "yarn install && yarn build --watch"
|
bash -c "yarn install && yarn build --watch"
|
||||||
environment:
|
environment:
|
||||||
|
@ -60,6 +62,7 @@ services:
|
||||||
container_name: scinote_css_bundling_development
|
container_name: scinote_css_bundling_development
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
|
user: scinote
|
||||||
command: >
|
command: >
|
||||||
bash -c "yarn build:css --watch"
|
bash -c "yarn build:css --watch"
|
||||||
environment:
|
environment:
|
||||||
|
@ -77,6 +80,7 @@ services:
|
||||||
container_name: scinote_tailwind_development
|
container_name: scinote_tailwind_development
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
|
user: scinote
|
||||||
command: >
|
command: >
|
||||||
bash -c "rails tailwindcss:watch"
|
bash -c "rails tailwindcss:watch"
|
||||||
environment:
|
environment:
|
||||||
|
|
|
@ -35,8 +35,8 @@
|
||||||
"@vuepic/vue-datepicker": "^7.2.0",
|
"@vuepic/vue-datepicker": "^7.2.0",
|
||||||
"@vueuse/components": "^10.5.0",
|
"@vueuse/components": "^10.5.0",
|
||||||
"@vueuse/core": "^10.5.0",
|
"@vueuse/core": "^10.5.0",
|
||||||
"ag-grid-community": "^30.1.0",
|
"ag-grid-community": "^31.3.4",
|
||||||
"ag-grid-vue3": "^30.2.1",
|
"ag-grid-vue3": "^31.3.4",
|
||||||
"ajv": "6.12.6",
|
"ajv": "6.12.6",
|
||||||
"autoprefixer": "10.4.14",
|
"autoprefixer": "10.4.14",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.4",
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
"tui-color-picker": "^2.2.0",
|
"tui-color-picker": "^2.2.0",
|
||||||
"tui-image-editor": "github:scinote-eln/tui.image-editor#3_15_2_updated",
|
"tui-image-editor": "github:scinote-eln/tui.image-editor#3_15_2_updated",
|
||||||
"twemoji": "^12.1.4",
|
"twemoji": "^12.1.4",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.5.4",
|
||||||
"vue-loader": "^16.0.0",
|
"vue-loader": "^16.0.0",
|
||||||
"vue3-draggable-resizable": "^1.6.5",
|
"vue3-draggable-resizable": "^1.6.5",
|
||||||
"vue3-perfect-scrollbar": "^1.6.1",
|
"vue3-perfect-scrollbar": "^1.6.1",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue