mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 06:35:56 +08:00
Merge branch 'develop' into e2e
This commit is contained in:
commit
430daa4c8e
|
@ -21,6 +21,24 @@
|
|||
{
|
||||
"beforeLineComment": false
|
||||
}
|
||||
],
|
||||
"max-len": [
|
||||
"error",
|
||||
{
|
||||
"code": 120
|
||||
}
|
||||
],
|
||||
"vue/max-len": [
|
||||
"error",
|
||||
{
|
||||
"code": 120,
|
||||
"template": 240,
|
||||
"tabWidth": 2
|
||||
}
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"never"
|
||||
]
|
||||
},
|
||||
"globals": {
|
|
@ -4,7 +4,8 @@ ruby:
|
|||
|
||||
eslint:
|
||||
enabled: true
|
||||
config_file: app/assets/.eslintrc.json
|
||||
version: 8.1.0
|
||||
config_file: .eslintrc.json
|
||||
|
||||
scss:
|
||||
config_file: .scss-lint.yml
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -57,6 +57,7 @@ gem 'jbuilder' # JSON structures via a Builder-style DSL
|
|||
gem 'logging', '~> 2.0.0'
|
||||
gem 'nested_form_fields'
|
||||
gem 'nokogiri', '~> 1.14.3' # HTML/XML parser
|
||||
gem 'noticed'
|
||||
gem 'rails_autolink', '~> 1.1', '>= 1.1.6'
|
||||
gem 'rgl' # Graph framework for project diagram calculations
|
||||
gem 'roo', '~> 2.10.0' # Spreadsheet parser
|
||||
|
|
25
Gemfile.lock
25
Gemfile.lock
|
@ -50,7 +50,7 @@ GIT
|
|||
|
||||
GIT
|
||||
remote: https://github.com/scinote-eln/yomu
|
||||
revision: 020ab670b2919f3b436e926a890d1dad23d75676
|
||||
revision: fb518a5fbab82f692dea4ae1fdf30eae5df62590
|
||||
branch: master
|
||||
specs:
|
||||
yomu (0.2.4)
|
||||
|
@ -301,6 +301,8 @@ GEM
|
|||
discard (1.2.1)
|
||||
activerecord (>= 4.2, < 8)
|
||||
docile (1.4.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (5.6.6)
|
||||
railties (>= 5)
|
||||
down (5.4.1)
|
||||
|
@ -324,6 +326,9 @@ GEM
|
|||
faraday-net_http (3.0.2)
|
||||
fastimage (2.2.7)
|
||||
ffi (1.15.5)
|
||||
ffi-compiler (1.0.1)
|
||||
ffi (>= 1.0.0)
|
||||
rake
|
||||
figaro (1.2.0)
|
||||
thor (>= 0.14.0, < 2)
|
||||
fugit (1.8.1)
|
||||
|
@ -338,6 +343,14 @@ GEM
|
|||
nokogiri (~> 1.0)
|
||||
hashdiff (1.0.1)
|
||||
hashie (5.0.0)
|
||||
http (5.1.1)
|
||||
addressable (~> 2.8)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 2.2)
|
||||
llhttp-ffi (~> 0.4.0)
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (2.3.0)
|
||||
httparty (0.21.0)
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
|
@ -387,6 +400,9 @@ GEM
|
|||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
little-plugger (1.1.4)
|
||||
llhttp-ffi (0.4.0)
|
||||
ffi-compiler (~> 1.0)
|
||||
rake (~> 13.0)
|
||||
logging (2.0.0)
|
||||
little-plugger (~> 1.1)
|
||||
multi_json (~> 1.10)
|
||||
|
@ -434,6 +450,9 @@ GEM
|
|||
racc (~> 1.4)
|
||||
nokogiri (1.14.5-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
noticed (1.6.3)
|
||||
http (>= 4.0.0)
|
||||
rails (>= 5.2.0)
|
||||
oauth2 (2.0.9)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
jwt (>= 1.0, < 3.0)
|
||||
|
@ -661,6 +680,9 @@ GEM
|
|||
uglifier (4.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
underscore-rails (1.8.3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (2.4.2)
|
||||
uniform_notifier (1.16.0)
|
||||
version_gem (1.1.3)
|
||||
|
@ -748,6 +770,7 @@ DEPENDENCIES
|
|||
nested_form_fields
|
||||
newrelic_rpm
|
||||
nokogiri (~> 1.14.3)
|
||||
noticed
|
||||
omniauth (~> 2.1)
|
||||
omniauth-azure-activedirectory-v2
|
||||
omniauth-linkedin-oauth2
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"jquery": true
|
||||
},
|
||||
"extends": ["airbnb"],
|
||||
"rules": {
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"func-names": ["error", "never"],
|
||||
"spaced-comment": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"markers": ["="]
|
||||
}
|
||||
],
|
||||
"lines-around-comment": [
|
||||
"warn",
|
||||
{
|
||||
"beforeLineComment": false
|
||||
}
|
||||
],
|
||||
"max-len": ["error", { "code": 120 }]
|
||||
}
|
||||
}
|
|
@ -615,6 +615,7 @@ var ExperimnetTable = {
|
|||
this.appendRows(result.data);
|
||||
this.initDueDatePicker(result.data);
|
||||
this.handleNoResults();
|
||||
this.initProvisioningStatusPolling();
|
||||
}, 100);
|
||||
|
||||
InfiniteScroll.init(this.table, {
|
||||
|
|
81
app/assets/javascripts/my_modules/assigned_users.js
Normal file
81
app/assets/javascripts/my_modules/assigned_users.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
/* global I18n dropdownSelector HelperModule animateSpinner */
|
||||
/* eslint-disable no-use-before-define */
|
||||
(function() {
|
||||
function initAssignedUsersSelector() {
|
||||
var myModuleUserSelector = '#module-assigned-users-selector';
|
||||
|
||||
dropdownSelector.init(myModuleUserSelector, {
|
||||
closeOnSelect: true,
|
||||
labelHTML: true,
|
||||
tagClass: 'my-module-user-tags',
|
||||
tagLabel: (data) => {
|
||||
return `<img class="img-responsive block-inline" src="${data.params.avatar_url}" alt="${data.label}"/>
|
||||
<span class="user-full-name block-inline">${data.label}</span>`;
|
||||
},
|
||||
customDropdownIcon: () => {
|
||||
return '';
|
||||
},
|
||||
optionLabel: (data) => {
|
||||
if (data.params.avatar_url) {
|
||||
return `<span class="global-avatar-container" style="margin-top: 10px">
|
||||
<img src="${data.params.avatar_url}" alt="${data.label}"/></span>
|
||||
<span style="margin-left: 10px">${data.label}</span>`;
|
||||
}
|
||||
|
||||
return data.label;
|
||||
},
|
||||
onSelect: function() {
|
||||
var selectElement = $(myModuleUserSelector);
|
||||
var lastUser = selectElement.next().find('.ds-tags').last();
|
||||
var lastUserId = lastUser.find('.tag-label').data('ds-tag-id');
|
||||
var newUser;
|
||||
|
||||
if (lastUserId > 0) {
|
||||
newUser = {
|
||||
user_my_module: {
|
||||
user_id: lastUserId
|
||||
}
|
||||
};
|
||||
} else {
|
||||
newUser = {
|
||||
user_my_module: {
|
||||
user_id: selectElement.val()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$.post(selectElement.data('users-create-url'), newUser, function(result) {
|
||||
dropdownSelector.removeValue(myModuleUserSelector, 0, '', true);
|
||||
dropdownSelector.addValue(myModuleUserSelector, {
|
||||
value: result.user.id,
|
||||
label: result.user.full_name,
|
||||
params: {
|
||||
avatar_url: result.user.avatar_url,
|
||||
user_module_id: result.user.user_module_id
|
||||
}
|
||||
}, true);
|
||||
}).fail(function() {
|
||||
dropdownSelector.removeValue(myModuleUserSelector, lastUserId, '', true);
|
||||
});
|
||||
},
|
||||
onUnSelect: (id) => {
|
||||
var umID = $(myModuleUserSelector).find(`option[value="${id}"]`).data('params').user_module_id;
|
||||
|
||||
$.ajax({
|
||||
url: `${$(myModuleUserSelector).data('update-module-users-url')}/${umID}`,
|
||||
type: 'DELETE',
|
||||
success: () => {
|
||||
dropdownSelector.closeDropdown(myModuleUserSelector);
|
||||
},
|
||||
error: (r) => {
|
||||
if (r.status === 403) {
|
||||
HelperModule.flashAlertMsg(I18n.t('general.no_permissions'), 'danger');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).getContainer(myModuleUserSelector).addClass('my-module-users-container');
|
||||
}
|
||||
|
||||
initAssignedUsersSelector();
|
||||
}());
|
|
@ -57,6 +57,10 @@ var RepositoryDatatable = (function(global) {
|
|||
}
|
||||
|
||||
function restoreColumnSizes() {
|
||||
const scrollBody = $('.dataTables_scrollBody');
|
||||
if (scrollBody[0].offsetWidth > scrollBody[0].clientWidth) {
|
||||
scrollBody.css('width', `calc(100% + ${scrollBody[0].offsetWidth - scrollBody[0].clientWidth}px)`);
|
||||
}
|
||||
TABLE.colResize.restore();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* global HelperModule PerfectScrollbar */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
var ShareModal = (function() {
|
||||
const ShareModal = (function() {
|
||||
function init() {
|
||||
var form = $('.share-repo-modal').find('form');
|
||||
var sharedCBs = form.find("input[name='share_team_ids[]']");
|
||||
|
@ -13,21 +13,18 @@ var ShareModal = (function() {
|
|||
form.find('.teams-list').find('input.sci-checkbox, .permission-selector')
|
||||
.toggleClass('hidden', selectAllCheckbox.is(':checked'));
|
||||
form.find('.all-teams .sci-toggle-checkbox')
|
||||
.toggleClass('hidden', !selectAllCheckbox.is(':checked'))
|
||||
.attr('disabled', !selectAllCheckbox.is(':checked'));
|
||||
.toggleClass('hidden', !selectAllCheckbox.is(':checked'));
|
||||
|
||||
selectAllCheckbox.change(function() {
|
||||
form.find('.teams-list').find('input.sci-checkbox, .permission-selector')
|
||||
.toggleClass('hidden', this.checked);
|
||||
form.find('.all-teams .sci-toggle-checkbox').toggleClass('hidden', !this.checked)
|
||||
.attr('disabled', !this.checked);
|
||||
form.find('.all-teams .sci-toggle-checkbox').toggleClass('hidden', !this.checked);
|
||||
});
|
||||
|
||||
sharedCBs.change(function() {
|
||||
var selectedTeams = form.find('.teams-list .sci-checkbox:checked').length;
|
||||
form.find('#select_all_teams').prop('indeterminate', selectedTeams > 0);
|
||||
$('#editable_' + this.value).toggleClass('hidden', !this.checked)
|
||||
.attr('disabled', !this.checked);
|
||||
$('#editable_' + this.value).toggleClass('hidden', !this.checked);
|
||||
});
|
||||
|
||||
if (form.find('.teams-list').length) new PerfectScrollbar(form.find('.teams-list')[0]);
|
||||
|
|
|
@ -262,6 +262,10 @@ var RepositoryColumns = (function() {
|
|||
if (!_.isEmpty(searchText)) {
|
||||
TABLE.search(searchText).draw();
|
||||
}
|
||||
const scrollBody = $('.dataTables_scrollBody');
|
||||
if (scrollBody[0].offsetWidth > scrollBody[0].clientWidth) {
|
||||
scrollBody.css('width', `calc(100% + ${scrollBody[0].offsetWidth - scrollBody[0].clientWidth}px)`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -84,8 +84,6 @@
|
|||
|
||||
.sci-navigation--notificaitons-flyout-notification {
|
||||
border-bottom: $border-tertiary;
|
||||
display: grid;
|
||||
grid-template-columns: max-content auto;
|
||||
padding: 1rem 0;
|
||||
|
||||
.sci-navigation--notificaitons-flyout-notification-icon {
|
||||
|
@ -94,7 +92,7 @@
|
|||
border-radius: 50%;
|
||||
color: $color-white;
|
||||
display: flex;
|
||||
grid-row: 1 / 4;
|
||||
grid-row: 1 / 5;
|
||||
height: 2rem;
|
||||
justify-content: center;
|
||||
margin-right: .75rem;
|
||||
|
|
|
@ -137,7 +137,7 @@
|
|||
--dp-secondary-color: var(--sn-grey);
|
||||
--dp-border-color: var(--sn-light-grey);
|
||||
--dp-menu-border-color: var(--sn-light-grey);
|
||||
--dp-border-color-hover: var(--sn-light-grey);
|
||||
--dp-border-color-hover: var(--sn-sleepy-grey);
|
||||
--dp-disabled-color: var(--sn-super-light-grey);
|
||||
--dp-scroll-bar-background: var(--sn-white);
|
||||
--dp-scroll-bar-color: var(--sn-grey);
|
||||
|
|
|
@ -141,7 +141,7 @@
|
|||
transition: .2s;
|
||||
transition-property: top, bottom, box-shadow;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
z-index: 999;
|
||||
|
||||
.empty-dropdown {
|
||||
opacity: .6;
|
||||
|
|
|
@ -52,16 +52,14 @@ input[type="checkbox"].sci-toggle-checkbox {
|
|||
}
|
||||
}
|
||||
|
||||
&:focus + .sci-toggle-checkbox-label {
|
||||
box-shadow: 0 0 0 4px var(--sn-science-blue-hover);
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&.hidden + .sci-toggle-checkbox-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:checked + .sci-toggle-checkbox-label {
|
||||
border-color: var(--sn-blue);
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@
|
|||
|
||||
.step-elements {
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
|
||||
.step-timestamp {
|
||||
position: relative;
|
||||
|
|
|
@ -71,6 +71,7 @@ module AccessPermissions
|
|||
|
||||
log_activity(:assign_user_to_project, { user_target: user_assignment.user.id,
|
||||
role: user_assignment.user_role.name })
|
||||
|
||||
created_count += 1
|
||||
propagate_job(user_assignment)
|
||||
end
|
||||
|
@ -99,7 +100,14 @@ module AccessPermissions
|
|||
raise ActiveRecord::RecordInvalid
|
||||
end
|
||||
|
||||
propagate_job(user_assignment, destroy: true)
|
||||
UserAssignments::PropagateAssignmentJob.perform_now(
|
||||
@project,
|
||||
user_assignment.user.id,
|
||||
user_assignment.user_role,
|
||||
current_user.id,
|
||||
destroy: true
|
||||
)
|
||||
|
||||
log_activity(:unassign_user_from_project, { user_target: user_assignment.user.id,
|
||||
role: user_assignment.user_role.name })
|
||||
|
||||
|
|
|
@ -88,7 +88,10 @@ module Api
|
|||
metadata_cells = metadata[:cells]
|
||||
data = contents['data']
|
||||
|
||||
if data.present? && data[0].present? && (data.size * data[0].size) < metadata_cells.size
|
||||
if data.present? && data[0].present?
|
||||
data_size = (data[0].is_a?(Array) ? data.size * data[0].size : data.size)
|
||||
|
||||
if data_size < metadata_cells.size
|
||||
error_message = I18n.t('api.core.errors.table.metadata.detail_too_many_cells')
|
||||
raise ActionController::BadRequest, error_message
|
||||
end
|
||||
|
@ -97,3 +100,4 @@ module Api
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
25
app/controllers/api/v2/base_controller.rb
Normal file
25
app/controllers/api/v2/base_controller.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V2
|
||||
class BaseController < Api::V1::BaseController
|
||||
private
|
||||
|
||||
def load_result(key = :result_id)
|
||||
@result = @task.results.find(params.require(key))
|
||||
|
||||
raise PermissionError.new(Result, :read) unless can_read_result?(@result)
|
||||
end
|
||||
|
||||
def load_result_text(key = :result_text_id)
|
||||
@result_text = @result.result_texts.find(params.require(key))
|
||||
raise PermissionError.new(Result, :read) unless can_read_result?(@result)
|
||||
end
|
||||
|
||||
def load_result_table(key = :table_id)
|
||||
@table = @result.tables.find(params.require(key))
|
||||
raise PermissionError.new(Result, :read) unless can_read_result?(@result)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
74
app/controllers/api/v2/result_assets_controller.rb
Normal file
74
app/controllers/api/v2/result_assets_controller.rb
Normal file
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V2
|
||||
class ResultAssetsController < BaseController
|
||||
before_action :load_team, :load_project, :load_experiment, :load_task, :load_result
|
||||
before_action :check_manage_permission, only: %i(create destroy)
|
||||
before_action :load_asset, only: %i(show destroy)
|
||||
before_action :check_upload_type, only: :create
|
||||
|
||||
def index
|
||||
result_assets =
|
||||
timestamps_filter(@result.result_assets).page(params.dig(:page, :number))
|
||||
.per(params.dig(:page, :size))
|
||||
|
||||
render jsonapi: result_assets, each_serializer: ResultAssetSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
render jsonapi: @asset.result_asset, serializer: ResultAssetSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
asset = if @form_multipart_upload
|
||||
@result.assets.new(asset_params.merge({ team_id: @team.id }))
|
||||
else
|
||||
blob = ActiveStorage::Blob.create_and_upload!(
|
||||
io: StringIO.new(Base64.decode64(asset_params[:file_data])),
|
||||
filename: asset_params[:file_name],
|
||||
content_type: asset_params[:file_type]
|
||||
)
|
||||
@result.assets.new(file: blob, team: @team)
|
||||
end
|
||||
|
||||
asset.save!(context: :on_api_upload)
|
||||
asset.post_process_file
|
||||
|
||||
render jsonapi: asset.result_asset,
|
||||
serializer: ResultAssetSerializer,
|
||||
status: :created
|
||||
end
|
||||
|
||||
def destroy
|
||||
@asset.destroy!
|
||||
render body: nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def asset_params
|
||||
raise TypeError unless params.require(:data).require(:type) == 'attachments'
|
||||
|
||||
return params.require(:data).require(:attributes).permit(:file) if @form_multipart_upload
|
||||
|
||||
attr_list = %i(file_data file_type file_name)
|
||||
params.require(:data).require(:attributes).require(attr_list)
|
||||
params.require(:data).require(:attributes).permit(attr_list)
|
||||
end
|
||||
|
||||
def load_asset
|
||||
@asset = @result.assets.find(params.require(:id))
|
||||
raise PermissionError.new(Result, :read) unless can_read_result?(@result)
|
||||
end
|
||||
|
||||
def check_upload_type
|
||||
@form_multipart_upload = true if params.dig(:data, :attributes, :file)
|
||||
end
|
||||
|
||||
def check_manage_permission
|
||||
raise PermissionError.new(Result, :manage) unless can_manage_result?(@result)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
102
app/controllers/api/v2/result_tables_controller.rb
Normal file
102
app/controllers/api/v2/result_tables_controller.rb
Normal file
|
@ -0,0 +1,102 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V2
|
||||
class ResultTablesController < BaseController
|
||||
before_action :load_team, :load_project, :load_experiment, :load_task, :load_result
|
||||
before_action only: %i(show update destroy) do
|
||||
load_result_table(:id)
|
||||
end
|
||||
before_action :check_manage_permission, only: %i(create update destroy)
|
||||
|
||||
def index
|
||||
result_tables = timestamps_filter(@result.result_tables).page(params.dig(:page, :number))
|
||||
.per(params.dig(:page, :size))
|
||||
|
||||
render jsonapi: result_tables, each_serializer: ResultTableSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
render jsonapi: @table.result_table, serializer: ResultTableSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
table = @result.tables.new(table_params.merge!(team: @team, created_by: current_user))
|
||||
|
||||
@result.with_lock do
|
||||
@result.result_orderable_elements.create!(
|
||||
position: @result.result_orderable_elements.size,
|
||||
orderable: table.result_table
|
||||
)
|
||||
|
||||
table.save!
|
||||
end
|
||||
|
||||
render jsonapi: table.result_table, serializer: ResultTableSerializer, status: :created
|
||||
end
|
||||
|
||||
def update
|
||||
@table.assign_attributes(table_params)
|
||||
|
||||
if @table.changed? && @table.save!
|
||||
render jsonapi: @table.result_table, serializer: ResultTableSerializer
|
||||
else
|
||||
render body: nil, status: :no_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@table.destroy!
|
||||
render body: nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_manage_permission
|
||||
raise PermissionError.new(Result, :manage) unless can_manage_result?(@result)
|
||||
end
|
||||
|
||||
def convert_plate_template(metadata_params)
|
||||
if metadata_params.present? && metadata_params['plateTemplate']
|
||||
metadata_params['plateTemplate'] = ActiveRecord::Type::Boolean.new.cast(metadata_params['plateTemplate'])
|
||||
end
|
||||
end
|
||||
|
||||
def table_params
|
||||
raise TypeError unless params.require(:data).require(:type) == 'tables'
|
||||
|
||||
attributes_params = params.require(:data).require(:attributes).permit(
|
||||
:name,
|
||||
:contents,
|
||||
metadata: [
|
||||
:plateTemplate,
|
||||
{ cells: %i(col row className) }
|
||||
]
|
||||
)
|
||||
|
||||
convert_plate_template(attributes_params[:metadata])
|
||||
validate_metadata_params(attributes_params)
|
||||
attributes_params
|
||||
end
|
||||
|
||||
def validate_metadata_params(attributes_params)
|
||||
metadata = attributes_params[:metadata]
|
||||
contents = JSON.parse(attributes_params[:contents] || '{}')
|
||||
|
||||
if metadata.present? && metadata[:cells].present? && contents.present?
|
||||
metadata_cells = metadata[:cells]
|
||||
data = contents['data']
|
||||
|
||||
if data.present? && data[0].present?
|
||||
data_size = (data[0].is_a?(Array) ? data.size * data[0].size : data.size)
|
||||
|
||||
if data_size < metadata_cells.size
|
||||
error_message = I18n.t('api.core.errors.table.metadata.detail_too_many_cells')
|
||||
raise ActionController::BadRequest, error_message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
66
app/controllers/api/v2/result_texts_controller.rb
Normal file
66
app/controllers/api/v2/result_texts_controller.rb
Normal file
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V2
|
||||
class ResultTextsController < BaseController
|
||||
before_action :load_team, :load_project, :load_experiment, :load_task, :load_result
|
||||
before_action only: %i(show update destroy) do
|
||||
load_result_text(:id)
|
||||
end
|
||||
before_action :check_manage_permission, only: %i(create update destroy)
|
||||
|
||||
def index
|
||||
result_texts = timestamps_filter(@result.result_texts).page(params.dig(:page, :number))
|
||||
.per(params.dig(:page, :size))
|
||||
|
||||
render jsonapi: result_texts, each_serializer: ResultTextSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
render jsonapi: @result_text, serializer: ResultTextSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
result_text = @result.result_texts.new(result_text_params)
|
||||
|
||||
@result.with_lock do
|
||||
@result.result_orderable_elements.create!(
|
||||
position: @result.result_orderable_elements.size,
|
||||
orderable: result_text
|
||||
)
|
||||
|
||||
result_text.save!
|
||||
end
|
||||
|
||||
render jsonapi: result_text, serializer: ResultTextSerializer, status: :created
|
||||
end
|
||||
|
||||
def update
|
||||
@result_text.assign_attributes(result_text_params)
|
||||
|
||||
if @result_text.changed? && @result_text.save!
|
||||
render jsonapi: @result_text, serializer: ResultTextSerializer, status: :ok
|
||||
else
|
||||
render body: nil, status: :no_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@result_text.destroy!
|
||||
render body: nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_manage_permission
|
||||
raise PermissionError.new(Result, :manage) unless can_manage_result?(@result)
|
||||
end
|
||||
|
||||
def result_text_params
|
||||
raise TypeError unless params.require(:data).require(:type) == 'result_texts'
|
||||
|
||||
params.require(:data).require(:attributes).permit(:text, :name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
76
app/controllers/api/v2/results_controller.rb
Normal file
76
app/controllers/api/v2/results_controller.rb
Normal file
|
@ -0,0 +1,76 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V2
|
||||
class ResultsController < BaseController
|
||||
before_action :load_team, :load_project, :load_experiment, :load_task
|
||||
before_action only: %i(show update destroy) do
|
||||
load_result(:id)
|
||||
end
|
||||
before_action :check_create_permissions, only: :create
|
||||
before_action :check_delete_permissions, only: :destroy
|
||||
before_action :check_update_permissions, only: :update
|
||||
|
||||
def index
|
||||
results = timestamps_filter(@task.results).page(params.dig(:page, :number))
|
||||
.per(params.dig(:page, :size))
|
||||
render jsonapi: results, each_serializer: ResultSerializer,
|
||||
include: include_params
|
||||
end
|
||||
|
||||
def show
|
||||
render jsonapi: @result, serializer: ResultSerializer,
|
||||
include: include_params
|
||||
end
|
||||
|
||||
def create
|
||||
@result = Result.create!(
|
||||
user: current_user,
|
||||
my_module: @task,
|
||||
name: result_params[:name]
|
||||
)
|
||||
render jsonapi: @result, serializer: ResultSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
@result.assign_attributes(result_params)
|
||||
|
||||
if @result.changed? && @result.save!
|
||||
render jsonapi: @result, serializer: ResultSerializer
|
||||
else
|
||||
render body: nil, status: :no_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@result.destroy!
|
||||
render body: nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_create_permissions
|
||||
raise PermissionError.new(MyModule, :manage) unless can_manage_my_module?(@task)
|
||||
end
|
||||
|
||||
def check_delete_permissions
|
||||
raise PermissionError.new(Result, :delete) unless can_delete_result?(@result)
|
||||
end
|
||||
|
||||
def check_update_permissions
|
||||
raise PermissionError.new(Result, :manage) unless can_manage_result?(@result)
|
||||
end
|
||||
|
||||
def permitted_includes
|
||||
%w(comments result_texts tables assets)
|
||||
end
|
||||
|
||||
def result_params
|
||||
raise TypeError unless params.require(:data).require(:type) == 'results'
|
||||
|
||||
params.require(:data).require(:attributes).require(:name)
|
||||
params.require(:data).permit(attributes: %i(name archived))[:attributes]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -264,11 +264,7 @@ class AssetsController < ApplicationController
|
|||
end
|
||||
|
||||
# Return edit url and asset info
|
||||
render json: {
|
||||
attributes: AssetSerializer.new(asset, scope: { user: current_user }).as_json,
|
||||
success: true,
|
||||
edit_url: edit_url
|
||||
}, status: :ok
|
||||
render json: asset, scope: { user: current_user }
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
|
|
@ -48,6 +48,19 @@ module AssetsActions
|
|||
asset_name: { id: asset.id, value_for: 'file_name' },
|
||||
action: action
|
||||
})
|
||||
elsif asset.repository_cell.present?
|
||||
repository = asset.repository_cell.repository_row.repository
|
||||
Activities::CreateActivityService
|
||||
.call(activity_type: :edit_image_on_inventory_item,
|
||||
owner: current_user,
|
||||
subject: repository,
|
||||
team: repository.team,
|
||||
message_items: {
|
||||
repository: repository.id,
|
||||
repository_row: asset.repository_cell.repository_row.id,
|
||||
asset_name: { id: asset.id, value_for: 'file_name' },
|
||||
action: action
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,6 +48,7 @@ module StepsActions
|
|||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: checklist_item.text,
|
||||
subject: step.protocol,
|
||||
title: t('notifications.checklist_title',
|
||||
user: current_user.full_name,
|
||||
step: step.name),
|
||||
|
@ -59,6 +60,7 @@ module StepsActions
|
|||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: step_text.text,
|
||||
subject: step.protocol,
|
||||
title: t('notifications.step_text_title',
|
||||
user: current_user.full_name,
|
||||
step: step.name),
|
||||
|
@ -70,6 +72,7 @@ module StepsActions
|
|||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: checklist.name,
|
||||
subject: step.protocol,
|
||||
title: t('notifications.checklist_title',
|
||||
user: current_user.full_name,
|
||||
step: step.name),
|
||||
|
@ -81,6 +84,7 @@ module StepsActions
|
|||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: step.description,
|
||||
subject: step.protocol,
|
||||
title: t('notifications.step_description_title',
|
||||
user: current_user.full_name,
|
||||
step: step.name),
|
||||
|
|
|
@ -602,6 +602,7 @@ class ExperimentsController < ApplicationController
|
|||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: @experiment.description,
|
||||
subject: @experiment,
|
||||
title: t('notifications.experiment_annotation_title',
|
||||
experiment: @experiment.name,
|
||||
user: current_user.full_name),
|
||||
|
|
|
@ -264,6 +264,7 @@ class MyModuleRepositoriesController < ApplicationController
|
|||
smart_annotation_notification(
|
||||
old_text: nil,
|
||||
new_text: comment,
|
||||
subject: module_repository_row.repository_row,
|
||||
title: t('notifications.my_module_consumption_comment_annotation_title',
|
||||
repository_item: module_repository_row.repository_row.name,
|
||||
repository: @repository.name,
|
||||
|
|
|
@ -8,6 +8,7 @@ class MyModuleShareableLinksController < ApplicationController
|
|||
results_show)
|
||||
before_action :check_view_permissions, only: :show
|
||||
before_action :check_manage_permissions, except: %i(protocol_show
|
||||
show
|
||||
repository_index_dt
|
||||
repository_snapshot_index_dt
|
||||
download_asset
|
||||
|
|
|
@ -67,6 +67,7 @@ class MyModulesController < ApplicationController
|
|||
subject: @my_module,
|
||||
message_items: { my_module: @my_module.id }
|
||||
)
|
||||
log_user_designation_activity
|
||||
redirect_to canvas_experiment_path(@experiment) if params[:my_module][:view_mode] == 'canvas'
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
render json: @my_module.errors, status: :unprocessable_entity
|
||||
|
@ -529,6 +530,14 @@ class MyModulesController < ApplicationController
|
|||
log_activity(type_of, @my_module, message_items)
|
||||
end
|
||||
|
||||
def log_user_designation_activity
|
||||
users = User.where.not(id: current_user.id).where(id: params[:my_module][:user_ids])
|
||||
|
||||
users.each do |user|
|
||||
log_activity(:designate_user_to_my_module, @my_module, { user_target: user.id })
|
||||
end
|
||||
end
|
||||
|
||||
def log_activity(type_of, my_module = nil, message_items = {})
|
||||
my_module ||= @my_module
|
||||
message_items = { my_module: my_module.id }.merge(message_items)
|
||||
|
@ -552,6 +561,7 @@ class MyModulesController < ApplicationController
|
|||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: @my_module.description,
|
||||
subject: @my_module,
|
||||
title: t('notifications.my_module_description_annotation_title',
|
||||
my_module: @my_module.name,
|
||||
user: current_user.full_name),
|
||||
|
@ -566,6 +576,7 @@ class MyModulesController < ApplicationController
|
|||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: @my_module.protocol.description,
|
||||
subject: @my_module,
|
||||
title: t('notifications.my_module_protocol_annotation_title',
|
||||
my_module: @my_module.name,
|
||||
user: current_user.full_name),
|
||||
|
|
|
@ -1079,6 +1079,7 @@ class ProtocolsController < ApplicationController
|
|||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: @protocol.description,
|
||||
subject: @protocol,
|
||||
title: t('notifications.protocol_description_annotation_title',
|
||||
user: current_user.full_name,
|
||||
protocol: @protocol.name),
|
||||
|
|
|
@ -328,6 +328,7 @@ class RepositoriesController < ApplicationController
|
|||
render json: {
|
||||
html: render_to_string(
|
||||
partial: 'shared/flash_errors',
|
||||
formats: :html,
|
||||
locals: { error_title: t('repositories.import_records.error_message.errors_list_title'),
|
||||
error: t('repositories.import_records.error_message.no_repository_name') }
|
||||
)
|
||||
|
@ -357,6 +358,7 @@ class RepositoriesController < ApplicationController
|
|||
if repositories.present? && current_user.has_available_exports?
|
||||
current_user.increase_daily_exports_counter!
|
||||
RepositoriesExportJob.perform_later(repositories.pluck(:id), user_id: current_user.id, team_id: current_team.id)
|
||||
log_activity(:export_inventories, inventories: repositories.pluck(:name).join(', '))
|
||||
render json: { message: t('zip_export.export_request_success') }
|
||||
else
|
||||
render json: { message: t('zip_export.export_error') }, status: :unprocessable_entity
|
||||
|
@ -364,14 +366,18 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def export_repository_stock_items
|
||||
row_ids = @repository.repository_rows.where(id: params[:row_ids]).pluck(:id)
|
||||
if row_ids.any?
|
||||
repository_rows = @repository.repository_rows.where(id: params[:row_ids]).pluck(:id, :name)
|
||||
if repository_rows.any?
|
||||
RepositoryStockZipExportJob.perform_later(
|
||||
user_id: current_user.id,
|
||||
params: {
|
||||
repository_row_ids: row_ids
|
||||
repository_row_ids: repository_rows.map { |row| row[0] }
|
||||
}
|
||||
)
|
||||
log_activity(
|
||||
:export_inventory_stock_consumption,
|
||||
inventory_items: repository_rows.map { |row| row[1] }.join(', ')
|
||||
)
|
||||
render json: { message: t('zip_export.export_request_success') }
|
||||
else
|
||||
render json: { message: t('zip_export.export_error') }, status: :unprocessable_entity
|
||||
|
@ -532,6 +538,7 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def log_activity(type_of, message_items = {})
|
||||
if @repository.present?
|
||||
message_items = { repository: @repository.id }.merge(message_items)
|
||||
|
||||
Activities::CreateActivityService
|
||||
|
@ -540,6 +547,14 @@ class RepositoriesController < ApplicationController
|
|||
subject: @repository,
|
||||
team: @repository.team,
|
||||
message_items: message_items)
|
||||
else
|
||||
Activities::CreateActivityService
|
||||
.call(activity_type: type_of,
|
||||
owner: current_user,
|
||||
subject: @current_team,
|
||||
team: @current_team,
|
||||
message_items: message_items)
|
||||
end
|
||||
end
|
||||
|
||||
def set_breadcrumbs_items
|
||||
|
|
|
@ -210,7 +210,7 @@ class RepositoryRowsController < ApplicationController
|
|||
return render json: { name: @repository_row.name } if update_params['repository_row'].present?
|
||||
|
||||
column = row_cell_update.column
|
||||
cell = row_cell_update.cell
|
||||
cell = row_cell_update.cell&.reload || row_cell_update.cell
|
||||
data = { value_type: column.data_type, id: column.id, value: nil }
|
||||
|
||||
return render json: data if cell.blank?
|
||||
|
@ -452,6 +452,7 @@ class RepositoryRowsController < ApplicationController
|
|||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: cell.value.data,
|
||||
subject: cell.repository_column.repository,
|
||||
title: t('notifications.repository_annotation_title',
|
||||
user: current_user.full_name,
|
||||
column: cell.repository_column.name,
|
||||
|
|
|
@ -100,6 +100,7 @@ module ResultElements
|
|||
smart_annotation_notification(
|
||||
old_text: (old_text if old_text),
|
||||
new_text: @result_text.text,
|
||||
subject: @result,
|
||||
title: t('notifications.result_annotation_title',
|
||||
result: @result.name,
|
||||
user: current_user.full_name),
|
||||
|
|
|
@ -121,6 +121,7 @@ class ResultTextsController < ApplicationController
|
|||
smart_annotation_notification(
|
||||
old_text: (old_text if old_text),
|
||||
new_text: @result_text.text,
|
||||
subject: @result,
|
||||
title: t('notifications.result_annotation_title',
|
||||
result: @result.name,
|
||||
user: current_user.full_name),
|
||||
|
|
|
@ -60,6 +60,7 @@ class TeamRepositoriesController < ApplicationController
|
|||
|
||||
def check_sharing_permissions
|
||||
render_403 unless can_share_repository?(@repository)
|
||||
render_403 if !@repository.shareable_write? && update_params[:write_permissions].present?
|
||||
end
|
||||
|
||||
def teams_to_share
|
||||
|
|
|
@ -4,22 +4,17 @@ class UserNotificationsController < ApplicationController
|
|||
prepend_before_action -> { request.env['devise.skip_trackable'] = true }, only: :unseen_counter
|
||||
|
||||
def index
|
||||
page = (params[:page] || 1).to_i
|
||||
notifications = load_notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT).without_count
|
||||
page = (params.dig(:page, :number) || 1).to_i
|
||||
notifications = load_notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT)
|
||||
|
||||
render json: {
|
||||
notifications: notification_serializer(notifications),
|
||||
next_page: notifications.next_page
|
||||
}
|
||||
render json: notifications, each_serializer: NotificationSerializer
|
||||
|
||||
UserNotification.where(
|
||||
notification_id: notifications.except(:select).where.not(type_of: 2).select(:id)
|
||||
).seen_by_user(current_user)
|
||||
notifications.mark_as_read!
|
||||
end
|
||||
|
||||
def unseen_counter
|
||||
render json: {
|
||||
unseen: load_notifications.where('user_notifications.checked = ?', false).size
|
||||
unseen: load_notifications.where(read_at: nil).size
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -27,21 +22,8 @@ class UserNotificationsController < ApplicationController
|
|||
|
||||
def load_notifications
|
||||
current_user.notifications
|
||||
.select(:id, :type_of, :title, :message, :created_at, 'user_notifications.checked')
|
||||
.in_app
|
||||
.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def notification_serializer(notifications)
|
||||
notifications.map do |notification|
|
||||
{
|
||||
id: notification.id,
|
||||
type_of: notification.type_of,
|
||||
title: notification.title,
|
||||
message: notification.message,
|
||||
created_at: I18n.l(notification.created_at, format: :full),
|
||||
today: notification.created_at.today?,
|
||||
checked: notification.checked
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,9 +4,9 @@ module Users
|
|||
class PreferencesController < ApplicationController
|
||||
before_action :load_user, only: [
|
||||
:index,
|
||||
:update,
|
||||
:update_togglable_settings
|
||||
:update
|
||||
]
|
||||
before_action :set_breadcrumbs_items, only: %i(index)
|
||||
layout 'fluid'
|
||||
|
||||
def index
|
||||
|
@ -20,30 +20,6 @@ module Users
|
|||
end
|
||||
end
|
||||
|
||||
def update_togglable_settings
|
||||
read_from_params(:assignments_notification) do |val|
|
||||
@user.assignments_notification = val
|
||||
end
|
||||
read_from_params(:recent_notification) do |val|
|
||||
@user.recent_notification = val
|
||||
end
|
||||
read_from_params(:recent_notification_email) do |val|
|
||||
@user.recent_email_notification = val
|
||||
end
|
||||
read_from_params(:assignments_notification_email) do |val|
|
||||
@user.assignments_email_notification = val
|
||||
end
|
||||
if @user.save
|
||||
render json: {
|
||||
status: :ok
|
||||
}
|
||||
else
|
||||
render json: {
|
||||
status: :unprocessable_entity
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_user
|
||||
|
@ -51,12 +27,21 @@ module Users
|
|||
end
|
||||
|
||||
def update_params
|
||||
params.require(:user).permit(:time_zone, :date_format)
|
||||
params.require(:user).permit(:time_zone, :date_format, notifications_settings: {})
|
||||
end
|
||||
|
||||
def read_from_params(name)
|
||||
yield(params.include?(name) ? true : false)
|
||||
end
|
||||
|
||||
def set_breadcrumbs_items
|
||||
@breadcrumbs_items = [{
|
||||
label: t('notifications.breadcrumb'),
|
||||
url: preferences_path
|
||||
}]
|
||||
|
||||
@breadcrumbs_items
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -71,6 +71,7 @@ module ApplicationHelper
|
|||
message = options.fetch(:message) { :message_must_be_present }
|
||||
old_text = options[:old_text] || ''
|
||||
new_text = options[:new_text]
|
||||
subject = options[:subject]
|
||||
return if new_text.blank?
|
||||
|
||||
sa_user = /\[\@(.*?)~([0-9a-zA-Z]+)\]/
|
||||
|
@ -96,17 +97,21 @@ module ApplicationHelper
|
|||
target_user = User.find_by_id(user_id)
|
||||
next unless target_user
|
||||
|
||||
generate_annotation_notification(target_user, title, message)
|
||||
generate_annotation_notification(target_user, title, subject)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_annotation_notification(target_user, title, message)
|
||||
notification = Notification.create(
|
||||
type_of: :assignment,
|
||||
def generate_annotation_notification(target_user, title, subject)
|
||||
GeneralNotification.send_notifications(
|
||||
{
|
||||
type: :smart_annotation_added,
|
||||
title: sanitize_input(title),
|
||||
message: sanitize_input(message)
|
||||
subject_id: subject.id,
|
||||
subject_class: subject.class.name,
|
||||
subject_name: subject.respond_to?(:name) && subject.name,
|
||||
user: target_user
|
||||
}
|
||||
)
|
||||
UserNotification.create(notification: notification, user: target_user) if target_user.assignments_notification
|
||||
end
|
||||
|
||||
def custom_link_open_new_tab(text)
|
||||
|
|
|
@ -125,6 +125,7 @@ module CommentHelper
|
|||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: comment.message,
|
||||
subject: result,
|
||||
title: t('notifications.result_comment_annotation_title',
|
||||
result: result.name,
|
||||
user: current_user.full_name),
|
||||
|
@ -147,6 +148,7 @@ module CommentHelper
|
|||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: comment.message,
|
||||
subject: project,
|
||||
title: t('notifications.project_comment_annotation_title',
|
||||
project: project.name,
|
||||
user: current_user.full_name),
|
||||
|
@ -160,6 +162,7 @@ module CommentHelper
|
|||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: comment.message,
|
||||
subject: step.protocol,
|
||||
title: t('notifications.step_comment_annotation_title',
|
||||
step: step.name,
|
||||
user: current_user.full_name),
|
||||
|
@ -184,6 +187,7 @@ module CommentHelper
|
|||
smart_annotation_notification(
|
||||
old_text: old_text,
|
||||
new_text: comment.message,
|
||||
subject: my_module,
|
||||
title: t('notifications.my_module_comment_annotation_title',
|
||||
my_module: my_module.name,
|
||||
user: current_user.full_name),
|
||||
|
|
|
@ -10,22 +10,21 @@ module NotificationsHelper
|
|||
unassigned_user: target_user.name,
|
||||
team: team.name,
|
||||
unassigned_by_user: user.name)
|
||||
if role
|
||||
title = I18n.t('notifications.assign_user_to_team',
|
||||
assigned_user: target_user.name,
|
||||
role: role,
|
||||
team: team.name,
|
||||
assigned_by_user: user.name) if role
|
||||
assigned_by_user: user.name)
|
||||
end
|
||||
message = "#{I18n.t('search.index.team')} #{team.name}"
|
||||
end
|
||||
|
||||
notification = Notification.create(
|
||||
type_of: :assignment,
|
||||
GeneralNotification.send_notifications({
|
||||
type: role ? :invite_user_to_team : :remove_user_from_team,
|
||||
title: sanitize_input(title),
|
||||
message: sanitize_input(message)
|
||||
)
|
||||
|
||||
if target_user.assignments_notification
|
||||
notification.create_user_notification(target_user)
|
||||
end
|
||||
message: sanitize_input(message),
|
||||
user: target_user
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -236,13 +236,13 @@ module RepositoryDatatableHelper
|
|||
def linked_repository_default_columns(record)
|
||||
{
|
||||
'1': assigned_row(record),
|
||||
'2': escape_input(record.external_id),
|
||||
'3': record.code,
|
||||
'4': escape_input(record.name),
|
||||
'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)
|
||||
'2': record.code,
|
||||
'3': escape_input(record.name),
|
||||
'4': I18n.l(record.created_at, format: :full),
|
||||
'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),
|
||||
'8': escape_input(record.external_id)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ window.initManageStockValueModalComponent = () => {
|
|||
app.component('ManageStockValueModal', ManageStockValueModal);
|
||||
app.use(PerfectScrollbar);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
mountWithTurbolinks(app, '#manageStockValueModal');
|
||||
mountWithTurbolinks(app, '#manageStockValueModal', () => {
|
||||
window.manageStockModalComponent = null;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
10
app/javascript/packs/vue/user_preferences.js
Normal file
10
app/javascript/packs/vue/user_preferences.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import PerfectScrollbar from 'vue3-perfect-scrollbar';
|
||||
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||
import UserPreferences from '../../vue/user_preferences/container.vue';
|
||||
import { mountWithTurbolinks } from './helpers/turbolinks.js';
|
||||
|
||||
const app = createApp({});
|
||||
app.component('UserPreferences', UserPreferences);
|
||||
app.use(PerfectScrollbar);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
mountWithTurbolinks(app, '#user_preferences');
|
|
@ -98,8 +98,8 @@ export default {
|
|||
},
|
||||
reloadCurrentLevel: function() {
|
||||
if (this.reloadCurrentLevel && (
|
||||
this.currentItemId.length == 0 ||
|
||||
this.menuItems.filter(item => item.id == this.currentItemId)
|
||||
this.currentItemId?.length === 0
|
||||
|| this.menuItems.filter((item) => item.id === this.currentItemId)
|
||||
)) {
|
||||
this.loadTree();
|
||||
}
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
<template>
|
||||
<div class="sci-navigation--notificaitons-flyout-notification">
|
||||
<div class="sci-navigation--notificaitons-flyout-notification-icon" :class="notification.type_of">
|
||||
<i :class="icon"></i>
|
||||
</div>
|
||||
<div class="sci-navigation--notificaitons-flyout-notification-date">
|
||||
{{ notification.created_at }}
|
||||
{{ notification.attributes.created_at }}
|
||||
</div>
|
||||
<div class="sci-navigation--notificaitons-flyout-notification-title"
|
||||
v-html="notification.title"
|
||||
:data-seen="notification.checked"></div>
|
||||
<div v-html="notification.message" class="sci-navigation--notificaitons-flyout-notification-message"></div>
|
||||
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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -21,7 +26,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
icon() {
|
||||
switch(this.notification.type_of) {
|
||||
switch(this.notification.attributes.type_of) {
|
||||
case 'deliver':
|
||||
return 'fas fa-truck';
|
||||
case 'assignment':
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
<script>
|
||||
|
||||
import NotificationItem from './notification_item.vue'
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
|
||||
export default {
|
||||
name: 'NotificationsFlyout',
|
||||
|
@ -39,17 +40,17 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
notifications: [],
|
||||
nextPage: 1,
|
||||
nextPageUrl: null,
|
||||
scrollBar: null,
|
||||
loadingPage: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.nextPageUrl = this.notificationsUrl;
|
||||
this.loadNotifications();
|
||||
},
|
||||
mounted() {
|
||||
let container = this.$refs.scrollContainer.$el
|
||||
document.body.style.overflow = 'hidden'
|
||||
|
||||
container.addEventListener('ps-scroll-y', (e) => {
|
||||
if (e.target.scrollTop + e.target.clientHeight >= e.target.scrollHeight - 20) {
|
||||
|
@ -57,31 +58,38 @@ export default {
|
|||
}
|
||||
})
|
||||
},
|
||||
destroyed() {
|
||||
document.body.style.overflow = 'scroll'
|
||||
beforeUnmount() {
|
||||
document.body.style.overflow = 'scroll';
|
||||
},
|
||||
computed: {
|
||||
filteredNotifications() {
|
||||
this.loadNotifications();
|
||||
},
|
||||
todayNotifications() {
|
||||
return this.notifications.filter(n => n.today);
|
||||
return this.notifications.filter(n => n.attributes.today);
|
||||
},
|
||||
olderNotifications() {
|
||||
return this.notifications.filter(n => !n.today);
|
||||
return this.notifications.filter(n => !n.attributes.today);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadNotifications() {
|
||||
if (this.nextPage == null || this.loadingPage) return;
|
||||
if (this.nextPageUrl == null || this.loadingPage) return;
|
||||
|
||||
this.loadingPage = true;
|
||||
$.getJSON(this.notificationsUrl, { page: this.nextPage }, (result) => {
|
||||
this.notifications = this.notifications.concat(result.notifications);
|
||||
this.nextPage = result.next_page;
|
||||
|
||||
axios.get(this.nextPageUrl)
|
||||
.then(response => {
|
||||
this.notifications = this.notifications.concat(response.data.data);
|
||||
this.nextPageUrl = response.data.links.next;
|
||||
this.loadingPage = false;
|
||||
this.$emit('update:unseenNotificationsCount');
|
||||
})
|
||||
.catch(error => {
|
||||
this.loadingPage = false;
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,6 +122,15 @@
|
|||
)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
notificationsOpened(newVal) {
|
||||
if (newVal === true) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else if (newVal === false) {
|
||||
document.body.style.overflow = 'scroll';
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchData() {
|
||||
$.get(this.url, (result) => {
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
alwaysAllowSave: true,
|
||||
menuFilter: this.menuFilter,
|
||||
beforeReadOnlyChange: this.readOnlyHandler,
|
||||
showCircularity: true,
|
||||
ToolBarProps: {
|
||||
toolList: [
|
||||
'saveTool',
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="portocol-header-left-part grow">
|
||||
<template v-if="headerSticked && moduleName">
|
||||
<i class="sn-icon sn-icon-navigator sci--layout--navigator-open cursor-pointer p-1.5 border rounded border-sn-light-grey mr-4"></i>
|
||||
<div @click="scrollTop" class="task-section-title w-[calc(100%_-_4rem)] min-w-[5rem] cursor-pointer">
|
||||
<div @click="scrollTop" class="task-section-title w-[calc(100%_-_35rem)] min-w-[5rem] cursor-pointer">
|
||||
<h2 class="truncate leading-6">{{ moduleName }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -25,7 +25,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="actions-block">
|
||||
<div class="protocol-buttons-group">
|
||||
<div class="protocol-buttons-group shrink-0">
|
||||
<a v-if="urls.add_step_url"
|
||||
class="btn btn-secondary"
|
||||
:title="i18n.t('protocols.steps.new_step_title')"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<transition enter-class="translate-x-full w-0"
|
||||
<transition enter-from-class="translate-x-full w-0"
|
||||
enter-active-class="transition-all ease-sharp duration-[588ms]"
|
||||
leave-active-class="transition-all ease-sharp duration-[588ms]"
|
||||
leave-to-class="translate-x-full w-0">
|
||||
|
@ -12,9 +12,12 @@
|
|||
class="sticky top-0 right-0 bg-white flex z-50 flex-col h-[78px] pt-6">
|
||||
<div class="header flex w-full h-[30px] pr-6">
|
||||
<repository-item-sidebar-title v-if="defaultColumns"
|
||||
:editable="permissions?.can_manage && !defaultColumns?.archived" :name="defaultColumns.name"
|
||||
@update="update"></repository-item-sidebar-title>
|
||||
<i id="close-icon" @click="toggleShowHideSidebar(currentItemUrl)"
|
||||
:editable="permissions?.can_manage && !defaultColumns?.archived"
|
||||
:name="defaultColumns.name"
|
||||
:archived="defaultColumns.archived"
|
||||
@update="update">
|
||||
</repository-item-sidebar-title>
|
||||
<i id="close-icon" @click="toggleShowHideSidebar(null)"
|
||||
class="sn-icon sn-icon-close ml-auto cursor-pointer my-auto mx-0"></i>
|
||||
</div>
|
||||
<div id="divider" class="w-500 bg-sn-light-grey flex items-center self-stretch h-px mt-6 mr-6"></div>
|
||||
|
@ -27,7 +30,7 @@
|
|||
|
||||
<div v-else class="flex flex-1 flex-grow-1 justify-between" ref="scrollSpyContent" id="scrollSpyContent">
|
||||
|
||||
<div id="left-col" class="flex flex-col gap-4">
|
||||
<div id="left-col" class="flex flex-col gap-4 max-w-[350px]">
|
||||
|
||||
<!-- INFORMATION -->
|
||||
<section id="information-section">
|
||||
|
@ -128,7 +131,7 @@
|
|||
<!-- ASSIGNED -->
|
||||
<section id="assigned-section" class="flex flex-col" ref="assignedSectionRef">
|
||||
<div
|
||||
class="flex flex-row text-base font-semibold w-[350px] pb-4 leading-7 items-center justify-between transition-colors duration-300"
|
||||
class="flex flex-row text-lg font-semibold w-[350px] pb-4 leading-7 items-center justify-between transition-colors duration-300"
|
||||
ref="assigned-label"
|
||||
id="assigned-label"
|
||||
>
|
||||
|
@ -156,7 +159,7 @@
|
|||
</div>
|
||||
<div v-for="(assigned, index) in assignedModules.viewable_modules" :key="`assigned_module_${index}`"
|
||||
class="flex flex-col w-[350px] h-auto gap-4">
|
||||
<div class="flex flex-col gap-3.5">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div v-for="(item, index_assigned) in assigned" :key="`assigned_element_${index_assigned}`">
|
||||
{{ i18n.t(`repositories.item_card.assigned.labels.${item.type}`) }}
|
||||
<a :href="item.url" class="text-sn-science-blue hover:text-sn-science-blue hover:no-underline">
|
||||
|
@ -177,7 +180,7 @@
|
|||
|
||||
<!-- QR -->
|
||||
<section id="qr-section" ref="QR-label">
|
||||
<div id="QR-label" class="font-inter text-base font-semibold leading-7 mb-4 mt-0 transition-colors duration-300">
|
||||
<div id="QR-label" class="font-inter text-lg font-semibold leading-7 mb-4 mt-0 transition-colors duration-300">
|
||||
{{ i18n.t('repositories.item_card.section.qr') }}
|
||||
</div>
|
||||
<div class="bar-code-container">
|
||||
|
@ -190,12 +193,36 @@
|
|||
|
||||
<!-- NAVIGATION -->
|
||||
<div v-if="isShowing && !dataLoading" ref="navigationRef" id="navigation"
|
||||
class="flex item-end gap-x-4 min-w-[130px] min-h-[130px] h-fit sticky top-0 right-[4px] ">
|
||||
class="flex item-end gap-x-4 min-w-[130px] min-h-[130px] h-fit sticky top-0 pr-6 [scrollbar-gutter:stable_both-edges] ">
|
||||
<scroll-spy :itemsToCreate="[
|
||||
{ id: 'highlight-item-1', textId: 'text-item-1', labelAlias: 'information_label', label: 'information-label', sectionId: 'information-section' },
|
||||
{ id: 'highlight-item-2', textId: 'text-item-2', labelAlias: 'custom_columns_label', label: 'custom-columns-label', sectionId: 'custom-columns-section' },
|
||||
{ id: 'highlight-item-3', textId: 'text-item-3', labelAlias: 'assigned_label', label: 'assigned-label', sectionId: 'assigned-section' },
|
||||
{ id: 'highlight-item-4', textId: 'text-item-4', labelAlias: 'QR_label', label: 'QR-label', sectionId: 'qr-section' }
|
||||
{
|
||||
id: 'highlight-item-1',
|
||||
textId: 'text-item-1',
|
||||
labelAlias: 'information_label',
|
||||
label: 'information-label',
|
||||
sectionId: 'information-section'
|
||||
},
|
||||
{
|
||||
id: 'highlight-item-2',
|
||||
textId: 'text-item-2',
|
||||
labelAlias: 'custom_columns_label',
|
||||
label: 'custom-columns-label',
|
||||
sectionId: 'custom-columns-section'
|
||||
},
|
||||
{
|
||||
id: 'highlight-item-3',
|
||||
textId: 'text-item-3',
|
||||
labelAlias: 'assigned_label',
|
||||
label: 'assigned-label',
|
||||
sectionId: 'assigned-section'
|
||||
},
|
||||
{
|
||||
id: 'highlight-item-4',
|
||||
textId: 'text-item-4',
|
||||
labelAlias: 'QR_label',
|
||||
label: 'QR-label',
|
||||
sectionId: 'qr-section'
|
||||
}
|
||||
]" v-show="isShowing">
|
||||
</scroll-spy>
|
||||
</div>
|
||||
|
@ -252,6 +279,11 @@ export default {
|
|||
inRepository: false
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
reloadRepoItemSidebar: this.reload,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
window.repositoryItemSidebarComponent = this;
|
||||
},
|
||||
|
@ -288,28 +320,27 @@ export default {
|
|||
this.isShowing = true;
|
||||
this.loadRepositoryRow(repositoryRowUrl);
|
||||
this.currentItemUrl = repositoryRowUrl;
|
||||
return
|
||||
return;
|
||||
}
|
||||
// click on the same item - should just open/close it
|
||||
else if (this.currentItemUrl === repositoryRowUrl) {
|
||||
this.isShowing = !this.isShowing;
|
||||
return
|
||||
// same item click
|
||||
if (repositoryRowUrl === this.currentItemUrl) {
|
||||
if (this.isShowing) {
|
||||
this.toggleShowHideSidebar(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// explicit close (from emit)
|
||||
else if (repositoryRowUrl === null) {
|
||||
if (repositoryRowUrl === null) {
|
||||
this.isShowing = false;
|
||||
this.currentItemUrl = null;
|
||||
this.myModuleId = null;
|
||||
return
|
||||
return;
|
||||
}
|
||||
// click on a different item - if the item card is already showing should just fetch new data
|
||||
else {
|
||||
this.isShowing = true;
|
||||
this.myModuleId = myModuleId;
|
||||
this.loadRepositoryRow(repositoryRowUrl);
|
||||
this.currentItemUrl = repositoryRowUrl;
|
||||
return
|
||||
}
|
||||
},
|
||||
loadRepositoryRow(repositoryRowUrl) {
|
||||
this.dataLoading = true
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<inline-edit v-if="editable" class="item-name my-auto text-xl font-semibold" :value="name" :characterLimit="255"
|
||||
: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>
|
||||
<h4 v-else class="item-name my-auto truncate text-xl" :title="name">
|
||||
{{ name }}
|
||||
<h4 v-else class="item-name my-auto truncate text-xl" :title="computedName">
|
||||
{{ computedName }}
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
|
@ -16,9 +17,16 @@ export default {
|
|||
components: {
|
||||
"inline-edit": InlineEdit
|
||||
},
|
||||
emits: ['update'],
|
||||
props: {
|
||||
editable: Boolean,
|
||||
name: String,
|
||||
archived: Boolean,
|
||||
},
|
||||
computed: {
|
||||
computedName() {
|
||||
return this.archived ? `(A) ${this.name}` : this.name;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateName(name) {
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
:updatePath="updatePath"
|
||||
:optionsPath="column.options_path"
|
||||
:inArchivedRepositoryRow="inArchivedRepositoryRow"
|
||||
:decimals="column.decimals"
|
||||
:canEdit="permissions.can_manage && !inArchivedRepositoryRow"
|
||||
:editingField="editingField"
|
||||
@setEditingField="editingField = $event"
|
||||
|
|
|
@ -91,7 +91,6 @@ export default {
|
|||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.params = defaultParams;
|
||||
},
|
||||
formatDateTime(date, field = null) {
|
||||
|
|
|
@ -1,231 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
@click="enableEdit"
|
||||
v-click-outside="validateAndSave"
|
||||
class="text-sn-dark-grey font-inter text-sm font-normal leading-5 w-full rounded relative"
|
||||
:class="editableClassName"
|
||||
>
|
||||
<div v-if="dateType === 'date'">
|
||||
<div v-if="isEditing || values?.datetime" ref="edit">
|
||||
<DateTimePicker
|
||||
:disabled="!canEdit"
|
||||
@change="formatDateTime($event)"
|
||||
:selectorId="`DatePicker${colId}`"
|
||||
:dateOnly="true"
|
||||
:defaultValue="dateValue(values?.datetime)"
|
||||
:standAlone="true"
|
||||
/>
|
||||
</div>
|
||||
<div v-else ref="view" :class="{ 'text-sn-dark-grey': !canEdit, 'text-sn-grey': canEdit }" >
|
||||
{{ i18n.t(`repositories.item_card.repository_date_value.${canEdit ? 'placeholder' : 'no_date'}`) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="dateType === 'dateRange'">
|
||||
<div v-if="isEditing || (timeFrom?.datetime && timeTo?.datetime)" ref="edit" class="w-full flex align-center">
|
||||
<div>
|
||||
<DateTimePicker
|
||||
:disabled="!canEdit"
|
||||
@change="formatDateTime($event, 'start_time')"
|
||||
:selectorId="`DatePickerStart${colId}`"
|
||||
:dateOnly="true"
|
||||
:defaultValue="dateValue(timeFrom?.datetime)"
|
||||
:standAlone="true"
|
||||
:dateClassName="hasMonthText() ? 'w-[135px]' : 'w-[90px]'"
|
||||
/>
|
||||
</div>
|
||||
<span class="mr-3">-</span>
|
||||
<div>
|
||||
<DateTimePicker
|
||||
:disabled="!canEdit"
|
||||
@change="formatDateTime($event, 'end_time')"
|
||||
:selectorId="`DatePickerEnd${colId}`"
|
||||
:dateOnly="true"
|
||||
:defaultValue="dateValue(timeTo?.datetime)"
|
||||
:standAlone="true"
|
||||
:dateClassName="hasMonthText() ? 'w-[135px]' : 'ml-2 w-[90px]'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else ref="view" :class="{ 'text-sn-dark-grey': !canEdit, 'text-sn-grey': canEdit }" >
|
||||
{{ i18n.t(`repositories.item_card.repository_date_range_value.${canEdit ? 'placeholder' : 'no_date_range'}`) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="dateType === 'dateTime'">
|
||||
<div v-if="isEditing || values?.datetime" ref="edit" class="w-full">
|
||||
<DateTimePicker
|
||||
:disabled="!canEdit"
|
||||
@change="formatDateTime"
|
||||
:selectorId="`DatePicker${colId}`"
|
||||
:defaultValue="dateValue(values?.datetime)"
|
||||
:standAlone="true"
|
||||
:dateClassName="hasMonthText() ? 'w-[135px]' : 'w-[90px]'"
|
||||
timeClassName="w-11"
|
||||
/>
|
||||
</div>
|
||||
<div v-else ref="view" :class="{ 'text-sn-dark-grey': !canEdit, 'text-sn-grey': canEdit }" >
|
||||
{{ i18n.t(`repositories.item_card.repository_date_time_value.${canEdit ? 'placeholder' : 'no_date_time'}`) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="dateType === 'dateTimeRange'">
|
||||
<div v-if="isEditing || (timeFrom?.datetime && timeTo?.datetime)" ref="edit" class="w-full flex">
|
||||
<div>
|
||||
<DateTimePicker
|
||||
:disabled="!canEdit"
|
||||
@change="formatDateTime($event, 'start_time')"
|
||||
:selectorId="`DatePickerStart${colId}`"
|
||||
:defaultValue="dateValue(timeFrom?.datetime)"
|
||||
:timeOnly="false"
|
||||
:dateOnly="false"
|
||||
:standAlone="true"
|
||||
:dateClassName="hasMonthText() ? 'w-[135px]' : 'w-[90px]'"
|
||||
timeClassName="w-11"
|
||||
/>
|
||||
</div>
|
||||
<span class="mx-1">-</span>
|
||||
<div>
|
||||
<DateTimePicker
|
||||
:disabled="!canEdit"
|
||||
@change="formatDateTime($event, 'end_time')"
|
||||
:selectorId="`DatePickerEnd${colId}`"
|
||||
:defaultValue="dateValue(timeTo?.datetime)"
|
||||
:timeOnly="false"
|
||||
:dateOnly="false"
|
||||
:standAlone="true"
|
||||
:dateClassName="hasMonthText() ? 'w-[135px]' : 'ml-2 w-[90px]'"
|
||||
timeClassName="w-11"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else ref="view" :class="{ 'text-sn-dark-grey': !canEdit, 'text-sn-grey': canEdit }" >
|
||||
{{ i18n.t(`repositories.item_card.repository_date_time_range_value.${canEdit ? 'placeholder' : 'no_date_time_range'}`) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="dateType === 'time'">
|
||||
<div v-if="isEditing || values?.datetime" ref="edit">
|
||||
<DateTimePicker
|
||||
:disabled="!canEdit"
|
||||
@change="formatDateTime"
|
||||
:selectorId="`DatePicker${colId}`"
|
||||
:timeOnly="true"
|
||||
:defaultValue="dateValue(values?.datetime)"
|
||||
:standAlone="true"
|
||||
timeClassName="w-11"
|
||||
/>
|
||||
</div>
|
||||
<div v-else ref="view" :class="{ 'text-sn-dark-grey': !canEdit, 'text-sn-grey': canEdit }">
|
||||
{{ i18n.t(`repositories.item_card.repository_time_value.${ canEdit ? 'placeholder' : 'no_time'}`) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="dateType === 'timeRange'">
|
||||
<div v-if="isEditing || (timeFrom?.datetime && timeTo?.datetime)" ref="edit" class="w-full flex">
|
||||
<div>
|
||||
<DateTimePicker
|
||||
:disabled="!canEdit"
|
||||
@change="formatDateTime($event, 'start_time')"
|
||||
:selectorId="`DatePickerStart${colId}`"
|
||||
:timeOnly="true"
|
||||
:defaultValue="dateValue(timeFrom?.datetime)"
|
||||
:standAlone="true"
|
||||
timeClassName="w-11"
|
||||
/>
|
||||
</div>
|
||||
<span class="mx-1">-</span>
|
||||
<div>
|
||||
<DateTimePicker
|
||||
:disabled="!canEdit"
|
||||
@change="formatDateTime($event, 'end_time')"
|
||||
:selectorId="`DatePickerEnd${colId}`"
|
||||
:timeOnly="true"
|
||||
:defaultValue="dateValue(timeTo?.datetime)"
|
||||
:standAlone="true"
|
||||
timeClassName="ml-2 w-11"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else ref="view" :class="{ 'text-sn-dark-grey': !canEdit, 'text-sn-grey': canEdit }">
|
||||
{{ i18n.t(`repositories.item_card.repository_time_range_value.${canEdit ? 'placeholder' : 'no_time_range'}`) }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="absolute right-2 top-1.5" v-if="values?.reminder">
|
||||
<Reminder :value="values" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sn-delete-red text-xs w-full " :class="{ visible: errorMessage, invisible: !errorMessage }">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { vOnClickOutside } from '@vueuse/components'
|
||||
import date_time_range from './../mixins/date_time_range';
|
||||
import DateTimePicker from '../../shared/date_time_picker.vue';
|
||||
import Reminder from './../reminder.vue';
|
||||
|
||||
export default {
|
||||
name: 'DateTimeRange',
|
||||
mixins: [date_time_range],
|
||||
components: {
|
||||
DateTimePicker,
|
||||
Reminder
|
||||
},
|
||||
directives: {
|
||||
'click-outside': vOnClickOutside
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
values: {},
|
||||
errorMessage: null,
|
||||
params: null,
|
||||
cellUpdatePath: null,
|
||||
timeFrom: null,
|
||||
timeTo: null,
|
||||
isEditing: false,
|
||||
initValue: null,
|
||||
initStartDate: null,
|
||||
initEndDate: null
|
||||
}
|
||||
},
|
||||
props: {
|
||||
dateType: String,
|
||||
colVal: null,
|
||||
colId: null,
|
||||
updatePath: null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
editingField: false,
|
||||
canEdit: { type: Boolean, default: false }
|
||||
|
||||
},
|
||||
computed: {
|
||||
editableClassName() {
|
||||
const className = 'border-solid border-[1px] py-2 px-3 sci-cursor-edit'
|
||||
if (this.canEdit && this.errorMessage) return `${className} border-sn-delete-red`;
|
||||
if (this.canEdit && this.isEditing) return `${className} border-sn-science-blue`;
|
||||
if (this.canEdit) return `${className} border-sn-light-grey hover:border-sn-sleepy-grey`;
|
||||
return ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.cellUpdatePath = this.updatePath;
|
||||
this.values = this.colVal || {};
|
||||
this.timeFrom = this.startTime
|
||||
this.timeTo = this.endTime
|
||||
this.errorMessage = null;
|
||||
this.setParams();
|
||||
this.initDate = this.colVal?.datetime;
|
||||
this.initStartDate = this.startTime?.datetime;
|
||||
this.initEndDate = this.endTime?.datetime;
|
||||
},
|
||||
watch: {
|
||||
isEditing(newValue) {
|
||||
if (!newValue) return;
|
||||
// Focus input field to open date picker
|
||||
this.$nextTick(() => {
|
||||
$(this.$refs.edit)?.find('input')[0]?.focus();
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,19 +1,28 @@
|
|||
<template>
|
||||
<div id="repository-asset-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="font-inter text-sm font-semibold leading-5 truncate" :title="colName">
|
||||
{{ colName }}
|
||||
</div>
|
||||
<div class="w-fit absolute right-0 top-7">
|
||||
<a v-if="!file_name && (!uploading || error) && canEdit"
|
||||
class="btn-text-link font-normal" @click="openFileChooser">
|
||||
class="btn-text-link font-normal min-w-fit pl-4" @click="openFileChooser">
|
||||
{{ i18n.t('repositories.item_card.repository_asset_value.add_asset') }}
|
||||
</a>
|
||||
<div v-if="file_name && !uploading && canEdit" class="flex whitespace-nowrap gap-4 min-w-fit pl-4">
|
||||
<a class="btn-text-link font-normal" @click="openFileChooser">
|
||||
{{ i18n.t('general.replace') }}
|
||||
</a>
|
||||
<a class="btn-text-link font-normal" @click="clearFile">
|
||||
{{ i18n.t('general.delete') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!uploading">
|
||||
<div v-if="file_name">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="w-full cursor-pointer text-sn-science-blue relative" @mouseover="tooltipShowing = true" @mouseout="tooltipShowing = false">
|
||||
<a class="w-full inline-block file-preview-link truncate" :id="modalPreviewLinkId" data-no-turbolink="true"
|
||||
<div class="w-full cursor-pointer relative" @mouseover="tooltipShowing = true" @mouseout="tooltipShowing = false">
|
||||
<a class="w-full inline-block file-preview-link truncate text-sn-science-blue" :id="modalPreviewLinkId" data-no-turbolink="true"
|
||||
data-id="true" data-status="asset-present" :data-preview-url=this?.preview_url :href=this?.url>
|
||||
{{ file_name }}
|
||||
</a>
|
||||
|
@ -21,10 +30,6 @@
|
|||
:preview_url="preview_url" :icon_html="icon_html" :medium_preview_url="medium_preview_url">
|
||||
</tooltip-preview>
|
||||
</div>
|
||||
<div v-if="canEdit" class="flex whitespace-nowrap gap-4 pl-4">
|
||||
<a class="btn-text-link font-normal" @click="openFileChooser"> {{ i18n.t('general.replace') }} </a>
|
||||
<a class="btn-text-link font-normal" @click="clearFile"> {{ i18n.t('general.delete') }} </a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!error" class="flex flex-row items-center font-inter text-sm font-normal leading-5 justify-between"
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
:options="checklistItems"
|
||||
:placeholder="i18n.t('repositories.item_card.dropdown_placeholder')"
|
||||
:no-options-placeholder="i18n.t('repositories.item_card.dropdown_placeholder')"
|
||||
className="h-[38px] !pl-3"
|
||||
className="h-[38px] pl-3"
|
||||
optionsClassName="max-h-[300px]"
|
||||
></checklist-select>
|
||||
</div>
|
||||
|
|
|
@ -1,35 +1,30 @@
|
|||
<template>
|
||||
<div id="repository-date-range-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
|
||||
<div class="font-inter text-sm font-semibold leading-5 truncate" :title="colName">
|
||||
{{ colName }}
|
||||
</div>
|
||||
<DateTimeRange
|
||||
dateType="dateRange"
|
||||
:startTime="colVal?.start_time"
|
||||
:endTime="colVal?.end_time"
|
||||
<div class="flex flex-col gap-2">
|
||||
|
||||
<DateTimeComponent
|
||||
mode="date"
|
||||
:range="true"
|
||||
:colVal="colVal"
|
||||
:colId="colId"
|
||||
:colName="colName"
|
||||
:updatePath="updatePath"
|
||||
:canEdit="canEdit"
|
||||
:editingField="editingField"
|
||||
@setEditingField="$emit('setEditingField', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DateTimeRange from './DateTimeRange.vue';
|
||||
import DateTimeComponent from './date_time_component.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepositoryDateRangeValue',
|
||||
components: { DateTimeRange },
|
||||
components: { DateTimeComponent },
|
||||
props: {
|
||||
data_type: String,
|
||||
colId: Number,
|
||||
colName: String,
|
||||
colVal: null,
|
||||
updatePath: null,
|
||||
editingField: null,
|
||||
colVal: Object,
|
||||
updatePath: String,
|
||||
canEdit: { type: Boolean, default: false },
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
<template>
|
||||
<div id="repository-date-time-range-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
|
||||
<div class="font-inter text-sm font-semibold leading-5 truncate" :title="colName">
|
||||
{{ colName }}
|
||||
</div>
|
||||
<DateTimeRange
|
||||
:editingField="editingField"
|
||||
@setEditingField="$emit('setEditingField', $event)"
|
||||
dateType="dateTimeRange"
|
||||
:startTime="colVal?.start_time"
|
||||
:endTime="colVal?.end_time"
|
||||
<div class="flex flex-col gap-2">
|
||||
<DateTimeComponent
|
||||
mode="datetime"
|
||||
:range="true"
|
||||
:colVal="colVal"
|
||||
:colId="colId"
|
||||
:colName="colName"
|
||||
:updatePath="updatePath"
|
||||
:canEdit="canEdit"
|
||||
/>
|
||||
|
@ -18,18 +13,17 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import DateTimeRange from './DateTimeRange.vue';
|
||||
import DateTimeComponent from './date_time_component.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepositoryDateTimeRangeValue',
|
||||
components: { DateTimeRange },
|
||||
components: { DateTimeComponent },
|
||||
props: {
|
||||
data_type: String,
|
||||
colId: Number,
|
||||
colName: String,
|
||||
colVal: Object,
|
||||
updatePath: null,
|
||||
editingField: null,
|
||||
updatePath: String,
|
||||
canEdit: { type: Boolean, default: false }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
<template>
|
||||
<div id="repository-date-time-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
|
||||
<div class="font-inter text-sm font-semibold leading-5 truncate" :title="colName">
|
||||
{{ colName }}
|
||||
</div>
|
||||
<DateTimeRange
|
||||
:editingField="editingField"
|
||||
@setEditingField="$emit('setEditingField', $event)"
|
||||
dateType="dateTime"
|
||||
<div class="flex flex-col gap-2">
|
||||
<DateTimeComponent
|
||||
mode="datetime"
|
||||
:colVal="colVal"
|
||||
:colId="colId"
|
||||
:colName="colName"
|
||||
:updatePath="updatePath"
|
||||
:canEdit="canEdit"
|
||||
/>
|
||||
|
@ -16,18 +12,17 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import DateTimeRange from './DateTimeRange.vue';
|
||||
import DateTimeComponent from './date_time_component.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepositoryDateTimeValue',
|
||||
components: { DateTimeRange },
|
||||
components: { DateTimeComponent },
|
||||
props: {
|
||||
data_type: String,
|
||||
colId: Number,
|
||||
colName: String,
|
||||
colVal: Object,
|
||||
updatePath: String,
|
||||
editingField: null,
|
||||
canEdit: { type: Boolean, default: false }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,22 @@
|
|||
<template>
|
||||
<div id="repository-date-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
|
||||
<div class="font-inter text-sm font-semibold leading-5 truncate" :title="colName">
|
||||
{{ colName }}
|
||||
</div>
|
||||
<DateTimeRange
|
||||
:editingField="editingField"
|
||||
@setEditingField="$emit('setEditingField', $event)"
|
||||
dateType="date"
|
||||
<div class="flex flex-col gap2">
|
||||
<DateTimeComponent
|
||||
mode="date"
|
||||
:colVal="colVal"
|
||||
:colId="colId"
|
||||
:colName="colName"
|
||||
:updatePath="updatePath"
|
||||
:dataType="data_type"
|
||||
:canEdit="canEdit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DateTimeRange from './DateTimeRange.vue';
|
||||
import DateTimeComponent from './date_time_component.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepositoryDateValue',
|
||||
components: { DateTimeRange },
|
||||
components: { DateTimeComponent },
|
||||
props: {
|
||||
data_type: String,
|
||||
colId: Number,
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
:placeholder="i18n.t('repositories.item_card.dropdown_placeholder')"
|
||||
:no-options-placeholder="i18n.t('repositories.item_card.dropdown_placeholder')"
|
||||
:searchPlaceholder="i18n.t('repositories.item_card.dropdown_placeholder')"
|
||||
className="h-[38px] !pl-3"
|
||||
customClass="!h-[38px] !pl-3 sci-cursor-edit"
|
||||
optionsClassName="max-h-[300px]"
|
||||
></select-search>
|
||||
<div v-else-if="text"
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="canEdit" class="w-full">
|
||||
<div v-if="canEdit" class="w-full contents">
|
||||
<text-area :initialValue="(colVal)?.toLocaleString('fullwide', {useGrouping:false}) || ''"
|
||||
:noContentPlaceholder="i18n.t('repositories.item_card.repository_number_value.placeholder')"
|
||||
:placeholder="i18n.t('repositories.item_card.repository_number_value.placeholder')"
|
||||
|
@ -65,11 +65,8 @@ export default {
|
|||
colName: String,
|
||||
colVal: Number,
|
||||
permissions: null,
|
||||
canEdit: { type: Boolean, defaul: false}
|
||||
},
|
||||
created() {
|
||||
// constants
|
||||
this.decimals = Number(document.getElementById(`${this.colId}`).dataset['metadataDecimals']) || 0;
|
||||
decimals: { type: Number, default: 0 },
|
||||
canEdit: { type: Boolean, default: false },
|
||||
},
|
||||
methods: {
|
||||
toggleCollapse() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div id="repository-status-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
|
||||
<div ref="container" id="repository-status-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
|
||||
<div class="font-inter text-sm font-semibold leading-5 truncate" :title="colName">
|
||||
{{ colName }}
|
||||
</div>
|
||||
|
@ -17,7 +17,7 @@
|
|||
:placeholder="i18n.t('repositories.item_card.dropdown_placeholder')"
|
||||
:no-options-placeholder="i18n.t('repositories.item_card.dropdown_placeholder')"
|
||||
:searchPlaceholder="i18n.t('repositories.item_card.dropdown_placeholder')"
|
||||
className="h-[38px] !pl-3"
|
||||
customClass="!h-[38px] !pl-2 sci-cursor-edit"
|
||||
optionsClassName="max-h-[300px]"
|
||||
></select-search>
|
||||
<div v-else-if="status && icon"
|
||||
|
@ -88,16 +88,25 @@ export default {
|
|||
this.isLoading = false;
|
||||
this.selected = this.id;
|
||||
});
|
||||
this.replaceEmojiesInDropdown();
|
||||
},
|
||||
methods: {
|
||||
changeSelected(id) {
|
||||
this.selected = id;
|
||||
if (id) {
|
||||
if (id || id === null) {
|
||||
this.update(id);
|
||||
this.replaceEmojiesInDropdown();
|
||||
}
|
||||
},
|
||||
parseEmoji(content) {
|
||||
return twemoji.parse(content);
|
||||
},
|
||||
replaceEmojiesInDropdown() {
|
||||
setTimeout(() => {
|
||||
twemoji.size = "24x24";
|
||||
twemoji.base = '/images/twemoji/';
|
||||
twemoji.parse(this.$refs.container);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
:data-manage-stock-url="values?.stock_url"
|
||||
:data-repository-row-id="repositoryId"
|
||||
>
|
||||
<div v-if="values?.stock_formatted" :data-manage-stock-url="values?.stock_url" class="text-sn-dark-grey font-inter text-sm font-normal leading-5 stock-value">
|
||||
<div v-if="values?.stock_formatted" :data-manage-stock-url="values?.stock_url"
|
||||
class="text-sn-dark-grey font-inter text-sm font-normal leading-5 stock-value overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{{ values.stock_formatted }}
|
||||
</div>
|
||||
<div v-else class="font-inter text-sm font-normal leading-5" :class="{ 'text-sn-dark-grey': !canEdit, 'text-sn-grey': canEdit }">
|
||||
|
@ -37,7 +38,7 @@
|
|||
},
|
||||
computed: {
|
||||
editableClassName() {
|
||||
const className = 'border-solid border-[1px] p-2 manage-repository-stock-value-link sci-cursor-edit'
|
||||
const className = 'border-solid border-[1px] p-2 pl-3 manage-repository-stock-value-link sci-cursor-edit'
|
||||
if (this.canEdit && this.isEditing) return `${className} border-sn-science-blue`;
|
||||
if (this.canEdit) return `${className} border-sn-light-grey hover:border-sn-sleepy-grey`;
|
||||
return ''
|
||||
|
@ -48,7 +49,7 @@
|
|||
stock_formatted: null,
|
||||
stock_amount: null,
|
||||
low_stock_threshold: null,
|
||||
isEditing: null,
|
||||
isEditing: false,
|
||||
values: null
|
||||
}
|
||||
},
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="canEdit">
|
||||
<div v-if="canEdit" class="w-full contents">
|
||||
<text-area :initialValue="colVal?.edit"
|
||||
:noContentPlaceholder="i18n.t('repositories.item_card.repository_text_value.placeholder')"
|
||||
:placeholder="i18n.t('repositories.item_card.repository_text_value.placeholder')"
|
||||
|
@ -27,15 +27,16 @@
|
|||
@update="update"
|
||||
className="px-3" />
|
||||
</div>
|
||||
<div v-else-if="colVal?.edit"
|
||||
<div v-else-if="colVal?.view"
|
||||
ref="textRef"
|
||||
class="text-sn-dark-grey box-content text-sm font-normal leading-5 overflow-y-auto pr-3 py-2 rounded w-[calc(100%-2rem)]]"
|
||||
v-html="colVal?.view"
|
||||
class="text-sn-dark-grey box-content text-sm font-normal leading-5
|
||||
overflow-y-auto pr-3 rounded w-[calc(100%-2rem)]]"
|
||||
:class="{
|
||||
'max-h-[4rem]': collapsed,
|
||||
'max-h-[40rem]': !collapsed
|
||||
}"
|
||||
>
|
||||
{{ colVal?.edit }}
|
||||
</div>
|
||||
<div v-else class="text-sn-dark-grey font-inter text-sm font-normal leading-5 pr-3 py-2 w-[calc(100%-2rem)]]">
|
||||
{{ i18n.t("repositories.item_card.repository_text_value.no_text") }}
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
<template>
|
||||
<div id="repository-time-range-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
|
||||
<div class="font-inter text-sm font-semibold leading-5 truncate" :title="colName">
|
||||
{{ colName }}
|
||||
</div>
|
||||
<DateTimeRange
|
||||
:editingField="editingField"
|
||||
@setEditingField="$emit('setEditingField', $event)"
|
||||
dateType="timeRange"
|
||||
:startTime="colVal?.start_time"
|
||||
:endTime="colVal?.end_time"
|
||||
<div class="flex flex-col gap-2">
|
||||
<DateTimeComponent
|
||||
mode="time"
|
||||
:range="true"
|
||||
:colVal="colVal"
|
||||
:colId="colId"
|
||||
:colName="colName"
|
||||
:updatePath="updatePath"
|
||||
:canEdit="canEdit"
|
||||
/>
|
||||
|
@ -18,10 +13,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import DateTimeRange from './DateTimeRange.vue';
|
||||
import DateTimeComponent from './date_time_component.vue';
|
||||
export default {
|
||||
name: 'RepositoryTimeRangeValue',
|
||||
components: { DateTimeRange },
|
||||
components: { DateTimeComponent },
|
||||
props: {
|
||||
data_type: String,
|
||||
colId: Number,
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
<template>
|
||||
<div id="repository-time-value-wrapper" class="flex flex-col min-min-h-[46px] h-auto gap-[6px]">
|
||||
<div class="font-inter text-sm font-semibold leading-5 truncate" :title="colName">
|
||||
{{ colName }}
|
||||
</div>
|
||||
<DateTimeRange
|
||||
:editingField="editingField"
|
||||
@setEditingField="$emit('setEditingField', $event)"
|
||||
dateType="time"
|
||||
<div class="flex flex-col gap-2">
|
||||
<DateTimeComponent
|
||||
mode="time"
|
||||
:colVal="colVal"
|
||||
:colId="colId"
|
||||
:colName="colName"
|
||||
:updatePath="updatePath"
|
||||
:canEdit="canEdit"
|
||||
/>
|
||||
|
@ -16,10 +12,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import DateTimeRange from './DateTimeRange.vue';
|
||||
import DateTimeComponent from './date_time_component.vue';
|
||||
export default {
|
||||
name: 'RepositoryTimeValue',
|
||||
components: { DateTimeRange },
|
||||
components: { DateTimeComponent },
|
||||
props: {
|
||||
data_type: String,
|
||||
colId: Number,
|
||||
|
|
|
@ -42,7 +42,6 @@ export default {
|
|||
},
|
||||
|
||||
mounted() {
|
||||
console.log('mounted');
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.initializeComponent();
|
||||
this.$nextTick(() => {
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
<template>
|
||||
<div class="flex gap-1">
|
||||
<div class="text-sm font-bold truncate" :title="colName">
|
||||
{{ colName }}
|
||||
</div>
|
||||
<div v-if="colVal.reminder" class="bg-sn-alert-passion w-1.5 h-1.5 rounded" :title="colVal.reminder_text"></div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<template v-if="!canEdit">
|
||||
<span v-if="range">
|
||||
<template v-if="colVal.start_time && colVal.end_time">
|
||||
{{ colVal.start_time.formatted }} - {{ colVal.end_time.formatted }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ viewPlaceholder }}
|
||||
</template>
|
||||
</span>
|
||||
<span v-else >
|
||||
<template v-if="colVal.formatted">
|
||||
{{ colVal.formatted }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ viewPlaceholder }}
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>
|
||||
<span class="text-xs capitalize" v-if="range">{{ i18n.t('general.from') }}</span>
|
||||
<DateTimePicker :defaultValue="defaultStartDate" @closed="update" @change="updateStartDate" :mode="mode" :placeholder="placeholder" :clearable="true"/>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs capitalize" v-if="range">{{ i18n.t('general.to') }}</span>
|
||||
<DateTimePicker :defaultValue="defaultEndDate" @closed="update" v-if="range" @change="updateEndDate" :placeholder="placeholder" :mode="mode" :clearable="true"/>
|
||||
</div>
|
||||
<div class="text-xs text-sn-delete-red" v-if="error">{{ error }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DateTimePicker from '../../shared/date_time_picker.vue';
|
||||
import Reminder from '../reminder.vue';
|
||||
|
||||
export default {
|
||||
name: 'DateTimeComponent',
|
||||
components: {
|
||||
DateTimePicker,
|
||||
Reminder
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
error: null,
|
||||
defaultStartDate: null,
|
||||
defaultEndDate: null,
|
||||
}
|
||||
},
|
||||
inject: ['reloadRepoItemSidebar'],
|
||||
props: {
|
||||
mode: String,
|
||||
range: { type: Boolean, default: false },
|
||||
colVal: { type: Object, default: {} },
|
||||
colId: Number,
|
||||
updatePath: String,
|
||||
canEdit: { type: Boolean, default: false },
|
||||
colName: String,
|
||||
},
|
||||
created() {
|
||||
if (this.range) {
|
||||
if (this.colVal.start_time?.datetime) this.startDate = new Date(this.colVal.start_time.datetime)
|
||||
if (this.colVal.end_time?.datetime) this.endDate = new Date(this.colVal.end_time.datetime)
|
||||
} else {
|
||||
if (this.colVal.datetime) this.startDate = new Date(this.colVal.datetime)
|
||||
}
|
||||
|
||||
this.defaultStartDate = this.startDate;
|
||||
this.defaultEndDate = this.endDate;
|
||||
},
|
||||
computed: {
|
||||
value: {
|
||||
get () {
|
||||
if (this.range) {
|
||||
if (!(this.startDate instanceof Date) && !(this.endDate instanceof Date)) return null;
|
||||
|
||||
return {
|
||||
start_time: this.formatDate(this.startDate),
|
||||
end_time: this.formatDate(this.endDate)
|
||||
};
|
||||
} else {
|
||||
if (!(this.startDate instanceof Date)) return null;
|
||||
|
||||
return this.formatDate(this.startDate);
|
||||
}
|
||||
},
|
||||
},
|
||||
placeholder() {
|
||||
switch (this.mode) {
|
||||
case 'date':
|
||||
return this.i18n.t('repositories.item_card.repository_date_value.placeholder');
|
||||
case 'time':
|
||||
return this.i18n.t('repositories.item_card.repository_time_value.placeholder');
|
||||
case 'datetime':
|
||||
return this.i18n.t('repositories.item_card.repository_date_time_value.placeholder');
|
||||
}
|
||||
},
|
||||
viewPlaceholder() {
|
||||
switch (this.mode) {
|
||||
case 'date':
|
||||
if (this.range) {
|
||||
return this.i18n.t('repositories.item_card.repository_date_range_value.no_date_range');
|
||||
}
|
||||
return this.i18n.t('repositories.item_card.repository_date_value.no_date');
|
||||
case 'time':
|
||||
if (this.range) {
|
||||
return this.i18n.t('repositories.item_card.repository_time_range_value.no_time_range');
|
||||
}
|
||||
return this.i18n.t('repositories.item_card.repository_time_value.no_time');
|
||||
case 'datetime':
|
||||
if (this.range) {
|
||||
return this.i18n.t('repositories.item_card.repository_date_time_range_value.no_date_time_range');
|
||||
}
|
||||
return this.i18n.t('repositories.item_card.repository_date_time_value.no_date_time');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateStartDate(date) {
|
||||
this.startDate = date;
|
||||
if (!(this.startDate instanceof Date)) this.update();
|
||||
},
|
||||
updateEndDate(date) {
|
||||
this.endDate = date;
|
||||
if (!(this.endDate instanceof Date)) this.update();
|
||||
},
|
||||
validateValue() {
|
||||
this.error = null;
|
||||
// Date is not changed
|
||||
if (this.defaultStartDate == this.startDate && this.defaultEndDate == this.endDate) return false;
|
||||
|
||||
if (this.range) {
|
||||
// Both empty
|
||||
if (!(this.startDate instanceof Date) && !(this.endDate instanceof Date)) return true;
|
||||
|
||||
// One empty
|
||||
if (!(this.startDate instanceof Date) || !(this.endDate instanceof Date)) {
|
||||
this.error = this.i18n.t('repositories.item_card.date_time.errors.not_valid_range')
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start date is after end date
|
||||
if (this.startDate > this.endDate) {
|
||||
this.error = this.i18n.t('repositories.item_card.date_time.errors.not_valid_range')
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
update() {
|
||||
const params = {}
|
||||
|
||||
if (!this.validateValue()) return;
|
||||
|
||||
params[this.colId] = this.value
|
||||
$.ajax({
|
||||
method: 'PUT',
|
||||
url: this.updatePath,
|
||||
dataType: 'json',
|
||||
data: { repository_cells: params },
|
||||
success: () => {
|
||||
this.defaultStartDate = this.startDate;
|
||||
this.defaultEndDate = this.endDate;
|
||||
if ($('.dataTable')[0]) {
|
||||
$('.dataTable').DataTable().ajax.reload(null, false);
|
||||
this.reloadRepoItemSidebar();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
formatDate(date) {
|
||||
if (!(date instanceof Date)) return null;
|
||||
|
||||
const y = date.getFullYear();
|
||||
const m = date.getMonth() + 1;
|
||||
const d = date.getDate();
|
||||
const hours = date.getHours();
|
||||
const mins = date.getMinutes();
|
||||
return `${y}/${m}/${d} ${hours}:${mins}`;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -21,7 +21,7 @@
|
|||
</template>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body !pt-[6px]">
|
||||
<p class="text-sm pb-6"> {{ i18n.t('repository_stock_values.manage_modal.enter_amount') }}</p>
|
||||
<form class="flex flex-col gap-6" @submit.prevent novalidate>
|
||||
<fieldset class="w-full flex justify-between">
|
||||
|
@ -72,17 +72,17 @@
|
|||
<template v-if="stockValue?.id">
|
||||
<div class="flex justify-between w-full items-center">
|
||||
<div class="flex flex-col w-[220px] h-24 border-rounded bg-sn-super-light-grey justify-between text-center">
|
||||
<span class="text-sm text-sn-grey leading-5">{{ i18n.t('repository_stock_values.manage_modal.current_stock') }}</span>
|
||||
<span class="text-sm text-sn-grey leading-5 pt-2">{{ i18n.t('repository_stock_values.manage_modal.current_stock') }}</span>
|
||||
<span class="text-2xl text-sn-black font-semibold leading-8" :class="{ 'text-sn-delete-red': stockValue.amount < 0 }">{{ stockValue.amount }}</span>
|
||||
<span class="text-sm text0sn-black leading-5">{{ initUnitLabel }}</span>
|
||||
<span class="text-sm text0sn-black leading-5 pb-2">{{ initUnitLabel }}</span>
|
||||
</div>
|
||||
<i class="sn-icon sn-icon-arrow-right"></i>
|
||||
<div class="flex flex-col w-[220px] h-24 border-rounded bg-sn-super-light-grey justify-between text-center">
|
||||
<span class="text-sm text-sn-grey leading-5">{{ i18n.t('repository_stock_values.manage_modal.new_stock') }}</span>
|
||||
<span class="text-sm text-sn-grey leading-5 pt-2">{{ i18n.t('repository_stock_values.manage_modal.new_stock') }}</span>
|
||||
<span class="text-2xl text-sn-black font-semibold leading-8" :class="{ 'text-sn-delete-red': newAmount < 0 }">
|
||||
{{ (newAmount || newAmount === 0) ? newAmount : '-' }}
|
||||
</span>
|
||||
<span class="text-sm text0sn-black leading-5">{{ unitLabel }}</span>
|
||||
<span class="text-sm text0sn-black leading-5 pb-2">{{ unitLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -209,6 +209,9 @@
|
|||
},
|
||||
methods: {
|
||||
setOperation($event) {
|
||||
if ($event !== this.operation) {
|
||||
this.amount = null;
|
||||
}
|
||||
this.operation = $event;
|
||||
if ([2, 3].includes($event)) {
|
||||
this.unit = this.stockValue.unit;
|
||||
|
|
|
@ -132,7 +132,7 @@
|
|||
}
|
||||
).then(
|
||||
(response) => {
|
||||
this.results = [response.data.data, ...this.results];
|
||||
this.results = [{ newResult: true, ...response.data.data }, ...this.results];
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
v-model="sharedEnabled"
|
||||
id="checkbox"
|
||||
class="sci-toggle-checkbox"
|
||||
:disabled="!canShare"
|
||||
tabindex="0"
|
||||
@change="checkboxChange"
|
||||
@keyup.enter="handleCheckboxEnter"/>
|
||||
|
@ -46,7 +47,7 @@
|
|||
:class="{ 'error': error }"
|
||||
v-model="description"
|
||||
:placeholder="i18n.t('shareable_links.modal.description_placeholder')"
|
||||
:disabled="!sharedEnabled"
|
||||
:disabled="!sharedEnabled || !canShare"
|
||||
@focus="editing = true">
|
||||
</textarea>
|
||||
</div>
|
||||
|
@ -110,6 +111,10 @@
|
|||
characterLimit: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
canShare: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
type="button"
|
||||
:class="shareClass"
|
||||
:title="shareValue"
|
||||
:disabled="disabled"
|
||||
@click="openModal">
|
||||
<span class="sn-icon sn-icon-shared"></span>
|
||||
<span class="text-sm">
|
||||
|
@ -19,6 +18,7 @@
|
|||
:characterLimit="255"
|
||||
@enable="enableShare"
|
||||
@disable="disableShare"
|
||||
:canShare="canShare"
|
||||
@close="closeModal"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,7 +38,7 @@
|
|||
type: String,
|
||||
required: true
|
||||
},
|
||||
disabled: {
|
||||
canShare: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
|
|
|
@ -12,12 +12,12 @@
|
|||
}"
|
||||
:disabled="disabled"
|
||||
@click="toggle">
|
||||
<span>{{ valueLabel || this.placeholder || this.i18n.t('general.select') }}</span>
|
||||
<span class="overflow-hidden text-ellipsis">{{ valueLabel || this.placeholder || this.i18n.t('general.select') }}</span>
|
||||
<i class="sn-icon" :class="{ 'sn-icon-down': !isOpen, 'sn-icon-up': isOpen}"></i>
|
||||
</button>
|
||||
<div :style="optionPositionStyle" class="py-2.5 z-10 bg-white rounded border-[1px] border-sn-light-grey shadow-sn-menu-sm" :class="{ 'hidden': !isOpen }">
|
||||
<div v-if="withButtons" class="px-2.5">
|
||||
<div class="flex gap-2 pl-2 pb-2.5 justify-start items-center w-[calc(100%-10px)]">
|
||||
<div v-if="withButtons" class="px-2.5 pb-[1px]">
|
||||
<div class="flex gap-2 pl-2 justify-start items-center w-[calc(100%-10px)]">
|
||||
<div class="btn btn-light !text-xs h-[30px] px-0 active:bg-sn-super-light-blue"
|
||||
@click="selectedValues = []"
|
||||
:class="{
|
||||
|
@ -48,7 +48,7 @@
|
|||
<input v-model="selectedValues" :value="option.id" :id="option.id" type="checkbox" class="sci-checkbox project-card-selector">
|
||||
<label :for="option.id" class="sci-checkbox-label"></label>
|
||||
</div>
|
||||
<span class="text-ellipsis overflow-hidden max-h-[4rem] ml-1">{{ option.label }}</span>
|
||||
<span :title="option.label" class="text-ellipsis overflow-hidden max-h-[4rem] ml-1 whitespace-normal line-clamp-3">{{ option.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
|
|
|
@ -157,7 +157,6 @@
|
|||
deleteAttachment(id) {
|
||||
this.$emit('attachment:deleted', id)
|
||||
},
|
||||
|
||||
initMarvinJS() {
|
||||
// legacy logic from app/assets/javascripts/sitewide/marvinjs_editor.js
|
||||
MarvinJsEditor.initNewButton(
|
||||
|
@ -165,16 +164,6 @@
|
|||
() => this.$emit('attachment:uploaded')
|
||||
);
|
||||
},
|
||||
openWopiFileModal() {
|
||||
this.initWopiFileModal(this.parent, (_e, data, status) => {
|
||||
if (status === 'success') {
|
||||
this.$emit('attachment:uploaded', data);
|
||||
} else {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleDropdownPosition() {
|
||||
this.$refs.actionsDropdownButton.classList.toggle("dropup", !this.isInViewport(this.$refs.actionsDropdown));
|
||||
},
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
@update="updateText"
|
||||
@delete="removeItem()"
|
||||
@keypress="keyPressHandler"
|
||||
@blur="editingText = false"
|
||||
@blur="onBlurHandler"
|
||||
/>
|
||||
<span v-if="!editingText && (!checklistItem.attributes.urls || deleteUrl)" class="absolute right-0 top-0.5 leading-6 tw-hidden group-hover/checklist-item-header:inline-block !text-sn-blue cursor-pointer" @click="showDeleteModal" tabindex="0">
|
||||
<i class="sn-icon sn-icon-delete"></i>
|
||||
|
@ -139,6 +139,11 @@
|
|||
this.checklistItem.attributes.checked = this.$refs.checkbox.checked;
|
||||
this.$emit('toggle', this.checklistItem);
|
||||
},
|
||||
onBlurHandler() {
|
||||
this.$nextTick(() => {
|
||||
this.editingText = false;
|
||||
});
|
||||
},
|
||||
updateText(text, withKey) {
|
||||
if (text.length === 0) {
|
||||
this.disableTextEdit();
|
||||
|
|
|
@ -36,9 +36,10 @@ export default {
|
|||
button.click();
|
||||
},
|
||||
openWopiFileModal() {
|
||||
this.initWopiFileModal(this.attachmentsParent, (_e, data, status) => {
|
||||
this.initWopiFileModal(this.attachmentsParent, (_e, attachmentData, status) => {
|
||||
if (status === 'success') {
|
||||
this.addAttachment(data)
|
||||
const attachment = attachmentData.data;
|
||||
this.addAttachment(attachment);
|
||||
} else {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
|
||||
}
|
||||
|
|
|
@ -35,8 +35,8 @@
|
|||
@keyup.enter="!editingTable && enableTableEdit()">
|
||||
<div ref="hotTable" class="hot-table-container" @click="!editingTable && enableTableEdit()">
|
||||
</div>
|
||||
<div v-if="editingTable" class="text-xs pt-3 pb-2 text-sn-grey">
|
||||
{{ i18n.t('protocols.steps.table.edit_message') }}
|
||||
<div class="text-xs pt-3 pb-2 text-sn-grey h-1">
|
||||
<span v-if="editingTable">{{ i18n.t('protocols.steps.table.edit_message') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<deleteElementModal v-if="confirmingDelete" @confirm="deleteElement" @cancel="closeDeleteModal"/>
|
||||
|
@ -163,8 +163,9 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const { row = 0, col = 0 } = this.selectedCell || {};
|
||||
this.editingTable = true;
|
||||
this.$nextTick(() => this.tableObject.selectCell(0,0));
|
||||
this.$nextTick(() => this.tableObject.selectCell(row,col));
|
||||
},
|
||||
disableTableEdit() {
|
||||
this.editingTable = false;
|
||||
|
@ -268,8 +269,12 @@
|
|||
preventOverflow: 'horizontal',
|
||||
readOnly: !this.editingTable,
|
||||
afterUnlisten: () => {
|
||||
this.updatingTableData = true;
|
||||
this.updateTable();
|
||||
this.editingTable = false;
|
||||
},
|
||||
afterSelection: (r, c, r2, c2) => {
|
||||
if (r === r2 && c === c2) {
|
||||
this.selectedCell = { row: r, col: c };
|
||||
}
|
||||
},
|
||||
afterChange: () => {
|
||||
if (this.editingTable == false) return;
|
||||
|
|
|
@ -113,7 +113,7 @@
|
|||
},
|
||||
computed: {
|
||||
wrapTables() {
|
||||
const container = $(`<span>${this.element.attributes.orderable.text_view}</span>`);
|
||||
const container = $(`<span class="text-base">${this.element.attributes.orderable.text_view}</span>`);
|
||||
container.find('table').toArray().forEach((table) => {
|
||||
if ($(table).parent().hasClass('table-wrapper')) return;
|
||||
$(table).css('float', 'none').wrapAll(`
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
:class="{
|
||||
'only-time': mode == 'time',
|
||||
}"
|
||||
@closed="closedHandler"
|
||||
@cleared="clearedHandler"
|
||||
v-model="compDatetime"
|
||||
:teleport="teleport"
|
||||
:no-today="true"
|
||||
|
@ -91,7 +93,7 @@
|
|||
watch: {
|
||||
defaultValue: function () {
|
||||
this.datetime = this.defaultValue;
|
||||
if (this.defaultValue) {
|
||||
if (this.defaultValue instanceof Date) {
|
||||
this.time = {
|
||||
hours: this.defaultValue.getHours(),
|
||||
minutes: this.defaultValue.getMinutes()
|
||||
|
@ -103,7 +105,7 @@
|
|||
|
||||
this.time = null;
|
||||
|
||||
if (this.datetime) {
|
||||
if (this.datetime instanceof Date) {
|
||||
this.time = {
|
||||
hours: this.datetime.getHours(),
|
||||
minutes: this.datetime.getMinutes()
|
||||
|
@ -186,6 +188,12 @@
|
|||
close() {
|
||||
this.$refs.datetimePicker.closeMenu();
|
||||
},
|
||||
closedHandler() {
|
||||
this.$emit('closed');
|
||||
},
|
||||
clearedHandler() {
|
||||
this.$emit('cleared');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
v-model="newValue"
|
||||
@keydown="handleKeypress"
|
||||
@blur="handleBlur"
|
||||
@keyup.escape="cancelEdit"
|
||||
@keyup.escape="cancelEdit && this.atWhoOpened"
|
||||
@focus="setCaretAtEnd"/>
|
||||
<textarea v-else
|
||||
ref="input"
|
||||
|
@ -28,7 +28,7 @@
|
|||
v-model="newValue"
|
||||
@keydown="handleKeypress"
|
||||
@blur="handleBlur"
|
||||
@keyup.escape="cancelEdit"
|
||||
@keyup.escape="cancelEdit && this.atWhoOpened"
|
||||
@focus="setCaretAtEnd"/>
|
||||
</template>
|
||||
<div
|
||||
|
@ -73,7 +73,8 @@
|
|||
smartAnnotation: { type: Boolean, default: false },
|
||||
editOnload: { type: Boolean, default: false },
|
||||
defaultValue: { type: String, default: '' },
|
||||
singleLine: { type: Boolean, default: true }
|
||||
singleLine: { type: Boolean, default: true },
|
||||
preventLeavingUntilFilled: { type: Boolean, default: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -123,6 +124,10 @@
|
|||
},
|
||||
error() {
|
||||
if (!this.allowBlank && this.isBlank) {
|
||||
if (this.preventLeavingUntilFilled) {
|
||||
this.addPreventFromLeaving(document.body);
|
||||
}
|
||||
|
||||
return this.i18n.t('inline_edit.errors.blank', { attribute: this.attributeName })
|
||||
}
|
||||
if(this.characterLimit && this.newValue.length > this.characterLimit) {
|
||||
|
@ -146,10 +151,25 @@
|
|||
)
|
||||
}
|
||||
|
||||
this.removePreventFromLeaving(document.body);
|
||||
return false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
removePreventFromLeaving(domEl) {
|
||||
domEl.removeEventListener('click', this.preventClicks, true);
|
||||
domEl.removeEventListener('mousedown', this.preventClicks, true);
|
||||
domEl.removeEventListener('mouseup', this.preventClicks, true);
|
||||
},
|
||||
addPreventFromLeaving(domEl) {
|
||||
domEl.addEventListener('click', this.preventClicks, true);
|
||||
domEl.addEventListener('mousedown', this.preventClicks, true);
|
||||
domEl.addEventListener('mouseup', this.preventClicks, true);
|
||||
},
|
||||
preventClicks(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
},
|
||||
handleAutofocus() {
|
||||
if (this.autofocus || !this.placeholder && this.isBlank || this.editOnload && this.isBlank) {
|
||||
this.enableEdit();
|
||||
|
@ -220,6 +240,9 @@
|
|||
sel.collapse(sel.anchorNode, offset);
|
||||
},
|
||||
handleKeypress(e) {
|
||||
this.atWhoOpened = $('.atwho-view:visible').length > 0
|
||||
if (this.atWhoOpened) return;
|
||||
|
||||
if (e.key == 'Escape') {
|
||||
this.cancelEdit();
|
||||
} else if (e.key == 'Enter' && this.saveOnEnter && e.shiftKey == false) {
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
class="sn-select__options !relative !top-0 !left-[-1px] !shadow-none scroll-container px-2.5 pt-0 block"
|
||||
:class="{ [optionsClassName]: true }"
|
||||
>
|
||||
|
||||
<div v-if="options.length" class="flex flex-col gap-[1px]">
|
||||
<div
|
||||
v-for="option in options"
|
||||
|
@ -78,9 +77,6 @@
|
|||
valueLabel() {
|
||||
let option = this.options.find((o) => o[0] === this.value);
|
||||
return option && option[1];
|
||||
},
|
||||
focusElement() {
|
||||
return this.$refs.focusElement || this.$scopedSlots.default()[0].context.$refs.focusElement;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -96,7 +92,8 @@
|
|||
if (this.isOpen) {
|
||||
this.$emit('open');
|
||||
this.$nextTick(() => {
|
||||
this.focusElement.focus();
|
||||
this.$emit('focus');
|
||||
this.$refs.focusElement?.focus();
|
||||
});
|
||||
this.$refs.optionsContainer.scrollTop = 0;
|
||||
this.updateOptionPosition();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<Select
|
||||
class="sn-select sn-select--search"
|
||||
class="sn-select sn-select--search hover:border-sn-sleepy-grey"
|
||||
:class="customClass"
|
||||
:className="className"
|
||||
:optionsClassName="optionsClassName"
|
||||
:withEditCursor="withEditCursor"
|
||||
|
@ -14,6 +15,7 @@
|
|||
@blur="blur"
|
||||
@open="open"
|
||||
@close="close"
|
||||
@focus="focus"
|
||||
>
|
||||
<input ref="focusElement" v-model="query" type="text" class="sn-select__search-input" :placeholder="searchPlaceholder" />
|
||||
<span class="sn-select__value">{{ valueLabel || (placeholder || i18n.t('general.select')) }}</span>
|
||||
|
@ -38,7 +40,8 @@
|
|||
disabled: { type: Boolean },
|
||||
isLoading: { type: Boolean, default: false },
|
||||
className: { type: String, default: '' },
|
||||
optionsClassName: { type: String, default: '' }
|
||||
optionsClassName: { type: String, default: '' },
|
||||
customClass: { type: String, default: '' }
|
||||
},
|
||||
components: { Select },
|
||||
data() {
|
||||
|
@ -75,6 +78,9 @@
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.focusElement.focus();
|
||||
},
|
||||
blur() {
|
||||
this.isOpen = false;
|
||||
this.$emit('blur');
|
||||
|
|
142
app/javascript/vue/user_preferences/container.vue
Normal file
142
app/javascript/vue/user_preferences/container.vue
Normal file
|
@ -0,0 +1,142 @@
|
|||
<template>
|
||||
<div class="content-pane flexible with-grey-background">
|
||||
<div class="content-header">
|
||||
<div class="title-row">
|
||||
<h1 class="mt-0">
|
||||
{{ i18n.t('users.settings.account.preferences.title') }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 mb-4 bg-sn-white rounded">
|
||||
<div>
|
||||
<h2 class="mt-0">{{ i18n.t("users.settings.account.preferences.edit.time_zone_label") }}</h2>
|
||||
<div class="text-sn-dark-grey mb-4">
|
||||
<p>{{ i18n.t("users.settings.account.preferences.edit.time_zone_sublabel") }}</p>
|
||||
</div>
|
||||
<SelectSearch
|
||||
class="max-w-[40ch]"
|
||||
:value="selectedTimeZone"
|
||||
@change="setTimeZone"
|
||||
:options="timeZones"
|
||||
/>
|
||||
</div>
|
||||
<div class="sci-divider my-6 inline-block"></div>
|
||||
<div>
|
||||
<h2 class="mt-0">{{ i18n.t("users.settings.account.preferences.edit.date_format_label") }}</h2>
|
||||
<div class="text-sn-dark-grey mb-4">
|
||||
<p>{{ i18n.t("users.settings.account.preferences.edit.date_format_sublabel") }}</p>
|
||||
</div>
|
||||
<SelectSearch
|
||||
class="max-w-[40ch]"
|
||||
:value="selectedDateFormat"
|
||||
@change="setDateFormat"
|
||||
:options="dateFormats"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 mb-4 bg-sn-white rounded">
|
||||
<h2 class="mt-0">{{ i18n.t('notifications.title') }}</h2>
|
||||
<div class="text-sn-dark-grey">
|
||||
<p>{{ i18n.t('notifications.sub_title') }}</p>
|
||||
</div>
|
||||
<table v-if="notificationsSettings">
|
||||
<template v-for="(_subGroups, group) in notificationsGroups" :key="group">
|
||||
<div class="contents">
|
||||
<tr>
|
||||
<td colspan=3 class="pt-6"><h3>{{ i18n.t(`notifications.groups.${group}`) }}</h3></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="p-2.5 text-base w-32">{{ i18n.t('notifications.in_app') }}</td>
|
||||
<td class="p-2.5 text-base w-32">{{ i18n.t('notifications.email') }}</td>
|
||||
</tr>
|
||||
</div>
|
||||
<template v-for="(_notifications, subGroup, i) in notificationsGroups[group]" :key="subGroup">
|
||||
<tr v-if="subGroup !== 'always_on'"
|
||||
class="text-base border-transparent border-b-sn-super-light-grey border-solid"
|
||||
:class="{'border-t-sn-super-light-grey': i == 0}"
|
||||
>
|
||||
<td class="p-2.5 pr-10">{{ i18n.t(`notifications.sub_groups.${subGroup}`) }}</td>
|
||||
<td class="p-2.5">
|
||||
<div class="sci-toggle-checkbox-container">
|
||||
<input v-model="notificationsSettings[subGroup]['in_app']" type="checkbox" class="sci-toggle-checkbox" @change="setNotificationsSettings"/>
|
||||
<label class="sci-toggle-checkbox-label"></label>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-2.5">
|
||||
<div class="sci-toggle-checkbox-container">
|
||||
<input v-model="notificationsSettings[subGroup]['email']" type="checkbox" class="sci-toggle-checkbox" @change="setNotificationsSettings"/>
|
||||
<label class="sci-toggle-checkbox-label"></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import SelectSearch from "../shared/select_search.vue";
|
||||
import axios from '../../packs/custom_axios.js';
|
||||
|
||||
export default {
|
||||
name: "UserPreferences",
|
||||
props: {
|
||||
userSettings: Object,
|
||||
timeZones: Array,
|
||||
dateFormats: Array,
|
||||
updateUrl: String,
|
||||
notificationsGroups: Object
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
selectedTimeZone: null,
|
||||
selectedDateFormat: null,
|
||||
notificationsSettings: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.selectedTimeZone = this.userSettings.time_zone;
|
||||
this.selectedDateFormat = this.userSettings.date_format;
|
||||
this.notificationsSettings = {...this.emptySettings, ...this.userSettings.notifications_settings};
|
||||
|
||||
},
|
||||
computed: {
|
||||
emptySettings() {
|
||||
let settings = {};
|
||||
for (const group in this.notificationsGroups) {
|
||||
for (const subGroup in this.notificationsGroups[group]) {
|
||||
settings[subGroup] = { in_app: false, email: false };
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
SelectSearch,
|
||||
PerfectScrollbar
|
||||
},
|
||||
methods: {
|
||||
setTimeZone(value) {
|
||||
this.selectedTimeZone = value;
|
||||
axios.put(this.updateUrl, {
|
||||
user: { time_zone: value }
|
||||
})
|
||||
},
|
||||
setDateFormat(value) {
|
||||
this.selectedDateFormat = value;
|
||||
axios.put(this.updateUrl, {
|
||||
user: { date_format: value }
|
||||
})
|
||||
},
|
||||
setNotificationsSettings() {
|
||||
axios.put(this.updateUrl, {
|
||||
user: { notifications_settings: this.notificationsSettings }
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -24,12 +24,14 @@ module FailedDeliveryNotifiableJob
|
|||
@user = User.find_by(id: arguments.last[:user_id])
|
||||
return if @user.blank?
|
||||
|
||||
notification = Notification.create!(
|
||||
type_of: :deliver_error,
|
||||
DeliveryNotification.send_notifications(
|
||||
{
|
||||
title: failed_notification_title,
|
||||
message: failed_notification_message
|
||||
message: failed_notification_message,
|
||||
error: true,
|
||||
user: @user
|
||||
}
|
||||
)
|
||||
notification.create_user_notification(@user)
|
||||
end
|
||||
|
||||
def failed_notification_title
|
||||
|
|
13
app/jobs/my_modules/due_date_reminder_job.rb
Normal file
13
app/jobs/my_modules/due_date_reminder_job.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MyModules
|
||||
class DueDateReminderJob < ApplicationJob
|
||||
def perform
|
||||
my_modules = MyModule.uncomplete.approaching_due_dates
|
||||
|
||||
my_modules.each do |task|
|
||||
TaskDueDateNotification.send_notifications({ my_module_id: task.id })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
7
app/jobs/notification_cleanup_job.rb
Normal file
7
app/jobs/notification_cleanup_job.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NotificationCleanupJob < ApplicationJob
|
||||
def perform
|
||||
Notification.where('created_at < ?', 3.months.ago).delete_all
|
||||
end
|
||||
end
|
|
@ -136,15 +136,17 @@ module Protocols
|
|||
"href='#{Rails.application.routes.url_helpers.rails_blob_path(@tmp_files.take.file)}'>" \
|
||||
"#{@tmp_files.take.file.filename}</a>"
|
||||
|
||||
notification = Notification.create!(
|
||||
type_of: :deliver,
|
||||
title: I18n.t('protocols.import_export.import_protocol_notification.title', link: original_file_download_link),
|
||||
DeliveryNotification.send_notifications(
|
||||
{
|
||||
title:
|
||||
I18n.t('protocols.import_export.import_protocol_notification.title', link: original_file_download_link),
|
||||
message: "#{I18n.t('protocols.import_export.import_protocol_notification.message')} " \
|
||||
"<a data-id='#{@protocol.id}' data-turbolinks='false' " \
|
||||
"href='#{Rails.application.routes.url_helpers.protocol_path(@protocol)}'>" \
|
||||
"#{@protocol.name}</a>"
|
||||
"#{@protocol.name}</a>",
|
||||
user: @user
|
||||
}
|
||||
)
|
||||
notification.create_user_notification(@user)
|
||||
end
|
||||
|
||||
# Overrides method from FailedDeliveryNotifiableJob concern
|
||||
|
|
|
@ -21,16 +21,22 @@ module Reports
|
|||
report.docx_ready!
|
||||
report_path = Rails.application.routes.url_helpers
|
||||
.reports_path(team: report.team.id, preview_report_id: report.id, preview_type: :docx)
|
||||
notification = Notification.create(
|
||||
type_of: :deliver,
|
||||
|
||||
DeliveryNotification.send_notifications(
|
||||
{
|
||||
title: I18n.t('projects.reports.index.generation.completed_docx_notification_title'),
|
||||
message: I18n.t('projects.reports.index.generation.completed_notification_message',
|
||||
report_link: "<a href='#{report_path}'>#{escape_input(report.name)}</a>",
|
||||
team_name: escape_input(report.team.name))
|
||||
team_name: escape_input(report.team.name)),
|
||||
subject_id: report_id,
|
||||
subject_class: 'Report',
|
||||
subject_name: report.name,
|
||||
report_type: 'docx',
|
||||
user: user
|
||||
}
|
||||
)
|
||||
|
||||
Reports::DocxPreviewJob.perform_now(report.id)
|
||||
notification.create_user_notification(user)
|
||||
ensure
|
||||
I18n.backend.date_format = nil
|
||||
file.close
|
||||
|
|
|
@ -162,14 +162,19 @@ module Reports
|
|||
def create_notification_for_user
|
||||
report_path = Rails.application.routes.url_helpers
|
||||
.reports_path(team: @report.team.id, preview_report_id: @report.id, preview_type: :pdf)
|
||||
notification = Notification.create(
|
||||
type_of: :deliver,
|
||||
DeliveryNotification.send_notifications(
|
||||
{
|
||||
title: I18n.t('projects.reports.index.generation.completed_pdf_notification_title'),
|
||||
message: I18n.t('projects.reports.index.generation.completed_notification_message',
|
||||
report_link: "<a href='#{report_path}'>#{escape_input(@report.name)}</a>",
|
||||
team_name: escape_input(@report.team.name))
|
||||
team_name: escape_input(@report.team.name)),
|
||||
subject_id: @report.id,
|
||||
subject_class: 'Report',
|
||||
subject_name: @report.name,
|
||||
report_type: 'pdf',
|
||||
user: @user
|
||||
}
|
||||
)
|
||||
notification.create_user_notification(@user)
|
||||
end
|
||||
|
||||
def append_result_asset_previews
|
||||
|
@ -222,9 +227,9 @@ module Reports
|
|||
merged_file
|
||||
end
|
||||
|
||||
def prepend_title_page(file, template, report, renderer)
|
||||
unless File.exist?(Rails.root.join('app', 'views', 'reports', 'templates', template, 'cover.html.erb'))
|
||||
return file
|
||||
def prepend_title_page
|
||||
unless File.exist?(Rails.root.join('app', 'views', 'reports', 'templates', @template, 'cover.html.erb'))
|
||||
return @file
|
||||
end
|
||||
|
||||
total_pages = 0
|
||||
|
|
|
@ -83,8 +83,8 @@ class RepositoriesExportJob < ApplicationJob
|
|||
end
|
||||
|
||||
def generate_notification
|
||||
notification = Notification.create!(
|
||||
type_of: :deliver,
|
||||
DeliveryNotification.send_notifications(
|
||||
{
|
||||
title: I18n.t('zip_export.notification_title'),
|
||||
message: "<a data-id='#{@zip_export.id}' " \
|
||||
"data-turbolinks='false' " \
|
||||
|
@ -92,9 +92,10 @@ class RepositoriesExportJob < ApplicationJob
|
|||
.routes
|
||||
.url_helpers
|
||||
.zip_exports_download_export_all_path(@zip_export)}'>" \
|
||||
"#{@zip_export.zip_file_name}</a>"
|
||||
"#{@zip_export.zip_file_name}</a>",
|
||||
user: @user
|
||||
}
|
||||
)
|
||||
notification.create_user_notification(@user)
|
||||
end
|
||||
|
||||
# Overrides method from FailedDeliveryNotifiableJob concern
|
||||
|
|
39
app/jobs/repository_item_date_reminder_job.rb
Normal file
39
app/jobs/repository_item_date_reminder_job.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RepositoryItemDateReminderJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
process_repository_values(RepositoryDateTimeValue, DateTime.current)
|
||||
process_repository_values(RepositoryDateValue, Date.current)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_repository_values(model, comparison_value)
|
||||
model
|
||||
.joins(repository_cell: { repository_column: :repository })
|
||||
.where(notification_sent: false, repositories: { type: 'Repository' })
|
||||
.where('repository_date_time_values.updated_at >= ?', 2.days.ago)
|
||||
.where( # date(time) values that are within the reminder range
|
||||
"data <= " \
|
||||
"(?::timestamp + CAST(((repository_columns.metadata->>'reminder_unit')::int * " \
|
||||
"(repository_columns.metadata->>'reminder_value')::int) || ' seconds' AS Interval))",
|
||||
comparison_value
|
||||
).find_each do |value|
|
||||
repository_row = RepositoryRow.find(value.repository_cell.repository_row_id)
|
||||
repository_column = RepositoryColumn.find(value.repository_cell.repository_column_id)
|
||||
|
||||
RepositoryItemDateNotification
|
||||
.send_notifications({
|
||||
"#{value.class.name.underscore}_id": value.id,
|
||||
repository_row_id: repository_row.id,
|
||||
repository_row_name: repository_row.name,
|
||||
repository_column_id: repository_column.id,
|
||||
repository_column_name: repository_column.name,
|
||||
reminder_unit: repository_column.metadata['reminder_unit'],
|
||||
reminder_value: repository_column.metadata['reminder_value']
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
|
@ -35,7 +35,7 @@ class RepositoryZipExportJob < ZipExportJob
|
|||
repository,
|
||||
nil,
|
||||
params[:my_module_id].present?)
|
||||
File.binwrite("#{dir}/export.csv", data)
|
||||
File.binwrite("#{dir}/export.csv", data.encode('UTF-8', invalid: :replace, undef: :replace))
|
||||
end
|
||||
|
||||
def failed_notification_title
|
||||
|
|
|
@ -34,8 +34,8 @@ class ZipExportJob < ApplicationJob
|
|||
end
|
||||
|
||||
def generate_notification!
|
||||
notification = Notification.create!(
|
||||
type_of: :deliver,
|
||||
DeliveryNotification.send_notifications(
|
||||
{
|
||||
title: I18n.t('zip_export.notification_title'),
|
||||
message: "<a data-id='#{@zip_export.id}' " \
|
||||
"data-turbolinks='false' " \
|
||||
|
@ -43,8 +43,9 @@ class ZipExportJob < ApplicationJob
|
|||
.routes
|
||||
.url_helpers
|
||||
.zip_exports_download_path(@zip_export)}'>" \
|
||||
"#{@zip_export.zip_file_name}</a>"
|
||||
"#{@zip_export.zip_file_name}</a>",
|
||||
user: @user
|
||||
}
|
||||
)
|
||||
notification.create_user_notification(@user)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,4 +22,16 @@ class AppMailer < Devise::Mailer
|
|||
}.merge(opts)
|
||||
mail(headers)
|
||||
end
|
||||
|
||||
def general_notification(opts = {})
|
||||
@user = params[:recipient]
|
||||
@notification = params[:record].to_notification
|
||||
|
||||
mail(
|
||||
{
|
||||
to: @user.email,
|
||||
subject: I18n.t('notifications.email_title')
|
||||
}.merge(opts)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ module Cloneable
|
|||
last_clone_number =
|
||||
parent.public_send(self.class.table_name)
|
||||
.select("substring(#{self.class.table_name}.name, '(?:^#{clone_label} )(\\d+)')::int AS clone_number")
|
||||
.where('name ~ ?', "^#{clone_label} \\d+ - #{name}$")
|
||||
.where('name ~ ?', "^#{clone_label} \\d+ - #{Regexp.escape(name)}$")
|
||||
.order(clone_number: :asc)
|
||||
.last&.clone_number
|
||||
|
||||
|
|
|
@ -9,104 +9,14 @@ module GenerateNotificationModel
|
|||
end
|
||||
|
||||
def generate_notification_from_activity
|
||||
return if notification_recipients.none?
|
||||
|
||||
message = generate_activity_content(self, no_links: true, no_custom_links: true)
|
||||
description = generate_notification_description_elements(subject).reverse.join(' | ')
|
||||
|
||||
notification = Notification.create(
|
||||
type_of: notification_type,
|
||||
title: sanitize_input(message),
|
||||
message: sanitize_input(description),
|
||||
generator_user_id: owner.id
|
||||
)
|
||||
|
||||
notification_recipients.each do |user|
|
||||
notification.create_user_notification(user)
|
||||
end
|
||||
params = { activity_id: id, type: "#{type_of}_activity".to_sym }
|
||||
ActivityNotification.send_notifications(params, later: true)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def notification_recipients
|
||||
users = []
|
||||
|
||||
case subject
|
||||
when Project
|
||||
users = subject.users
|
||||
when Experiment
|
||||
users = subject.users
|
||||
when MyModule
|
||||
users = subject.designated_users
|
||||
# Also send to the user that was unassigned,
|
||||
# and is therefore no longer present on the module.
|
||||
if type_of == 'undesignate_user_from_my_module'
|
||||
users += User.where(id: values.dig('message_items', 'user_target', 'id'))
|
||||
end
|
||||
when Protocol
|
||||
users = subject.in_repository? ? [] : subject.my_module.designated_users
|
||||
when Result
|
||||
users = subject.my_module.designated_users
|
||||
when Repository
|
||||
users = subject.team.users
|
||||
when Team
|
||||
users = subject.users
|
||||
when Report
|
||||
users = subject.team.users
|
||||
when ProjectFolder
|
||||
users = subject.team.users
|
||||
end
|
||||
users - [owner]
|
||||
end
|
||||
|
||||
# This method returns unsanitized elements. They must be sanitized before saving to DB
|
||||
def generate_notification_description_elements(object, elements = [])
|
||||
case object
|
||||
when Project
|
||||
path = Rails.application.routes.url_helpers.project_path(object)
|
||||
elements << "#{I18n.t('search.index.project')} <a href='#{path}'>#{object.name}</a>"
|
||||
when Experiment
|
||||
path = Rails.application.routes.url_helpers.my_modules_experiment_path(object)
|
||||
elements << "#{I18n.t('search.index.experiment')} <a href='#{path}'>#{object.name}</a>"
|
||||
generate_notification_description_elements(object.project, elements)
|
||||
when MyModule
|
||||
path = if object.archived?
|
||||
Rails.application.routes.url_helpers.my_modules_experiment_path(object.experiment, view_mode: :archived)
|
||||
else
|
||||
Rails.application.routes.url_helpers.protocols_my_module_path(object)
|
||||
end
|
||||
elements << "#{I18n.t('search.index.module')} <a href='#{path}'>#{object.name}</a>"
|
||||
generate_notification_description_elements(object.experiment, elements)
|
||||
when Protocol
|
||||
if object.in_repository?
|
||||
path = Rails.application.routes.url_helpers.protocols_path(team: object.team.id)
|
||||
elements << "#{I18n.t('search.index.protocol')} <a href='#{path}'>#{object.name}</a>"
|
||||
generate_notification_description_elements(object.team, elements)
|
||||
else
|
||||
generate_notification_description_elements(object.my_module, elements)
|
||||
end
|
||||
when Result
|
||||
generate_notification_description_elements(object.my_module, elements)
|
||||
when Repository
|
||||
path = Rails.application.routes.url_helpers.repository_path(object, team: object.team.id)
|
||||
elements << "#{I18n.t('search.index.repository')} <a href='#{path}'>#{object.name}</a>"
|
||||
generate_notification_description_elements(object.team, elements)
|
||||
when Team
|
||||
path = Rails.application.routes.url_helpers.projects_path(team: object.id)
|
||||
elements << "#{I18n.t('search.index.team')} <a href='#{path}'>#{object.name}</a>"
|
||||
when Report
|
||||
path = Rails.application.routes.url_helpers.reports_path(team: object.team.id)
|
||||
elements << "#{I18n.t('search.index.report')} <a href='#{path}'>#{object.name}</a>"
|
||||
generate_notification_description_elements(object.team, elements)
|
||||
when ProjectFolder
|
||||
generate_notification_description_elements(object.team, elements)
|
||||
end
|
||||
|
||||
elements
|
||||
end
|
||||
|
||||
def notifiable?
|
||||
type_of.in? ::Extends::NOTIFIABLE_ACTIVITIES
|
||||
NotificationExtends::NOTIFICATIONS_TYPES.key?("#{type_of}_activity".to_sym)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -114,14 +24,4 @@ module GenerateNotificationModel
|
|||
def generate_notification
|
||||
CreateNotificationFromActivityJob.perform_later(self) if notifiable?
|
||||
end
|
||||
|
||||
def notification_type
|
||||
return :recent_changes unless instance_of?(Activity)
|
||||
|
||||
if type_of.in? Activity::ASSIGNMENT_TYPES
|
||||
:assignment
|
||||
else
|
||||
:recent_changes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class LinkedRepository < Repository
|
||||
enum permission_level: Extends::SHARED_OBJECTS_PERMISSION_LEVELS.except(:shared_write)
|
||||
|
||||
def shareable_write?
|
||||
false
|
||||
end
|
||||
|
||||
def default_table_state
|
||||
state = Constants::REPOSITORY_TABLE_DEFAULT_STATE.deep_dup
|
||||
state['order'] = [[3, 'asc']]
|
||||
|
@ -12,13 +18,13 @@ class LinkedRepository < Repository
|
|||
def default_sortable_columns
|
||||
[
|
||||
'assigned',
|
||||
'repository_rows.external_id',
|
||||
'repository_rows.id',
|
||||
'repository_rows.name',
|
||||
'repository_rows.created_at',
|
||||
'users.full_name',
|
||||
'repository_rows.archived_on',
|
||||
'archived_bies_repository_rows.full_name'
|
||||
'archived_bies_repository_rows.full_name',
|
||||
'repository_rows.external_id'
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ class MyModule < ApplicationRecord
|
|||
|
||||
before_validation :archiving_and_restoring_extras, on: :update, if: :archived_changed?
|
||||
before_save -> { report_elements.destroy_all }, if: -> { !new_record? && experiment_id_changed? }
|
||||
before_save :reset_due_date_notification_sent, if: -> { due_date_changed? }
|
||||
around_save :exec_status_consequences, if: :my_module_status_id_changed?
|
||||
before_create :create_blank_protocol
|
||||
before_create :assign_default_status_flow
|
||||
|
@ -139,6 +140,11 @@ class MyModule < ApplicationRecord
|
|||
joins(experiment: :project).where(experiment: { projects: { team: teams } })
|
||||
end
|
||||
|
||||
def self.approaching_due_dates
|
||||
where(due_date_notification_sent: false)
|
||||
.where('due_date > ? AND due_date <= ?', DateTime.current, DateTime.current + 1.day)
|
||||
end
|
||||
|
||||
def parent
|
||||
experiment
|
||||
end
|
||||
|
@ -529,6 +535,10 @@ class MyModule < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def reset_due_date_notification_sent
|
||||
self.due_date_notification_sent = false
|
||||
end
|
||||
|
||||
def archiving_and_restoring_extras
|
||||
if archived?
|
||||
# Removes connections with other modules
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue