mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 06:35:56 +08:00
Merge branch 'develop' into features/inventory-items-relationships
This commit is contained in:
commit
372ef32f03
|
@ -35,6 +35,10 @@
|
|||
"template": 240,
|
||||
"tabWidth": 2
|
||||
}
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"never"
|
||||
]
|
||||
},
|
||||
"globals": {
|
||||
|
|
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: 09b7b4910f59453970aab03d7b3ddb60b41db89a
|
||||
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
|
||||
|
|
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;
|
||||
|
|
|
@ -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 })
|
||||
|
||||
|
|
|
@ -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,14 +538,23 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def log_activity(type_of, message_items = {})
|
||||
message_items = { repository: @repository.id }.merge(message_items)
|
||||
if @repository.present?
|
||||
message_items = { repository: @repository.id }.merge(message_items)
|
||||
|
||||
Activities::CreateActivityService
|
||||
.call(activity_type: type_of,
|
||||
owner: current_user,
|
||||
subject: @repository,
|
||||
team: @repository.team,
|
||||
message_items: message_items)
|
||||
Activities::CreateActivityService
|
||||
.call(activity_type: type_of,
|
||||
owner: current_user,
|
||||
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
|
||||
|
|
|
@ -460,6 +460,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,
|
||||
title: sanitize_input(title),
|
||||
message: sanitize_input(message)
|
||||
def generate_annotation_notification(target_user, title, subject)
|
||||
GeneralNotification.send_notifications(
|
||||
{
|
||||
type: :smart_annotation_added,
|
||||
title: sanitize_input(title),
|
||||
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)
|
||||
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
|
||||
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)
|
||||
end
|
||||
message = "#{I18n.t('search.index.team')} #{team.name}"
|
||||
end
|
||||
|
||||
notification = Notification.create(
|
||||
type_of: :assignment,
|
||||
title: sanitize_input(title),
|
||||
message: sanitize_input(message)
|
||||
)
|
||||
|
||||
if target_user.assignments_notification
|
||||
notification.create_user_notification(target_user)
|
||||
end
|
||||
GeneralNotification.send_notifications({
|
||||
type: role ? :invite_user_to_team : :remove_user_from_team,
|
||||
title: sanitize_input(title),
|
||||
message: sanitize_input(message),
|
||||
user: target_user
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -242,13 +242,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');
|
|
@ -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,12 +40,13 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
notifications: [],
|
||||
nextPage: 1,
|
||||
nextPageUrl: null,
|
||||
scrollBar: null,
|
||||
loadingPage: false
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.nextPageUrl = this.notificationsUrl;
|
||||
this.loadNotifications();
|
||||
},
|
||||
mounted() {
|
||||
|
@ -64,23 +66,30 @@ export default {
|
|||
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;
|
||||
this.loadingPage = false;
|
||||
this.$emit('update:unseenNotificationsCount');
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
@ -240,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) {
|
||||
|
|
|
@ -80,9 +80,6 @@
|
|||
valueLabel() {
|
||||
let option = this.options.find((o) => o[0] === this.value);
|
||||
return option && option[1];
|
||||
},
|
||||
focusElement() {
|
||||
return this.$refs.focusElement || this.$parent.$refs.focusElement;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -98,7 +95,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();
|
||||
|
|
|
@ -16,6 +16,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>
|
||||
|
@ -100,6 +101,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,
|
||||
title: failed_notification_title,
|
||||
message: failed_notification_message
|
||||
DeliveryNotification.send_notifications(
|
||||
{
|
||||
title: failed_notification_title,
|
||||
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),
|
||||
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>"
|
||||
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>",
|
||||
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,
|
||||
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))
|
||||
|
||||
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)),
|
||||
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,
|
||||
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))
|
||||
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)),
|
||||
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,18 +83,19 @@ class RepositoriesExportJob < ApplicationJob
|
|||
end
|
||||
|
||||
def generate_notification
|
||||
notification = Notification.create!(
|
||||
type_of: :deliver,
|
||||
title: I18n.t('zip_export.notification_title'),
|
||||
message: "<a data-id='#{@zip_export.id}' " \
|
||||
"data-turbolinks='false' " \
|
||||
"href='#{Rails.application
|
||||
.routes
|
||||
.url_helpers
|
||||
.zip_exports_download_export_all_path(@zip_export)}'>" \
|
||||
"#{@zip_export.zip_file_name}</a>"
|
||||
DeliveryNotification.send_notifications(
|
||||
{
|
||||
title: I18n.t('zip_export.notification_title'),
|
||||
message: "<a data-id='#{@zip_export.id}' " \
|
||||
"data-turbolinks='false' " \
|
||||
"href='#{Rails.application
|
||||
.routes
|
||||
.url_helpers
|
||||
.zip_exports_download_export_all_path(@zip_export)}'>" \
|
||||
"#{@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,17 +34,18 @@ class ZipExportJob < ApplicationJob
|
|||
end
|
||||
|
||||
def generate_notification!
|
||||
notification = Notification.create!(
|
||||
type_of: :deliver,
|
||||
title: I18n.t('zip_export.notification_title'),
|
||||
message: "<a data-id='#{@zip_export.id}' " \
|
||||
"data-turbolinks='false' " \
|
||||
"href='#{Rails.application
|
||||
.routes
|
||||
.url_helpers
|
||||
.zip_exports_download_path(@zip_export)}'>" \
|
||||
"#{@zip_export.zip_file_name}</a>"
|
||||
DeliveryNotification.send_notifications(
|
||||
{
|
||||
title: I18n.t('zip_export.notification_title'),
|
||||
message: "<a data-id='#{@zip_export.id}' " \
|
||||
"data-turbolinks='false' " \
|
||||
"href='#{Rails.application
|
||||
.routes
|
||||
.url_helpers
|
||||
.zip_exports_download_path(@zip_export)}'>" \
|
||||
"#{@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
|
||||
|
|
|
@ -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
|
||||
|
|
9
app/models/non_existant_record.rb
Normal file
9
app/models/non_existant_record.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NonExistantRecord
|
||||
attr_reader :name
|
||||
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
end
|
|
@ -1,19 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Notification < ApplicationRecord
|
||||
has_many :user_notifications, inverse_of: :notification, dependent: :destroy
|
||||
has_many :users, through: :user_notifications
|
||||
belongs_to :generator_user, class_name: 'User', optional: true
|
||||
include Noticed::Model
|
||||
|
||||
enum type_of: Extends::NOTIFICATIONS_TYPES
|
||||
belongs_to :recipient, polymorphic: true
|
||||
|
||||
def create_user_notification(user)
|
||||
return if user == generator_user
|
||||
return unless can_send_to_user?(user)
|
||||
return unless user.enabled_notifications_for?(type_of.to_sym, :web)
|
||||
|
||||
user_notifications.create!(user: user)
|
||||
end
|
||||
scope :in_app, lambda {
|
||||
where.not("notifications.params ? 'hide_in_app' AND notifications.params->'hide_in_app' = 'true'")
|
||||
}
|
||||
|
||||
private
|
||||
|
||||
|
|
|
@ -337,7 +337,7 @@ class Project < ApplicationRecord
|
|||
def auto_assign_project_members
|
||||
return if skip_user_assignments
|
||||
|
||||
UserAssignments::ProjectGroupAssignmentJob.perform_later(
|
||||
UserAssignments::ProjectGroupAssignmentJob.perform_now(
|
||||
team,
|
||||
self,
|
||||
last_modified_by&.id || created_by&.id
|
||||
|
|
|
@ -11,7 +11,7 @@ class Repository < RepositoryBase
|
|||
ID_PREFIX = 'IN'
|
||||
include PrefixedIdModel
|
||||
|
||||
enum permission_level: Extends::SHARED_INVENTORIES_PERMISSION_LEVELS
|
||||
enum permission_level: Extends::SHARED_OBJECTS_PERMISSION_LEVELS
|
||||
|
||||
belongs_to :archived_by,
|
||||
foreign_key: :archived_by_id,
|
||||
|
@ -55,8 +55,8 @@ class Repository < RepositoryBase
|
|||
.where(team: teams)
|
||||
.or(accessible_repositories.where(team_shared_objects: { team: teams }))
|
||||
.or(accessible_repositories
|
||||
.where(permission_level: [Extends::SHARED_INVENTORIES_PERMISSION_LEVELS[:shared_read],
|
||||
Extends::SHARED_INVENTORIES_PERMISSION_LEVELS[:shared_write]]))
|
||||
.where(permission_level: [Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:shared_read],
|
||||
Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:shared_write]]))
|
||||
accessible_repositories.distinct
|
||||
}
|
||||
|
||||
|
@ -112,6 +112,10 @@ class Repository < RepositoryBase
|
|||
teams.blank? ? self : where(team: teams)
|
||||
end
|
||||
|
||||
def shareable_write?
|
||||
true
|
||||
end
|
||||
|
||||
def permission_parent
|
||||
team
|
||||
end
|
||||
|
@ -204,7 +208,7 @@ class Repository < RepositoryBase
|
|||
new_repo = dup
|
||||
new_repo.created_by = created_by
|
||||
new_repo.name = name
|
||||
new_repo.permission_level = Extends::SHARED_INVENTORIES_PERMISSION_LEVELS[:not_shared]
|
||||
new_repo.permission_level = Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:not_shared]
|
||||
new_repo.save!
|
||||
|
||||
# Clone columns (only if new_repo was saved)
|
||||
|
|
|
@ -70,6 +70,7 @@ class RepositoryAssetValue < ApplicationRecord
|
|||
asset.last_modified_by = user
|
||||
self.last_modified_by = user
|
||||
asset.save! && save!
|
||||
asset.post_process_file(repository_cell.repository_column.repository.team)
|
||||
end
|
||||
|
||||
def snapshot!(cell_snapshot)
|
||||
|
|
|
@ -9,6 +9,7 @@ class RepositoryDateTimeValueBase < ApplicationRecord
|
|||
inverse_of: :modified_repository_date_time_values
|
||||
has_one :repository_cell, as: :value, dependent: :destroy
|
||||
accepts_nested_attributes_for :repository_cell
|
||||
before_save :reset_notification_sent, if: -> { data_changed? }
|
||||
|
||||
validates :repository_cell, :data, :type, presence: true
|
||||
|
||||
|
@ -33,4 +34,10 @@ class RepositoryDateTimeValueBase < ApplicationRecord
|
|||
)
|
||||
value_snapshot.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_notification_sent
|
||||
self.notification_sent = false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -45,7 +45,7 @@ class RepositorySnapshot < RepositoryBase
|
|||
my_module: my_module,
|
||||
created_by: created_by,
|
||||
team: my_module.experiment.project.team,
|
||||
permission_level: Extends::SHARED_INVENTORIES_PERMISSION_LEVELS[:not_shared])
|
||||
permission_level: Extends::SHARED_OBJECTS_PERMISSION_LEVELS[:not_shared])
|
||||
repository_snapshot.provisioning!
|
||||
repository_snapshot.reload
|
||||
end
|
||||
|
|
|
@ -18,6 +18,8 @@ class RepositoryStockValue < ApplicationRecord
|
|||
validates :low_stock_threshold, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
||||
|
||||
before_save :update_consumption_stock_units, if: :repository_stock_unit_item_id_changed?
|
||||
after_save :send_low_stock_notification, if: -> { status == :low }
|
||||
|
||||
after_create do
|
||||
next if is_a?(RepositoryStockConsumptionValue)
|
||||
|
||||
|
@ -199,4 +201,18 @@ class RepositoryStockValue < ApplicationRecord
|
|||
.my_module_repository_rows
|
||||
.update_all(repository_stock_unit_item_id: repository_stock_unit_item_id)
|
||||
end
|
||||
|
||||
def send_low_stock_notification
|
||||
repository_row = repository_cell.repository_row
|
||||
repository = repository_row.repository
|
||||
|
||||
return unless repository.class.name == 'Repository'
|
||||
|
||||
LowStockNotification.send_notifications({
|
||||
repository_row_id: repository_cell.repository_row_id,
|
||||
repository_row_name: repository_row.name,
|
||||
repository_id: repository_row.repository_id,
|
||||
repository_name: repository.name
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,7 +41,7 @@ class User < ApplicationRecord
|
|||
recent: true,
|
||||
recent_email: false,
|
||||
system_message_email: false
|
||||
}
|
||||
}.merge(Extends::DEFAULT_USER_NOTIFICATION_SETTINGS)
|
||||
}.freeze
|
||||
|
||||
DEFAULT_OTP_DRIFT_TIME_SECONDS = 10
|
||||
|
@ -307,8 +307,7 @@ class User < ApplicationRecord
|
|||
inverse_of: :created_by,
|
||||
dependent: :destroy
|
||||
|
||||
has_many :user_notifications, inverse_of: :user
|
||||
has_many :notifications, through: :user_notifications
|
||||
has_many :notifications, as: :recipient, dependent: :destroy, inverse_of: :recipient
|
||||
has_many :zip_exports, inverse_of: :user, dependent: :destroy
|
||||
has_many :view_states, dependent: :destroy
|
||||
|
||||
|
@ -322,7 +321,6 @@ class User < ApplicationRecord
|
|||
has_many :hidden_repository_cell_reminders, dependent: :destroy
|
||||
|
||||
before_validation :downcase_email!
|
||||
before_destroy :destroy_notifications
|
||||
|
||||
def name
|
||||
full_name
|
||||
|
@ -514,40 +512,6 @@ class User < ApplicationRecord
|
|||
user_identities.exists?(provider: provider)
|
||||
end
|
||||
|
||||
# json friendly attributes
|
||||
NOTIFICATIONS_TYPES = %w(assignments_notification recent_notification
|
||||
assignments_email_notification
|
||||
recent_email_notification)
|
||||
|
||||
# declare notifications getters
|
||||
NOTIFICATIONS_TYPES.each do |name|
|
||||
define_method(name) do
|
||||
attr_name = name.gsub('_notification', '')
|
||||
notifications_settings.fetch(attr_name.to_sym)
|
||||
end
|
||||
end
|
||||
|
||||
# declare notifications setters
|
||||
NOTIFICATIONS_TYPES.each do |name|
|
||||
define_method("#{name}=") do |value|
|
||||
attr_name = name.gsub('_notification', '').to_sym
|
||||
notifications_settings[attr_name] = value
|
||||
end
|
||||
end
|
||||
|
||||
def enabled_notifications_for?(notification_type, channel)
|
||||
return true if %i(deliver deliver_error).include?(notification_type)
|
||||
|
||||
case channel
|
||||
when :web
|
||||
notification_type == :recent_changes && recent_notification ||
|
||||
notification_type == :assignment && assignments_notification
|
||||
when :email
|
||||
notification_type == :recent_changes && recent_email_notification ||
|
||||
notification_type == :assignment && assignments_email_notification
|
||||
end
|
||||
end
|
||||
|
||||
def increase_daily_exports_counter!
|
||||
range = Time.now.utc.beginning_of_day.to_i..Time.now.utc.end_of_day.to_i
|
||||
last_export = export_vars[:last_export_timestamp] || 0
|
||||
|
@ -670,25 +634,6 @@ class User < ApplicationRecord
|
|||
self.email = email.downcase
|
||||
end
|
||||
|
||||
def destroy_notifications
|
||||
# Find all notifications where user is the only reference
|
||||
# on the notification, and destroy all such notifications
|
||||
# (user_notifications are destroyed when notification is
|
||||
# destroyed). We try to do this efficiently (hence in_groups_of).
|
||||
nids_all = notifications.pluck(:id)
|
||||
nids_all.in_groups_of(1000, false) do |nids|
|
||||
Notification
|
||||
.where(id: nids)
|
||||
.joins(:user_notifications)
|
||||
.group('notifications.id')
|
||||
.having('count(notification_id) <= 1')
|
||||
.destroy_all
|
||||
end
|
||||
|
||||
# Now, simply destroy all user notification relations left
|
||||
user_notifications.destroy_all
|
||||
end
|
||||
|
||||
def clear_view_cache
|
||||
Rails.cache.delete_matched(%r{^views\/users\/#{id}-})
|
||||
end
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserNotification < ApplicationRecord
|
||||
include NotificationsHelper
|
||||
|
||||
belongs_to :user, optional: true
|
||||
belongs_to :notification, optional: true
|
||||
|
||||
after_create :send_email
|
||||
|
||||
def self.unseen_notification_count(user)
|
||||
where('user_id = ? AND checked = false', user.id).count
|
||||
end
|
||||
|
||||
def self.seen_by_user(user)
|
||||
where(user: user).where(checked: false).update_all(checked: true)
|
||||
end
|
||||
|
||||
def send_email
|
||||
send_email_notification(user, notification) if user.enabled_notifications_for?(notification.type_of.to_sym, :email)
|
||||
end
|
||||
end
|
41
app/notifications/activity_notification.rb
Normal file
41
app/notifications/activity_notification.rb
Normal file
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityNotification < BaseNotification
|
||||
include SearchHelper
|
||||
include GlobalActivitiesHelper
|
||||
include InputSanitizeHelper
|
||||
include ActionView::Helpers::TextHelper
|
||||
include ApplicationHelper
|
||||
include ActiveRecord::Sanitization::ClassMethods
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
def message
|
||||
params[:message] if params[:legacy]
|
||||
end
|
||||
|
||||
def title
|
||||
if params[:legacy]
|
||||
params[:title]
|
||||
else
|
||||
generate_activity_content(activity)
|
||||
end
|
||||
end
|
||||
|
||||
def subject
|
||||
activity.subject unless params[:legacy]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_team
|
||||
@current_team ||= recipient.teams.find_by(id: recipient.current_team_id)
|
||||
end
|
||||
|
||||
def current_user
|
||||
recipient
|
||||
end
|
||||
|
||||
def activity
|
||||
@activity ||= Activity.find_by(id: params[:activity_id])
|
||||
end
|
||||
end
|
55
app/notifications/base_notification.rb
Normal file
55
app/notifications/base_notification.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BaseNotification < Noticed::Base
|
||||
deliver_by :database, if: :database_notification?
|
||||
deliver_by :email, mailer: 'AppMailer', method: :general_notification, if: :email_notification?
|
||||
|
||||
def self.send_notifications(params, later: true)
|
||||
recipients_class =
|
||||
"Recipients::#{NotificationExtends::NOTIFICATIONS_TYPES[subtype || params[:type]][:recipients_module]}".constantize
|
||||
recipients_class.new(params).recipients.each do |recipient|
|
||||
if later
|
||||
with(params).deliver_later(recipient)
|
||||
else
|
||||
with(params).deliver(recipient)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.subtype; end
|
||||
|
||||
def subtype
|
||||
self.class.subtype || params[:type]
|
||||
end
|
||||
|
||||
def subject; end
|
||||
|
||||
def message
|
||||
params[:message]
|
||||
end
|
||||
|
||||
def title
|
||||
params[:title]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def database_notification?
|
||||
# always save all notifications,
|
||||
# but flag if they should display in app or not
|
||||
|
||||
params[:hide_in_app] = recipient.notifications_settings.dig(notification_subgroup.to_s, 'in_app') != true
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def email_notification?
|
||||
recipient.notifications_settings.dig(notification_subgroup.to_s, 'email')
|
||||
end
|
||||
|
||||
def notification_subgroup
|
||||
NotificationExtends::NOTIFICATIONS_GROUPS.values.reduce({}, :merge).find do |_sg, n|
|
||||
n.include?(subtype.to_sym)
|
||||
end[0]
|
||||
end
|
||||
end
|
23
app/notifications/delivery_notification.rb
Normal file
23
app/notifications/delivery_notification.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DeliveryNotification < BaseNotification
|
||||
def self.subtype
|
||||
:delivery
|
||||
end
|
||||
|
||||
def message
|
||||
params[:message]
|
||||
end
|
||||
|
||||
def title
|
||||
params[:title]
|
||||
end
|
||||
|
||||
def subject
|
||||
return unless params[:subject_id] && params[:subject_class]
|
||||
|
||||
params[:subject_class].constantize.find(params[:subject_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
NonExistantRecord.new(params[:subject_name])
|
||||
end
|
||||
end
|
22
app/notifications/general_notification.rb
Normal file
22
app/notifications/general_notification.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class GeneralNotification < BaseNotification
|
||||
def message
|
||||
params[:message]
|
||||
end
|
||||
|
||||
def title
|
||||
params[:title]
|
||||
end
|
||||
|
||||
def subtype
|
||||
params[:type]
|
||||
end
|
||||
|
||||
def subject
|
||||
subject_class = params[:subject_class].constantize
|
||||
subject_class.find(params[:subject_id])
|
||||
rescue NameError, ActiveRecord::RecordNotFound
|
||||
NonExistantRecord.new(params[:subject_name])
|
||||
end
|
||||
end
|
27
app/notifications/low_stock_notification.rb
Normal file
27
app/notifications/low_stock_notification.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class LowStockNotification < BaseNotification
|
||||
def self.subtype
|
||||
:item_low_stock_reminder
|
||||
end
|
||||
|
||||
def title
|
||||
I18n.t(
|
||||
'notifications.content.item_low_stock_reminder.message_html',
|
||||
repository_row_name: subject.name,
|
||||
repository: repository.name
|
||||
)
|
||||
end
|
||||
|
||||
def subject
|
||||
RepositoryRow.find(params[:repository_row_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
NonExistantRecord.new(params[:repository_row_name])
|
||||
end
|
||||
|
||||
def repository
|
||||
Repository.find(params[:repository_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
NonExistantRecord.new(params[:repository_name])
|
||||
end
|
||||
end
|
13
app/notifications/recipients/assigned_group_recipients.rb
Normal file
13
app/notifications/recipients/assigned_group_recipients.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Recipients::AssignedGroupRecipients
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def recipients
|
||||
activity = Activity.find(@params[:activity_id])
|
||||
project = activity.subject
|
||||
project.team.users.where.not(id: project.user_assignments.where(assigned: 'manually').select(:user_id))
|
||||
end
|
||||
end
|
13
app/notifications/recipients/assigned_recipients.rb
Normal file
13
app/notifications/recipients/assigned_recipients.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Recipients::AssignedRecipients
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def recipients
|
||||
activity = Activity.find(@params[:activity_id])
|
||||
User.where(id: activity.values.dig('message_items', 'user_target', 'id'))
|
||||
.where.not(id: activity.owner_id)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Recipients
|
||||
class DesignateToMyModuleRecipients < MyModuleDesignatedRecipients
|
||||
private
|
||||
|
||||
def activity_recipients
|
||||
activity = Activity.find(@params[:activity_id])
|
||||
user = User.find_by(id: activity.values.dig('message_items', 'user_target', 'id'))
|
||||
|
||||
return [] if user.id == activity.owner_id
|
||||
|
||||
[user]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
11
app/notifications/recipients/direct_recipient.rb
Normal file
11
app/notifications/recipients/direct_recipient.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Recipients::DirectRecipient
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def recipients
|
||||
[@params[:user]]
|
||||
end
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Recipients::MyModuleDesignatedRecipients
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def recipients
|
||||
if @params[:activity_id]
|
||||
activity_recipients
|
||||
else
|
||||
MyModule.find(@params[:my_module_id]).designated_users
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def activity_recipients
|
||||
activity = Activity.find(@params[:activity_id])
|
||||
case activity.subject_type
|
||||
when 'MyModule'
|
||||
users = activity.subject.designated_users
|
||||
when 'Protocol', 'Result'
|
||||
users = activity.subject.my_module.designated_users
|
||||
when 'Step'
|
||||
users = activity.subject.protocol.my_module.designated_users
|
||||
end
|
||||
|
||||
users.where.not(id: activity.owner_id)
|
||||
end
|
||||
end
|
12
app/notifications/recipients/repository_item_recipients.rb
Normal file
12
app/notifications/recipients/repository_item_recipients.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Recipients::RepositoryItemRecipients
|
||||
def initialize(params)
|
||||
@repository_row_id = params[:repository_row_id]
|
||||
end
|
||||
|
||||
def recipients
|
||||
repository_row = RepositoryRow.find(@repository_row_id)
|
||||
repository_row.repository.team.users
|
||||
end
|
||||
end
|
12
app/notifications/recipients/user_changed_recipient.rb
Normal file
12
app/notifications/recipients/user_changed_recipient.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Recipients::UserChangedRecipient
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def recipients
|
||||
activity = Activity.find(@params[:activity_id])
|
||||
User.where(id: activity.values.dig('message_items', 'user_changed', 'id'))
|
||||
end
|
||||
end
|
55
app/notifications/repository_item_date_notification.rb
Normal file
55
app/notifications/repository_item_date_notification.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RepositoryItemDateNotification < BaseNotification
|
||||
def self.subtype
|
||||
:item_date_reminder
|
||||
end
|
||||
|
||||
def title
|
||||
unit = human_readable_unit(column.metadata['reminder_unit'], column.metadata['reminder_value'])
|
||||
I18n.t(
|
||||
'notifications.content.item_date_reminder.message_html',
|
||||
repository_row_name: subject.name,
|
||||
value: column.metadata['reminder_value'],
|
||||
units: unit
|
||||
)
|
||||
end
|
||||
|
||||
def subject
|
||||
RepositoryRow.find(params[:repository_row_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
NonExistantRecord.new(params[:repository_row_name])
|
||||
end
|
||||
|
||||
def column
|
||||
RepositoryColumn.find(params[:repository_column_id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
NonExistantRecord.new(params[:repository_column_name])
|
||||
end
|
||||
|
||||
after_deliver do
|
||||
if params[:repository_date_time_value_id]
|
||||
RepositoryDateTimeValue.find(params[:repository_date_time_value_id]).update(notification_sent: true)
|
||||
elsif params[:repository_date_value_id]
|
||||
RepositoryDateValue.find(params[:repository_date_value_id]).update(notification_sent: true)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def human_readable_unit(seconds, value)
|
||||
return unless seconds
|
||||
|
||||
units_hash = {
|
||||
'2419200' => 'month',
|
||||
'604800' => 'week',
|
||||
'86400' => 'day'
|
||||
}
|
||||
|
||||
base_unit = units_hash.fetch(seconds) do
|
||||
raise ArgumentError, "Unrecognized time unit for seconds value: #{seconds}"
|
||||
end
|
||||
|
||||
value.to_i > 1 ? base_unit.pluralize : base_unit
|
||||
end
|
||||
end
|
22
app/notifications/task_due_date_notification.rb
Normal file
22
app/notifications/task_due_date_notification.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TaskDueDateNotification < BaseNotification
|
||||
def self.subtype
|
||||
:my_module_due_date_reminder
|
||||
end
|
||||
|
||||
def title
|
||||
I18n.t(
|
||||
'notifications.content.my_module_due_date_reminder.message_html',
|
||||
my_module_name: subject.name
|
||||
)
|
||||
end
|
||||
|
||||
def subject
|
||||
MyModule.find(params[:my_module_id])
|
||||
end
|
||||
|
||||
after_deliver do
|
||||
MyModule.find(params[:my_module_id]).update_column(:due_date_notification_sent, true)
|
||||
end
|
||||
end
|
|
@ -59,7 +59,7 @@ Canaid::Permissions.register_for(Repository) do
|
|||
|
||||
# repository: share
|
||||
can :share_repository do |user, repository|
|
||||
can_manage_repository?(user, repository)
|
||||
!repository.shared_with?(user.current_team) && repository.permission_granted?(user, RepositoryPermissions::SHARE)
|
||||
end
|
||||
|
||||
# repository: make a snapshot with assigned rows
|
||||
|
|
91
app/serializers/notification_serializer.rb
Normal file
91
app/serializers/notification_serializer.rb
Normal file
|
@ -0,0 +1,91 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NotificationSerializer < ActiveModel::Serializer
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
attributes :id, :title, :message, :created_at, :read_at, :type, :breadcrumbs, :checked, :today
|
||||
|
||||
def title
|
||||
object.to_notification.title
|
||||
end
|
||||
|
||||
def breadcrumbs
|
||||
subject = object.to_notification.subject
|
||||
generate_breadcrumbs(subject, []) if subject
|
||||
end
|
||||
|
||||
def message
|
||||
object.to_notification.message
|
||||
end
|
||||
|
||||
def created_at
|
||||
I18n.l(object.created_at, format: :full)
|
||||
end
|
||||
|
||||
def today
|
||||
object.created_at.today?
|
||||
end
|
||||
|
||||
def checked
|
||||
object.read_at.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_breadcrumbs(subject, breadcrumbs)
|
||||
return [] if subject.is_a?(NonExistantRecord)
|
||||
|
||||
case subject
|
||||
when Project
|
||||
parent = subject.team
|
||||
url = project_path(subject)
|
||||
when Experiment
|
||||
parent = subject.project
|
||||
url = my_modules_experiment_path(subject)
|
||||
when MyModule
|
||||
parent = subject.experiment
|
||||
url = protocols_my_module_path(subject)
|
||||
when Protocol
|
||||
if subject.in_repository?
|
||||
parent = subject.team
|
||||
url = protocol_path(subject)
|
||||
else
|
||||
parent = subject.my_module
|
||||
url = protocols_my_module_path(subject.my_module)
|
||||
end
|
||||
when Result
|
||||
parent = subject.my_module
|
||||
url = my_module_results_path(subject.my_module)
|
||||
when ProjectFolder
|
||||
parent = subject.team
|
||||
url = project_folder_path(subject)
|
||||
when RepositoryBase
|
||||
parent = subject.team
|
||||
url = repository_path(subject)
|
||||
when RepositoryRow
|
||||
parent = subject.team
|
||||
url = repository_path(subject.repository)
|
||||
when Report
|
||||
parent = subject.team
|
||||
url = reports_path(
|
||||
preview_report_id: subject.id,
|
||||
preview_type: object.params[:report_type],
|
||||
team_id: subject.team.id
|
||||
)
|
||||
when LabelTemplate
|
||||
parent = subject.team
|
||||
url = label_template_path(subject)
|
||||
when Team
|
||||
parent = nil
|
||||
url = projects_path(team: subject.id)
|
||||
end
|
||||
|
||||
breadcrumbs << { name: subject.name, url: url } if subject.name.present?
|
||||
|
||||
if parent
|
||||
generate_breadcrumbs(parent, breadcrumbs)
|
||||
else
|
||||
breadcrumbs.reverse
|
||||
end
|
||||
end
|
||||
end
|
|
@ -149,6 +149,19 @@ module WopiUtil
|
|||
asset_name: { id: @asset.id, value_for: 'file_name' },
|
||||
action: action
|
||||
})
|
||||
elsif @assoc.is_a?(RepositoryCell)
|
||||
repository = @assoc.repository_row.repository
|
||||
Activities::CreateActivityService
|
||||
.call(activity_type: :edit_wopi_file_on_inventory_item,
|
||||
owner: current_user,
|
||||
subject: repository,
|
||||
team: repository.team,
|
||||
message_items: {
|
||||
repository: repository.id,
|
||||
repository_row: @assoc.repository_row.id,
|
||||
asset_name: { id: @asset.id, value_for: 'file_name' },
|
||||
action: action
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div id="taskSecondaryMenu" class="sticky-header-element bg-sn-white border-b border-solid border-0 border-sn-sleepy-grey rounded-t px-4 py-2 top-0 sticky flex items-center flex-wrap z-[106]">
|
||||
<div id="taskSecondaryMenu" class="sticky-header-element bg-sn-white border-b border-solid border-0 border-sn-sleepy-grey rounded-t px-4 py-2 pb-[16px] top-0 sticky flex items-center flex-wrap z-[106]">
|
||||
<div class="flex items-center gap-4 mr-auto">
|
||||
<% if can_read_experiment?(@my_module.experiment) %>
|
||||
<a class="p-3 border-b-4 border-transparent hover:no-underline uppercase text-bold capitalize <%= is_module_protocols? ? "text-sn-blue" : "text-sn-grey" %>"
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<div class="module-tags">
|
||||
<div class="tags-wrapper">
|
||||
<span class="sn-icon block-icon sn-icon-users mr-2.5"></span>
|
||||
<span class="hidden-xs hidden-sm"><%= t('my_modules.details.assigned_users') %></span>
|
||||
<span class="hidden-xs hidden-sm mr-1"><%= t('my_modules.details.assigned_users') %></span>
|
||||
<%= render partial: "user_my_modules/index", locals: { my_module: @my_module } %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<share-task-container
|
||||
shareable-link-url="<%= my_module_shareable_link_path(@my_module) %>"
|
||||
:shared="<%= @my_module.shared? %>"
|
||||
:disabled="<%= !can_share_my_module?(@my_module) %>" />
|
||||
:can-share="<%= can_share_my_module?(@my_module) %>" />
|
||||
</div>
|
||||
|
||||
<%= javascript_include_tag 'vue_share_task_container' %>
|
||||
|
|
|
@ -35,9 +35,6 @@
|
|||
</div>
|
||||
</th>
|
||||
<th id="assigned" data-unmanageable="true"><%= t("repositories.table.assigned") %></th>
|
||||
<% if @repository.is_a?(LinkedRepository) %>
|
||||
<th id="row-external-id"><%= t('repositories.table.external_id') %></th>
|
||||
<% end %>
|
||||
<th id="row-id"><%= t("repositories.table.id") %></th>
|
||||
<th id="row-name"><%= t("repositories.table.row_name") %></th>
|
||||
<th id="relationship"><%= t("repositories.table.relationship") %></th>
|
||||
|
@ -45,6 +42,9 @@
|
|||
<th id="added-by" ><%= t("repositories.table.added_by") %></th>
|
||||
<th id="archived-on"><%= t("repositories.table.archived_on") %></th>
|
||||
<th id="archived-by"><%= t("repositories.table.archived_by") %></th>
|
||||
<% if @repository.is_a?(LinkedRepository) %>
|
||||
<th id="row-external-id"><%= t('repositories.table.external_id') %></th>
|
||||
<% end %>
|
||||
<% repository.repository_columns.order(:id).each do |column| %>
|
||||
<th
|
||||
class="repository-column <%= 'row-stock item-stock' if column.data_type == 'RepositoryStockValue' %>"
|
||||
|
|
|
@ -18,14 +18,17 @@
|
|||
<div class="all-teams">
|
||||
<span class="team-selector" title="<%= t("repositories.index.modal_share.all_teams_tooltip") %>">
|
||||
<span class="sci-checkbox-container">
|
||||
<%= check_box_tag 'select_all_teams', 0, @repository.shared_read? || @repository.shared_write?, { class: 'sci-checkbox' } %>
|
||||
<%= check_box_tag 'select_all_teams', true, @repository.shared_read? || @repository.shared_write?, { class: 'sci-checkbox' } %>
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</span>
|
||||
<%= t("repositories.index.modal_share.all_teams") %>
|
||||
</span>
|
||||
<span class="permission-selector">
|
||||
<span class="sci-toggle-checkbox-container">
|
||||
<%= check_box_tag 'select_all_write_permission', 0, @repository.shared_write?, { class: 'hidden sci-toggle-checkbox' }%>
|
||||
<%= check_box_tag 'select_all_write_permission', true, @repository.shared_write?, {
|
||||
disabled: !@repository.shareable_write?,
|
||||
class: 'hidden sci-toggle-checkbox'
|
||||
} %>
|
||||
<span class="sci-toggle-checkbox-label"></span>
|
||||
</span>
|
||||
</span>
|
||||
|
@ -35,7 +38,7 @@
|
|||
<div class="team-container">
|
||||
<div class="team-selector">
|
||||
<span class="sci-checkbox-container">
|
||||
<%= check_box_tag 'share_team_ids[]', t.id, @repository.private_shared_with?(t), {id: "shared_#{t.id}", class: "sci-checkbox"} %>
|
||||
<%= check_box_tag 'share_team_ids[]', t.id, @repository.private_shared_with?(t), { id: "shared_#{t.id}", class: "sci-checkbox" } %>
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</span>
|
||||
<%= t.name %>
|
||||
|
@ -44,6 +47,7 @@
|
|||
<span class="sci-toggle-checkbox-container">
|
||||
<%= check_box_tag 'write_permissions[]', t.id, @repository.private_shared_with_write?(t), {
|
||||
id: "editable_#{t.id}",
|
||||
disabled: !@repository.shareable_write?,
|
||||
class: (@repository.private_shared_with?(t) ? 'sci-toggle-checkbox' : 'sci-toggle-checkbox hidden')
|
||||
}.compact %>
|
||||
<span class="sci-toggle-checkbox-label"></span>
|
||||
|
|
|
@ -19,3 +19,5 @@
|
|||
'data-view-mode': !can_manage_my_module_designated_users?(my_module)
|
||||
} %>
|
||||
</div>
|
||||
|
||||
<%= javascript_include_tag 'my_modules/assigned_users' %>
|
||||
|
|
8
app/views/users/mailer/general_notification.html.erb
Normal file
8
app/views/users/mailer/general_notification.html.erb
Normal file
|
@ -0,0 +1,8 @@
|
|||
<p>Hello <%= @user.name %>,</p>
|
||||
|
||||
<p><%= I18n.t("notifications.email_title") %></p>
|
||||
<p>Type: <%= @notification.title %></p>
|
||||
|
||||
<p>
|
||||
<%= sanitize_input(prepend_server_url_to_links(@notification.message)) %>
|
||||
</p>
|
|
@ -1,106 +1,17 @@
|
|||
<% provide(:head_title, t("users.settings.account.preferences.head_title")) %>
|
||||
<% provide(:container_class, "no-second-nav-container") %>
|
||||
|
||||
<%= render partial: "users/settings/sidebar" %>
|
||||
<div class="tab-content user-account-preferences">
|
||||
<div class="tab-pane content-pane active" role="tabpanel">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-12">
|
||||
<h1 class="preferences-title"><%= t('users.settings.account.preferences.title') %></h1>
|
||||
<div class="time-zone-container">
|
||||
<%= label_tag t("users.settings.account.preferences.edit.time_zone_label") %>
|
||||
<div class="time-zone-selector-container">
|
||||
<%= select_tag "time-zone-input-field",
|
||||
options_for_select(
|
||||
ActiveSupport::TimeZone.all.map{ |tz|
|
||||
[tz.formatted_offset + " " + tz.name, tz.name]
|
||||
},
|
||||
@user.settings[:time_zone]
|
||||
),{
|
||||
'data-path-to-update': update_preferences_path(format: :json),
|
||||
class: 'hidden'
|
||||
}
|
||||
%>
|
||||
</div>
|
||||
<small><%= t("users.settings.account.preferences.edit.time_zone_sublabel") %></small>
|
||||
</div>
|
||||
|
||||
<div class="date-format-container">
|
||||
<%= label_tag t("users.settings.account.preferences.edit.date_format_label") %>
|
||||
<div class="date-format-selector-container">
|
||||
<%= select_tag "date-format-input-field",
|
||||
options_for_select(
|
||||
Constants::SUPPORTED_DATE_FORMATS.map { |df|
|
||||
["#{l(Time.new(2024, 4, 22), format: :full_date, date_format: df)}", df]
|
||||
},
|
||||
@user.settings[:date_format]
|
||||
),{
|
||||
'data-path-to-update': update_preferences_path(format: :json),
|
||||
class: 'hidden'
|
||||
}
|
||||
%>
|
||||
</div>
|
||||
<small><%= t("users.settings.account.preferences.edit.date_format_sublabel") %></small>
|
||||
</div>
|
||||
<hr>
|
||||
<%= form_for(@user,
|
||||
url: update_togglable_settings_path(format: :json),
|
||||
html: { method: :post, id: 'togglable-settings-panel' },
|
||||
remote: true) do |f| %>
|
||||
<div class="preferences-settings-container">
|
||||
<h4><%= t('notifications.title') %></h4>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<strong><%=t 'notifications.form.assignments' %></strong>
|
||||
<p><%=t 'notifications.form.assignments_description' %></p>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<%=t 'notifications.form.notification_scinote' %>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<%= check_box_tag :assignments_notification, @user.assignments_notification %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<%=t 'notifications.form.notification_email' %>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<%= check_box_tag :assignments_notification_email, @user.assignments_email_notification %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<strong><%=t 'notifications.form.recent_notification' %></strong>
|
||||
<p><%=t 'notifications.form.recent_notification_description' %></p>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<%=t 'notifications.form.notification_scinote' %>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<%= check_box_tag :recent_notification, @user.recent_notification %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<%=t 'notifications.form.notification_email' %>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<%= check_box_tag :recent_notification_email, @user.recent_email_notification %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane tab-pane-settings" role="tabpanel"></div>
|
||||
<div id="user_preferences" class="contents">
|
||||
<user-preferences
|
||||
:update-url = "'<%= update_preferences_path(format: :json) %>'"
|
||||
:user-settings = "<%= @user.settings.to_json %>"
|
||||
:time-zones = "<%= ActiveSupport::TimeZone.all.map{ |tz|
|
||||
[ tz.name, tz.formatted_offset + " " + tz.name]
|
||||
}.to_json %>"
|
||||
:date-formats = "<%= Constants::SUPPORTED_DATE_FORMATS.map { |df|
|
||||
[df, "#{l(Time.new(2024, 4, 22), format: :full_date, date_format: df)}"]
|
||||
}.to_json %>"
|
||||
:notifications-groups = "<%= NotificationExtends::NOTIFICATIONS_GROUPS.to_json %>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<%= javascript_include_tag "users/settings/account/preferences/index" %>
|
||||
<%= javascript_include_tag "vue_user_preferences" %>
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
</div>
|
||||
<ul class="webhooks-list collapse" id="activityFilter<%= filter.id %>">
|
||||
<li class="create-webhook-container hidden">
|
||||
<%= form_with model: Webhook.new, url: users_settings_webhooks_path(filter_id: filter.id, sort: @current_sort), class: 'webhook-form' do |f| %>
|
||||
<%= form_with model: Webhook.new, url: users_settings_webhooks_path(filter_id: filter.id, sort: @current_sort), class: 'webhook-form', data: { remote: true } do |f| %>
|
||||
<%= render partial: 'webhook_form', locals: {f: f} %>
|
||||
<% end %>
|
||||
</li>
|
||||
|
|
|
@ -31,6 +31,7 @@ Rails.application.config.assets.precompile += %w(my_modules/status_flow.js)
|
|||
Rails.application.config.assets.precompile += %w(my_modules/protocols/protocol_status_bar.js)
|
||||
Rails.application.config.assets.precompile += %w(my_modules/results.js)
|
||||
Rails.application.config.assets.precompile += %w(my_modules/stock.js)
|
||||
Rails.application.config.assets.precompile += %w(my_modules/assigned_users.js)
|
||||
Rails.application.config.assets.precompile += %w(my_modules/tags.js)
|
||||
Rails.application.config.assets.precompile += %w(my_modules/archived.js)
|
||||
Rails.application.config.assets.precompile += %w(my_modules/pwa_mobile_app.js)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue