mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-03-04 19:53:19 +08:00
Merge branch 'develop' into features/protocol-template-renaming
This commit is contained in:
commit
fdcf14efd6
283 changed files with 7516 additions and 1611 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -95,3 +95,7 @@ public/marvin4js-license.cxl
|
|||
|
||||
/app/assets/builds/*
|
||||
!/app/assets/builds/.keep
|
||||
|
||||
# Ignore automatically generated js-routes files.
|
||||
/app/javascript/routes.js
|
||||
/app/javascript/routes.d.ts
|
||||
|
|
|
@ -384,7 +384,7 @@ Layout/DefEndAlignment:
|
|||
EnforcedStyleAlignWith: start_of_line
|
||||
|
||||
Layout/LineLength:
|
||||
Max: 120
|
||||
Max: 180
|
||||
AllowHeredoc: true
|
||||
AllowURI: true
|
||||
URISchemes:
|
||||
|
|
|
@ -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
|
||||
- chmod +x docker-compose
|
||||
- sudo mv docker-compose /usr/local/bin
|
||||
- sudo chown --recursive 1000 .
|
||||
- make docker-ci
|
||||
script:
|
||||
- make tests-ci
|
||||
|
|
11
Dockerfile
11
Dockerfile
|
@ -1,4 +1,4 @@
|
|||
FROM ruby:3.2.2-bookworm
|
||||
FROM ruby:3.2-bookworm
|
||||
MAINTAINER SciNote <info@scinote.net>
|
||||
|
||||
# additional dependecies
|
||||
|
@ -20,14 +20,15 @@ RUN apt-get update -qq && \
|
|||
fonts-wqy-microhei \
|
||||
fonts-wqy-zenhei \
|
||||
libfile-mimeinfo-perl \
|
||||
chromium-driver \
|
||||
chromium \
|
||||
chromium-sandbox \
|
||||
yarnpkg && \
|
||||
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/*
|
||||
|
||||
ENV PATH=/usr/share/nodejs/yarn/bin:$PATH
|
||||
|
||||
RUN yarn add puppeteer@npm:puppeteer-core
|
||||
RUN yarn add puppeteer@npm:puppeteer-core@^22.15.0
|
||||
|
||||
ENV BUNDLE_PATH /usr/local/bundle/
|
||||
|
||||
|
@ -35,6 +36,10 @@ ENV BUNDLE_PATH /usr/local/bundle/
|
|||
ENV APP_HOME /usr/src/app
|
||||
ENV PATH $APP_HOME/bin:$PATH
|
||||
RUN mkdir $APP_HOME
|
||||
RUN adduser --uid 1000 scinote
|
||||
RUN chown scinote:scinote $APP_HOME
|
||||
USER scinote
|
||||
ENV CHROMIUM_PATH=$APP_HOME/bin/chromium
|
||||
WORKDIR $APP_HOME
|
||||
|
||||
CMD rails s -b 0.0.0.0
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Building stage
|
||||
FROM ruby:3.2.2-bookworm AS builder
|
||||
FROM ruby:3.2-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 \
|
||||
|
@ -23,7 +23,7 @@ COPY . $APP_HOME
|
|||
RUN rm -f $APP_HOME/config/application.yml $APP_HOME/production.env
|
||||
WORKDIR $APP_HOME
|
||||
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 path '/usr/src/app/tmp/bundle' && \
|
||||
bundle install --jobs `nproc` && \
|
||||
|
@ -34,14 +34,14 @@ RUN \
|
|||
|
||||
RUN \
|
||||
--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 \
|
||||
SECRET_KEY_BASE=dummy \
|
||||
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
|
||||
FROM ruby:3.2.2-bookworm AS runner
|
||||
FROM ruby:3.2-bookworm AS runner
|
||||
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
|
||||
|
@ -76,9 +76,10 @@ RUN \
|
|||
libvips42 \
|
||||
graphviz \
|
||||
chromium \
|
||||
chromium-sandbox \
|
||||
libfile-mimeinfo-perl \
|
||||
yarnpkg && \
|
||||
/usr/share/nodejs/yarn/bin/yarn add puppeteer@npm:puppeteer-core && \
|
||||
/usr/share/nodejs/yarn/bin/yarn add puppeteer@npm:puppeteer-core@^22.15.0 && \
|
||||
apt-get install -y libreoffice && \
|
||||
ln -s /usr/lib/x86_64-linux-gnu/libvips.so.42 /usr/lib/x86_64-linux-gnu/libvips.so
|
||||
|
||||
|
@ -88,6 +89,8 @@ RUN \
|
|||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
touch /etc/build-${BUILD_TIMESTAMP} && \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get remove -y *-dev && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get update -qq && \
|
||||
apt-get upgrade -y && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
@ -98,7 +101,10 @@ ENV GEM_HOME=$APP_HOME/vendor/bundle/ruby/3.2.0
|
|||
ENV PATH=$GEM_HOME/bin:$PATH
|
||||
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
|
||||
|
||||
WORKDIR $APP_HOME
|
||||
|
||||
|
|
7
Gemfile
7
Gemfile
|
@ -2,7 +2,7 @@
|
|||
|
||||
source 'http://rubygems.org'
|
||||
|
||||
ruby '3.2.2'
|
||||
ruby '~> 3.2.2'
|
||||
|
||||
gem 'activerecord-session_store'
|
||||
gem 'bootsnap', require: false
|
||||
|
@ -62,6 +62,7 @@ gem 'logging', '~> 2.0.0'
|
|||
gem 'nested_form_fields'
|
||||
gem 'nokogiri', '~> 1.16.5' # HTML/XML parser
|
||||
gem 'noticed'
|
||||
gem 'oj'
|
||||
gem 'rails_autolink', '~> 1.1', '>= 1.1.6'
|
||||
gem 'rgl' # Graph framework for project diagram calculations
|
||||
gem 'roo', '~> 2.10.0' # Spreadsheet parser
|
||||
|
@ -81,9 +82,6 @@ gem 'aws-sdk-lambda'
|
|||
gem 'aws-sdk-rails'
|
||||
gem 'aws-sdk-s3'
|
||||
gem 'delayed_job_active_record'
|
||||
gem 'devise-async',
|
||||
git: 'https://github.com/mhfs/devise-async.git',
|
||||
branch: 'devise-4.x'
|
||||
gem 'image_processing'
|
||||
gem 'img2zpl', git: 'https://github.com/scinote-eln/img2zpl'
|
||||
gem 'rufus-scheduler'
|
||||
|
@ -94,6 +92,7 @@ gem 'graphviz'
|
|||
|
||||
gem 'cssbundling-rails'
|
||||
gem 'jsbundling-rails'
|
||||
gem 'js-routes'
|
||||
|
||||
gem 'tailwindcss-rails', '~> 2.4'
|
||||
|
||||
|
|
75
Gemfile.lock
75
Gemfile.lock
|
@ -5,14 +5,6 @@ GIT
|
|||
sneaky-save (0.1.3)
|
||||
activerecord (>= 3.2.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/mhfs/devise-async.git
|
||||
revision: 177f6363a002f7ff28f1d289c8cab7ad8d9cb8c5
|
||||
branch: devise-4.x
|
||||
specs:
|
||||
devise-async (0.10.2)
|
||||
devise (>= 4.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/scinote-eln/canaid
|
||||
revision: bba1b817d1c9b0c7e0440a83d0f62848aabc0a1b
|
||||
|
@ -105,9 +97,9 @@ GEM
|
|||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_model_serializers (0.10.13)
|
||||
actionpack (>= 4.1, < 7.1)
|
||||
activemodel (>= 4.1, < 7.1)
|
||||
active_model_serializers (0.10.14)
|
||||
actionpack (>= 4.1)
|
||||
activemodel (>= 4.1)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (7.0.8.4)
|
||||
|
@ -203,6 +195,7 @@ GEM
|
|||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
rouge (>= 1.0.0)
|
||||
bigdecimal (3.1.8)
|
||||
bindata (2.5.0)
|
||||
binding_of_caller (1.0.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
|
@ -210,7 +203,7 @@ GEM
|
|||
msgpack (~> 1.2)
|
||||
brakeman (6.1.2)
|
||||
racc
|
||||
builder (3.2.4)
|
||||
builder (3.3.0)
|
||||
bullet (7.0.7)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
|
@ -248,7 +241,7 @@ GEM
|
|||
combine_pdf (1.0.23)
|
||||
matrix
|
||||
ruby-rc4 (>= 0.1.5)
|
||||
concurrent-ruby (1.3.1)
|
||||
concurrent-ruby (1.3.4)
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
|
@ -324,8 +317,8 @@ GEM
|
|||
railties (>= 5)
|
||||
down (5.4.1)
|
||||
addressable (~> 2.8)
|
||||
erubi (1.12.0)
|
||||
et-orbi (1.2.7)
|
||||
erubi (1.13.0)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
execjs (2.8.1)
|
||||
factory_bot (6.2.1)
|
||||
|
@ -348,8 +341,8 @@ GEM
|
|||
rake
|
||||
figaro (1.2.0)
|
||||
thor (>= 0.14.0, < 2)
|
||||
fugit (1.8.1)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
fugit (1.11.1)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
|
@ -372,7 +365,7 @@ GEM
|
|||
httparty (0.21.0)
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (1.14.5)
|
||||
i18n (1.14.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (3.9.2)
|
||||
i18n (>= 0.6.6)
|
||||
|
@ -380,7 +373,7 @@ GEM
|
|||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
iniparse (1.5.0)
|
||||
jbuilder (2.11.5)
|
||||
jbuilder (2.13.0)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.6.2)
|
||||
|
@ -388,6 +381,8 @@ GEM
|
|||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
js-routes (2.2.8)
|
||||
railties (>= 4)
|
||||
jsbundling-rails (1.1.1)
|
||||
railties (>= 6.0.0)
|
||||
json (2.6.3)
|
||||
|
@ -441,8 +436,7 @@ GEM
|
|||
mime-types-data (3.2023.0218.1)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.23.1)
|
||||
minitest (5.25.1)
|
||||
msgpack (1.7.1)
|
||||
multi_json (1.15.0)
|
||||
multi_test (1.1.0)
|
||||
|
@ -463,13 +457,13 @@ GEM
|
|||
net-smtp (0.4.0.1)
|
||||
net-protocol
|
||||
newrelic_rpm (9.2.2)
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.16.5)
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.7)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-arm64-darwin)
|
||||
nokogiri (1.16.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-x86_64-linux)
|
||||
nokogiri (1.16.7-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
noticed (1.6.3)
|
||||
http (>= 4.0.0)
|
||||
|
@ -481,7 +475,10 @@ GEM
|
|||
rack (>= 1.2, < 4)
|
||||
snaky_hash (~> 2.0)
|
||||
version_gem (~> 1.1)
|
||||
omniauth (2.1.1)
|
||||
oj (3.16.6)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
omniauth (2.1.2)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 2.2.3)
|
||||
rack-protection
|
||||
|
@ -495,9 +492,9 @@ GEM
|
|||
omniauth-rails_csrf_protection (1.0.1)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-saml (2.1.0)
|
||||
omniauth (~> 2.0)
|
||||
ruby-saml (~> 1.12)
|
||||
omniauth-saml (2.2.1)
|
||||
omniauth (~> 2.1)
|
||||
ruby-saml (~> 1.17)
|
||||
omniauth_openid_connect (0.7.1)
|
||||
omniauth (>= 1.9, < 3)
|
||||
openid_connect (~> 2.2)
|
||||
|
@ -515,6 +512,7 @@ GEM
|
|||
validate_url
|
||||
webfinger (~> 2.0)
|
||||
orm_adapter (0.5.0)
|
||||
ostruct (0.6.0)
|
||||
overcommit (0.60.0)
|
||||
childprocess (>= 0.6.3, < 5)
|
||||
iniparse (~> 1.4)
|
||||
|
@ -545,10 +543,10 @@ GEM
|
|||
pry (>= 0.10.4)
|
||||
psych (3.3.4)
|
||||
public_suffix (5.0.1)
|
||||
puma (6.4.2)
|
||||
puma (6.4.3)
|
||||
nio4r (~> 2.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.9)
|
||||
rack-attack (6.6.1)
|
||||
rack (>= 1.0, < 3)
|
||||
|
@ -561,8 +559,9 @@ GEM
|
|||
faraday-follow_redirects
|
||||
json-jwt (>= 1.11.0)
|
||||
rack (>= 2.1.0)
|
||||
rack-protection (3.0.6)
|
||||
rack
|
||||
rack-protection (3.2.0)
|
||||
base64 (>= 0.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rails (7.0.8.4)
|
||||
|
@ -617,8 +616,7 @@ GEM
|
|||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.2.8)
|
||||
strscan (>= 3.0.9)
|
||||
rexml (3.3.7)
|
||||
rgl (0.6.3)
|
||||
pairing_heap (>= 0.3.0)
|
||||
rexml (~> 3.2, >= 3.2.4)
|
||||
|
@ -670,7 +668,7 @@ GEM
|
|||
rubocop (>= 1.33.0, < 2.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-rc4 (0.1.5)
|
||||
ruby-saml (1.16.0)
|
||||
ruby-saml (1.17.0)
|
||||
nokogiri (>= 1.13.10)
|
||||
rexml
|
||||
ruby-vips (2.1.4)
|
||||
|
@ -710,7 +708,6 @@ GEM
|
|||
activesupport (>= 5.2)
|
||||
sprockets (>= 3.0.0)
|
||||
stream (0.5.5)
|
||||
strscan (3.1.0)
|
||||
swd (2.0.2)
|
||||
activesupport (>= 3)
|
||||
attr_required (>= 0.0.5)
|
||||
|
@ -829,6 +826,7 @@ DEPENDENCIES
|
|||
image_processing
|
||||
img2zpl!
|
||||
jbuilder
|
||||
js-routes
|
||||
jsbundling-rails
|
||||
json-jwt
|
||||
json_matchers
|
||||
|
@ -841,6 +839,7 @@ DEPENDENCIES
|
|||
newrelic_rpm
|
||||
nokogiri (~> 1.16.5)
|
||||
noticed
|
||||
oj
|
||||
omniauth (~> 2.1)
|
||||
omniauth-azure-activedirectory-v2
|
||||
omniauth-linkedin-oauth2
|
||||
|
@ -899,4 +898,4 @@ RUBY VERSION
|
|||
ruby 3.2.2p53
|
||||
|
||||
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"
|
||||
|
||||
docker:
|
||||
@docker-compose build
|
||||
@docker-compose --progress plain build
|
||||
|
||||
docker-ci:
|
||||
@docker-compose --progress plain build web
|
||||
|
||||
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:
|
||||
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
|
||||
Doorkeeper::Rake.load_tasks
|
||||
# Update js-routes file before javascript build
|
||||
task 'javascript:build' => 'js:routes:typescript'
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1.36.0
|
||||
1.37.0
|
||||
|
|
|
@ -7,14 +7,15 @@
|
|||
let taskId = $(this).closest('.task-selector-container').data('task-id');
|
||||
let index = $.inArray(taskId, selectedTasks);
|
||||
|
||||
window.actionToolbarComponent.fetchActions({ my_module_ids: selectedTasks });
|
||||
|
||||
// If checkbox is checked and row ID is not in list of selected folder IDs
|
||||
if (this.checked && index === -1) {
|
||||
selectedTasks.push(taskId);
|
||||
} else if (!this.checked && index !== -1) {
|
||||
selectedTasks.splice(index, 1);
|
||||
}
|
||||
|
||||
const items = selectedTasks.length ? JSON.stringify(selectedTasks.map((item) => ({ id: item }))) : [];
|
||||
window.actionToolbarComponent.fetchActions({ items });
|
||||
});
|
||||
|
||||
function restoreMyModules(url, ids) {
|
||||
|
@ -63,7 +64,32 @@
|
|||
});
|
||||
}
|
||||
|
||||
function initAccessModal() {
|
||||
$('#module-archive').on('click', '#openAccessModal', (e) => {
|
||||
e.preventDefault();
|
||||
const container = document.getElementById('accessModalContainer');
|
||||
const target = e.currentTarget;
|
||||
|
||||
$.get(target.dataset.url, (data) => {
|
||||
const object = {
|
||||
...data.data.attributes,
|
||||
id: data.data.id,
|
||||
type: data.data.type
|
||||
};
|
||||
const { rolesUrl } = container.dataset;
|
||||
const params = {
|
||||
object,
|
||||
roles_path: rolesUrl
|
||||
};
|
||||
const modal = $('#accessModalComponent').data('accessModal');
|
||||
modal.params = params;
|
||||
modal.open();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.initActionToolbar();
|
||||
initRestoreMyModules();
|
||||
initMoveButton();
|
||||
initAccessModal();
|
||||
}());
|
||||
|
|
|
@ -317,9 +317,7 @@ var RepositoryDatatable = (function(global) {
|
|||
|
||||
checkAvailableColumns();
|
||||
|
||||
RepositoryDatatableRowEditor.switchRowToEditMode(row);
|
||||
|
||||
changeToEditMode();
|
||||
RepositoryDatatableRowEditor.switchRowToEditMode(row, changeToEditMode);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -692,6 +690,7 @@ var RepositoryDatatable = (function(global) {
|
|||
},
|
||||
rowCallback: function(row, data) {
|
||||
$(row).attr('data-editable', data.recordEditable);
|
||||
$(row).attr('data-info-url', data.recordInfoUrl);
|
||||
$(row).attr('data-manage-stock-url', data.manageStockUrl);
|
||||
// Get row ID
|
||||
let rowId = data.DT_RowId;
|
||||
|
@ -1003,10 +1002,8 @@ var RepositoryDatatable = (function(global) {
|
|||
$(TABLE_ID).find('.repository-row-edit-icon').remove();
|
||||
|
||||
rowsSelected.forEach(function(rowNumber) {
|
||||
RepositoryDatatableRowEditor.switchRowToEditMode(TABLE.row('#' + rowNumber));
|
||||
RepositoryDatatableRowEditor.switchRowToEditMode(TABLE.row('#' + rowNumber), changeToEditMode);
|
||||
});
|
||||
|
||||
changeToEditMode();
|
||||
})
|
||||
.on('click', '#assignRepositoryRecords', function(e) {
|
||||
e.preventDefault();
|
||||
|
|
|
@ -173,11 +173,17 @@ var RepositoryDatatableRowEditor = (function() {
|
|||
TABLE.columns.adjust();
|
||||
}
|
||||
|
||||
function switchRowToEditMode(row) {
|
||||
function enableEditMode(row, isEditable) {
|
||||
if (!isEditable) {
|
||||
HelperModule.flashAlertMsg(I18n.t('repositories.table.row_locked'), 'danger');
|
||||
return false;
|
||||
}
|
||||
|
||||
let $row = $(row.node());
|
||||
let itemId = row.id();
|
||||
let formId = `repositoryRowForm${itemId}`;
|
||||
let requestUrl = $(TABLE.table().node()).data('current-uri');
|
||||
|
||||
let rowForm = $(`
|
||||
<form id="${formId}"
|
||||
class="${EDIT_FORM_CLASS_NAME} ${GLOBAL_CONSTANTS.HAS_UNSAVED_DATA_CLASS_NAME}"
|
||||
|
@ -213,6 +219,26 @@ var RepositoryDatatableRowEditor = (function() {
|
|||
initAssetCellActions($row);
|
||||
|
||||
TABLE.columns.adjust();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function switchRowToEditMode(row, editEnabledCallback) {
|
||||
// Editable property was already preloaded
|
||||
if (row.data().editable !== undefined) {
|
||||
if (enableEditMode(row, row.data().editable)) editEnabledCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
// Need to fetch editable property
|
||||
$.ajax({
|
||||
url: row.data().recordInfoUrl,
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
success: (data) => {
|
||||
if (enableEditMode(row, data.editable)) editEnabledCallback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
|
|
|
@ -284,6 +284,7 @@ var RepositoryColumns = (function() {
|
|||
let editableRow = ($(el).attr('data-editable-row') === 'true') ? 'has-permissions' : '';
|
||||
let editUrl = $(el).attr('data-edit-column-url');
|
||||
let destroyUrl = $(el).attr('data-destroy-column-url');
|
||||
const isDisabled = $(el).attr('data-disabled') === 'true';
|
||||
let thederName;
|
||||
|
||||
if ($(el).find('.modal-tooltiptext').length > 0) {
|
||||
|
@ -315,7 +316,9 @@ var RepositoryColumns = (function() {
|
|||
<span class="vis-controls">
|
||||
<span class="vis sn-icon ${visClass}" title="${visText}" data-e2e="e2e-BT-invItems-manageColumnsModal-${e2eName}-visibility"></span>
|
||||
</span>
|
||||
<div class="text truncate" title="${thederName}" data-e2e="e2e-TX-invItems-manageColumnsModal-${e2eName}-columnName">${thederName}</div>
|
||||
<div class="text truncate" title="${thederName}" data-e2e="e2e-TX-invItems-manageColumnsModal-${e2eName}-columnName">
|
||||
${thederName} ${isDisabled ? `<span data-e2e="e2e-LB-invItems-manageColumnsModal-${e2eName}-disabled"></span>` : ''}
|
||||
</div>
|
||||
<span class="column-type pull-right shrink-0">${
|
||||
getColumnTypeText(el, colId) || `<i class="sn-icon sn-icon-locked-task" data-e2e="e2e-IC-invItems-manageColumnsModal-${e2eName}-locked"></i>`
|
||||
}</span>
|
||||
|
|
|
@ -19,5 +19,6 @@ const GLOBAL_CONSTANTS = {
|
|||
ASSET_POLLING_INTERVAL: <%= Constants::ASSET_POLLING_INTERVAL %>,
|
||||
ASSET_SYNC_URL: '<%= Constants::ASSET_SYNC_URL %>',
|
||||
GLOBAL_SEARCH_PREVIEW_LIMIT: <%= Constants::GLOBAL_SEARCH_PREVIEW_LIMIT %>,
|
||||
SEARCH_LIMIT: <%= Constants::SEARCH_LIMIT %>
|
||||
SEARCH_LIMIT: <%= Constants::SEARCH_LIMIT %>,
|
||||
SCINOTE_EDIT_RESTRICTED_EXTENSIONS: <%= Constants::SCINOTE_EDIT_RESTRICTED_EXTENSIONS %>
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
@import "tailwind/buttons";
|
||||
@import "tailwind/modals";
|
||||
@import "tailwind/flyouts";
|
||||
@import "tailwind/radio";
|
||||
@import "tailwind/loader.css";
|
||||
|
||||
@tailwind base;
|
||||
|
@ -69,6 +70,6 @@ html {
|
|||
|
||||
@keyframes shine-lines {
|
||||
0% { background-position: -150px }
|
||||
|
||||
|
||||
40%, 100% { background-position: 320px }
|
||||
}
|
||||
|
|
|
@ -26,5 +26,11 @@
|
|||
border-width: 0;
|
||||
height: 1px;
|
||||
margin: 0 16px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reminders-view-mode {
|
||||
.row-reminders-footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
flex-direction: column;
|
||||
height: calc(100vh - 8rem);
|
||||
padding: 1.5rem;
|
||||
width: 400px;
|
||||
width: 600px;
|
||||
|
||||
.sci--navigation--notificaitons-flyout-title {
|
||||
@include font-h2;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// scss-lint:disable SelectorDepth QualifyingElement
|
||||
|
||||
/*
|
||||
:root {
|
||||
--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;
|
||||
}
|
||||
}
|
|
@ -1362,6 +1362,10 @@ th.custom-field .modal-tooltiptext {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.tooltip-open {
|
||||
background-color: $color-concrete;
|
||||
color: $color-black;
|
||||
|
|
|
@ -124,7 +124,7 @@ module Api
|
|||
Result.transaction do
|
||||
old_checksum = asset.file.blob.checksum
|
||||
if @form_multipart_upload
|
||||
asset.file.attach(result_file_params[:file])
|
||||
asset.attach_file_version(result_file_params[:file])
|
||||
else
|
||||
blob = create_blob_from_params
|
||||
asset.update!(file: blob)
|
||||
|
|
|
@ -18,7 +18,7 @@ module Api
|
|||
end
|
||||
|
||||
def create
|
||||
inventory_item_to_link = RepositoryRow.where(repository: Repository.accessible_by_teams(@team))
|
||||
inventory_item_to_link = RepositoryRow.where(repository: Repository.viewable_by_user(current_user, @team))
|
||||
.find(connection_params[:child_id])
|
||||
child_connection = @inventory_item.child_connections.create!(
|
||||
child: inventory_item_to_link,
|
||||
|
|
|
@ -20,7 +20,7 @@ module Api
|
|||
end
|
||||
|
||||
def create
|
||||
inventory_item_to_link = RepositoryRow.where(repository: Repository.accessible_by_teams(@team))
|
||||
inventory_item_to_link = RepositoryRow.where(repository: Repository.viewable_by_user(current_user, @team))
|
||||
.find(connection_params[:parent_id])
|
||||
parent_connection = @inventory_item.parent_connections.create!(
|
||||
parent: inventory_item_to_link,
|
||||
|
|
|
@ -16,7 +16,8 @@ class AssetSyncController < ApplicationController
|
|||
asset_sync_token = current_user.asset_sync_tokens.find_or_create_by(asset_id: params[:asset_id])
|
||||
|
||||
unless asset_sync_token.token_valid?
|
||||
asset_sync_token = current_user.asset_sync_tokens.create(asset_id: params[:asset_id])
|
||||
asset_sync_token =
|
||||
current_user.asset_sync_tokens.create(asset_id: params[:asset_id])
|
||||
end
|
||||
|
||||
render json: AssetSyncTokenSerializer.new(asset_sync_token).as_json
|
||||
|
@ -27,34 +28,32 @@ class AssetSyncController < ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
if @asset_sync_token.conflicts?(request.headers['VersionToken'])
|
||||
ActiveRecord::Base.transaction do
|
||||
conflict_response = AssetSyncTokenSerializer.new(conflicting_asset_copy_token).as_json
|
||||
error_message = { message: I18n.t('assets.conflict_error', filename: @asset.file.filename) }
|
||||
log_activity(:create)
|
||||
render json: conflict_response.merge(error_message), status: :conflict
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
orig_file_size = @asset.file_size
|
||||
asset_conflicts = @asset_sync_token.conflicts?(request.headers['VersionToken'])
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
@asset.update(last_modified_by: current_user)
|
||||
if wopi_file?(@asset)
|
||||
@asset.update_contents(request.body)
|
||||
else
|
||||
@asset.file.attach(io: request.body, filename: @asset.file.filename)
|
||||
@asset.attach_file_version(io: request.body, filename: @asset.file.filename)
|
||||
@asset.touch
|
||||
end
|
||||
|
||||
@asset.team.release_space(orig_file_size)
|
||||
@asset.post_process_file
|
||||
|
||||
log_activity(:edit)
|
||||
end
|
||||
|
||||
if asset_conflicts
|
||||
ActiveRecord::Base.transaction do
|
||||
conflict_response = AssetSyncTokenSerializer.new(@asset_sync_token).as_json
|
||||
error_message = { message: I18n.t('assets.conflict_error', filename: @asset.file.filename) }
|
||||
render json: conflict_response.merge(error_message), status: :conflict
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
render json: AssetSyncTokenSerializer.new(@asset_sync_token).as_json
|
||||
end
|
||||
|
||||
|
@ -94,7 +93,7 @@ class AssetSyncController < ApplicationController
|
|||
metadata: @asset.blob.metadata
|
||||
)
|
||||
|
||||
new_asset.file.attach(blob)
|
||||
new_asset.attach_file_version(blob)
|
||||
|
||||
case @asset.parent
|
||||
when Step
|
||||
|
|
|
@ -19,13 +19,16 @@ class AssetsController < ApplicationController
|
|||
before_action :load_vars, except: :create_wopi_file
|
||||
before_action :check_read_permission, except: %i(edit destroy duplicate create_wopi_file toggle_view_mode)
|
||||
before_action :check_manage_permission, only: %i(edit destroy duplicate rename toggle_view_mode)
|
||||
before_action :check_restore_permission, only: :restore_version
|
||||
|
||||
def file_preview
|
||||
editable = can_manage_asset?(@asset) && (@asset.repository_asset_value.blank? ||
|
||||
!@asset.repository_cell.repository_row.repository.is_a?(SoftLockedRepository))
|
||||
render json: { html: render_to_string(
|
||||
partial: 'shared/file_preview/content',
|
||||
locals: {
|
||||
asset: @asset,
|
||||
can_edit: can_manage_asset?(@asset),
|
||||
can_edit: editable,
|
||||
gallery: params[:gallery],
|
||||
preview: params[:preview]
|
||||
},
|
||||
|
@ -195,7 +198,7 @@ class AssetsController < ApplicationController
|
|||
return render_403 unless can_read_team?(@asset.team)
|
||||
|
||||
@asset.last_modified_by = current_user
|
||||
@asset.file.attach(io: params.require(:image), filename: orig_file_name)
|
||||
@asset.attach_file_version(io: params.require(:image), filename: orig_file_name)
|
||||
@asset.save!
|
||||
create_edit_image_activity(@asset, current_user, :finish_editing)
|
||||
# release previous image space
|
||||
|
@ -240,9 +243,9 @@ class AssetsController < ApplicationController
|
|||
|
||||
# Asset validation
|
||||
asset = Asset.new(created_by: current_user, team: current_team)
|
||||
asset.file.attach(io: StringIO.new,
|
||||
filename: "#{params[:file_name]}.#{params[:file_type]}",
|
||||
content_type: wopi_content_type(params[:file_type]))
|
||||
asset.attach_file_version(io: StringIO.new,
|
||||
filename: "#{params[:file_name]}.#{params[:file_type]}",
|
||||
content_type: wopi_content_type(params[:file_type]))
|
||||
|
||||
unless asset.valid?(:wopi_file_creation)
|
||||
render json: {
|
||||
|
@ -395,6 +398,55 @@ class AssetsController < ApplicationController
|
|||
render json: { checksum: @asset.file.blob.checksum }
|
||||
end
|
||||
|
||||
def versions
|
||||
blobs =
|
||||
[@asset.file.blob] +
|
||||
@asset.previous_files.map(&:blob).sort_by { |b| -1 * b.metadata['version'].to_i }[0..(VersionedAttachments.enabled? ? -1 : 1)]
|
||||
render(
|
||||
json: ActiveModel::SerializableResource.new(
|
||||
blobs,
|
||||
each_serializer: ActiveStorage::BlobSerializer
|
||||
).as_json.merge(
|
||||
enabled: VersionedAttachments.enabled?,
|
||||
enable_url: ENV.fetch('SCINOTE_FILE_VERSIONING_ENABLE_URL', nil)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def restore_version
|
||||
render_403 unless VersionedAttachments.enabled?
|
||||
|
||||
@asset.restore_file_version(params[:version].to_i)
|
||||
|
||||
message_items = {
|
||||
version: params[:version].to_i,
|
||||
file: @asset.file_name
|
||||
}
|
||||
|
||||
case @asset.parent
|
||||
when Step
|
||||
if @asset.parent.protocol.in_module?
|
||||
message_items.merge!({ my_module: @assoc.protocol.my_module.id, step: @asset.parent.id })
|
||||
log_restore_activity(:task_step_restore_asset_version, @assoc.protocol,
|
||||
@assoc.protocol.team, @assoc.my_module&.project, message_items)
|
||||
else
|
||||
message_items.merge!({ protocol: @assoc.protocol.id, step: @asset.parent.id })
|
||||
log_restore_activity(:protocol_step_restore_asset_version, @assoc.protocol,
|
||||
@assoc.protocol.team, nil, message_items)
|
||||
end
|
||||
when Result
|
||||
message_items.merge!({ result: @assoc.id, my_module: @assoc.my_module.id })
|
||||
log_restore_activity(:task_result_restore_asset_version, @assoc,
|
||||
@assoc.my_module.team, @assoc.my_module.project, message_items)
|
||||
when RepositoryCell
|
||||
message_items.merge!({ repository_column: @assoc.repository_column.id, repository: @repository.id })
|
||||
log_restore_activity(:repository_column_restore_asset_version, @repository,
|
||||
@repository.team, nil, message_items)
|
||||
end
|
||||
|
||||
render json: @asset.file.blob
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_vars
|
||||
|
@ -424,6 +476,10 @@ class AssetsController < ApplicationController
|
|||
render_403 and return unless can_manage_asset?(@asset)
|
||||
end
|
||||
|
||||
def check_restore_permission
|
||||
render_403 and return unless can_restore_asset?(@asset)
|
||||
end
|
||||
|
||||
def append_wd_params(url)
|
||||
exclude_params = %w(wdPreviousSession wdPreviousCorrelation)
|
||||
wd_params = params.as_json.select { |key, _value| key[/^wd.*/] && !(exclude_params.include? key) }.to_query
|
||||
|
@ -470,4 +526,14 @@ class AssetsController < ApplicationController
|
|||
result: result.id
|
||||
}.merge(message_items))
|
||||
end
|
||||
|
||||
def log_restore_activity(type_of, subject, team, project = nil, message_items = {})
|
||||
Activities::CreateActivityService
|
||||
.call(activity_type: type_of,
|
||||
owner: current_user,
|
||||
subject: subject,
|
||||
team: team,
|
||||
project: project,
|
||||
message_items: message_items)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,7 +27,7 @@ class AtWhoController < ApplicationController
|
|||
if params[:repository_id].present?
|
||||
Repository.find_by(id: params[:repository_id])
|
||||
else
|
||||
Repository.active.accessible_by_teams(@team).first
|
||||
Repository.active.viewable_by_user(current_user, @team).first
|
||||
end
|
||||
|
||||
items = []
|
||||
|
@ -54,8 +54,8 @@ class AtWhoController < ApplicationController
|
|||
end
|
||||
|
||||
def menu
|
||||
repositories = Repository.active.accessible_by_teams(@team)
|
||||
render json: {
|
||||
repositories = Repository.active.viewable_by_user(current_user, @team)
|
||||
render json: {
|
||||
html: render_to_string(partial: 'shared/smart_annotation/menu',
|
||||
locals: { repositories: repositories },
|
||||
formats: :html)
|
||||
|
|
|
@ -28,6 +28,8 @@ module ActiveStorage
|
|||
check_tinymce_asset_read_permissions(attachment.record)
|
||||
when 'Experiment'
|
||||
can_read_experiment?(attachment.record)
|
||||
when 'StorageLocation'
|
||||
can_read_storage_location?(attachment.record)
|
||||
when 'Report'
|
||||
can_read_project?(attachment.record.project)
|
||||
when 'User'
|
||||
|
|
|
@ -9,7 +9,7 @@ module Dashboard
|
|||
date = params[:date].in_time_zone(current_user.time_zone)
|
||||
start_date = date.at_beginning_of_month.utc - 8.days
|
||||
end_date = date.at_end_of_month.utc + 15.days
|
||||
due_dates = current_user.my_modules.active.uncomplete
|
||||
due_dates = current_user.my_modules.readable_by_user(current_user).active.uncomplete
|
||||
.joins(experiment: :project)
|
||||
.where(experiments: { archived: false })
|
||||
.where(projects: { archived: false })
|
||||
|
@ -23,7 +23,7 @@ module Dashboard
|
|||
date = params[:date].in_time_zone(current_user.time_zone)
|
||||
start_date = date.utc
|
||||
end_date = date.end_of_day.utc
|
||||
my_modules = current_user.my_modules.active.uncomplete
|
||||
my_modules = current_user.my_modules.readable_by_user(current_user).active.uncomplete
|
||||
.joins(experiment: :project)
|
||||
.where(experiments: { archived: false })
|
||||
.where(projects: { archived: false })
|
||||
|
|
|
@ -72,10 +72,9 @@ class GeneSequenceAssetsController < ApplicationController
|
|||
|
||||
ensure_asset!
|
||||
|
||||
@asset.file.purge
|
||||
@asset.preview_image.purge
|
||||
|
||||
@asset.file.attach(
|
||||
@asset.attach_file_version(
|
||||
io: StringIO.new(params[:sequence_data].to_json),
|
||||
filename: "#{params[:sequence_name]}.json"
|
||||
)
|
||||
|
|
|
@ -15,7 +15,7 @@ class HiddenRepositoryCellRemindersController < ApplicationController
|
|||
private
|
||||
|
||||
def load_repository
|
||||
@repository = Repository.accessible_by_teams(current_team).find_by(id: params[:repository_id])
|
||||
@repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id])
|
||||
render_404 unless @repository
|
||||
end
|
||||
|
||||
|
|
|
@ -15,7 +15,14 @@ class MyModuleRepositoriesController < ApplicationController
|
|||
@draw = params[:draw].to_i
|
||||
per_page = params[:length].to_i < 1 ? Constants::REPOSITORY_DEFAULT_PAGE_SIZE : params[:length].to_i
|
||||
page = (params[:start].to_i / per_page) + 1
|
||||
datatable_service = RepositoryDatatableService.new(@repository, params, current_user, @my_module)
|
||||
if params[:simple_view]
|
||||
rows_view = 'repository_rows/simple_view_index'
|
||||
preload_cells = false
|
||||
else
|
||||
rows_view = 'repository_rows/index'
|
||||
preload_cells = true
|
||||
end
|
||||
datatable_service = RepositoryDatatableService.new(@repository, params, current_user, @my_module, preload_cells: preload_cells)
|
||||
|
||||
@datatable_params = {
|
||||
view_mode: params[:view_mode],
|
||||
|
@ -26,19 +33,9 @@ class MyModuleRepositoriesController < ApplicationController
|
|||
|
||||
@all_rows_count = datatable_service.all_count
|
||||
@columns_mappings = datatable_service.mappings
|
||||
|
||||
if params[:simple_view]
|
||||
repository_rows = datatable_service.repository_rows
|
||||
rows_view = 'repository_rows/simple_view_index'
|
||||
else
|
||||
repository_rows = datatable_service.repository_rows
|
||||
.preload(:repository_columns,
|
||||
:created_by,
|
||||
repository_cells: { value: @repository.cell_preload_includes })
|
||||
rows_view = 'repository_rows/index'
|
||||
end
|
||||
repository_rows = datatable_service.repository_rows
|
||||
@repository_rows = repository_rows.page(page).per(per_page)
|
||||
|
||||
@filtered_rows_count = @repository_rows.load.take&.filtered_count || 0
|
||||
render rows_view
|
||||
end
|
||||
|
||||
|
@ -145,7 +142,7 @@ class MyModuleRepositoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def repositories_list_html
|
||||
@assigned_repositories = @my_module.live_and_snapshot_repositories_list
|
||||
@assigned_repositories = @my_module.readable_live_and_snapshot_repositories_list(current_user)
|
||||
render json: {
|
||||
html: render_to_string(partial: 'my_modules/repositories/repositories_list'),
|
||||
assigned_rows_count: @assigned_repositories.map(&:assigned_rows_count).sum
|
||||
|
@ -162,7 +159,7 @@ class MyModuleRepositoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def repositories_dropdown_list
|
||||
@repositories = Repository.accessible_by_teams(current_team).joins("
|
||||
@repositories = Repository.viewable_by_user(current_user).joins("
|
||||
LEFT OUTER JOIN repository_rows ON
|
||||
repository_rows.repository_id = repositories.id
|
||||
LEFT OUTER JOIN my_module_repository_rows ON
|
||||
|
|
|
@ -16,20 +16,16 @@ class MyModuleRepositorySnapshotsController < ApplicationController
|
|||
|
||||
@all_rows_count = datatable_service.all_count
|
||||
@columns_mappings = datatable_service.mappings
|
||||
repository_rows = datatable_service.repository_rows
|
||||
|
||||
if params[:simple_view]
|
||||
repository_rows = datatable_service.repository_rows
|
||||
@repository = @repository_snapshot
|
||||
rows_view = 'repository_rows/simple_view_index'
|
||||
else
|
||||
repository_rows =
|
||||
datatable_service.repository_rows
|
||||
.preload(:repository_columns,
|
||||
:created_by,
|
||||
repository_cells: { value: @repository_snapshot.cell_preload_includes })
|
||||
rows_view = 'repository_rows/snapshot_index'
|
||||
end
|
||||
@repository_rows = repository_rows.page(page).per(per_page)
|
||||
|
||||
@filtered_rows_count = @repository_rows.load.take&.filtered_count || 0
|
||||
render rows_view
|
||||
end
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ class MyModuleShareableLinksController < ApplicationController
|
|||
@draw = params[:draw].to_i
|
||||
per_page = params[:length].to_i < 1 ? Constants::REPOSITORY_DEFAULT_PAGE_SIZE : params[:length].to_i
|
||||
page = (params[:start].to_i / per_page) + 1
|
||||
datatable_service = RepositoryDatatableService.new(@repository, params, nil, @my_module)
|
||||
datatable_service = RepositoryDatatableService.new(@repository, params, nil, @my_module, preload_cells: false, disable_reminders: true)
|
||||
|
||||
@datatable_params = {
|
||||
view_mode: params[:view_mode],
|
||||
|
@ -76,6 +76,7 @@ class MyModuleShareableLinksController < ApplicationController
|
|||
@columns_mappings = datatable_service.mappings
|
||||
|
||||
@repository_rows = datatable_service.repository_rows.page(page).per(per_page)
|
||||
@filtered_rows_count = @repository_rows.load.take&.filtered_count || 0
|
||||
|
||||
render 'repository_rows/simple_view_index'
|
||||
end
|
||||
|
@ -84,13 +85,14 @@ class MyModuleShareableLinksController < ApplicationController
|
|||
@draw = params[:draw].to_i
|
||||
per_page = params[:length].to_i < 1 ? Constants::REPOSITORY_DEFAULT_PAGE_SIZE : params[:length].to_i
|
||||
page = (params[:start].to_i / per_page) + 1
|
||||
datatable_service = RepositorySnapshotDatatableService.new(@repository_snapshot, params, nil, @my_module)
|
||||
datatable_service = RepositorySnapshotDatatableService.new(@repository_snapshot, params, nil, @my_module, preload_cells: false)
|
||||
|
||||
@all_rows_count = datatable_service.all_count
|
||||
@columns_mappings = datatable_service.mappings
|
||||
|
||||
@repository = @repository_snapshot
|
||||
@repository_rows = datatable_service.repository_rows.page(page).per(per_page)
|
||||
@filtered_rows_count = @repository_rows.load.take&.filtered_count || 0
|
||||
|
||||
render 'repository_rows/simple_view_index'
|
||||
end
|
||||
|
|
|
@ -304,7 +304,7 @@ class MyModulesController < ApplicationController
|
|||
|
||||
def protocols
|
||||
@protocol = @my_module.protocol
|
||||
@assigned_repositories = @my_module.live_and_snapshot_repositories_list
|
||||
@assigned_repositories = @my_module.readable_live_and_snapshot_repositories_list(current_user)
|
||||
end
|
||||
|
||||
def protocol
|
||||
|
@ -410,7 +410,7 @@ class MyModulesController < ApplicationController
|
|||
actions:
|
||||
Toolbars::MyModulesService.new(
|
||||
current_user,
|
||||
my_module_ids: JSON.parse(params[:items]).map { |i| i['id'] }
|
||||
my_module_ids: params[:items].present? ? JSON.parse(params[:items]).map { |i| i['id'] } : params[:items]
|
||||
).actions
|
||||
}
|
||||
end
|
||||
|
@ -651,7 +651,7 @@ class MyModulesController < ApplicationController
|
|||
|
||||
@navigator = {
|
||||
url: tree_navigator_my_module_path(@my_module),
|
||||
archived: params[:view_mode] == 'archived',
|
||||
archived: @my_module.archived_branch? || params[:view_mode] == 'archived',
|
||||
id: @my_module.code
|
||||
}
|
||||
end
|
||||
|
|
|
@ -312,7 +312,7 @@ class ReportsController < ApplicationController
|
|||
|
||||
def load_wizard_vars
|
||||
@templates = Extends::REPORT_TEMPLATES
|
||||
live_repositories = Repository.accessible_by_teams(current_team).sort_by { |r| r.name.downcase }
|
||||
live_repositories = Repository.viewable_by_user(current_user).sort_by { |r| r.name.downcase }
|
||||
snapshots_of_deleted = RepositorySnapshot.left_outer_joins(:original_repository)
|
||||
.where(team: current_team)
|
||||
.where.not(original_repository: live_repositories)
|
||||
|
@ -348,7 +348,7 @@ class ReportsController < ApplicationController
|
|||
def load_available_repositories
|
||||
@available_repositories = []
|
||||
repositories = Repository.active
|
||||
.accessible_by_teams(current_team)
|
||||
.viewable_by_user(current_user)
|
||||
.name_like(search_params[:query])
|
||||
.limit(Constants::SEARCH_LIMIT)
|
||||
repositories.each do |repository|
|
||||
|
|
|
@ -10,18 +10,17 @@ class RepositoriesController < ApplicationController
|
|||
include MyModulesHelper
|
||||
|
||||
before_action :load_repository, except: %i(index create create_modal sidebar archive restore actions_toolbar
|
||||
export_modal export_repositories)
|
||||
before_action :load_repositories, only: :index
|
||||
export_modal export_repositories list)
|
||||
before_action :load_repositories, only: %i(index list)
|
||||
before_action :load_repositories_for_archiving, only: :archive
|
||||
before_action :load_repositories_for_restoring, only: :restore
|
||||
before_action :check_view_all_permissions, only: %i(index sidebar)
|
||||
before_action :check_view_all_permissions, only: %i(index sidebar list)
|
||||
before_action :check_view_permissions, except: %i(index create_modal create update destroy parse_sheet
|
||||
import_records sidebar archive restore actions_toolbar
|
||||
export_modal export_repositories)
|
||||
export_modal export_repositories list)
|
||||
before_action :check_manage_permissions, only: %i(rename_modal update)
|
||||
before_action :check_delete_permissions, only: %i(destroy destroy_modal)
|
||||
before_action :check_archive_permissions, only: %i(archive restore)
|
||||
before_action :check_share_permissions, only: :share_modal
|
||||
before_action :check_create_permissions, only: %i(create_modal create)
|
||||
before_action :check_copy_permissions, only: %i(copy_modal copy)
|
||||
before_action :set_inline_name_editing, only: %i(show)
|
||||
|
@ -44,6 +43,34 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def list
|
||||
results = @repositories.select(:id, :name, 'LOWER(repositories.name)')
|
||||
results = results.name_like(params[:query]) if params[:query].present?
|
||||
results = results.joins(:repository_rows).distinct if params[:non_empty].present?
|
||||
results = results.active if params[:active].present?
|
||||
|
||||
render json: { data: results.order('LOWER(repositories.name) asc').map { |r| [r.id, r.name] } }
|
||||
end
|
||||
|
||||
def rows_list
|
||||
results = @repository.repository_rows
|
||||
if params[:query].present?
|
||||
results = results.where_attributes_like(
|
||||
['repository_rows.name', RepositoryRow::PREFIXED_ID_SQL],
|
||||
params[:query]
|
||||
)
|
||||
end
|
||||
results = results.active if params[:active].present?
|
||||
|
||||
results = results.order('LOWER(repository_rows.name) asc').page(params[:page])
|
||||
|
||||
render json: {
|
||||
paginated: true,
|
||||
next_page: results.next_page,
|
||||
data: results.map { |r| [r.id, r.name] }
|
||||
}
|
||||
end
|
||||
|
||||
def sidebar
|
||||
render json: {
|
||||
html: render_to_string(partial: 'repositories/sidebar', locals: {
|
||||
|
@ -101,15 +128,6 @@ class RepositoriesController < ApplicationController
|
|||
}
|
||||
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
|
||||
# synchronously hide currently visible reminders
|
||||
if params[:visible_reminder_repository_row_ids].present?
|
||||
|
@ -298,14 +316,14 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def import_records
|
||||
render_403 unless can_create_repository_rows?(Repository.accessible_by_teams(current_team)
|
||||
render_403 unless can_create_repository_rows?(Repository.viewable_by_user(current_user)
|
||||
.find_by(id: import_params[:id]))
|
||||
# Check if there exist mapping for repository record (it's mandatory)
|
||||
if import_params[:mappings].present? && import_params[:mappings].value?('-1')
|
||||
status = ImportRepository::ImportRecords
|
||||
.new(
|
||||
temp_file: TempFile.find_by(id: import_params[:file_id]),
|
||||
repository: Repository.accessible_by_teams(current_team).find_by(id: import_params[:id]),
|
||||
repository: Repository.viewable_by_user(current_user).find_by(id: import_params[:id]),
|
||||
mappings: import_params[:mappings],
|
||||
session: session,
|
||||
user: current_user,
|
||||
|
@ -452,12 +470,12 @@ class RepositoriesController < ApplicationController
|
|||
|
||||
def load_repository
|
||||
repository_id = params[:id] || params[:repository_id]
|
||||
@repository = Repository.accessible_by_teams(current_user.teams).find_by(id: repository_id)
|
||||
@repository = Repository.viewable_by_user(current_user).find_by(id: repository_id)
|
||||
render_404 unless @repository
|
||||
end
|
||||
|
||||
def load_repositories
|
||||
@repositories = Repository.accessible_by_teams(current_team)
|
||||
@repositories = Repository.viewable_by_user(current_user)
|
||||
end
|
||||
|
||||
def load_repositories_for_archiving
|
||||
|
@ -477,7 +495,7 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def set_inline_name_editing
|
||||
return unless can_manage_repository?(@repository)
|
||||
return unless can_manage_repository?(@repository) && !@repository.is_a?(SoftLockedRepository)
|
||||
|
||||
@inline_editable_title_config = {
|
||||
name: 'title',
|
||||
|
@ -522,10 +540,6 @@ class RepositoriesController < ApplicationController
|
|||
render_403 unless can_delete_repository?(@repository)
|
||||
end
|
||||
|
||||
def check_share_permissions
|
||||
render_403 unless can_share_repository?(@repository)
|
||||
end
|
||||
|
||||
def repository_params
|
||||
params.require(:repository).permit(:name)
|
||||
end
|
||||
|
|
|
@ -107,7 +107,7 @@ class RepositoryColumnsController < ApplicationController
|
|||
AvailableRepositoryColumn = Struct.new(:id, :name)
|
||||
|
||||
def load_repository
|
||||
@repository = Repository.accessible_by_teams(current_team).find_by(id: params[:repository_id])
|
||||
@repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id])
|
||||
render_404 unless @repository
|
||||
end
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ class RepositoryRowConnectionsController < ApplicationController
|
|||
end
|
||||
|
||||
def repositories
|
||||
repositories = Repository.accessible_by_teams(current_team)
|
||||
repositories = Repository.viewable_by_user(current_user)
|
||||
.search_by_name_and_id(current_user, current_user.teams, params[:query])
|
||||
.order(name: :asc)
|
||||
.page(params[:page] || 1)
|
||||
|
@ -69,7 +69,7 @@ class RepositoryRowConnectionsController < ApplicationController
|
|||
end
|
||||
|
||||
def repository_rows
|
||||
selected_repository = Repository.accessible_by_teams(current_team).find(params[:selected_repository_id])
|
||||
selected_repository = Repository.viewable_by_user(current_user).find(params[:selected_repository_id])
|
||||
|
||||
repository_rows = selected_repository.repository_rows
|
||||
.where.not(id: @repository_row.id)
|
||||
|
@ -93,14 +93,14 @@ class RepositoryRowConnectionsController < ApplicationController
|
|||
|
||||
return render_422(t('.invalid_params')) unless @relation_type
|
||||
|
||||
@connection_repository = Repository.accessible_by_teams(current_team)
|
||||
@connection_repository = Repository.viewable_by_user(current_user)
|
||||
.find_by(id: connection_params[:connection_repository_id])
|
||||
return render_404 unless @connection_repository
|
||||
return render_403 unless can_connect_repository_rows?(@connection_repository)
|
||||
end
|
||||
|
||||
def load_repository
|
||||
@repository = Repository.accessible_by_teams(current_team).find_by(id: params[:repository_id])
|
||||
@repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id])
|
||||
render_404 unless @repository
|
||||
end
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ class RepositoryRowsController < ApplicationController
|
|||
include ApplicationHelper
|
||||
include MyModulesHelper
|
||||
include RepositoryDatatableHelper
|
||||
include StorageLocationsHelper
|
||||
|
||||
before_action :load_repository, except: %i(show print rows_to_print print_zpl validate_label_template_columns)
|
||||
before_action :load_repository_or_snapshot, only: %i(show print rows_to_print print_zpl
|
||||
|
@ -27,15 +28,10 @@ class RepositoryRowsController < ApplicationController
|
|||
|
||||
@all_rows_count = datatable_service.all_count
|
||||
@columns_mappings = datatable_service.mappings
|
||||
@repository_rows = datatable_service.repository_rows
|
||||
.preload(:repository_columns,
|
||||
:created_by,
|
||||
:archived_by,
|
||||
repository_cells: { value: @repository.cell_preload_includes })
|
||||
.page(page)
|
||||
.per(per_page)
|
||||
|
||||
@repository_rows = @repository_rows.where(archived: params[:archived]) unless @repository.archived?
|
||||
repository_rows = datatable_service.repository_rows
|
||||
repository_rows = repository_rows.where(archived: params[:archived]) unless @repository.archived?
|
||||
@repository_rows = repository_rows.page(page).per(per_page)
|
||||
@filtered_rows_count = @repository_rows.load.take&.filtered_count || 0
|
||||
rescue RepositoryFilters::ColumnNotFoundException
|
||||
render json: { custom_error: I18n.t('repositories.show.repository_filter.errors.column_not_found') }
|
||||
rescue RepositoryFilters::ValueNotFoundException
|
||||
|
@ -328,7 +324,7 @@ class RepositoryRowsController < ApplicationController
|
|||
def active_reminder_repository_cells
|
||||
reminder_cells = @repository_row.repository_cells.with_active_reminder(current_user).distinct
|
||||
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
|
||||
})
|
||||
}
|
||||
|
@ -358,14 +354,14 @@ class RepositoryRowsController < ApplicationController
|
|||
AvailableRepositoryRow = Struct.new(:id, :name, :has_file_attached)
|
||||
|
||||
def load_repository
|
||||
@repository = Repository.accessible_by_teams(current_team)
|
||||
@repository = Repository.viewable_by_user(current_user)
|
||||
.eager_load(:repository_columns)
|
||||
.find_by(id: params[:repository_id])
|
||||
render_404 unless @repository
|
||||
end
|
||||
|
||||
def load_repository_or_snapshot
|
||||
@repository = Repository.accessible_by_teams(current_team).find_by(id: params[:repository_id]) ||
|
||||
@repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id]) ||
|
||||
RepositorySnapshot.find_by(id: params[:repository_id])
|
||||
return render_404 unless @repository
|
||||
end
|
||||
|
|
|
@ -70,7 +70,7 @@ class RepositoryTableFiltersController < ApplicationController
|
|||
private
|
||||
|
||||
def load_repository
|
||||
@repository = Repository.accessible_by_teams(current_team).find_by(id: params[:repository_id])
|
||||
@repository = Repository.viewable_by_user(current_user).find_by(id: params[:repository_id])
|
||||
render_403 unless can_read_repository?(@repository)
|
||||
end
|
||||
|
||||
|
|
|
@ -125,7 +125,7 @@ class ResultAssetsController < ApplicationController
|
|||
ActiveRecord::Base.transaction do
|
||||
params[:results_files].each do |index, file|
|
||||
asset = Asset.create!(created_by: current_user, last_modified_by: current_user, team: current_team)
|
||||
asset.file.attach(file[:signed_blob_id])
|
||||
asset.attach_file_version(file[:signed_blob_id])
|
||||
result = Result.create!(user: current_user,
|
||||
my_module: @my_module,
|
||||
name: params[:results_names][index],
|
||||
|
|
|
@ -90,7 +90,7 @@ class ResultsController < ApplicationController
|
|||
team: @my_module.team,
|
||||
view_mode: @result.assets_view_mode
|
||||
)
|
||||
@asset.file.attach(params[:signed_blob_id])
|
||||
@asset.attach_file_version(params[:signed_blob_id])
|
||||
@asset.post_process_file
|
||||
end
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ class StepsController < ApplicationController
|
|||
team: @protocol.team,
|
||||
view_mode: @step.assets_view_mode
|
||||
)
|
||||
@asset.file.attach(params[:signed_blob_id])
|
||||
@asset.attach_file_version(params[:signed_blob_id])
|
||||
@asset.post_process_file
|
||||
|
||||
default_message_items = {
|
||||
|
|
153
app/controllers/storage_location_repository_rows_controller.rb
Normal file
153
app/controllers/storage_location_repository_rows_controller.rb
Normal file
|
@ -0,0 +1,153 @@
|
|||
# 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 move)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@storage_location_repository_row.with_lock do
|
||||
if @storage_location_repository_row.save
|
||||
log_activity(:storage_location_repository_row_created)
|
||||
render json: @storage_location_repository_row,
|
||||
serializer: Lists::StorageLocationRepositoryRowSerializer
|
||||
else
|
||||
render json: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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: { errors: @storage_location_repository_row.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def move
|
||||
ActiveRecord::Base.transaction do
|
||||
@original_storage_location = @storage_location_repository_row.storage_location
|
||||
@original_position = @storage_location_repository_row.human_readable_position
|
||||
|
||||
@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,
|
||||
{
|
||||
storage_location_original: @original_storage_location.id,
|
||||
position_original: @original_position
|
||||
}
|
||||
)
|
||||
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.find(
|
||||
storage_location_repository_row_params[:storage_location_id]
|
||||
)
|
||||
render_404 unless can_read_storage_location?(@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_repository_rows?(@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.storage_location,
|
||||
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
|
329
app/controllers/storage_locations_controller.rb
Normal file
329
app/controllers/storage_locations_controller.rb
Normal file
|
@ -0,0 +1,329 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StorageLocationsController < ApplicationController
|
||||
include ActionView::Helpers::TextHelper
|
||||
include ApplicationHelper
|
||||
include TeamsHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
before_action :switch_team_with_param, only: %i(index show)
|
||||
before_action :check_storage_locations_enabled, except: :unassign_rows
|
||||
before_action :load_storage_location, only: %i(update destroy duplicate move show available_positions unassign_rows export_container import_container)
|
||||
before_action :check_read_permissions, except: %i(index create tree actions_toolbar import_container unassign_rows)
|
||||
before_action :check_manage_repository_rows_permissions, only: %i(import_container unassign_rows)
|
||||
before_action :check_create_permissions, only: :create
|
||||
before_action :check_manage_permissions, only: %i(update destroy duplicate move)
|
||||
before_action :set_breadcrumbs_items, only: %i(index show)
|
||||
|
||||
def index
|
||||
@parent_location = StorageLocation.find(storage_location_params[:parent_id]) if storage_location_params[:parent_id]
|
||||
|
||||
render_403 if @parent_location && !can_read_storage_location?(@parent_location)
|
||||
|
||||
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),
|
||||
shared_object:
|
||||
@parent_location &&
|
||||
StorageLocation.select('*').select(StorageLocation.shared_sql_select(current_user)).find(@parent_location.root_storage_location.id)
|
||||
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')
|
||||
storage_location_annotation_notification
|
||||
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]
|
||||
old_description = @storage_location.description
|
||||
@storage_location.update(storage_location_params)
|
||||
|
||||
if @storage_location.save
|
||||
log_activity('storage_location_edited')
|
||||
storage_location_annotation_notification(old_description) if old_description != @storage_location.description
|
||||
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!(current_user, current_team)
|
||||
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
|
||||
StorageLocation.find(move_params[:destination_storage_location_id])
|
||||
end
|
||||
|
||||
render_403 and return if destination_storage_location && !can_manage_storage_location?(destination_storage_location)
|
||||
|
||||
@storage_location.update!(parent: destination_storage_location)
|
||||
|
||||
log_activity('storage_location_moved', {
|
||||
storage_location_original: original_storage_location&.id, # nil if moved from root
|
||||
storage_location_destination: destination_storage_location&.id # nil if moved to root
|
||||
})
|
||||
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 = StorageLocation.viewable_by_user(current_user, current_team)
|
||||
.where(
|
||||
parent: nil,
|
||||
container: [false, params[:container] == 'true']
|
||||
)
|
||||
records = records.where(team_id: params[:team_id]) if params[:team_id]
|
||||
|
||||
render json: {
|
||||
locations: storage_locations_recursive_builder(records),
|
||||
movable_to_root: params[:team_id] && current_team.id == params[:team_id].to_i
|
||||
}
|
||||
end
|
||||
|
||||
def available_positions
|
||||
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
|
||||
if (result[:assigned_count] + result[:unassigned_count]).positive?
|
||||
log_activity(
|
||||
:storage_location_imported,
|
||||
{
|
||||
assigned_count: result[:assigned_count],
|
||||
unassigned_count: result[:unassigned_count]
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
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.find(storage_location_params[:id])
|
||||
@parent_location = @storage_location.parent
|
||||
render_404 unless can_read_storage_location?(@storage_location)
|
||||
end
|
||||
|
||||
def check_read_permissions
|
||||
render_403 unless can_read_storage_location?(@storage_location)
|
||||
end
|
||||
|
||||
def check_create_permissions
|
||||
render_403 if @parent_location && !can_manage_storage_location?(@parent_location.team)
|
||||
|
||||
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 check_manage_repository_rows_permissions
|
||||
render_403 unless can_manage_storage_location_repository_rows?(@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 = StorageLocation.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.order('LOWER(storage_locations.name) ASC').map do |storage_location|
|
||||
{
|
||||
storage_location: storage_location,
|
||||
can_manage: (can_manage_storage_location?(storage_location) unless storage_location.parent_id),
|
||||
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 storage_location_annotation_notification(old_text = nil)
|
||||
url = if @storage_location.container
|
||||
storage_location_path(@storage_location.id)
|
||||
else
|
||||
storage_locations_path(parent_id: @storage_location.id)
|
||||
end
|
||||
|
||||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: @storage_location.description,
|
||||
subject: @storage_location,
|
||||
title: t('notifications.storage_location_annotation_title',
|
||||
storage_location: @storage_location.name,
|
||||
user: current_user.full_name),
|
||||
message: t('notifications.storage_location_annotation_message_html',
|
||||
storage_location: link_to(@storage_location.name, url))
|
||||
)
|
||||
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
|
164
app/controllers/team_shared_objects_controller.rb
Normal file
164
app/controllers/team_shared_objects_controller.rb
Normal file
|
@ -0,0 +1,164 @@
|
|||
# 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_permission_level =
|
||||
if params[:select_all_teams]
|
||||
params[:select_all_write_permission] ? :shared_write : :shared_read
|
||||
else
|
||||
:not_shared
|
||||
end
|
||||
|
||||
# Global share
|
||||
if @model.globally_shareable?
|
||||
@model.permission_level = global_permission_level
|
||||
|
||||
if @model.permission_level_changed?
|
||||
@model.save!
|
||||
@model.team_shared_objects.each(&:destroy!) unless global_permission_level == :not_shared
|
||||
case @model
|
||||
when Repository
|
||||
setup_repository_global_share_activity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Share to specific teams
|
||||
params[:team_share_params].each do |t|
|
||||
next unless t['private_shared_with']
|
||||
|
||||
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 = (Team.order(:name).all - [@model.team]).filter { |t| can_read_team?(t) || @model.private_shared_with?(t) }
|
||||
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
|
|
@ -5,11 +5,18 @@ class UserNotificationsController < ApplicationController
|
|||
|
||||
def index
|
||||
page = (params.dig(:page, :number) || 1).to_i
|
||||
notifications = load_notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT)
|
||||
notifications = load_notifications
|
||||
|
||||
case params[:tab]
|
||||
when 'read'
|
||||
notifications = notifications.where.not(read_at: nil)
|
||||
when 'unread'
|
||||
notifications = notifications.where(read_at: nil)
|
||||
end
|
||||
|
||||
notifications = notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT)
|
||||
|
||||
render json: notifications, each_serializer: NotificationSerializer
|
||||
|
||||
notifications.mark_as_read!
|
||||
end
|
||||
|
||||
def unseen_counter
|
||||
|
@ -18,6 +25,17 @@ class UserNotificationsController < ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
def mark_all_read
|
||||
load_notifications.mark_as_read!
|
||||
render json: { success: true }
|
||||
end
|
||||
|
||||
def toggle_read
|
||||
notification = current_user.notifications.find(params[:id])
|
||||
notification.update(read_at: (params[:mark_as_read] ? DateTime.now : nil))
|
||||
render json: notification, serializer: NotificationSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_notifications
|
||||
|
@ -25,5 +43,4 @@ class UserNotificationsController < ApplicationController
|
|||
.in_app
|
||||
.order(created_at: :desc)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ module Users
|
|||
skip_before_action :verify_authenticity_token
|
||||
before_action :sign_up_with_provider_enabled?,
|
||||
only: :linkedin
|
||||
before_action :check_sso_status, only: %i(customazureactivedirectory okta openid_connect)
|
||||
before_action :check_sso_status, only: %i(customazureactivedirectory okta openid_connect saml)
|
||||
|
||||
# You should configure your model like this:
|
||||
# devise :omniauthable, omniauth_providers: [:twitter]
|
||||
|
@ -38,7 +38,7 @@ module Users
|
|||
|
||||
if email.blank?
|
||||
# No email in the token so can not link or create user
|
||||
error_message = I18n.t('devise.azure.errors.no_email')
|
||||
missing_attribute = 'Email'
|
||||
return redirect_to after_omniauth_failure_path_for(resource_name)
|
||||
end
|
||||
|
||||
|
@ -47,7 +47,11 @@ module Users
|
|||
if user.blank?
|
||||
# Create new user and identity
|
||||
user = create_user_from_auth(email, auth)
|
||||
sign_in_and_redirect(user, event: :authentication)
|
||||
if user.errors.present?
|
||||
redirect_to after_omniauth_failure_path_for(resource_name)
|
||||
else
|
||||
sign_in_and_redirect(user, event: :authentication)
|
||||
end
|
||||
elsif provider_conf['auto_link_on_sign_in']
|
||||
# Link to existing local account
|
||||
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
|
||||
|
@ -65,7 +69,10 @@ module Users
|
|||
error_message ||= I18n.t('devise.azure.errors.generic')
|
||||
redirect_to after_omniauth_failure_path_for(resource_name)
|
||||
ensure
|
||||
if error_message
|
||||
if user&.errors.present? || missing_attribute.present?
|
||||
missing_attribute ||= user.errors.first.attribute.capitalize
|
||||
set_flash_message(:alert, :missing_attribute, attribute: missing_attribute)
|
||||
elsif error_message
|
||||
set_flash_message(:alert, :failure, kind: I18n.t('devise.azure.provider_name'), reason: error_message)
|
||||
else
|
||||
set_flash_message(:notice, :success, kind: I18n.t('devise.azure.provider_name'))
|
||||
|
@ -137,13 +144,18 @@ module Users
|
|||
user = User.find_by(email: auth.info.email.downcase)
|
||||
|
||||
if user.blank?
|
||||
user = create_user_from_auth(email, auth)
|
||||
user = create_user_from_auth(auth.info.email.downcase, auth)
|
||||
if user.errors.present?
|
||||
redirect_to after_omniauth_failure_path_for(resource_name)
|
||||
else
|
||||
sign_in_and_redirect(user, event: :authentication)
|
||||
end
|
||||
else
|
||||
# Link to existing local account
|
||||
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
|
||||
user.update!(confirmed_at: user.created_at) if user.confirmed_at.blank?
|
||||
sign_in_and_redirect(user, event: :authentication)
|
||||
end
|
||||
sign_in_and_redirect(user, event: :authentication)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e.message
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
|
@ -151,7 +163,9 @@ module Users
|
|||
error_message ||= I18n.t('devise.okta.errors.generic')
|
||||
redirect_to after_omniauth_failure_path_for(resource_name)
|
||||
ensure
|
||||
if error_message
|
||||
if user&.errors.present?
|
||||
set_flash_message(:alert, :missing_attribute, attribute: user.errors.first.attribute.capitalize)
|
||||
elsif error_message
|
||||
set_flash_message(:alert, :failure, kind: I18n.t('devise.okta.provider_name'), reason: error_message)
|
||||
else
|
||||
set_flash_message(:notice, :success, kind: I18n.t('devise.okta.provider_name'))
|
||||
|
@ -175,7 +189,7 @@ module Users
|
|||
|
||||
if email.blank?
|
||||
# No email in the token so can not link or create user
|
||||
error_message = I18n.t('devise.openid_connect.errors.no_email')
|
||||
missing_attribute = 'Email'
|
||||
return redirect_to after_omniauth_failure_path_for(resource_name)
|
||||
end
|
||||
|
||||
|
@ -184,7 +198,11 @@ module Users
|
|||
if user.blank?
|
||||
# Create new user and identity
|
||||
user = create_user_from_auth(email, auth)
|
||||
sign_in_and_redirect(user)
|
||||
if user.errors.present?
|
||||
redirect_to after_omniauth_failure_path_for(resource_name)
|
||||
else
|
||||
sign_in_and_redirect(user)
|
||||
end
|
||||
elsif provider_conf['auto_link_on_sign_in']
|
||||
# Link to existing local account
|
||||
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
|
||||
|
@ -202,7 +220,10 @@ module Users
|
|||
error_message ||= I18n.t('devise.openid_connect.errors.generic')
|
||||
redirect_to after_omniauth_failure_path_for(resource_name)
|
||||
ensure
|
||||
if error_message
|
||||
if user&.errors.present? || missing_attribute.present?
|
||||
missing_attribute ||= user.errors.first.attribute.capitalize
|
||||
set_flash_message(:alert, :missing_attribute, attribute: missing_attribute)
|
||||
elsif error_message
|
||||
set_flash_message(:alert, :failure, kind: I18n.t('devise.openid_connect.provider_name'), reason: error_message)
|
||||
else
|
||||
set_flash_message(:notice, :success, kind: I18n.t('devise.openid_connect.provider_name'))
|
||||
|
@ -226,7 +247,7 @@ module Users
|
|||
|
||||
if email.blank?
|
||||
# No email in the token so can not link or create user
|
||||
error_message = I18n.t('devise.saml.errors.no_email')
|
||||
missing_attribute = 'Email'
|
||||
return redirect_to after_omniauth_failure_path_for(resource_name)
|
||||
end
|
||||
|
||||
|
@ -234,7 +255,11 @@ module Users
|
|||
|
||||
if user.blank?
|
||||
user = create_user_from_auth(email, auth)
|
||||
sign_in_and_redirect(user)
|
||||
if user.errors.present?
|
||||
redirect_to after_omniauth_failure_path_for(resource_name)
|
||||
else
|
||||
sign_in_and_redirect(user)
|
||||
end
|
||||
elsif provider_conf['auto_link_on_sign_in']
|
||||
# Link to existing local account
|
||||
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
|
||||
|
@ -252,7 +277,10 @@ module Users
|
|||
error_message ||= I18n.t('devise.saml.errors.generic')
|
||||
redirect_to after_omniauth_failure_path_for(resource_name)
|
||||
ensure
|
||||
if error_message
|
||||
if user&.errors.present? || missing_attribute.present?
|
||||
missing_attribute ||= user.errors.first.attribute.to_s.capitalize
|
||||
set_flash_message(:alert, :missing_attribute, attribute: missing_attribute)
|
||||
elsif error_message
|
||||
set_flash_message(:alert, :failure, kind: I18n.t('devise.saml.provider_name'), reason: error_message)
|
||||
else
|
||||
set_flash_message(:notice, :success, kind: I18n.t('devise.saml.provider_name'))
|
||||
|
@ -306,20 +334,10 @@ module Users
|
|||
user.save!
|
||||
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
|
||||
user.update!(confirmed_at: user.created_at)
|
||||
end
|
||||
user
|
||||
end
|
||||
|
||||
def create_user_from_auth(email, auth)
|
||||
full_name = "#{auth.info.first_name} #{auth.info.last_name}"
|
||||
user = User.new(full_name: full_name,
|
||||
initials: generate_initials(full_name),
|
||||
email: email,
|
||||
password: generate_user_password)
|
||||
User.transaction do
|
||||
user.save!
|
||||
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
|
||||
user.update!(confirmed_at: user.created_at)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e.message
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
user
|
||||
end
|
||||
|
|
|
@ -208,11 +208,12 @@ class Users::RegistrationsController < Devise::RegistrationsController
|
|||
end
|
||||
|
||||
def regenerate_api_key
|
||||
current_user.regenerate_api_key!
|
||||
token = current_user.regenerate_api_key!
|
||||
|
||||
redirect_to(edit_user_registration_path(anchor: 'api-key'),
|
||||
flash: {
|
||||
success: t('users.registrations.edit.api_key.generated')
|
||||
success: t('users.registrations.edit.api_key.generated'),
|
||||
token: token
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@ module Users
|
|||
next unless Extends::WHITELISTED_USER_SETTINGS.include?(key.to_s)
|
||||
|
||||
case key.to_s
|
||||
when 'task_step_states'
|
||||
update_task_step_states(data)
|
||||
when 'task_step_states', 'result_states'
|
||||
update_object_states(data, key.to_s)
|
||||
else
|
||||
current_user.settings[key] = data
|
||||
end
|
||||
|
@ -34,18 +34,18 @@ module Users
|
|||
|
||||
private
|
||||
|
||||
def update_task_step_states(task_step_states_data)
|
||||
current_states = current_user.settings.fetch('task_step_states', {})
|
||||
def update_object_states(object_states_data, object_state_key)
|
||||
current_states = current_user.settings.fetch(object_state_key, {})
|
||||
|
||||
task_step_states_data.each do |step_id, collapsed|
|
||||
object_states_data.each do |object_id, collapsed|
|
||||
if collapsed
|
||||
current_states[step_id] = true
|
||||
current_states[object_id] = true
|
||||
else
|
||||
current_states.delete(step_id)
|
||||
current_states.delete(object_id)
|
||||
end
|
||||
end
|
||||
|
||||
current_user.settings['task_step_states'] = current_states
|
||||
current_user.settings[object_state_key] = current_states
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -200,7 +200,6 @@ class WopiController < ActionController::Base
|
|||
if @asset.lock == lock
|
||||
logger.warn 'WOPI: replacing file'
|
||||
|
||||
@team.release_space(@asset.estimated_size)
|
||||
@asset.last_modified_by = @user
|
||||
@asset.update_contents(request.body)
|
||||
@asset.save
|
||||
|
@ -220,7 +219,6 @@ class WopiController < ActionController::Base
|
|||
elsif !@asset.file_size.nil? && @asset.file_size.zero?
|
||||
logger.warn 'WOPI: initializing empty file'
|
||||
|
||||
@team.release_space(@asset.estimated_size)
|
||||
@asset.update_contents(request.body)
|
||||
@asset.last_modified_by = @user
|
||||
@asset.save
|
||||
|
|
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
|
|
@ -141,10 +141,9 @@ module ApplicationHelper
|
|||
# Check if text have smart annotations of users
|
||||
# and outputs a popover with user information
|
||||
def smart_annotation_filter_users(text, team, base64_encoded_imgs: false)
|
||||
sa_user = /\[\@(.*?)~([0-9a-zA-Z]+)\]/
|
||||
text.gsub(sa_user) do |el|
|
||||
match = el.match(sa_user)
|
||||
user = User.find_by_id(match[2].base62_decode)
|
||||
text.gsub(SmartAnnotations::TagToHtml::USER_REGEX) do |el|
|
||||
match = el.match(SmartAnnotations::TagToHtml::USER_REGEX)
|
||||
user = User.find_by(id: match[2].base62_decode)
|
||||
next unless user
|
||||
|
||||
popover_for_user_name(user, team, false, false, base64_encoded_imgs)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module GlobalActivitiesHelper
|
||||
include ActionView::Helpers::AssetTagHelper
|
||||
include ActionView::Helpers::UrlHelper
|
||||
include Canaid::Helpers::PermissionsHelper
|
||||
include InputSanitizeHelper
|
||||
|
||||
def generate_activity_content(activity, no_links: false, no_custom_links: false)
|
||||
|
@ -60,6 +61,9 @@ module GlobalActivitiesHelper
|
|||
when Repository
|
||||
path = repository_path(obj, team: obj.team.id)
|
||||
when RepositoryRow
|
||||
# Handle private repository rows
|
||||
return I18n.t('storage_locations.show.hidden') unless can_read_repository?(obj.repository)
|
||||
|
||||
return current_value unless obj.repository
|
||||
|
||||
path = repository_path(obj.repository, team: obj.repository.team.id)
|
||||
|
@ -108,6 +112,12 @@ module GlobalActivitiesHelper
|
|||
else
|
||||
project_folder_path(obj, team: obj.team.id)
|
||||
end
|
||||
when StorageLocation
|
||||
path = if obj.new_record?
|
||||
storage_locations_path(team: activity.team.id)
|
||||
else
|
||||
storage_location_path(obj, team: activity.team.id)
|
||||
end
|
||||
else
|
||||
return current_value
|
||||
end
|
||||
|
@ -121,8 +131,12 @@ module GlobalActivitiesHelper
|
|||
message_item['type'].constantize.new
|
||||
end
|
||||
|
||||
return I18n.t('storage_locations.show.hidden') if obj.is_a?(RepositoryRow) && !can_read_repository?(obj.repository)
|
||||
|
||||
return I18n.t('projects.index.breadcrumbs_root') if obj.is_a?(ProjectFolder) && obj.new_record?
|
||||
|
||||
return I18n.t('storage_locations.index.breadcrumbs_root') if obj.is_a?(StorageLocation) && obj.new_record?
|
||||
|
||||
return message_item['value'] unless obj
|
||||
|
||||
value = obj.public_send(message_item['value_for'] || 'name')
|
||||
|
|
|
@ -46,9 +46,8 @@ module InputSanitizeHelper
|
|||
sanitizer_config = Constants::INPUT_SANITIZE_CONFIG.deep_dup
|
||||
text = sanitize_input(text, tags, sanitizer_config: sanitizer_config)
|
||||
|
||||
if text =~ SmartAnnotations::TagToHtml::USER_REGEX || text =~ SmartAnnotations::TagToHtml::REGEX
|
||||
text = smart_annotation_parser(text, team, base64_encoded_imgs, preview_repository)
|
||||
end
|
||||
text = smart_annotation_parser(text, team, base64_encoded_imgs, preview_repository) if text.match?(SmartAnnotations::TagToHtml::ALL_REGEX)
|
||||
|
||||
auto_link(
|
||||
text,
|
||||
html: { target: '_blank' },
|
||||
|
|
|
@ -19,8 +19,16 @@ module LeftMenuBarHelper
|
|||
url: repositories_path,
|
||||
name: t('left_menu_bar.repositories'),
|
||||
icon: 'sn-icon-inventory',
|
||||
active: repositories_are_selected?,
|
||||
submenu: []
|
||||
active: repositories_are_selected? || storage_locations_are_selected?,
|
||||
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: "#",
|
||||
name: t('left_menu_bar.templates'),
|
||||
|
@ -63,6 +71,10 @@ module LeftMenuBarHelper
|
|||
controller_name == 'repositories'
|
||||
end
|
||||
|
||||
def storage_locations_are_selected?
|
||||
controller_name == 'storage_locations'
|
||||
end
|
||||
|
||||
def protocols_are_selected?
|
||||
controller_name == 'protocols'
|
||||
end
|
||||
|
|
|
@ -106,14 +106,4 @@ module ReportsHelper
|
|||
experiment_element.experiment.description
|
||||
end
|
||||
end
|
||||
|
||||
def assigned_to_report_repository_items(report, repository_name)
|
||||
repository = Repository.accessible_by_teams(report.team).where(name: repository_name).take
|
||||
return RepositoryRow.none if repository.blank?
|
||||
|
||||
my_modules = MyModule.joins(:experiment)
|
||||
.where(experiment: { project: report.project })
|
||||
.where(id: report.report_elements.my_module.select(:my_module_id))
|
||||
repository.repository_rows.joins(:my_modules).where(my_modules: my_modules)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,41 +5,38 @@ module RepositoryDatatableHelper
|
|||
include Rails.application.routes.url_helpers
|
||||
|
||||
def prepare_row_columns(repository_rows, repository, columns_mappings, team, options = {})
|
||||
# repository_rows collection is already preloaded in controllers, do not modify scopes or query params
|
||||
# otherwise it will result in duplicated SQL queries
|
||||
has_stock_management = repository.has_stock_management?
|
||||
stock_management_column_exists = repository.repository_columns.stock_type.exists?
|
||||
repository_row_connections_enabled = Repository.repository_row_connections_enabled?
|
||||
reminders_enabled = Repository.reminders_enabled?
|
||||
repository_rows = reminders_enabled ? with_reminders_status(repository_rows, repository) : repository_rows
|
||||
stock_managable = has_stock_management && !options[:disable_stock_management] &&
|
||||
can_manage_repository_stock?(repository)
|
||||
can_manage_repository_stock?(repository) &&
|
||||
!repository.is_a?(SoftLockedRepository)
|
||||
stock_consumption_permitted = has_stock_management && options[:include_stock_consumption] && options[:my_module] &&
|
||||
stock_consumption_permitted?(repository, options[:my_module])
|
||||
default_columns_method_name = "#{repository.class.name.underscore}_default_columns"
|
||||
|
||||
repository_rows.map do |record|
|
||||
row = public_send("#{repository.class.name.underscore}_default_columns", record)
|
||||
row.merge!(
|
||||
DT_RowId: record.id,
|
||||
DT_RowAttr: { 'data-state': row_style(record), 'data-e2e': "e2e-TR-invInventory-bodyRow-#{record.id}" },
|
||||
recordInfoUrl: Rails.application.routes.url_helpers.repository_repository_row_path(repository, record),
|
||||
rowRemindersUrl:
|
||||
Rails.application.routes.url_helpers
|
||||
.active_reminder_repository_cells_repository_repository_row_url(
|
||||
repository,
|
||||
record
|
||||
),
|
||||
relationshipsUrl:
|
||||
Rails.application.routes.url_helpers
|
||||
.relationships_repository_repository_row_url(record.repository_id, record.id),
|
||||
relationships_enabled: repository_row_connections_enabled,
|
||||
code: record.code
|
||||
)
|
||||
row = public_send(default_columns_method_name, record)
|
||||
row['code'] = record.code
|
||||
row['DT_RowId'] = record.id
|
||||
row['DT_RowAttr'] = { 'data-state': row_style(record), 'data-e2e': "e2e-TR-invInventory-bodyRow-#{record.id}" }
|
||||
row['recordInfoUrl'] = Rails.application.routes.url_helpers.repository_repository_row_path(repository.id, record.id)
|
||||
row['rowRemindersUrl'] = Rails.application.routes.url_helpers
|
||||
.active_reminder_repository_cells_repository_repository_row_url(repository.id, record.id)
|
||||
row['relationshipsUrl'] = Rails.application.routes.url_helpers
|
||||
.relationships_repository_repository_row_url(record.repository_id, record.id)
|
||||
row['relationships_enabled'] = repository_row_connections_enabled
|
||||
row['hasActiveReminders'] = record.has_active_reminders if reminders_enabled
|
||||
|
||||
if reminders_enabled
|
||||
row['hasActiveReminders'] = record.has_active_stock_reminders || record.has_active_datetime_reminders
|
||||
end
|
||||
|
||||
unless options[:view_mode]
|
||||
unless options[:view_mode] || repository.is_a?(SoftLockedRepository)
|
||||
row['recordUpdateUrl'] =
|
||||
Rails.application.routes.url_helpers.repository_repository_row_path(repository, record)
|
||||
row['recordEditable'] = record.editable?
|
||||
|
||||
# if the editable? property will be checked in a separate request, we can default it to true
|
||||
row['recordEditable'] = options[:omit_editable] ? true : record.editable?
|
||||
end
|
||||
|
||||
row['0'] = record[:row_assigned] if options[:my_module]
|
||||
|
@ -48,13 +45,7 @@ module RepositoryDatatableHelper
|
|||
custom_cells = record.repository_cells.filter { |cell| cell.value_type != 'RepositoryStockValue' }
|
||||
|
||||
custom_cells.each do |cell|
|
||||
row[columns_mappings[cell.repository_column.id]] =
|
||||
serialize_repository_cell_value(cell, team, repository, reminders_enabled: reminders_enabled)
|
||||
end
|
||||
|
||||
if repository.repository_columns.stock_type.exists?
|
||||
stock_cell = record.repository_cells.find { |cell| cell.value_type == 'RepositoryStockValue' }
|
||||
row['stock'] = serialize_repository_cell_value(record.repository_stock_cell, team, repository) if stock_cell.present?
|
||||
row[columns_mappings[cell.repository_column_id]] = serialize_repository_cell_value(cell, team, repository, reminders_enabled: reminders_enabled)
|
||||
end
|
||||
|
||||
if has_stock_management
|
||||
|
@ -97,6 +88,9 @@ module RepositoryDatatableHelper
|
|||
}
|
||||
}
|
||||
end
|
||||
elsif stock_management_column_exists
|
||||
stock_cell = record.repository_cells.find { |cell| cell.value_type == 'RepositoryStockValue' }
|
||||
row['stock'] = serialize_repository_cell_value(record.repository_stock_cell, team, repository) if stock_cell.present?
|
||||
end
|
||||
|
||||
row
|
||||
|
@ -104,9 +98,10 @@ module RepositoryDatatableHelper
|
|||
end
|
||||
|
||||
def prepare_simple_view_row_columns(repository_rows, repository, my_module, options = {})
|
||||
# repository_rows collection is already preloaded in controllers, do not modify scopes or query params
|
||||
# otherwise it will result in duplicated SQL queries
|
||||
has_stock_management = repository.has_stock_management?
|
||||
reminders_enabled = !options[:disable_reminders] && Repository.reminders_enabled?
|
||||
repository_rows = reminders_enabled ? with_reminders_status(repository_rows, repository) : repository_rows
|
||||
# Always disabled in a simple view
|
||||
stock_managable = false
|
||||
stock_consumption_permitted = has_stock_management && stock_consumption_permitted?(repository, my_module)
|
||||
|
@ -125,9 +120,7 @@ module RepositoryDatatableHelper
|
|||
)
|
||||
}
|
||||
|
||||
if reminders_enabled
|
||||
row['hasActiveReminders'] = record.has_active_stock_reminders || record.has_active_datetime_reminders
|
||||
end
|
||||
row['hasActiveReminders'] = record.has_active_reminders if reminders_enabled
|
||||
|
||||
if has_stock_management
|
||||
stock_present = record.repository_stock_cell.present?
|
||||
|
@ -192,15 +185,14 @@ module RepositoryDatatableHelper
|
|||
'1': record.code,
|
||||
'2': escape_input(record.name),
|
||||
'3': I18n.l(record.created_at, format: :full),
|
||||
'4': escape_input(record.created_by.full_name),
|
||||
'4': escape_input(record.created_by_full_name),
|
||||
'recordInfoUrl': Rails.application.routes.url_helpers
|
||||
.repository_repository_row_path(repository_snapshot, record)
|
||||
}
|
||||
|
||||
# Add custom columns
|
||||
record.repository_cells.each do |cell|
|
||||
row[columns_mappings[cell.repository_column.id]] =
|
||||
serialize_repository_cell_value(cell, team, repository_snapshot)
|
||||
row[columns_mappings[cell.repository_column_id]] = serialize_repository_cell_value(cell, team, repository_snapshot)
|
||||
end
|
||||
|
||||
if has_stock_management
|
||||
|
@ -238,11 +230,24 @@ module RepositoryDatatableHelper
|
|||
'3': escape_input(record.name),
|
||||
'4': "#{record.parent_connections_count || 0} / #{record.child_connections_count || 0}",
|
||||
'5': I18n.l(record.created_at, format: :full),
|
||||
'6': escape_input(record.created_by.full_name),
|
||||
'6': escape_input(record.created_by_full_name),
|
||||
'7': (record.updated_at ? I18n.l(record.updated_at, format: :full) : ''),
|
||||
'8': escape_input(record.last_modified_by.full_name),
|
||||
'8': escape_input(record.last_modified_by_full_name),
|
||||
'9': (record.archived_on ? I18n.l(record.archived_on, format: :full) : ''),
|
||||
'10': escape_input(record.archived_by&.full_name)
|
||||
'10': escape_input(record.archived_by_full_name)
|
||||
}
|
||||
end
|
||||
|
||||
def soft_locked_repository_default_columns(record)
|
||||
{
|
||||
'1': assigned_row(record),
|
||||
'2': record.code,
|
||||
'3': escape_input(record.name),
|
||||
'4': "#{record.parent_connections_count || 0} / #{record.child_connections_count || 0}",
|
||||
'5': I18n.l(record.created_at, format: :full),
|
||||
'6': escape_input(record.created_by_full_name),
|
||||
'7': (record.archived_on ? I18n.l(record.archived_on, format: :full) : ''),
|
||||
'8': escape_input(record.archived_by_full_name)
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -252,15 +257,35 @@ module RepositoryDatatableHelper
|
|||
'2': record.code,
|
||||
'3': escape_input(record.name),
|
||||
'4': I18n.l(record.created_at, format: :full),
|
||||
'5': escape_input(record.created_by.full_name),
|
||||
'5': escape_input(record.created_by_full_name),
|
||||
'6': (record.archived_on ? I18n.l(record.archived_on, format: :full) : ''),
|
||||
'7': escape_input(record.archived_by&.full_name),
|
||||
'7': escape_input(record.archived_by_full_name),
|
||||
'8': escape_input(record.external_id)
|
||||
}
|
||||
end
|
||||
|
||||
def serialize_repository_cell_value(cell, team, repository, options = {})
|
||||
serializer_class = "RepositoryDatatable::#{cell.repository_column.data_type}Serializer".constantize
|
||||
# case/when is used because it is much faster then .constantize
|
||||
serializer_class =
|
||||
case cell.repository_column.data_type
|
||||
when 'RepositoryTextValue' then RepositoryDatatable::RepositoryTextValueSerializer
|
||||
when 'RepositoryNumberValue' then RepositoryDatatable::RepositoryNumberValueSerializer
|
||||
when 'RepositoryListValue' then RepositoryDatatable::RepositoryListValueSerializer
|
||||
when 'RepositoryChecklistValue' then RepositoryDatatable::RepositoryChecklistValueSerializer
|
||||
when 'RepositoryStatusValue' then RepositoryDatatable::RepositoryStatusValueSerializer
|
||||
when 'RepositoryTimeValue' then RepositoryDatatable::RepositoryTimeValueSerializer
|
||||
when 'RepositoryDateValue' then RepositoryDatatable::RepositoryDateValueSerializer
|
||||
when 'RepositoryDateTimeValue' then RepositoryDatatable::RepositoryDateTimeValueSerializer
|
||||
when 'RepositoryDateRangeValue' then RepositoryDatatable::RepositoryDateRangeValueSerializer
|
||||
when 'RepositoryTimeRangeValue' then RepositoryDatatable::RepositoryTimeRangeValueSerializer
|
||||
when 'RepositoryDateTimeRangeValue' then RepositoryDatatable::RepositoryDateTimeRangeValueSerializer
|
||||
when 'RepositoryAssetValue' then RepositoryDatatable::RepositoryAssetValueSerializer
|
||||
when 'RepositoryStockValue' then RepositoryDatatable::RepositoryStockValueSerializer
|
||||
when 'RepositoryStockConsumptionValue' then RepositoryDatatable::RepositoryStockConsumptionValueSerializer
|
||||
else
|
||||
Extends::REPOSITORY_EXTRA_VALUE_SERIALIZERS[cell.value_type]
|
||||
end
|
||||
|
||||
serializer_class.new(
|
||||
cell.value,
|
||||
scope: {
|
||||
|
@ -279,35 +304,6 @@ module RepositoryDatatableHelper
|
|||
''
|
||||
end
|
||||
|
||||
def with_reminders_status(repository_rows, repository)
|
||||
# don't load reminders for archived repositories or snapshots
|
||||
if repository.archived? || repository.is_a?(RepositorySnapshot)
|
||||
return repository_rows.select('FALSE AS has_active_stock_reminders')
|
||||
.select('FALSE AS has_active_datetime_reminders')
|
||||
end
|
||||
|
||||
repository_cells = RepositoryCell.joins(
|
||||
"INNER JOIN repository_columns ON repository_columns.id = repository_cells.repository_column_id " \
|
||||
"AND repository_columns.repository_id = #{repository.id}"
|
||||
)
|
||||
|
||||
repository_rows
|
||||
.joins(
|
||||
"LEFT OUTER JOIN (#{RepositoryCell.stock_reminder_repository_cells_scope(repository_cells, current_user)
|
||||
.select(:id, :repository_row_id).to_sql}) " \
|
||||
"AS repository_cells_with_active_stock_reminders " \
|
||||
"ON repository_cells_with_active_stock_reminders.repository_row_id = repository_rows.id"
|
||||
)
|
||||
.joins(
|
||||
"LEFT OUTER JOIN (#{RepositoryCell.date_time_reminder_repository_cells_scope(repository_cells, current_user)
|
||||
.select(:id, :repository_row_id).to_sql}) " \
|
||||
"AS repository_cells_with_active_datetime_reminders " \
|
||||
"ON repository_cells_with_active_datetime_reminders.repository_row_id = repository_rows.id"
|
||||
)
|
||||
.select('COUNT(repository_cells_with_active_stock_reminders.id) > 0 AS has_active_stock_reminders')
|
||||
.select('COUNT(repository_cells_with_active_datetime_reminders.id) > 0 AS has_active_datetime_reminders')
|
||||
end
|
||||
|
||||
def stock_consumption_permitted?(repository, my_module)
|
||||
return false unless repository.is_a?(Repository) && current_user
|
||||
|
||||
|
@ -324,8 +320,4 @@ module RepositoryDatatableHelper
|
|||
def display_stock_warnings?(repository)
|
||||
!repository.is_a?(RepositorySnapshot)
|
||||
end
|
||||
|
||||
def repository_row_connections_enabled
|
||||
Repository.repository_row_connections_enabled?
|
||||
end
|
||||
end
|
||||
|
|
9
app/helpers/storage_locations_helper.rb
Normal file
9
app/helpers/storage_locations_helper.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
module StorageLocationsHelper
|
||||
def storage_locations_placeholder
|
||||
return if StorageLocation.storage_locations_enabled?
|
||||
|
||||
"<div class=\"p-4 rounded bg-sn-super-light-blue\">
|
||||
#{I18n.t('storage_locations.storage_locations_disabled')}
|
||||
</div>"
|
||||
end
|
||||
end
|
3
app/javascript/packs/tiny_mce.js
vendored
3
app/javascript/packs/tiny_mce.js
vendored
|
@ -326,9 +326,6 @@ window.TinyMCE = (() => {
|
|||
// Remove transition class
|
||||
$('.tox-editor-header').removeClass('tox-editor-dock-fadeout');
|
||||
|
||||
// Fixes the overflowing vertical controls bar for inserted text
|
||||
$('.tox-editor-header').css('display', 'contents');
|
||||
|
||||
// Init image toolbar
|
||||
initCssOverrides(editor);
|
||||
|
||||
|
|
22
app/javascript/packs/vue/design_system/breadcrumbs.js
Normal file
22
app/javascript/packs/vue/design_system/breadcrumbs.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||
import Breadcrumbs from '../../../vue/shared/breadcrumbs.vue';
|
||||
import { mountWithTurbolinks } from '../helpers/turbolinks.js';
|
||||
|
||||
const app = createApp({
|
||||
computed: {
|
||||
breadcrumbs() {
|
||||
return [
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: 'Very very very long name ', url: '' },
|
||||
{ name: 'Data', url: '' },
|
||||
{ name: 'Very very very very very very very very very very long name ', url: '' },
|
||||
{ name: 'Very very very very very very very long name ', url: '' },
|
||||
{ name: 'Very very very very very long name ', url: '' },
|
||||
{ name: 'Very very very very long name ', url: '' }
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
app.component('Breadcrumbs', Breadcrumbs);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
mountWithTurbolinks(app, '#breadcrumbs');
|
|
@ -10,6 +10,7 @@ const app = createApp({
|
|||
myModuleParams: null,
|
||||
myModuleUrl: null,
|
||||
tagsModalOpen: false,
|
||||
tagDeleted: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
@ -35,6 +36,10 @@ const app = createApp({
|
|||
this.myModuleParams = null;
|
||||
this.myModuleUrl = null;
|
||||
this.tagsModalOpen = false;
|
||||
|
||||
if ($('#canvas-container').length && this.tagDeleted) {
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
syncTags(tags) {
|
||||
// My module page
|
||||
|
|
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);
|
||||
window.StorageLocationsContainer = 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');
|
14
app/javascript/vue/label_template/renderers/default.vue
Normal file
14
app/javascript/vue/label_template/renderers/default.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<i v-if="params.data.default" class="sn-icon sn-icon-approval"></i>
|
||||
<span v-else></span>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
17
app/javascript/vue/label_template/renderers/format.vue
Normal file
17
app/javascript/vue/label_template/renderers/format.vue
Normal file
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<span>
|
||||
<span v-html="params.data.icon_url"></span>
|
||||
<span>{{ params.data.format }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
16
app/javascript/vue/label_template/renderers/name.vue
Normal file
16
app/javascript/vue/label_template/renderers/name.vue
Normal file
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<a :href="params.data.urls.show" :title="params.data.name">
|
||||
{{ params.data.name }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -30,28 +30,34 @@ import axios from '../../packs/custom_axios.js';
|
|||
|
||||
import DataTable from '../shared/datatable/table.vue';
|
||||
import DeleteModal from '../shared/confirmation_modal.vue';
|
||||
import NameRenderer from './renderers/name.vue';
|
||||
import DefaultRenderer from './renderers/default.vue';
|
||||
import FormatRenderer from './renderers/format.vue';
|
||||
|
||||
export default {
|
||||
name: 'LabelTemplatesTable',
|
||||
components: {
|
||||
DataTable,
|
||||
DeleteModal,
|
||||
NameRenderer,
|
||||
DefaultRenderer,
|
||||
FormatRenderer
|
||||
},
|
||||
props: {
|
||||
dataSource: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
actionsUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
createUrl: {
|
||||
type: String,
|
||||
type: String
|
||||
},
|
||||
syncFluicsUrl: {
|
||||
type: String,
|
||||
},
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -60,40 +66,40 @@ export default {
|
|||
{
|
||||
field: 'default',
|
||||
headerName: this.i18n.t('label_templates.index.default_label'),
|
||||
cellRenderer: this.defaultRenderer,
|
||||
sortable: true,
|
||||
cellRenderer: 'DefaultRenderer',
|
||||
sortable: true
|
||||
}, {
|
||||
field: 'name',
|
||||
headerName: this.i18n.t('label_templates.index.thead_name'),
|
||||
cellRenderer: this.labelNameRenderer,
|
||||
sortable: true,
|
||||
cellRenderer: 'NameRenderer',
|
||||
sortable: true
|
||||
}, {
|
||||
field: 'format',
|
||||
headerName: this.i18n.t('label_templates.index.format'),
|
||||
sortable: true,
|
||||
cellRenderer: ({ data: { format, icon_url: iconUrl } }) => `<span>${iconUrl}</span> <span>${format}</span>`
|
||||
cellRenderer: 'FormatRenderer'
|
||||
}, {
|
||||
field: 'description',
|
||||
headerName: this.i18n.t('label_templates.index.description'),
|
||||
sortable: true,
|
||||
sortable: true
|
||||
}, {
|
||||
field: 'modified_by',
|
||||
headerName: this.i18n.t('label_templates.index.updated_by'),
|
||||
sortable: true,
|
||||
sortable: true
|
||||
}, {
|
||||
field: 'updated_at',
|
||||
headerName: this.i18n.t('label_templates.index.updated_at'),
|
||||
sortable: true,
|
||||
sortable: true
|
||||
}, {
|
||||
field: 'created_by',
|
||||
headerName: this.i18n.t('label_templates.index.created_by'),
|
||||
sortable: true,
|
||||
sortable: true
|
||||
}, {
|
||||
field: 'created_at',
|
||||
headerName: this.i18n.t('label_templates.index.created_at'),
|
||||
sortable: true,
|
||||
},
|
||||
],
|
||||
sortable: true
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -106,7 +112,7 @@ export default {
|
|||
label: this.i18n.t('label_templates.index.toolbar.new'),
|
||||
type: 'emit',
|
||||
path: this.createUrl,
|
||||
buttonStyle: 'btn btn-primary',
|
||||
buttonStyle: 'btn btn-primary'
|
||||
});
|
||||
}
|
||||
if (this.syncFluicsUrl) {
|
||||
|
@ -121,21 +127,11 @@ export default {
|
|||
}
|
||||
return {
|
||||
left,
|
||||
right: [],
|
||||
right: []
|
||||
};
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
labelNameRenderer(params) {
|
||||
const editUrl = params.data.urls.show;
|
||||
return `<a href="${editUrl}" title="${params.data.name}">
|
||||
${params.data.name}
|
||||
</a>`;
|
||||
},
|
||||
defaultRenderer(params) {
|
||||
const defaultSelected = params.data.default;
|
||||
return defaultSelected ? '<i class="sn-icon sn-icon-approval"></i>' : '';
|
||||
},
|
||||
setDefault(action) {
|
||||
axios.post(action.path).then((response) => {
|
||||
this.reloadingTable = true;
|
||||
|
@ -175,8 +171,8 @@ export default {
|
|||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
:hiddenDataMessage="i18n.t('experiments.empty_state.no_active_modules_archived_branch')"
|
||||
scrollMode="infinite"
|
||||
@tableReloaded="reloadingTable = false"
|
||||
@reloadTable="reloadingTable = true"
|
||||
@create="newModalOpen = true"
|
||||
@edit="edit"
|
||||
@move="move"
|
||||
|
@ -56,6 +57,9 @@
|
|||
import axios from '../../packs/custom_axios.js';
|
||||
import DataTable from '../shared/datatable/table.vue';
|
||||
import ConfirmationModal from '../shared/confirmation_modal.vue';
|
||||
import NameRenderer from './renderers/name.vue';
|
||||
import ResultsRenderer from './renderers/results.vue';
|
||||
import StatusRenderer from './renderers/status.vue';
|
||||
import DueDateRenderer from './renderers/due_date.vue';
|
||||
import DesignatedUsers from './renderers/designated_users.vue';
|
||||
import TagsModal from './modals/tags.vue';
|
||||
|
@ -77,7 +81,10 @@ export default {
|
|||
NewModal,
|
||||
EditModal,
|
||||
MoveModal,
|
||||
AccessModal
|
||||
AccessModal,
|
||||
NameRenderer,
|
||||
ResultsRenderer,
|
||||
StatusRenderer
|
||||
},
|
||||
props: {
|
||||
dataSource: { type: String, required: true },
|
||||
|
@ -115,7 +122,7 @@ export default {
|
|||
field: 'name',
|
||||
headerName: this.i18n.t('experiments.table.column.task_name_html'),
|
||||
sortable: true,
|
||||
cellRenderer: this.nameRenderer
|
||||
cellRenderer: NameRenderer
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
|
@ -133,7 +140,7 @@ export default {
|
|||
field: 'results',
|
||||
headerName: this.i18n.t('experiments.table.column.results_html'),
|
||||
sortable: true,
|
||||
cellRenderer: this.resultsRenderer
|
||||
cellRenderer: ResultsRenderer
|
||||
},
|
||||
{
|
||||
field: 'age',
|
||||
|
@ -144,7 +151,7 @@ export default {
|
|||
field: 'status',
|
||||
headerName: this.i18n.t('experiments.table.column.status_html'),
|
||||
sortable: true,
|
||||
cellRenderer: this.statusRenderer,
|
||||
cellRenderer: StatusRenderer,
|
||||
minWidth: 120
|
||||
}
|
||||
];
|
||||
|
@ -321,53 +328,6 @@ export default {
|
|||
roles_path: this.userRolesUrl
|
||||
};
|
||||
},
|
||||
checkProvisioning(params) {
|
||||
if (params.data.provisioning_status === 'done') return;
|
||||
|
||||
axios.get(params.data.urls.provisioning_status).then((response) => {
|
||||
const provisioningStatus = response.data.provisioning_status;
|
||||
if (provisioningStatus === 'done') {
|
||||
this.reloadingTable = true;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.checkProvisioning(params);
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
},
|
||||
// Renderers
|
||||
nameRenderer(params) {
|
||||
const { name, urls } = params.data;
|
||||
const provisioningStatus = params.data.provisioning_status;
|
||||
if (provisioningStatus === 'in_progress') {
|
||||
setTimeout(() => {
|
||||
this.checkProvisioning(params);
|
||||
}, 5000);
|
||||
return `
|
||||
<span class="flex gap-2 items-center">
|
||||
<div title="${this.i18n.t('experiments.duplicate_tasks.duplicating')}"
|
||||
class="loading-overlay w-6 h-6 !relative shrink-0" data-toggle="tooltip" data-placement="right"></div>
|
||||
<span class="truncate">${name}</span>
|
||||
</span>`;
|
||||
}
|
||||
|
||||
return `<a href="${urls.show}" title="${name}" ><span class="truncate">${name}</span></a>`;
|
||||
},
|
||||
statusRenderer(params) {
|
||||
const { status } = params.data;
|
||||
|
||||
return `<span
|
||||
class="px-2 py-1 border border-solid rounded truncate ${!status.light_color ? 'text-sn-white' : ''}"
|
||||
style="background-color: ${status.color};"
|
||||
>
|
||||
${status.name}
|
||||
</span>`;
|
||||
},
|
||||
resultsRenderer(params) {
|
||||
const { results, urls } = params.data;
|
||||
|
||||
return `<a href="${urls.results}" >${results}</a>`;
|
||||
},
|
||||
usersFilterRenderer(option) {
|
||||
return `<div class="flex items-center gap-2">
|
||||
<img src="${option[2].avatar_url}" class="rounded-full w-6 h-6" />
|
||||
|
|
|
@ -147,7 +147,7 @@ import ConfirmationModal from '../../shared/confirmation_modal.vue';
|
|||
|
||||
export default {
|
||||
name: 'TagsModal',
|
||||
emits: ['close', 'tagsLoaded'],
|
||||
emits: ['close', 'tagsLoaded', 'tagDeleted'],
|
||||
props: {
|
||||
params: {
|
||||
required: true
|
||||
|
@ -288,6 +288,8 @@ export default {
|
|||
color: tag.attributes.color
|
||||
},
|
||||
my_module_id: this.params.id
|
||||
}).then(() => {
|
||||
this.$emit('tagsLoaded', this.allTags);
|
||||
});
|
||||
},
|
||||
createTag() {
|
||||
|
@ -309,6 +311,7 @@ export default {
|
|||
}
|
||||
}).then(() => {
|
||||
this.loadAlltags();
|
||||
this.$emit('tagDeleted', tag);
|
||||
document.body.style.overflow = 'hidden';
|
||||
});
|
||||
} else {
|
||||
|
|
50
app/javascript/vue/my_modules/renderers/name.vue
Normal file
50
app/javascript/vue/my_modules/renderers/name.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<template v-if="params.data.provisioning_status === 'in_progress'">
|
||||
<span class="flex gap-2 items-center">
|
||||
<div :title="this.i18n.t('experiments.duplicate_tasks.duplicating')"
|
||||
class="loading-overlay w-6 h-6 !relative shrink-0" data-toggle="tooltip" data-placement="right"></div>
|
||||
<span class="truncate">{{ params.data.name }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a :href="params.data.urls.show" :title="params.data.name" >
|
||||
<i v-if="params.data.locked" class="sn-icon sn-icon-locked-task"></i>
|
||||
<span class="truncate">{{ params.data.name }}</span>
|
||||
</a>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
|
||||
export default {
|
||||
name: 'NameRenderer',
|
||||
props: {
|
||||
params: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.params.data.provisioning_status === 'in_progress') {
|
||||
setTimeout(() => {
|
||||
this.checkProvisioning();
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkProvisioning() {
|
||||
if (this.params.data.provisioning_status === 'done') return;
|
||||
axios.get(this.params.data.urls.provisioning_status).then((response) => {
|
||||
const provisioningStatus = response.data.provisioning_status;
|
||||
if (provisioningStatus === 'done') {
|
||||
this.params.dtComponent.$emit('reloadTable', null, [this.params.data]);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.checkProvisioning();
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
14
app/javascript/vue/my_modules/renderers/results.vue
Normal file
14
app/javascript/vue/my_modules/renderers/results.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<a :href="params.data.urls.results" >{{ params.data.results }}</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ResultsRenderer',
|
||||
props: {
|
||||
params: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
20
app/javascript/vue/my_modules/renderers/status.vue
Normal file
20
app/javascript/vue/my_modules/renderers/status.vue
Normal file
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<span
|
||||
class="px-2 py-1 border border-solid rounded truncate"
|
||||
:class="{'text-sn-white' : !params.data.status.light_color}"
|
||||
:style="{'background-color': params.data.status.color}"
|
||||
>
|
||||
{{ params.data.status.name }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StatusRenderer',
|
||||
props: {
|
||||
params: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,41 +1,58 @@
|
|||
<template>
|
||||
<div class="sci-navigation--notificaitons-flyout-notification">
|
||||
<div class="sci-navigation--notificaitons-flyout-notification-date">
|
||||
{{ notification.attributes.created_at }}
|
||||
<div class="flex item-center">
|
||||
<div class="sci-navigation--notificaitons-flyout-notification-date">
|
||||
{{ notification.attributes.created_at }}
|
||||
</div>
|
||||
<div class="ml-auto cursor-pointer" @click="toggleRead()">
|
||||
<div v-if="!notification.attributes.checked" class="w-2.5 h-2.5 bg-sn-coral rounded-full cursor-pointer"></div>
|
||||
<div v-else class="w-2.5 h-2.5 border-2 border-sn-grey rounded-full border-solid cursor-pointer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sci-navigation--notificaitons-flyout-notification-title"
|
||||
v-html="notification.attributes.title"
|
||||
:data-seen="notification.attributes.checked"></div>
|
||||
<div v-html="notification.attributes.message" class="sci-navigation--notificaitons-flyout-notification-message"></div>
|
||||
<a :href="lastBreadcrumbUrl" @click="toggleRead(true)" class="hover:no-underline text-black hover:text-black">
|
||||
<div class="sci-navigation--notificaitons-flyout-notification-title"
|
||||
v-html="notification.attributes.title"
|
||||
:data-seen="notification.attributes.checked"></div>
|
||||
<div v-html="notification.attributes.message" class="sci-navigation--notificaitons-flyout-notification-message"></div>
|
||||
</a>
|
||||
<div v-if="notification.attributes.breadcrumbs" class="flex items-center flex-wrap gap-0.5">
|
||||
<template v-for="(breadcrumb, index) in notification.attributes.breadcrumbs" :key="index">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<i v-if="index > 0" class="sn-icon sn-icon-right"></i>
|
||||
<a :href="breadcrumb.url" :title="breadcrumb.name" class="truncate max-w-[20ch] inline-block">{{ breadcrumb.name }}</a>
|
||||
</div>
|
||||
</template>
|
||||
<Breadcrumbs :breadcrumbs="notification.attributes.breadcrumbs" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import Breadcrumbs from '../../shared/breadcrumbs.vue';
|
||||
|
||||
export default {
|
||||
name: 'NotificationItem',
|
||||
props: {
|
||||
notification: Object
|
||||
},
|
||||
components: {
|
||||
Breadcrumbs
|
||||
},
|
||||
computed: {
|
||||
icon() {
|
||||
switch (this.notification.attributes.type_of) {
|
||||
case 'deliver':
|
||||
return 'fas fa-truck';
|
||||
case 'assignment':
|
||||
return 'fas fa-list-alt';
|
||||
case 'recent_changes':
|
||||
return 'fas fa-list-alt';
|
||||
case 'deliver_error':
|
||||
return 'sn-icon sn-icon-alert-warning';
|
||||
lastBreadcrumbUrl() {
|
||||
if (!this.notification.attributes.breadcrumbs) {
|
||||
return null;
|
||||
}
|
||||
return this.notification.attributes.breadcrumbs[this.notification.attributes.breadcrumbs.length - 1]?.url;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleRead(check = false) {
|
||||
const params = {};
|
||||
if (!this.notification.attributes.checked || check) {
|
||||
params.mark_as_read = true;
|
||||
}
|
||||
|
||||
axios.post(this.notification.attributes.toggle_read_url, params)
|
||||
.then((response) => {
|
||||
const notification = response.data.data;
|
||||
this.$emit('updateNotification', notification);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,17 +6,45 @@
|
|||
{{ i18n.t('nav.settings') }}
|
||||
</a>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
@click="changeTab('all')"
|
||||
class="px-4 py-2 text-sn-grey cursor-pointer border-solid border-0 border-b-4 border-transparent"
|
||||
:class="{'!text-sn-black border-sn-blue': activeTab === 'all'}"
|
||||
>
|
||||
{{ i18n.t('nav.notifications.all') }}
|
||||
</div>
|
||||
<div
|
||||
@click="changeTab('unread')"
|
||||
class="px-4 py-2 text-sn-grey cursor-pointer border-solid border-0 border-b-4 border-transparent"
|
||||
:class="{'!text-sn-black border-sn-blue': activeTab === 'unread'}"
|
||||
>
|
||||
{{ i18n.t('nav.notifications.unread') }}
|
||||
</div>
|
||||
<div
|
||||
@click="changeTab('read')"
|
||||
class="px-4 py-2 text-sn-grey cursor-pointer border-solid border-0 border-b-4 border-transparent"
|
||||
:class="{'!text-sn-black border-sn-blue': activeTab === 'read'}"
|
||||
>
|
||||
{{ i18n.t('nav.notifications.read') }}
|
||||
</div>
|
||||
<div class="py-4 ml-auto cursor-pointer" @click="markAllNotificationsAsRead">
|
||||
{{ i18n.t('nav.notifications.read_all') }}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="!mt-0.5">
|
||||
<perfect-scrollbar @wheel="preventPropagation" ref="scrollContainer" class="sci--navigation--notificaitons-flyout-notifications">
|
||||
<div class="sci-navigation--notificaitons-flyout-subtitle" v-if="todayNotifications.length">
|
||||
{{ i18n.t('nav.notifications.today') }}
|
||||
</div>
|
||||
<NotificationItem v-for="notification in todayNotifications" :key="notification.type_of + '-' + notification.id"
|
||||
@updateNotification="updateNotification"
|
||||
:notification="notification" />
|
||||
<div class="sci-navigation--notificaitons-flyout-subtitle" v-if="olderNotifications.length">
|
||||
{{ i18n.t('nav.notifications.older') }}
|
||||
</div>
|
||||
<NotificationItem v-for="notification in olderNotifications" :key="notification.type_of + '-' + notification.id"
|
||||
@updateNotification="updateNotification"
|
||||
:notification="notification" />
|
||||
<div class="next-page-loader">
|
||||
<img src="/images/medium/loading.svg" v-if="loadingPage" />
|
||||
|
@ -37,6 +65,7 @@ export default {
|
|||
},
|
||||
props: {
|
||||
notificationsUrl: String,
|
||||
markAllNotificationsUrl: String,
|
||||
unseenNotificationsCount: Number,
|
||||
preferencesUrl: String
|
||||
},
|
||||
|
@ -45,11 +74,14 @@ export default {
|
|||
notifications: [],
|
||||
nextPageUrl: null,
|
||||
scrollBar: null,
|
||||
loadingPage: false
|
||||
activeTab: 'all',
|
||||
loadingPage: false,
|
||||
firstPageUrl: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.nextPageUrl = this.notificationsUrl;
|
||||
this.firstPageUrl = this.notificationsUrl;
|
||||
this.loadNotifications();
|
||||
},
|
||||
mounted() {
|
||||
|
@ -76,6 +108,27 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
changeTab(tab) {
|
||||
this.activeTab = tab;
|
||||
this.notifications = [];
|
||||
this.nextPageUrl = this.firstPageUrl;
|
||||
this.loadNotifications();
|
||||
},
|
||||
markAllNotificationsAsRead() {
|
||||
axios.post(this.markAllNotificationsUrl)
|
||||
.then(() => {
|
||||
this.notifications = this.notifications.map((n) => {
|
||||
n.attributes.checked = true;
|
||||
return n;
|
||||
});
|
||||
this.$emit('update:unseenNotificationsCount');
|
||||
});
|
||||
},
|
||||
updateNotification(notification) {
|
||||
const index = this.notifications.findIndex((n) => n.id === notification.id);
|
||||
this.notifications.splice(index, 1, notification);
|
||||
this.$emit('update:unseenNotificationsCount');
|
||||
},
|
||||
preventPropagation(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
@ -85,7 +138,7 @@ export default {
|
|||
|
||||
this.loadingPage = true;
|
||||
|
||||
axios.get(this.nextPageUrl)
|
||||
axios.get(this.nextPageUrl, { params: { tab: this.activeTab } })
|
||||
.then((response) => {
|
||||
this.notifications = this.notifications.concat(response.data.data);
|
||||
this.nextPageUrl = response.data.links.next;
|
||||
|
|
|
@ -45,8 +45,9 @@
|
|||
<NotificationsFlyout
|
||||
:preferencesUrl="user.preferences_url"
|
||||
:notificationsUrl="notificationsUrl"
|
||||
:markAllNotificationsUrl="markAllNotificationsUrl"
|
||||
:unseenNotificationsCount="unseenNotificationsCount"
|
||||
@update:unseenNotificationsCount="checkUnseenNotifications()"
|
||||
@update:unseenNotificationsCount="checkUnseenNotifications(false)"
|
||||
@close="$refs.notificationDropdown.$refs.field.click();"/>
|
||||
</template>
|
||||
</GeneralDropdown>
|
||||
|
@ -92,6 +93,7 @@ export default {
|
|||
props: {
|
||||
url: String,
|
||||
notificationsUrl: String,
|
||||
markAllNotificationsUrl: String,
|
||||
unseenNotificationsUrl: String,
|
||||
quickSearchUrl: String,
|
||||
teamsUrl: String,
|
||||
|
@ -119,7 +121,7 @@ export default {
|
|||
|
||||
$(document).on('turbolinks:load', () => {
|
||||
this.notificationsOpened = false;
|
||||
this.checkUnseenNotifications();
|
||||
this.checkUnseenNotifications(false);
|
||||
this.refreshCurrentTeam();
|
||||
this.hideSearch = !!document.getElementById('GlobalSearch');
|
||||
this.globalSearchKey += 1;
|
||||
|
@ -177,11 +179,13 @@ export default {
|
|||
searchValue(e) {
|
||||
window.open(`${this.searchUrl}?q=${e.target.value}`, '_self');
|
||||
},
|
||||
checkUnseenNotifications() {
|
||||
checkUnseenNotifications(repeat = true) {
|
||||
clearTimeout(this.unseenNotificationsTimeout);
|
||||
$.get(this.unseenNotificationsUrl, (result) => {
|
||||
this.unseenNotificationsCount = result.unseen;
|
||||
this.unseenNotificationsTimeout = setTimeout(this.checkUnseenNotifications, 30000);
|
||||
if (repeat) {
|
||||
this.unseenNotificationsTimeout = setTimeout(this.checkUnseenNotifications, 30000);
|
||||
}
|
||||
});
|
||||
},
|
||||
refreshCurrentTeam() {
|
||||
|
|
|
@ -68,6 +68,7 @@ import axios from '../../packs/custom_axios.js';
|
|||
|
||||
import DataTable from '../shared/datatable/table.vue';
|
||||
import UsersRenderer from './renderers/users.vue';
|
||||
import NameRenderer from './renderers/name.vue';
|
||||
import CommentsRenderer from '../shared/datatable/renderers/comments.vue';
|
||||
import ProjectCard from './card.vue';
|
||||
import ConfirmationModal from '../shared/confirmation_modal.vue';
|
||||
|
@ -84,6 +85,7 @@ export default {
|
|||
components: {
|
||||
DataTable,
|
||||
UsersRenderer,
|
||||
NameRenderer,
|
||||
ProjectCard,
|
||||
ConfirmationModal,
|
||||
EditProjectModal,
|
||||
|
@ -129,7 +131,7 @@ export default {
|
|||
flex: 1,
|
||||
headerName: this.i18n.t('projects.index.card.name'),
|
||||
sortable: true,
|
||||
cellRenderer: this.nameRenderer
|
||||
cellRenderer: 'NameRenderer'
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
|
@ -252,16 +254,6 @@ export default {
|
|||
<span title="${option[1]}" class="truncate">${option[1]}</span>
|
||||
</div>`;
|
||||
},
|
||||
nameRenderer(params) {
|
||||
const showUrl = params.data.urls.show;
|
||||
return `<a href="${showUrl}"
|
||||
class="flex items-center gap-1 hover:no-underline
|
||||
${!showUrl ? 'pointer-events-none text-sn-grey' : ''}"
|
||||
title="${params.data.name}">
|
||||
${params.data.folder ? '<i class="sn-icon mini sn-icon-mini-folder-left"></i>' : ''}
|
||||
<span class="truncate">${params.data.name} </span>
|
||||
</a>`;
|
||||
},
|
||||
openComments(_params, rows) {
|
||||
$(this.$refs.commentButton).data('objectId', rows[0].id);
|
||||
this.$refs.commentButton.click();
|
||||
|
|
|
@ -98,20 +98,19 @@ export default {
|
|||
if (this.query === '') {
|
||||
return this.foldersTree;
|
||||
}
|
||||
return this.foldersTree.map((folder) => (
|
||||
{
|
||||
folder: folder.folder,
|
||||
children: folder.children.filter((child) => (
|
||||
child.folder.name.toLowerCase().includes(this.query.toLowerCase())
|
||||
)),
|
||||
}
|
||||
)).filter((folder) => (
|
||||
folder.folder.name.toLowerCase().includes(this.query.toLowerCase())
|
||||
|| folder.children.length > 0
|
||||
));
|
||||
return this.filteredFoldersTreeHelper(this.foldersTree);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
filteredFoldersTreeHelper(foldersTree) {
|
||||
return foldersTree.map(({ folder, children }) => {
|
||||
if (folder.name.toLowerCase().includes(this.query.toLowerCase())) {
|
||||
return { folder, children };
|
||||
}
|
||||
const filteredChildren = this.filteredFoldersTreeHelper(children);
|
||||
return filteredChildren.length ? { folder, children: filteredChildren } : null;
|
||||
}).filter(Boolean);
|
||||
},
|
||||
selectFolder(folderId) {
|
||||
this.selectedFolderId = folderId;
|
||||
},
|
||||
|
|
21
app/javascript/vue/projects/renderers/name.vue
Normal file
21
app/javascript/vue/projects/renderers/name.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<a :href="params.data.urls.show"
|
||||
class="flex items-center gap-1 hover:no-underline"
|
||||
:class="{
|
||||
'pointer-events-none text-sn-grey': !params.data.urls.show
|
||||
}"
|
||||
:title="params.data.name">
|
||||
<i v-if="params.data.folder" class="sn-icon mini sn-icon-mini-folder-left"></i>
|
||||
<span class="truncate">{{ params.data.name }}</span>
|
||||
</a>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'NameRenderer',
|
||||
props: {
|
||||
params: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -19,8 +19,8 @@ export default {
|
|||
name: 'UsersRenderer',
|
||||
props: {
|
||||
params: {
|
||||
required: true,
|
||||
},
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
users() {
|
||||
|
|
|
@ -177,7 +177,7 @@
|
|||
<div v-for="(step, index) in steps" :key="step.id" class="step-block">
|
||||
<div v-if="index > 0 && urls.add_step_url" class="insert-step" @click="addStep(index)" data-e2e="e2e-BT-protocol-templateSteps-insertStep">
|
||||
<i class="sn-icon sn-icon-new-task"></i>
|
||||
<span>{{ i18n.t("protocols.steps.add_step") }}</span>
|
||||
<span class="mr-3">{{ i18n.t("protocols.steps.add_step") }}</span>
|
||||
</div>
|
||||
<Step
|
||||
ref="steps"
|
||||
|
@ -202,7 +202,7 @@
|
|||
/>
|
||||
<div v-if="(index === steps.length - 1) && urls.add_step_url" class="insert-step" @click="addStep(index + 1)" data-e2e="e2e-BT-protocol-templateSteps-insertStep">
|
||||
<i class="sn-icon sn-icon-new-task"></i>
|
||||
<span>{{ i18n.t("protocols.steps.add_step") }}</span>
|
||||
<span class="mr-3">{{ i18n.t("protocols.steps.add_step") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="steps.length > 0 && urls.add_step_url && inRepository" class="py-5">
|
||||
|
@ -225,6 +225,7 @@
|
|||
:title="i18n.t('protocols.reorder_steps.modal.title')"
|
||||
:items="steps"
|
||||
:includeNumbers="true"
|
||||
dataE2e="protocol-templateSteps-reorder"
|
||||
@reorder="updateStepOrder"
|
||||
@close="closeStepReorderModal"
|
||||
/>
|
||||
|
|
|
@ -1,30 +1,31 @@
|
|||
<template>
|
||||
<div ref="modal" @keydown.esc="cancel" class="modal publish-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-content" data-e2e="e2e-MD-publishProtocol">
|
||||
<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">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-publishProtocol-close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title" data-e2e="e2e-TX-publishProtocol-title">
|
||||
{{ i18n.t('protocols.publish_modal.title', { nr: protocol.attributes.version })}}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="sci-input-container">
|
||||
<label>{{ i18n.t('protocols.publish_modal.name')}}</label>
|
||||
<div>{{ protocol.attributes.name }}</div>
|
||||
<label data-e2e="e2e-TX-publishProtocol-templateNameLabel">{{ i18n.t('protocols.publish_modal.name')}}</label>
|
||||
<div data-e2e="e2e-TX-publishProtocol-templateName">{{ protocol.attributes.name }}</div>
|
||||
</div>
|
||||
<div class="sci-input-container">
|
||||
<label>{{ i18n.t('protocols.publish_modal.comment')}}</label>
|
||||
<label data-e2e="e2e-TX-publishProtocol-revisionNotesLabel">{{ i18n.t('protocols.publish_modal.comment')}}</label>
|
||||
<textarea ref="textarea"
|
||||
v-model="protocol.attributes.version_comment"
|
||||
class="sci-input-field"
|
||||
:placeholder="i18n.t('protocols.publish_modal.comment_placeholder')">
|
||||
:placeholder="i18n.t('protocols.publish_modal.comment_placeholder')"
|
||||
data-e2e="e2e-IF-publishProtocol-revisionNotes">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @click="cancel">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="confirm">{{ i18n.t('protocols.publish_modal.publish')}}</button>
|
||||
<button class="btn btn-secondary" @click="cancel" data-e2e="e2e-BT-publishProtocol-cancel">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="confirm" data-e2e="e2e-BT-publishProtocol-publish">{{ i18n.t('protocols.publish_modal.publish')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -146,7 +146,7 @@
|
|||
@attachment:viewMode="updateAttachmentViewMode"/>
|
||||
</div>
|
||||
<ContentToolbar
|
||||
v-if="orderedElements.length > 3"
|
||||
v-if="orderedElements.length > 2 && insertMenu.length > 0"
|
||||
:insertMenu="insertMenu"
|
||||
@create:table="(...args) => this.createElement('table', ...args)"
|
||||
@create:checklist="createElement('checklist')"
|
||||
|
@ -162,7 +162,7 @@
|
|||
<ReorderableItemsModal v-if="reordering"
|
||||
:title="i18n.t('protocols.steps.modals.reorder_elements.title', { step_position: step.attributes.position + 1 })"
|
||||
:items="reorderableElements"
|
||||
:dataE2e="`e2e-BT-protocol-step${step.id}-reorder`"
|
||||
:dataE2e="`protocol-step${step.id}-reorder`"
|
||||
@reorder="updateElementOrder"
|
||||
@close="closeReorderModal"
|
||||
/>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content ">
|
||||
<div class="modal-content " data-e2e="e2e-MD-linkedTasks">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close self-start" data-dismiss="modal" aria-label="Close">
|
||||
<button type="button" class="close self-start" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-linkedTasksModal-close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title line-clamp-3" style="display: -webkit-box;">
|
||||
<h4 class="modal-title line-clamp-3" style="display: -webkit-box;" data-e2e="e2e-TX-linkedTasksModal-title">
|
||||
{{ i18n.t('protocols.index.linked_children.title', { protocol: protocol.name }) }}
|
||||
</h4>
|
||||
</div>
|
||||
|
@ -18,6 +18,7 @@
|
|||
:options="versionsList"
|
||||
:value="selectedVersion"
|
||||
@change="changeSelectedVersion"
|
||||
e2eValue="e2e-DD-linkedTasksModal-version"
|
||||
></SelectDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,7 +26,11 @@
|
|||
<perfect-scrollbar
|
||||
class="max-h-96 relative flex flex-col gap-6 pr-8"
|
||||
@ps-scroll-y="onScroll" ref="linkedMyModules">
|
||||
<div v-for="(myModule, idx) in linkedMyModules" class="flex items-center gap-1 flex-wrap w-full">
|
||||
<div
|
||||
v-for="(myModule, idx) in linkedMyModules"
|
||||
class="flex items-center gap-1 flex-wrap w-full"
|
||||
:data-e2e="`e2e-BC-linkedTasksModal-${myModule.my_module_name.replaceAll(/\W/g, '')}`"
|
||||
>
|
||||
<div v-if="myModule.project_folder_name" class="flex items-center ">
|
||||
<a :href="myModule.project_folder_url"
|
||||
:title="myModule.project_folder_name"
|
||||
|
@ -67,7 +72,7 @@
|
|||
</perfect-scrollbar>
|
||||
</div>
|
||||
<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" data-e2e="e2e-BT-linkedTasksModal-cancel">{{ i18n.t('general.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -31,7 +31,12 @@
|
|||
</div>
|
||||
<div class="mt-6" :class="{'hidden': !visible}">
|
||||
<label class="sci-label">{{ i18n.t("protocols.new_protocol_modal.role_label") }}</label>
|
||||
<SelectDropdown :options="userRoles" :value="defaultRole" @change="changeRole" />
|
||||
<SelectDropdown
|
||||
:options="userRoles"
|
||||
:value="defaultRole"
|
||||
:data-e2e="`e2e-DD-newProtocolModal-defaultUserRole`"
|
||||
@change="changeRole"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-content" data-e2e="e2e-MD-protocolVersions">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-protocolVersionsModal-close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block">
|
||||
<h4 class="modal-title truncate !block" data-e2e="e2e-TX-protocolVersionsModal-title">
|
||||
{{ i18n.t('protocols.index.versions.title', { protocol: protocol.name }) }}
|
||||
</h4>
|
||||
</div>
|
||||
|
@ -15,9 +15,9 @@
|
|||
<img src="/images/medium/loading.svg" alt="Loading" class="p-4 rounded-xl bg-sn-white" />
|
||||
</div>
|
||||
<div class="max-h-[400px] overflow-y-auto">
|
||||
<div v-if="draft">
|
||||
<div v-if="draft" data-e2e="e2e-CO-protocolVersionsModal-draft">
|
||||
<div class="flex items-center gap-4">
|
||||
<a :href="draft.urls.show" class="hover:no-underline cursor-pointer shrink-0">
|
||||
<a :href="draft.urls.show" class="hover:no-underline cursor-pointer shrink-0" data-e2e="e2e-TL-protocolVersionsModal-draft-draftLink">
|
||||
<span v-if="draft.previous_number"
|
||||
v-html="i18n.t('protocols.index.versions.draft_html', {
|
||||
parent_version: draft.previous_number
|
||||
|
@ -25,7 +25,7 @@
|
|||
></span>
|
||||
<span v-else v-html="i18n.t('protocols.index.versions.first_draft_html')"></span>
|
||||
</a>
|
||||
<span class="text-xs" v-if="draft.modified_by">
|
||||
<span class="text-xs" v-if="draft.modified_by" data-e2e="e2e-TX-protocolVersionsModal-draft-timestamp">
|
||||
{{
|
||||
i18n.t('protocols.index.versions.draft_full_modification_info', {
|
||||
modified_on: draft.modified_on,
|
||||
|
@ -33,7 +33,7 @@
|
|||
})
|
||||
}}
|
||||
</span>
|
||||
<span class="text-xs" v-else>
|
||||
<span class="text-xs" v-else data-e2e="e2e-TX-protocolVersionsModal-draft-timestamp">
|
||||
{{
|
||||
i18n.t('protocols.index.versions.draft_update_modification_info', {
|
||||
modified_on: draft.modified_on
|
||||
|
@ -41,11 +41,17 @@
|
|||
}}
|
||||
</span>
|
||||
<div class="flex items-center gap-2 ml-auto">
|
||||
<button v-if="draft.urls.publish" class="btn btn-light" :disabled="updating" @click="publishDraft">
|
||||
<button v-if="draft.urls.publish" class="btn btn-light" :disabled="updating" @click="publishDraft" data-e2e="e2e-BT-protocolVersionsModal-draft-publish">
|
||||
<i class="sn-icon sn-icon-Publish"></i>
|
||||
{{ i18n.t('protocols.index.versions.publish') }}
|
||||
</button>
|
||||
<button v-if="draft.urls.destroy" @click="destroyDraft" :disabled="updating" class="btn btn-light icon-btn">
|
||||
<button
|
||||
v-if="draft.urls.destroy"
|
||||
@click="destroyDraft"
|
||||
:disabled="updating"
|
||||
class="btn btn-light icon-btn"
|
||||
data-e2e="e2e-BT-protocolVersionsModal-draft-deleteDraft"
|
||||
>
|
||||
<i class="sn-icon sn-icon-delete"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -60,16 +66,17 @@
|
|||
:singleLine="false"
|
||||
:attributeName="`${i18n.t('Draft')} ${i18n.t('comment')}`"
|
||||
@update="updateComment"
|
||||
:dataE2e="'protocolVersionsModal-draft-revisionNotes'"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="version in publishedVersions" :key="version.number">
|
||||
<div v-for="version in publishedVersions" :key="version.number" :data-e2e="`e2e-CO-protocolVersionsModal-version${version.number}`">
|
||||
<div class="flex items-center gap-4 group min-h-[40px]">
|
||||
<a :href="version.urls.show" class="hover:no-underline cursor-pointer shrink-0">
|
||||
<a :href="version.urls.show" class="hover:no-underline cursor-pointer shrink-0" :data-e2e="`e2e-TL-protocolVersionsModal-version${version.number}-versionLink`">
|
||||
<b>
|
||||
{{ i18n.t('protocols.index.versions.revision', { version: version.number }) }}
|
||||
</b>
|
||||
</a>
|
||||
<span class="text-xs">
|
||||
<span class="text-xs" :data-e2e="`e2e-TX-protocolVersionsModal-version${version.number}-timestamp`">
|
||||
{{
|
||||
i18n.t('protocols.index.versions.revision_publishing_info', {
|
||||
published_on: version.published_on,
|
||||
|
@ -83,11 +90,12 @@
|
|||
:title="i18n.t('protocols.index.versions.save_as_draft')"
|
||||
@click="saveAsDraft(version.urls.save_as_draft)"
|
||||
:disabled="draft || updating"
|
||||
:data-e2e="`e2e-BT-protocolVersionsModal-version${version.number}-saveAsDraft`"
|
||||
>
|
||||
<i class="sn-icon sn-icon-duplicate"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="mb-4" :data-e2e="`e2e-TX-protocolVersionsModal-version${version.number}-revisionNotes`">
|
||||
{{ version.comment }}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -104,6 +112,14 @@
|
|||
`"
|
||||
:confirmClass="'btn btn-danger'"
|
||||
:confirmText="i18n.t('protocols.delete_draft_modal.confirm')"
|
||||
:e2eAttributes="{
|
||||
modalName: 'e2e-MD-deleteProtocolDraft',
|
||||
title: 'e2e-TX-deleteProtocolDraftModal-title',
|
||||
content: 'e2e-TX-deleteProtocolDraftModal-content',
|
||||
close: 'e2e-BT-deleteProtocolDraftModal-close',
|
||||
cancel: 'e2e-BT-deleteProtocolDraftModal-cancel',
|
||||
confirm: 'e2e-BT-deleteProtocolDraftModal-delete'
|
||||
}"
|
||||
ref="destroyModal"
|
||||
></ConfirmationModal>
|
||||
</template>
|
||||
|
|
21
app/javascript/vue/protocols/renderers/name.vue
Normal file
21
app/javascript/vue/protocols/renderers/name.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<a v-if="params.data.urls.show"
|
||||
:href="params.data.urls.show"
|
||||
:title="params.data.name">
|
||||
{{ params.data.name }}
|
||||
</a>
|
||||
<span v-else class="text-sn-grey" :title="params.data.name">
|
||||
{{ params.data.name }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -42,6 +42,7 @@ import axios from '../../packs/custom_axios.js';
|
|||
|
||||
import DataTable from '../shared/datatable/table.vue';
|
||||
import UsersRenderer from '../projects/renderers/users.vue';
|
||||
import NameRenderer from './renderers/name.vue';
|
||||
import NewProtocolModal from './modals/new.vue';
|
||||
import AccessModal from '../shared/access_modal/modal.vue';
|
||||
import KeywordsRenderer from './renderers/keywords.vue';
|
||||
|
@ -55,6 +56,7 @@ export default {
|
|||
components: {
|
||||
DataTable,
|
||||
UsersRenderer,
|
||||
NameRenderer,
|
||||
NewProtocolModal,
|
||||
AccessModal,
|
||||
KeywordsRenderer,
|
||||
|
@ -116,7 +118,7 @@ export default {
|
|||
headerName: this.i18n.t('protocols.index.thead.name'),
|
||||
sortable: true,
|
||||
notSelectable: true,
|
||||
cellRenderer: this.nameRenderer
|
||||
cellRenderer: 'NameRenderer'
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
|
@ -337,14 +339,6 @@ export default {
|
|||
linkedMyModules(_event, rows) {
|
||||
[this.linkedMyModulesModalObject] = rows;
|
||||
},
|
||||
// renderers
|
||||
nameRenderer(params) {
|
||||
const { urls, name } = params.data;
|
||||
if (urls.show) {
|
||||
return `<a href="${urls.show}" title="${name}">${name}</a>`;
|
||||
}
|
||||
return `<span class="text-sn-grey" title="${name}">${name}</span>`;
|
||||
},
|
||||
usersFilterRenderer(option) {
|
||||
return `<div class="flex items-center gap-2">
|
||||
<img src="${option[2].avatar_url}" class="rounded-full w-6 h-6" />
|
||||
|
|
16
app/javascript/vue/reports/renderers/name.vue
Normal file
16
app/javascript/vue/reports/renderers/name.vue
Normal file
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<span :title="params.data.name">
|
||||
{{ params.data.name }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -43,6 +43,7 @@ import axios from '../../packs/custom_axios.js';
|
|||
import DataTable from '../shared/datatable/table.vue';
|
||||
import DocxRenderer from './renderers/docx.vue';
|
||||
import PdfRenderer from './renderers/pdf.vue';
|
||||
import NameRenderer from './renderers/name.vue';
|
||||
import ConfirmationModal from '../shared/confirmation_modal.vue';
|
||||
import SaveToInventoryModal from './modals/save_to_inventory.vue';
|
||||
import UpdateReportModal from './modals/update.vue';
|
||||
|
@ -60,6 +61,7 @@ export default {
|
|||
DataTable,
|
||||
DocxRenderer,
|
||||
PdfRenderer,
|
||||
NameRenderer,
|
||||
ConfirmationModal,
|
||||
SaveToInventoryModal,
|
||||
UpdateReportModal
|
||||
|
@ -108,7 +110,7 @@ export default {
|
|||
field: 'name',
|
||||
headerName: this.i18n.t('projects.reports.index.thead_name'),
|
||||
sortable: true,
|
||||
cellRenderer: ({ data: { name } }) => `<span title="${name}">${name}</span>`
|
||||
cellRenderer: 'NameRenderer'
|
||||
}, {
|
||||
field: 'code',
|
||||
headerName: this.i18n.t('projects.reports.index.thead_id'),
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
@changeStep="changeStep"
|
||||
@importRows="importRecords"
|
||||
@updateAutoMapping="updateAutoMapping"
|
||||
@updateAutoClearing="updateAutoClearing"
|
||||
/>
|
||||
<ExportModal
|
||||
v-else
|
||||
|
@ -49,7 +50,7 @@ export default {
|
|||
return {
|
||||
modalOpened: false,
|
||||
activeStep: 'UploadStep',
|
||||
params: { autoMapping: true },
|
||||
params: { autoMapping: true, autoClearing: false },
|
||||
modalId: null,
|
||||
loading: false
|
||||
};
|
||||
|
@ -62,6 +63,7 @@ export default {
|
|||
this.activeStep = 'UploadStep';
|
||||
this.params.selectedItems = null;
|
||||
this.params.autoMapping = true;
|
||||
this.params.autoClearing = false;
|
||||
this.fetchRepository();
|
||||
},
|
||||
fetchRepository() {
|
||||
|
@ -78,6 +80,11 @@ export default {
|
|||
},
|
||||
updateAutoMapping(value) {
|
||||
this.params.autoMapping = value;
|
||||
this.params.autoClearing = false;
|
||||
},
|
||||
updateAutoClearing() {
|
||||
this.params.autoMapping = false;
|
||||
this.params.autoClearing = true;
|
||||
},
|
||||
generatePreview(selectedItems, updateWithEmptyCells, onlyAddNewItems) {
|
||||
this.params.selectedItems = selectedItems;
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<div class="flex gap-6 items-center my-6">
|
||||
<div class="flex items-center gap-2" :title="i18n.t('repositories.import_records.steps.step2.autoMappingTooltip')">
|
||||
<div class="sci-checkbox-container">
|
||||
<input type="checkbox" class="sci-checkbox" @change="$emit('update-auto-mapping', $event.target.checked)" :checked="params.autoMapping" />
|
||||
<input type="checkbox" class="sci-checkbox" @change="toggleAutoMapping" :checked="params.autoMapping" />
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
{{ i18n.t('repositories.import_records.steps.step2.autoMappingText') }}
|
||||
|
@ -44,11 +44,11 @@
|
|||
|
||||
{{ i18n.t('repositories.import_records.steps.step2.importedFileText') }} {{ params.file_name }}
|
||||
<hr class="m-0 mt-6">
|
||||
<div class="grid grid-cols-[3rem_14.5rem_1.5rem_14.5rem_5rem_14.5rem] px-2">
|
||||
<div class="grid grid-cols-[3rem_14.5rem_1.5rem_14.5rem_5rem_14.5rem] px-2" :key="JSON.stringify(this.selectedItems)">
|
||||
|
||||
<div v-for="(column, key) in columnLabels" class="flex items-center px-2 py-2 font-bold">{{ column }}</div>
|
||||
|
||||
<template v-for="(item, index) in params.import_data.header" :key="item">
|
||||
<template v-for="(item, index) in params.import_data.header" :key="index">
|
||||
<MappingStepTableRow
|
||||
:index="index"
|
||||
:item="item"
|
||||
|
@ -57,6 +57,7 @@
|
|||
:value="this.selectedItems.find((item) => item.index === index)"
|
||||
@selection:changed="handleChange"
|
||||
:autoMapping="params.autoMapping"
|
||||
:autoClearing="params.autoClearing"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -140,7 +141,12 @@ export default {
|
|||
methods: {
|
||||
handleChange(payload) {
|
||||
this.error = null;
|
||||
const { index, key, value } = payload;
|
||||
const {
|
||||
index,
|
||||
key,
|
||||
value,
|
||||
autoMap
|
||||
} = payload;
|
||||
|
||||
const item = this.selectedItems.find((i) => i.index === index);
|
||||
const usedBeforeItem = this.selectedItems.find((i) => i.key === key && i.index !== index);
|
||||
|
@ -152,6 +158,15 @@ export default {
|
|||
|
||||
item.key = key;
|
||||
item.value = value;
|
||||
|
||||
this.$emit('updateAutoMapping', autoMap);
|
||||
},
|
||||
toggleAutoMapping(event) {
|
||||
if (event.target.checked) {
|
||||
this.$emit('updateAutoMapping', true);
|
||||
} else {
|
||||
this.$emit('updateAutoClearing');
|
||||
}
|
||||
},
|
||||
loadAvailableFields() {
|
||||
// Adding alreadySelected attribute for tracking
|
||||
|
|
|
@ -90,6 +90,10 @@ export default {
|
|||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
autoClearing: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
value: Object
|
||||
},
|
||||
data() {
|
||||
|
@ -122,11 +126,10 @@ export default {
|
|||
}
|
||||
},
|
||||
autoMapping(newVal) {
|
||||
if (newVal === true) {
|
||||
this.autoMap();
|
||||
} else {
|
||||
this.clearAutoMap();
|
||||
}
|
||||
if (newVal) this.autoMap();
|
||||
},
|
||||
autoClearing(newVal) {
|
||||
if (newVal) this.clearAutoMap();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -151,19 +154,31 @@ export default {
|
|||
return this.systemColumns.includes(column);
|
||||
},
|
||||
autoMap() {
|
||||
this.changeSelected(null);
|
||||
this.changeAutoSelected(null);
|
||||
Object.entries(this.params.import_data.available_fields).forEach(([key, value]) => {
|
||||
if (this.item === value) {
|
||||
this.changeSelected(key);
|
||||
this.changeAutoSelected(key);
|
||||
}
|
||||
});
|
||||
},
|
||||
clearAutoMap() {
|
||||
this.changeSelected('do_not_import');
|
||||
},
|
||||
changeSelected(e) {
|
||||
updateSelectedColumnType(e, autoMap) {
|
||||
const value = this.params.import_data.available_fields[e];
|
||||
this.selectedColumnType = { index: this.index, key: e, value };
|
||||
this.selectedColumnType = {
|
||||
index: this.index,
|
||||
key: e,
|
||||
value,
|
||||
autoMap
|
||||
};
|
||||
},
|
||||
changeAutoSelected(e) {
|
||||
this.updateSelectedColumnType(e, true);
|
||||
this.$emit('selection:changed', this.selectedColumnType);
|
||||
},
|
||||
changeSelected(e) {
|
||||
this.updateSelectedColumnType(e, false);
|
||||
this.$emit('selection:changed', this.selectedColumnType);
|
||||
}
|
||||
},
|
||||
|
|
22
app/javascript/vue/repositories/renderers/name.vue
Normal file
22
app/javascript/vue/repositories/renderers/name.vue
Normal file
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<a class="hover:no-underline flex items-center gap-1"
|
||||
:title="params.data.name"
|
||||
:href="params.data.urls.show"
|
||||
>
|
||||
<span class="truncate">
|
||||
<i v-if="params.data.shared || params.data.ishared" class="fas fa-users"></i>
|
||||
{{ params.data.name }}
|
||||
</span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -50,11 +50,31 @@
|
|||
:repository="duplicateRepository"
|
||||
@close="duplicateRepository = null"
|
||||
@duplicate="updateTable" />
|
||||
<ShareRepositoryModal
|
||||
<ShareObjectModal
|
||||
v-if="shareRepository"
|
||||
:repository="shareRepository"
|
||||
:object="shareRepository"
|
||||
:globalShareEnabled="true"
|
||||
:confirmationModal="$refs.shareConfirmationModal"
|
||||
@close="shareRepository = null"
|
||||
@share="updateTable" />
|
||||
<ConfirmationModal
|
||||
ref="shareConfirmationModal"
|
||||
:title="i18n.t('repositories.index.modal_confirm_sharing.title')"
|
||||
:description="`
|
||||
<p>${i18n.t('repositories.index.modal_confirm_sharing.description_1')}</p>
|
||||
<p><b>${i18n.t('repositories.index.modal_confirm_sharing.description_2')}</b></p>
|
||||
`"
|
||||
:confirmClass="'btn btn-danger'"
|
||||
:confirmText="i18n.t('repositories.index.modal_confirm_sharing.confirm')"
|
||||
:e2eAttributes="{
|
||||
modalName: 'e2e-MD-confirmSharingChanges',
|
||||
title: 'e2e-TX-confirmSharingChangesModal-title',
|
||||
content: 'e2e-TX-confirmSharingChangesModal-content',
|
||||
close: 'e2e-BT-confirmSharingChangesModal-close',
|
||||
cancel: 'e2e-BT-confirmSharingChangesModal-cancel',
|
||||
confirm: 'e2e-BT-confirmSharingChangesModal-delete'
|
||||
}"
|
||||
></ConfirmationModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -66,8 +86,9 @@ import ExportRepositoryModal from './modals/export.vue';
|
|||
import NewRepositoryModal from './modals/new.vue';
|
||||
import EditRepositoryModal from './modals/edit.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 NameRenderer from './renderers/name.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepositoriesTable',
|
||||
|
@ -78,7 +99,8 @@ export default {
|
|||
NewRepositoryModal,
|
||||
EditRepositoryModal,
|
||||
DuplicateRepositoryModal,
|
||||
ShareRepositoryModal
|
||||
NameRenderer,
|
||||
ShareObjectModal
|
||||
},
|
||||
props: {
|
||||
dataSource: {
|
||||
|
@ -138,7 +160,7 @@ export default {
|
|||
headerName: this.i18n.t('libraries.index.table.name'),
|
||||
sortable: true,
|
||||
notSelectable: true,
|
||||
cellRenderer: this.nameRenderer
|
||||
cellRenderer: 'NameRenderer'
|
||||
},
|
||||
{
|
||||
field: 'code',
|
||||
|
@ -277,23 +299,6 @@ export default {
|
|||
share(_event, rows) {
|
||||
const [repository] = rows;
|
||||
this.shareRepository = repository;
|
||||
},
|
||||
// Renderers
|
||||
nameRenderer(params) {
|
||||
const {
|
||||
name,
|
||||
urls,
|
||||
shared,
|
||||
ishared
|
||||
} = params.data;
|
||||
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}">
|
||||
<span class="truncate">${sharedIcon}${name}</span>
|
||||
</a>`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -312,6 +312,11 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<div v-if="!repository?.is_snapshot" id="divider" class="bg-sn-light-grey flex px-8 items-center self-stretch h-px "></div>
|
||||
<!-- Locations -->
|
||||
<section v-if="!repository?.is_snapshot" id="locations-section" ref="locationsSectionRef" 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>
|
||||
|
||||
<!-- QR -->
|
||||
|
@ -367,6 +372,7 @@ import ScrollSpy from './repository_values/ScrollSpy.vue';
|
|||
import CustomColumns from './customColumns.vue';
|
||||
import RepositoryItemSidebarTitle from './Title.vue';
|
||||
import UnlinkModal from './unlink_modal.vue';
|
||||
import Locations from './locations.vue';
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
|
||||
const items = [
|
||||
|
@ -405,6 +411,14 @@ const items = [
|
|||
{
|
||||
id: 'highlight-item-5',
|
||||
textId: 'text-item-5',
|
||||
labelAlias: 'locations_label',
|
||||
label: 'locations-label',
|
||||
sectionId: 'locations-section',
|
||||
showInSnapshot: false
|
||||
},
|
||||
{
|
||||
id: 'highlight-item-6',
|
||||
textId: 'text-item-6',
|
||||
labelAlias: 'QR_label',
|
||||
label: 'QR-label',
|
||||
sectionId: 'qr-section',
|
||||
|
@ -416,6 +430,7 @@ export default {
|
|||
name: 'RepositoryItemSidebar',
|
||||
components: {
|
||||
CustomColumns,
|
||||
Locations,
|
||||
'repository-item-sidebar-title': RepositoryItemSidebarTitle,
|
||||
'inline-edit': InlineEdit,
|
||||
'scroll-spy': ScrollSpy,
|
||||
|
@ -433,6 +448,7 @@ export default {
|
|||
repository: null,
|
||||
defaultColumns: null,
|
||||
customColumns: null,
|
||||
repositoryRow: null,
|
||||
parentsCount: 0,
|
||||
childrenCount: 0,
|
||||
parents: null,
|
||||
|
@ -591,6 +607,7 @@ export default {
|
|||
{ params: { my_module_id: this.myModuleId } }
|
||||
).then((response) => {
|
||||
const result = response.data;
|
||||
this.repositoryRow = result;
|
||||
this.repositoryRowId = result.id;
|
||||
this.repository = result.repository;
|
||||
this.optionsPath = result.options_path;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
:characterMinLimit="0" :allowBlank="false" :smartAnnotation="false"
|
||||
:preventLeavingUntilFilled="true"
|
||||
:attributeName="`${i18n.t('repositories.item_card.header_title')}`" :singleLine="true"
|
||||
@editingEnabled="editingName = true" @editingDisabled="editingName = false" @update="updateName" @delete="handleDelete"></inline-edit>
|
||||
@editingEnabled="editingName = true" @editingDisabled="editingName = false" @update="updateName"></inline-edit>
|
||||
<h4 v-else class="item-name my-auto truncate text-xl" :title="computedName">
|
||||
{{ computedName }}
|
||||
</h4>
|
||||
|
|
110
app/javascript/vue/repository_item_sidebar/locations.vue
Normal file
110
app/javascript/vue/repository_item_sidebar/locations.vue
Normal file
|
@ -0,0 +1,110 @@
|
|||
<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>
|
||||
<div class="mb-4">
|
||||
<div v-html="repositoryRow.storage_locations.placeholder"></div>
|
||||
</div>
|
||||
<template v-for="(location, index) in repositoryRow.storage_locations.locations" :key="location.id">
|
||||
<div>
|
||||
<div class="sci-divider my-4" v-if="index > 0"></div>
|
||||
<div class="flex gap-2 mb-3">
|
||||
<span class="shrink-0">{{ i18n.t('repositories.locations.container') }}:</span>
|
||||
<a v-if="location.readable" :href="containerUrl(location.id)">{{ location.name }}</a>
|
||||
<span v-else>{{ location.name }}</span>
|
||||
<i v-if="repositoryRow.permissions.can_manage && location.metadata.display_type !== 'grid'"
|
||||
@click="unassignRow(location.id, location.positions[0].id)"
|
||||
class="sn-icon sn-icon-unlink-italic-s cursor-pointer ml-auto"></i>
|
||||
</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"
|
||||
@click="unassignRow(location.id, position.id)"
|
||||
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"
|
||||
:selectedRowName="repositoryRow.default_columns.name"
|
||||
@close="openAssignModal = false; $emit('reloadRow'); reloadStorageLocations()"
|
||||
></AssignModal>
|
||||
<ConfirmationModal
|
||||
:title="i18n.t('storage_locations.show.unassign_modal.title')"
|
||||
:description="i18n.t('storage_locations.show.unassign_modal.description_single')"
|
||||
confirmClass="btn btn-danger"
|
||||
:confirmText="i18n.t('storage_locations.show.unassign_modal.button')"
|
||||
ref="unassignStorageLocationModal"
|
||||
></ConfirmationModal>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AssignModal from '../storage_locations/modals/assign.vue';
|
||||
import ConfirmationModal from '../shared/confirmation_modal.vue';
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
|
||||
import {
|
||||
storage_location_path,
|
||||
unassign_rows_storage_location_path
|
||||
} from '../../routes.js';
|
||||
|
||||
export default {
|
||||
name: 'RepositoryItemLocations',
|
||||
props: {
|
||||
repositoryRow: Object,
|
||||
repository: Object
|
||||
},
|
||||
components: {
|
||||
AssignModal,
|
||||
ConfirmationModal
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openAssignModal: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
containerUrl(id) {
|
||||
return storage_location_path(id);
|
||||
},
|
||||
formatPosition(position) {
|
||||
if (position) {
|
||||
return `${this.numberToLetter(position[0])}${position[1]}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
reloadStorageLocations() {
|
||||
if (window.StorageLocationsContainer) {
|
||||
window.StorageLocationsContainer.$refs.container.reloadingTable = true;
|
||||
}
|
||||
},
|
||||
numberToLetter(number) {
|
||||
return String.fromCharCode(96 + number);
|
||||
},
|
||||
async unassignRow(locationId, rowId) {
|
||||
const ok = await this.$refs.unassignStorageLocationModal.show();
|
||||
if (ok) {
|
||||
axios.post(unassign_rows_storage_location_path({ id: locationId }), { ids: [rowId] })
|
||||
.then(() => {
|
||||
this.$emit('reloadRow');
|
||||
this.reloadStorageLocations();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -16,14 +16,14 @@
|
|||
>
|
||||
</select-dropdown>
|
||||
</div>
|
||||
<div v-else-if="computedArrOfItemObjects && computedArrOfItemObjects.length > 0"
|
||||
<div v-else-if="colVal && colVal.length > 0"
|
||||
class="text-sn-dark-grey font-inter text-sm font-normal leading-5 w-[370px] overflow-x-auto flex flex-wrap gap-1">
|
||||
<span v-for="(checklistItem, index) in computedArrOfItemObjects"
|
||||
<span v-for="(checklistItem, index) in colVal"
|
||||
:key="index"
|
||||
:id="`checklist-item-${index}`"
|
||||
class="flex w-fit break-words">
|
||||
{{
|
||||
index + 1 === computedArrOfItemObjects.length
|
||||
index + 1 === colVal.length
|
||||
? checklistItem?.label
|
||||
: `${checklistItem?.label} |`
|
||||
}}
|
||||
|
@ -66,18 +66,6 @@ export default {
|
|||
this.selectedChecklistItemsIds = this.colVal.map((item) => String(item.value));
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedArrOfItemObjects() {
|
||||
const arrOfItemObjects = this.selectedChecklistItemsIds.map((id) => {
|
||||
const matchingItem = this.availableChecklistItems.find((item) => item[0] === id);
|
||||
return {
|
||||
id: matchingItem ? matchingItem[0] : null,
|
||||
label: matchingItem ? matchingItem[1] : null
|
||||
};
|
||||
});
|
||||
return arrOfItemObjects;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchChecklistItems() {
|
||||
this.isLoading = true;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue