mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 14:45:56 +08:00
Merge branch 'develop' into features/inventory-import-improvements
This commit is contained in:
commit
49d0f9e6b8
2
Gemfile
2
Gemfile
|
@ -60,7 +60,7 @@ gem 'i18n-js', '~> 3.6' # Localization in javascript files
|
|||
gem 'jbuilder' # JSON structures via a Builder-style DSL
|
||||
gem 'logging', '~> 2.0.0'
|
||||
gem 'nested_form_fields'
|
||||
gem 'nokogiri', '~> 1.16.2' # HTML/XML parser
|
||||
gem 'nokogiri', '~> 1.16.5' # HTML/XML parser
|
||||
gem 'noticed'
|
||||
gem 'rails_autolink', '~> 1.1', '>= 1.1.6'
|
||||
gem 'rgl' # Graph framework for project diagram calculations
|
||||
|
|
14
Gemfile.lock
14
Gemfile.lock
|
@ -439,7 +439,7 @@ GEM
|
|||
mime-types-data (3.2023.0218.1)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.5)
|
||||
mini_portile2 (2.8.6)
|
||||
minitest (5.22.2)
|
||||
msgpack (1.7.1)
|
||||
multi_json (1.15.0)
|
||||
|
@ -462,12 +462,12 @@ GEM
|
|||
net-protocol
|
||||
newrelic_rpm (9.2.2)
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.16.2)
|
||||
nokogiri (1.16.5)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.2-arm64-darwin)
|
||||
nokogiri (1.16.5-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.2-x86_64-linux)
|
||||
nokogiri (1.16.5-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
noticed (1.6.3)
|
||||
http (>= 4.0.0)
|
||||
|
@ -615,7 +615,8 @@ GEM
|
|||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.2.5)
|
||||
rexml (3.2.8)
|
||||
strscan (>= 3.0.9)
|
||||
rgl (0.6.3)
|
||||
pairing_heap (>= 0.3.0)
|
||||
rexml (~> 3.2, >= 3.2.4)
|
||||
|
@ -707,6 +708,7 @@ GEM
|
|||
activesupport (>= 5.2)
|
||||
sprockets (>= 3.0.0)
|
||||
stream (0.5.5)
|
||||
strscan (3.1.0)
|
||||
swd (2.0.2)
|
||||
activesupport (>= 3)
|
||||
attr_required (>= 0.0.5)
|
||||
|
@ -834,7 +836,7 @@ DEPENDENCIES
|
|||
logging (~> 2.0.0)
|
||||
nested_form_fields
|
||||
newrelic_rpm
|
||||
nokogiri (~> 1.16.2)
|
||||
nokogiri (~> 1.16.5)
|
||||
noticed
|
||||
omniauth (~> 2.1)
|
||||
omniauth-azure-activedirectory-v2
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
// listen for input event on the my_module_name input field
|
||||
$(`${newMyModuleModal} #my_module_name`).on('input', function () {
|
||||
if ($(this).val().trim() !== '') {
|
||||
if ($(this).val().trim().length > 1) {
|
||||
// enable the submit button if the input field is populated
|
||||
$('#new-modal-submit-btn').prop('disabled', false);
|
||||
} else {
|
||||
|
|
|
@ -147,10 +147,11 @@
|
|||
// initialize my_module tab remote loading
|
||||
$('#experimentTable, .my-modules-protocols-index, #experiment-canvas')
|
||||
.on('click', '.edit-tags-link', function() {
|
||||
if($('#tagsModalComponent').length) {
|
||||
$('#tagsModalComponent').data('tagsModal').open()
|
||||
if (window.tagsModal) {
|
||||
const url = $(this).closest('.module-large').data('module-url') || $(this).attr('href');
|
||||
window.tagsModal.open(url);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
bindEditTagsAjax();
|
||||
|
|
|
@ -570,13 +570,20 @@ function handleAnchorClick(event) {
|
|||
}
|
||||
};
|
||||
|
||||
// listen to clicks on links in navigator and leftMenuContainer
|
||||
$(document).ready(function() {
|
||||
const navigatorEl = $('.sci--layout-navigation-navigator');
|
||||
const leftMenuContainerEl = $('.sci--layout--left-menu-container');
|
||||
$(document).on('turbolinks:before-visit.canvas_edit', (e) => {
|
||||
const alertText = $("#update-canvas").attr("data-unsaved-work-text");
|
||||
|
||||
navigatorEl.on('click', 'a', handleAnchorClick);
|
||||
leftMenuContainerEl.on('click', 'a', handleAnchorClick);
|
||||
if (alertText) {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (confirm(alertText)) {
|
||||
$(document).off('turbolinks:before-visit.canvas_edit');
|
||||
return true;
|
||||
}
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
$(document).off('turbolinks:before-visit.canvas_edit');
|
||||
return true;
|
||||
});
|
||||
|
||||
function bindEditModeCloseWindow() {
|
||||
|
@ -969,7 +976,7 @@ function bindEditTagsAjax(elements) {
|
|||
$(elements).find("a.edit-tags-link")
|
||||
.on('click', function(){
|
||||
$(this).addClass('updated-module-tags');
|
||||
var modal = $(this).closest(".panel-default").find('.tags-modal-component').data('tagsModal').open();
|
||||
var modal = $('#tagsModalComponent');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@ var PdfPreview = (function() {
|
|||
|
||||
|
||||
function loadPdfDocument(canvas, page = 1) {
|
||||
var loadingPdf = pdfjsLib.getDocument(canvas.dataset.pdfUrl);
|
||||
var loadingPdf = pdfjsLib.getDocument({ url: canvas.dataset.pdfUrl, isEvalSupported: false });
|
||||
$(canvas).data('load-attempts', $(canvas).data('load-attempts') + 1);
|
||||
loadingPdf.promise
|
||||
.then(function(pdfDocument) {
|
||||
|
|
|
@ -104,8 +104,4 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('turbolinks:load', () => {
|
||||
$('#itemLandingPagelink').trigger('click');
|
||||
});
|
||||
}());
|
||||
|
|
23
app/assets/javascripts/sitewide/tooltips.js
Normal file
23
app/assets/javascripts/sitewide/tooltips.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
window.initTooltip = (element) => {
|
||||
$(element).tooltip({
|
||||
container: 'body',
|
||||
delay: { show: 300, hide: 150 },
|
||||
trigger: 'hover',
|
||||
placement: (_, source) => {
|
||||
const position = $(source).attr('data-tooltip-placement');
|
||||
return position || 'top';
|
||||
},
|
||||
template: `<div class="tooltip" role="tooltip"><div class="tooltip-arrow">
|
||||
</div><div class="tooltip-inner !max-w-[250px] text-left bg-black text-sn-white text-xs p-2"></div></div>`
|
||||
});
|
||||
};
|
||||
|
||||
window.destroyTooltip = (element) => {
|
||||
$(element).tooltip('destroy');
|
||||
};
|
||||
|
||||
$(document).on('turbolinks:load', function() {
|
||||
$(document).find('[data-render-tooltip]').each(function() {
|
||||
window.initTooltip(this);
|
||||
})
|
||||
});
|
|
@ -201,23 +201,27 @@ var zebraPrint = (function() {
|
|||
});
|
||||
}
|
||||
|
||||
updateProgressModalData(progressModal, printData.printer_name, PRINTER_STATUS_SEARCH, PRINTER_STATUS_SEARCH);
|
||||
device = findDevice(printData.printer_name);
|
||||
if (dataZebra?.responseJSON?.labels?.length && printData.number_of_copies > 0) {
|
||||
updateProgressModalData(progressModal, printData.printer_name, PRINTER_STATUS_SEARCH, PRINTER_STATUS_SEARCH);
|
||||
device = findDevice(printData.printer_name);
|
||||
|
||||
getPrinterStatus(device).then((device) => {
|
||||
if (device.status === I18n.t('label_printers.modal_printing_status.printer_status.ready')) {
|
||||
print(
|
||||
device,
|
||||
progressModal,
|
||||
printData.number_of_copies,
|
||||
printData.printer_name,
|
||||
dataZebra.responseJSON.labels,
|
||||
0,
|
||||
);
|
||||
} else {
|
||||
updateProgressModalData(progressModal, printData.printer_name, PRINTER_STATUS_ERROR, PRINTER_STATUS_ERROR);
|
||||
}
|
||||
});
|
||||
getPrinterStatus(device).then((device) => {
|
||||
if (device.status === I18n.t('label_printers.modal_printing_status.printer_status.ready')) {
|
||||
print(
|
||||
device,
|
||||
progressModal,
|
||||
printData.number_of_copies,
|
||||
printData.printer_name,
|
||||
dataZebra.responseJSON.labels,
|
||||
0
|
||||
);
|
||||
} else {
|
||||
updateProgressModalData(progressModal, printData.printer_name, PRINTER_STATUS_ERROR, PRINTER_STATUS_ERROR);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
updateProgressModalData(progressModal, printData.printer_name, PRINTER_STATUS_ERROR, PRINTER_STATUS_ERROR);
|
||||
}
|
||||
}).fail(() => {
|
||||
HelperModule.flashAlertMsg(I18n.t('repository_row.modal_print_label.general_error'), 'danger');
|
||||
});
|
||||
|
|
|
@ -78,10 +78,14 @@ input[type="checkbox"].sci-checkbox {
|
|||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
outline: 0;
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
&:focus + .sci-checkbox-label {
|
||||
outline: 4px solid var(--sn-science-blue-hover);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
|
||||
|
|
|
@ -106,6 +106,14 @@ module Api
|
|||
)
|
||||
end
|
||||
|
||||
rescue_from Api::V1::ApiKeyError do |e|
|
||||
render_error(
|
||||
e.message,
|
||||
I18n.t('api.core.invalid_api_key_detail'),
|
||||
:unauthorized
|
||||
)
|
||||
end
|
||||
|
||||
before_action :check_include_param, only: %i(index show)
|
||||
|
||||
def index
|
||||
|
|
|
@ -18,7 +18,11 @@ class AssetsController < ApplicationController
|
|||
|
||||
before_action :load_vars, except: :create_wopi_file
|
||||
before_action :check_read_permission, except: %i(edit destroy duplicate create_wopi_file toggle_view_mode)
|
||||
<<<<<<< HEAD
|
||||
before_action :check_manage_permission, only: %i(edit destroy duplicate toggle_view_mode rename)
|
||||
=======
|
||||
before_action :check_manage_permission, only: %i(edit destroy duplicate rename toggle_view_mode)
|
||||
>>>>>>> develop
|
||||
|
||||
def file_preview
|
||||
render json: { html: render_to_string(
|
||||
|
@ -311,6 +315,49 @@ class AssetsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def rename
|
||||
new_name = params.require(:asset).permit(:name)[:name]
|
||||
|
||||
if new_name.empty?
|
||||
render json: { error: I18n.t('assets.rename_modal.min_length_error') }, status: :unprocessable_entity
|
||||
return
|
||||
elsif new_name.length > Constants::NAME_MAX_LENGTH
|
||||
render json: { error: I18n.t('assets.rename_modal.max_length_error') }, status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
old_name = @asset.file_name
|
||||
@asset.last_modified_by = current_user
|
||||
@asset.rename_file(new_name)
|
||||
@asset.save!
|
||||
|
||||
case @asset.parent
|
||||
when Step
|
||||
message_items = { old_name:, new_name:, user: current_user.id }
|
||||
message_items[:my_module] = @assoc.protocol.my_module.id if @assoc.protocol.in_module?
|
||||
@asset.parent.touch
|
||||
log_step_activity(
|
||||
"#{@assoc.protocol.in_module? ? 'task' : 'protocol'}_step_asset_renamed",
|
||||
@assoc,
|
||||
@assoc.my_module&.project,
|
||||
message_items
|
||||
)
|
||||
when Result
|
||||
log_result_activity(
|
||||
:result_asset_renamed,
|
||||
@assoc,
|
||||
old_name:,
|
||||
new_name:,
|
||||
user: current_user.id,
|
||||
my_module: @assoc.my_module.id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
render json: @asset, serializer: AssetSerializer, user: current_user
|
||||
end
|
||||
|
||||
def duplicate
|
||||
ActiveRecord::Base.transaction do
|
||||
case @asset.parent
|
||||
|
@ -348,49 +395,6 @@ class AssetsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def rename
|
||||
new_name = params.require(:asset).permit(:name)[:name]
|
||||
|
||||
if new_name.empty?
|
||||
render json: { error: I18n.t('assets.rename_modal.min_length_error') }, status: :unprocessable_entity
|
||||
return
|
||||
elsif new_name.length > Constants::NAME_MAX_LENGTH
|
||||
render json: { error: I18n.t('assets.rename_modal.max_length_error') }, status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
old_name = @asset.name
|
||||
@asset.last_modified_by = current_user
|
||||
@asset.rename_file(new_name)
|
||||
@asset.save!
|
||||
|
||||
case @asset.parent
|
||||
when Step
|
||||
message_items = { old_name: old_name, new_name: new_name, user: current_user.id }
|
||||
message_items[:my_module] = @assoc.protocol.my_module.id if @assoc.protocol.in_module?
|
||||
|
||||
log_step_activity(
|
||||
"#{@assoc.protocol.in_module? ? 'task' : 'protocol'}_step_asset_renamed",
|
||||
@assoc,
|
||||
@assoc.my_module&.project,
|
||||
message_items
|
||||
)
|
||||
when Result
|
||||
log_result_activity(
|
||||
:result_asset_renamed,
|
||||
@assoc,
|
||||
old_name: old_name,
|
||||
new_name: new_name,
|
||||
user: current_user.id,
|
||||
my_module: @assoc.my_module.id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
render json: @asset, serializer: AssetSerializer, user: current_user
|
||||
end
|
||||
|
||||
def checksum
|
||||
render json: { checksum: @asset.file.blob.checksum }
|
||||
end
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class ApiKeyError < StandardError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module TokenAuthentication
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
|
@ -13,7 +20,23 @@ module TokenAuthentication
|
|||
raise JWT::InvalidPayload, I18n.t('api.core.no_azure_user_mapping') unless current_user
|
||||
end
|
||||
|
||||
def authenticate_with_api_key
|
||||
return unless Rails.configuration.x.core_api_key_enabled
|
||||
|
||||
@api_key = request.headers['Api-Key']
|
||||
return unless @api_key
|
||||
|
||||
@current_user = User.from_api_key(@api_key)
|
||||
|
||||
raise Api::V1::ApiKeyError, I18n.t('api.core.invalid_api_key') unless @current_user
|
||||
|
||||
@current_user
|
||||
end
|
||||
|
||||
def authenticate_request!
|
||||
# API key authentication successful
|
||||
return if authenticate_with_api_key
|
||||
|
||||
@token = request.headers['Authorization']&.sub('Bearer ', '')
|
||||
raise JWT::VerificationError, I18n.t('api.core.missing_token') unless @token
|
||||
|
||||
|
|
|
@ -40,7 +40,6 @@ class ProjectsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
def inventory_assigning_project_filter
|
||||
viewable_experiments = Experiment.viewable_by_user(current_user, current_team)
|
||||
assignable_my_modules = MyModule.repository_row_assignable_by_user(current_user)
|
||||
|
@ -260,7 +259,6 @@ class ProjectsController < ApplicationController
|
|||
render json: { data: user_roles_collection(Project.new).map(&:reverse) }
|
||||
end
|
||||
|
||||
|
||||
def actions_toolbar
|
||||
render json: {
|
||||
actions:
|
||||
|
|
|
@ -73,7 +73,7 @@ class RepositoriesController < ApplicationController
|
|||
|
||||
def table_toolbar
|
||||
render json: {
|
||||
html: render_to_string(partial: 'repositories/toolbar_buttons')
|
||||
html: render_to_string(partial: 'repositories/toolbar_buttons', locals: { view_mode: params[:view_mode]})
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -122,7 +122,7 @@ class ResultsController < ApplicationController
|
|||
|
||||
def destroy
|
||||
name = @result.name
|
||||
if @result.destroy
|
||||
if @result.discard
|
||||
log_activity(:destroy_result, { destroyed_result: name })
|
||||
render json: {}, status: :ok
|
||||
else
|
||||
|
@ -207,7 +207,7 @@ class ResultsController < ApplicationController
|
|||
def set_navigator
|
||||
@navigator = {
|
||||
url: tree_navigator_my_module_path(@my_module),
|
||||
archived: false,
|
||||
archived: @my_module.archived_branch?,
|
||||
id: @my_module.code
|
||||
}
|
||||
end
|
||||
|
|
|
@ -13,7 +13,8 @@ class SearchController < ApplicationController
|
|||
|
||||
case params[:group]
|
||||
when 'projects'
|
||||
search_by_name(Project)
|
||||
@model = Project
|
||||
search_by_name
|
||||
|
||||
render json: @records.includes(:team, :project_folder),
|
||||
each_serializer: GlobalSearch::ProjectSerializer,
|
||||
|
@ -22,7 +23,8 @@ class SearchController < ApplicationController
|
|||
next_page: (@records.next_page if @records.respond_to?(:next_page)),
|
||||
}
|
||||
when 'project_folders'
|
||||
search_by_name(ProjectFolder)
|
||||
@model = ProjectFolder
|
||||
search_by_name
|
||||
|
||||
render json: @records.includes(:team, :parent_folder),
|
||||
each_serializer: GlobalSearch::ProjectFolderSerializer,
|
||||
|
@ -32,7 +34,8 @@ class SearchController < ApplicationController
|
|||
}
|
||||
return
|
||||
when 'reports'
|
||||
search_by_name(Report)
|
||||
@model = Report
|
||||
search_by_name
|
||||
|
||||
render json: @records.includes(:team, :project, :user),
|
||||
each_serializer: GlobalSearch::ReportSerializer,
|
||||
|
@ -42,9 +45,10 @@ class SearchController < ApplicationController
|
|||
}
|
||||
return
|
||||
when 'module_protocols'
|
||||
search_by_name(Protocol, { in_repository: false })
|
||||
@model = Protocol
|
||||
search_by_name({ in_repository: false })
|
||||
|
||||
render json: @records.joins({ my_module: :experiment }, :team),
|
||||
render json: @records.includes({ my_module: :experiment }, :team),
|
||||
each_serializer: GlobalSearch::MyModuleProtocolSerializer,
|
||||
meta: {
|
||||
total: @records.total_count,
|
||||
|
@ -52,7 +56,8 @@ class SearchController < ApplicationController
|
|||
}
|
||||
return
|
||||
when 'experiments'
|
||||
search_by_name(Experiment)
|
||||
@model = Experiment
|
||||
search_by_name
|
||||
|
||||
render json: @records.includes(project: :team),
|
||||
each_serializer: GlobalSearch::ExperimentSerializer,
|
||||
|
@ -62,7 +67,8 @@ class SearchController < ApplicationController
|
|||
}
|
||||
return
|
||||
when 'tasks'
|
||||
search_by_name(MyModule)
|
||||
@model = MyModule
|
||||
search_by_name
|
||||
|
||||
render json: @records.includes(experiment: { project: :team }),
|
||||
each_serializer: GlobalSearch::MyModuleSerializer,
|
||||
|
@ -72,7 +78,8 @@ class SearchController < ApplicationController
|
|||
}
|
||||
return
|
||||
when 'results'
|
||||
search_by_name(Result)
|
||||
@model = Result
|
||||
search_by_name
|
||||
|
||||
render json: @records.includes(my_module: { experiment: { project: :team } }),
|
||||
each_serializer: GlobalSearch::ResultSerializer,
|
||||
|
@ -82,7 +89,8 @@ class SearchController < ApplicationController
|
|||
}
|
||||
return
|
||||
when 'protocols'
|
||||
search_by_name(Protocol, { in_repository: true })
|
||||
@model = Protocol
|
||||
search_by_name({ in_repository: true })
|
||||
|
||||
render json: @records,
|
||||
each_serializer: GlobalSearch::ProtocolSerializer,
|
||||
|
@ -94,7 +102,8 @@ class SearchController < ApplicationController
|
|||
when 'label_templates'
|
||||
return render json: [], meta: { disabled: true }, status: :ok unless LabelTemplate.enabled?
|
||||
|
||||
search_by_name(LabelTemplate)
|
||||
@model = LabelTemplate
|
||||
search_by_name
|
||||
|
||||
render json: @records,
|
||||
each_serializer: GlobalSearch::LabelTemplateSerializer,
|
||||
|
@ -104,6 +113,7 @@ class SearchController < ApplicationController
|
|||
}
|
||||
return
|
||||
when 'repository_rows'
|
||||
@model = RepositoryRow
|
||||
search_by_name(RepositoryRow)
|
||||
|
||||
render json: @records,
|
||||
|
@ -114,7 +124,8 @@ class SearchController < ApplicationController
|
|||
}
|
||||
return
|
||||
when 'assets'
|
||||
search_by_name(Asset)
|
||||
@model = Asset
|
||||
search_by_name
|
||||
includes = [{ step: { protocol: { my_module: :experiment } } }, { result: { my_module: :experiment } }, :team]
|
||||
|
||||
render json: @records.includes(includes),
|
||||
|
@ -150,13 +161,16 @@ class SearchController < ApplicationController
|
|||
|
||||
def object_quick_search(class_name)
|
||||
search_model = class_name.to_s.camelize.constantize
|
||||
search_method = search_model.method(search_model.respond_to?(:code) ? :search_by_name_and_id : :search_by_name)
|
||||
search_object_classes = ["#{class_name.pluralize}.name"]
|
||||
search_object_classes << search_model::PREFIXED_ID_SQL if search_model.respond_to?(:code)
|
||||
|
||||
search_method.call(current_user,
|
||||
current_team,
|
||||
params[:query],
|
||||
limit: Constants::QUICK_SEARCH_LIMIT)
|
||||
.order(updated_at: :desc)
|
||||
search_model.search_by_search_fields_with_boolean(current_user,
|
||||
current_team,
|
||||
params[:query],
|
||||
search_object_classes,
|
||||
limit: Constants::QUICK_SEARCH_LIMIT,
|
||||
fetch_latest_versions: class_name == 'protocol')
|
||||
.order(updated_at: :desc)
|
||||
end
|
||||
|
||||
def load_vars
|
||||
|
@ -186,32 +200,34 @@ class SearchController < ApplicationController
|
|||
|
||||
protected
|
||||
|
||||
def search_by_name(model, options = {})
|
||||
@records = model.search(current_user,
|
||||
@include_archived,
|
||||
@search_query,
|
||||
nil,
|
||||
teams: @teams,
|
||||
users: @users,
|
||||
options: options)
|
||||
def search_by_name(options = {})
|
||||
@records = @model.search(current_user,
|
||||
@include_archived,
|
||||
@search_query,
|
||||
nil,
|
||||
teams: @teams,
|
||||
users: @users,
|
||||
options: options)
|
||||
|
||||
filter_records(model) if @filters.present?
|
||||
filter_records if @filters.present?
|
||||
sort_records
|
||||
paginate_records
|
||||
end
|
||||
|
||||
def filter_records(model)
|
||||
filter_datetime!(model, :created_at) if @filters[:created_at].present?
|
||||
filter_datetime!(model, :updated_at) if @filters[:updated_at].present?
|
||||
filter_users!(model) if @filters[:users].present?
|
||||
def filter_records
|
||||
filter_datetime!(:created_at) if @filters[:created_at].present?
|
||||
filter_datetime!(:updated_at) if @filters[:updated_at].present?
|
||||
filter_users! if @filters[:users].present?
|
||||
end
|
||||
|
||||
def sort_records
|
||||
@records = case params[:sort]
|
||||
when 'atoz'
|
||||
@records.order(name: :asc)
|
||||
sort_attribute = @model.name == 'Asset' ? 'active_storage_blobs.filename' : 'name'
|
||||
@records.order(sort_attribute => :asc)
|
||||
when 'ztoa'
|
||||
@records.order(name: :desc)
|
||||
sort_attribute = @model.name == 'Asset' ? 'active_storage_blobs.filename' : 'name'
|
||||
@records.order(sort_attribute => :desc)
|
||||
when 'created_asc'
|
||||
@records.order(created_at: :asc)
|
||||
else
|
||||
|
@ -227,23 +243,29 @@ class SearchController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def filter_datetime!(model, attribute)
|
||||
model_name = model.model_name.collection
|
||||
def filter_datetime!(attribute)
|
||||
model_name = @model.model_name.collection
|
||||
if @filters[attribute][:on].present?
|
||||
from_date = Time.zone.parse(@filters[attribute][:on]).beginning_of_day.utc
|
||||
to_date = Time.zone.parse(@filters[attribute][:on]).end_of_day.utc
|
||||
elsif @filters[attribute][:from].present? && @filters[attribute][:to].present?
|
||||
from_date = Time.zone.parse(@filters[attribute][:from])
|
||||
to_date = Time.zone.parse(@filters[attribute][:to])
|
||||
end
|
||||
|
||||
from_date = Time.zone.parse(@filters[attribute][:from]) if @filters[attribute][:from].present?
|
||||
to_date = Time.zone.parse(@filters[attribute][:to]) if @filters[attribute][:to].present?
|
||||
|
||||
@records = @records.where("#{model_name}.#{attribute} >= ?", from_date) if from_date.present?
|
||||
@records = @records.where("#{model_name}.#{attribute} <= ?", to_date) if to_date.present?
|
||||
end
|
||||
|
||||
def filter_users!(model)
|
||||
@records = @records.joins("INNER JOIN activities ON #{model.model_name.collection}.id = activities.subject_id
|
||||
AND activities.subject_type= '#{model.name}'")
|
||||
.where('activities.owner_id': @filters[:users]&.values)
|
||||
def filter_users!
|
||||
@records = @records.joins("INNER JOIN activities ON #{@model.model_name.collection}.id = activities.subject_id
|
||||
AND activities.subject_type= '#{@model.name}'")
|
||||
|
||||
user_ids = @filters[:users]&.values
|
||||
@records = if @model.name == 'MyModule'
|
||||
@records.where('activities.owner_id IN (?) OR users.id IN (?)', user_ids, user_ids)
|
||||
else
|
||||
@records.where('activities.owner_id': user_ids)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ class TeamsController < ApplicationController
|
|||
before_action :check_export_projects_permissions, only: %i(export_projects_modal export_projects)
|
||||
|
||||
def visible_teams
|
||||
teams = current_user.teams
|
||||
teams = current_user.teams.order(:name)
|
||||
render json: teams, each_serializer: TeamSerializer
|
||||
end
|
||||
|
||||
|
|
|
@ -207,6 +207,24 @@ class Users::RegistrationsController < Devise::RegistrationsController
|
|||
render json: { qr_code: create_2fa_qr_code(current_user) }
|
||||
end
|
||||
|
||||
def regenerate_api_key
|
||||
current_user.regenerate_api_key!
|
||||
|
||||
redirect_to(edit_user_registration_path(anchor: 'api-key'),
|
||||
flash: {
|
||||
success: t('users.registrations.edit.api_key.generated')
|
||||
})
|
||||
end
|
||||
|
||||
def revoke_api_key
|
||||
current_user.revoke_api_key!
|
||||
|
||||
redirect_to(edit_user_registration_path(anchor: 'api-key'),
|
||||
flash: {
|
||||
success: t('users.registrations.edit.api_key.revoked')
|
||||
})
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Called upon creating User (before .save). Permits parameters and extracts
|
||||
|
|
|
@ -4,63 +4,69 @@ import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
|||
import TagsModal from '../../../vue/my_modules/modals/tags.vue';
|
||||
import { mountWithTurbolinks } from '../helpers/turbolinks.js';
|
||||
|
||||
window.initTagsModalComponent = (id) => {
|
||||
const app = createApp({
|
||||
data() {
|
||||
return {
|
||||
tagsModalOpen: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
$(this.$refs.tagsModal).data('tagsModal', this);
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.tagsModalOpen = true;
|
||||
},
|
||||
close() {
|
||||
this.tagsModalOpen = false;
|
||||
},
|
||||
syncTags(tags) {
|
||||
// My module page
|
||||
if ($('#module-tags-selector').length) {
|
||||
const assignedTags = tags.filter((i) => i.assigned).map((i) => (
|
||||
{
|
||||
value: i.id,
|
||||
label: i.attributes.name,
|
||||
params: {
|
||||
color: i.attributes.color
|
||||
}
|
||||
}
|
||||
));
|
||||
dropdownSelector.setData('#module-tags-selector', assignedTags);
|
||||
const app = createApp({
|
||||
data() {
|
||||
return {
|
||||
myModuleParams: null,
|
||||
tagsModalOpen: false,
|
||||
tagsUrl: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
window.tagsModal = this;
|
||||
},
|
||||
beforeUnmount() {
|
||||
delete window.tagsModal;
|
||||
},
|
||||
methods: {
|
||||
open(myModuleUrl) {
|
||||
$.ajax({
|
||||
url: myModuleUrl,
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
success: (data) => {
|
||||
this.myModuleParams = { ...data.data.attributes, id: data.data.id };
|
||||
this.tagsModalOpen = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
close() {
|
||||
this.myModuleParams = null;
|
||||
this.tagsModalOpen = false;
|
||||
},
|
||||
syncTags(tags) {
|
||||
// My module page
|
||||
if ($('#module-tags-selector').length) {
|
||||
const assignedTags = tags.filter((i) => i.assigned).map((i) => (
|
||||
{
|
||||
value: i.id,
|
||||
label: i.attributes.name,
|
||||
params: {
|
||||
color: i.attributes.color
|
||||
}
|
||||
}
|
||||
));
|
||||
dropdownSelector.setData('#module-tags-selector', assignedTags);
|
||||
}
|
||||
|
||||
// Canvas
|
||||
if ($('#canvas-container').length) {
|
||||
$.ajax({
|
||||
url: $('#canvas-container').attr('data-module-tags-url'),
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
success(data) {
|
||||
$.each(data.my_modules, (index, myModule) => {
|
||||
$(`div.panel[data-module-id='${myModule.id}']`)
|
||||
.find('.edit-tags-link')
|
||||
.html(myModule.tags_html);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// Canvas
|
||||
if ($('#canvas-container').length) {
|
||||
$.ajax({
|
||||
url: this.tagsUrl,
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
success(data) {
|
||||
$.each(data.my_modules, (index, myModule) => {
|
||||
$(`div.panel[data-module-id='${myModule.id}']`)
|
||||
.find('.edit-tags-link')
|
||||
.html(myModule.tags_html);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
app.component('tags-modal', TagsModal);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
mountWithTurbolinks(app, id);
|
||||
};
|
||||
|
||||
const tagsModalContainers = document.querySelectorAll('.vue-tags-modal:not(.initialized)');
|
||||
tagsModalContainers.forEach((container) => {
|
||||
$(container).addClass('initialized');
|
||||
window.initTagsModalComponent(`#${container.id}`);
|
||||
}
|
||||
});
|
||||
app.component('tags-modal', TagsModal);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
mountWithTurbolinks(app, '#tagsModalContainer');
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
'pointer-events-none !text-sn-grey': !params.urls.show,
|
||||
'!text-sn-black': dtComponent.currentViewMode === 'archived'
|
||||
}"
|
||||
class="font-bold mb-4 shrink-0 text-sn-blue hover:no-underline line-clamp-2 hover:text-sn-blue h-10">
|
||||
class="font-bold mb-4 shrink-0 text-sn-blue hover:no-underline line-clamp-3 hover:text-sn-blue h-16">
|
||||
{{ params.name }}
|
||||
</a>
|
||||
<div class="flex gap-4 mb-2.5">
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
<template>
|
||||
<div class="content-pane flexible with-grey-background">
|
||||
<div class="content-pane flexible with-grey-background" data-e2e="e2e-CO-globalSearch">
|
||||
<div class="content-header">
|
||||
<div class="title-row">
|
||||
<h1 class="mt-0">
|
||||
{{ i18n.t('search.index.results_title_html', { query: localQuery }) }}
|
||||
<h1 class="mt-0 truncate !inline">
|
||||
<StringWithEllipsis
|
||||
class="w-full"
|
||||
:endCharacters="5"
|
||||
:text="i18n.t('search.index.results_title_html', { query: localQuery })"></StringWithEllipsis>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,12 +14,13 @@
|
|||
<GeneralDropdown ref="historyContainer" :canOpen="canOpenHistory" :fieldOnlyOpen="true" >
|
||||
<template v-slot:field>
|
||||
<div class="left-icon sci-input-container-v2 w-72 input-sm"
|
||||
:title="i18n.t('nav.search')" :class="{'error': invalidQuery}">
|
||||
:title="i18n.t('nav.search')" :class="{'error': invalidQuery}" :data-e2e="'e2e-IF-globalSearch'">
|
||||
<input ref="searchField"
|
||||
type="text"
|
||||
class="!pr-9"
|
||||
:value="localQuery"
|
||||
@change="changeQuery"
|
||||
@keydown="focusHistoryItem"
|
||||
@keydown.enter="changeQuery"
|
||||
@blur="changeQuery"
|
||||
:placeholder="i18n.t('nav.search')"
|
||||
|
@ -24,34 +28,46 @@
|
|||
<i class="sn-icon sn-icon-search"></i>
|
||||
<i v-if="localQuery.length > 0"
|
||||
class="sn-icon cursor-pointer sn-icon-close absolute right-0 -top-0.5"
|
||||
@click="localQuery = ''; $refs.searchField.focus()"></i>
|
||||
@click="localQuery = ''; $refs.searchField.focus()" :title="i18n.t('nav.clear')" :data-e2e="'e2e-BT-globalSearch-clearInput'"></i>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:flyout >
|
||||
<div v-for="(query, i) in reversedPreviousQueries" @click="setQuery(query)" :key="i"
|
||||
ref="historyItems"
|
||||
tabindex="1"
|
||||
@keydown.enter="setQuery(query)"
|
||||
class="flex px-3 h-11 items-center gap-2 hover:bg-sn-super-light-grey cursor-pointer">
|
||||
<i class="sn-icon sn-icon-history-search"></i>
|
||||
{{ query }}
|
||||
<div class="max-w-[600px]">
|
||||
<div v-for="(query, i) in reversedPreviousQueries" @click="setQuery(query)" :key="i"
|
||||
ref="historyItems"
|
||||
@keydown="focusHistoryItem"
|
||||
tabindex="1"
|
||||
@keydown.enter="setQuery(query)"
|
||||
class="flex px-3 min-h-11 items-center gap-2 hover:bg-sn-super-light-grey cursor-pointer">
|
||||
<i class="sn-icon sn-icon-history-search"></i>
|
||||
{{ query }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</GeneralDropdown>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<button class="btn btn-secondary btn-sm" :class="{'active': activeGroup == 'ExperimentsComponent'}" @click="setActiveGroup('ExperimentsComponent')">
|
||||
<button class="btn btn-secondary btn-sm"
|
||||
:class="{'active': activeGroup == 'ExperimentsComponent'}"
|
||||
@click="setActiveGroup('ExperimentsComponent')"
|
||||
:data-e2e="'e2e-BT-globalSearch-experiments'">
|
||||
{{ i18n.t('search.index.experiments') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" :class="{'active': activeGroup == 'MyModulesComponent'}" @click="setActiveGroup('MyModulesComponent')">
|
||||
<button class="btn btn-secondary btn-sm"
|
||||
:class="{'active': activeGroup == 'MyModulesComponent'}"
|
||||
@click="setActiveGroup('MyModulesComponent')"
|
||||
:data-e2e="'e2e-BT-globalSearch-tasks'">
|
||||
{{ i18n.t('search.index.tasks') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" :class="{'active': activeGroup == 'ResultsComponent'}" @click="setActiveGroup('ResultsComponent')">
|
||||
<button class="btn btn-secondary btn-sm"
|
||||
:class="{'active': activeGroup == 'ResultsComponent'}"
|
||||
@click="setActiveGroup('ResultsComponent')"
|
||||
:data-e2e="'e2e-BT-globalSearch-taskResults'">
|
||||
{{ i18n.t('search.index.task_results') }}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-light btn-sm" @click="filterModalOpened = true">
|
||||
<i class="sn-icon sn-icon-search-options"></i>
|
||||
<span class="tw-hidden lg:inline">{{ i18n.t('search.index.more_search_options') }}</span>
|
||||
<button class="btn btn-light btn-sm" @click="filterModalOpened = true" :data-e2e="'e2e-BT-globalSearch-openFilterModal'">
|
||||
<i class="sn-icon sn-icon-search-options" :title="i18n.t('search.index.more_search_options')"></i>
|
||||
<span class="tw-hidden xl:inline">{{ i18n.t('search.index.more_search_options') }}</span>
|
||||
<span
|
||||
v-if="activeFilters.length > 0"
|
||||
class="absolute -right-1 -top-1 rounded-full bg-sn-science-blue text-white flex items-center justify-center w-4 h-4 text-[9px]"
|
||||
|
@ -61,14 +77,14 @@
|
|||
</button>
|
||||
<template v-if="activeFilters.length > 0">
|
||||
<div class="h-4 w-[1px] bg-sn-grey"></div>
|
||||
<button class="btn btn-light btn-sm" @click="resetFilters">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
<span class="tw-hidden lg:inline">{{ i18n.t('search.index.clear_filters') }}</span>
|
||||
<button class="btn btn-light btn-sm" @click="resetFilters" :data-e2e="'e2e-BT-globalSearch-resetFilters'">
|
||||
<i class="sn-icon sn-icon-close" :title="i18n.t('search.index.clear_filters')"></i>
|
||||
<span class="tw-hidden xl:inline">{{ i18n.t('search.index.clear_filters') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<button v-if="activeGroup" class="btn btn-light btn-sm" @click="resetGroup">
|
||||
<i class="sn-icon sn-icon-undo"></i>
|
||||
<span class="tw-hidden lg:inline">{{ i18n.t('search.index.all_results') }}</span>
|
||||
<button v-if="activeGroup" class="btn btn-light btn-sm" @click="resetGroup" :data-e2e="'e2e-BT-globalSearch-resetGroup'">
|
||||
<i class="sn-icon sn-icon-undo" :title="i18n.t('search.index.all_results')"></i>
|
||||
<span class="tw-hidden xl:inline">{{ i18n.t('search.index.all_results') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<template v-for="group in searchGroups">
|
||||
|
@ -85,7 +101,7 @@
|
|||
@updated="calculateTotalElements"
|
||||
/>
|
||||
</template>
|
||||
<div v-if="totalElements === 0" class="bg-white rounded p-4">
|
||||
<div v-if="totalElements === 0 && activeGroup === null" class="bg-white rounded p-4">
|
||||
<NoSearchResult />
|
||||
</div>
|
||||
<teleport to='body'>
|
||||
|
@ -103,6 +119,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
/* global HelperModule GLOBAL_CONSTANTS */
|
||||
|
||||
import FoldersComponent from './groups/folders.vue';
|
||||
import ProjectsComponent from './groups/projects.vue';
|
||||
import ExperimentsComponent from './groups/experiments.vue';
|
||||
|
@ -117,6 +135,7 @@ import ReportsComponent from './groups/reports.vue';
|
|||
import FiltersModal from './filters_modal.vue';
|
||||
import GeneralDropdown from '../shared/general_dropdown.vue';
|
||||
import NoSearchResult from './groups/helpers/no_search_result.vue';
|
||||
import StringWithEllipsis from '../shared/string_with_ellipsis.vue';
|
||||
|
||||
export default {
|
||||
emits: ['search', 'selectGroup'],
|
||||
|
@ -141,6 +160,10 @@ export default {
|
|||
currentTeam: {
|
||||
type: Number || String,
|
||||
required: true
|
||||
},
|
||||
singleTeam: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -157,7 +180,8 @@ export default {
|
|||
ReportsComponent,
|
||||
FiltersModal,
|
||||
GeneralDropdown,
|
||||
NoSearchResult
|
||||
NoSearchResult,
|
||||
StringWithEllipsis
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -167,6 +191,7 @@ export default {
|
|||
previousQueries: [],
|
||||
invalidQuery: false,
|
||||
activeGroup: null,
|
||||
focusedHistoryItem: null,
|
||||
totalElements: 0,
|
||||
searchGroups: [
|
||||
'FoldersComponent',
|
||||
|
@ -215,7 +240,7 @@ export default {
|
|||
to: null
|
||||
},
|
||||
include_archived: urlParams.get('include_archived') === 'true',
|
||||
teams: urlParams.getAll('teams[]').map((team) => parseInt(team, 10)),
|
||||
teams: (this.singleTeam ? [] : urlParams.getAll('teams[]').map((team) => parseInt(team, 10))),
|
||||
users: urlParams.getAll('users[]').map((user) => parseInt(user, 10)),
|
||||
group: urlParams.get('group')
|
||||
};
|
||||
|
@ -264,8 +289,15 @@ export default {
|
|||
|
||||
this.localQuery = event.target.value;
|
||||
|
||||
if (event.target.value.length < 2) {
|
||||
if (event.target.value.length < GLOBAL_CONSTANTS.NAME_MIN_LENGTH) {
|
||||
this.invalidQuery = true;
|
||||
HelperModule.flashAlertMsg(this.i18n.t('general.query.length_too_short', { min_length: GLOBAL_CONSTANTS.NAME_MIN_LENGTH }), 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.value.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH) {
|
||||
this.invalidQuery = true;
|
||||
HelperModule.flashAlertMsg(this.i18n.t('general.query.length_too_long', { max_length: GLOBAL_CONSTANTS.NAME_MAX_LENGTH }), 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -295,6 +327,28 @@ export default {
|
|||
this.activeGroup = null;
|
||||
this.filters.group = null;
|
||||
},
|
||||
focusHistoryItem(event) {
|
||||
if (this.focusedHistoryItem === null && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
|
||||
this.focusedHistoryItem = 0;
|
||||
this.$refs.historyItems[this.focusedHistoryItem].focus();
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
this.focusedHistoryItem += 1;
|
||||
if (this.focusedHistoryItem >= this.$refs.historyItems.length) {
|
||||
this.focusedHistoryItem = 0;
|
||||
}
|
||||
this.$refs.historyItems[this.focusedHistoryItem].focus();
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
this.focusedHistoryItem -= 1;
|
||||
if (this.focusedHistoryItem < 0) {
|
||||
this.focusedHistoryItem = this.$refs.historyItems.length - 1;
|
||||
}
|
||||
this.$refs.historyItems[this.focusedHistoryItem].focus();
|
||||
} else if (event.key === 'Escape') {
|
||||
this.$refs.historyContainer.isOpen = false;
|
||||
}
|
||||
},
|
||||
resetFilters() {
|
||||
this.filters = {
|
||||
created_at: {
|
||||
|
|
|
@ -1,39 +1,48 @@
|
|||
<template>
|
||||
<div class="max-w-[600px] py-3.5">
|
||||
<div class="flex flex-col pb-6 overflow-y-auto max-h-[75vh]">
|
||||
<div class="sci-label mb-2">{{ i18n.t('search.filters.by_type') }}</div>
|
||||
<div class="flex flex-col pb-6 overflow-y-auto w-[calc(100%_+_1rem)] px-2 -ml-2 max-h-[calc(80vh_-_160px)]">
|
||||
<div class="sci-label mb-2" data-e2e="e2e-TX-globalSearch-filters-filterByType">{{ i18n.t('search.filters.by_type') }}</div>
|
||||
<div class="flex items-center gap-2 flex-wrap mb-6">
|
||||
<template v-for="group in searchGroups" :key="group.value">
|
||||
<button class="btn btn-secondary btn-xs"
|
||||
ref="groupButtons"
|
||||
:class="{'active': activeGroup === group.value}"
|
||||
@click="setActiveGroup(group.value)">
|
||||
@click="setActiveGroup(group.value)"
|
||||
:data-e2e="`e2e-BT-globalSearch-filters-${group.label.toLowerCase().replaceAll(/\s+/g, '')}`">
|
||||
{{ group.label }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="sci-label mb-2">{{ i18n.t('search.filters.by_created_date') }}</div>
|
||||
<div class="sci-label mb-2" data-e2e="e2e-TX-globalSearch-filters-filterByCreated">{{ i18n.t('search.filters.by_created_date') }}</div>
|
||||
<DateFilter
|
||||
:date="createdAt"
|
||||
ref="createdAtComponent"
|
||||
class="mb-6"
|
||||
@change="(v) => {this.createdAt = v}"
|
||||
e2eValue="globalSearch-filters-createdDate"
|
||||
></DateFilter>
|
||||
<div class="sci-label mb-2">{{ i18n.t('search.filters.by_updated_date') }}</div>
|
||||
<div class="sci-label mb-2" data-e2e="e2e-TX-globalSearch-filters-filterByUpdated">{{ i18n.t('search.filters.by_updated_date') }}</div>
|
||||
<DateFilter
|
||||
:date="updatedAt"
|
||||
ref="updatedAtComponent"
|
||||
class="mb-6"
|
||||
@change="(v) => {this.updatedAt = v}"
|
||||
e2eValue="globalSearch-filters-updatedDate"
|
||||
></DateFilter>
|
||||
<div class="sci-label mb-2">{{ i18n.t('search.filters.by_team') }}</div>
|
||||
<SelectDropdown :options="teams"
|
||||
class="mb-6"
|
||||
:with-checkboxes="true"
|
||||
:clearable="true"
|
||||
:multiple="true"
|
||||
:value="selectedTeams"
|
||||
@change="(v) => {selectedTeams = v}" />
|
||||
<div class="sci-label mb-2 flex items-center gap-2">
|
||||
<template v-if="teams.length > 1">
|
||||
<div class="sci-label mb-2" data-e2e="e2e-TX-globalSearch-filters-filterByTeam">{{ i18n.t('search.filters.by_team') }}</div>
|
||||
<SelectDropdown :options="teams"
|
||||
class="mb-6"
|
||||
:with-checkboxes="true"
|
||||
:clearable="true"
|
||||
:searchable="true"
|
||||
:multiple="true"
|
||||
:value="selectedTeams"
|
||||
:placeholder="i18n.t('search.filters.by_team_placeholder')"
|
||||
@change="selectTeams"
|
||||
e2eValue="e2e-DC-globalSearch-filters-teams" />
|
||||
</template>
|
||||
<div class="sci-label mb-2 flex items-center gap-2" data-e2e="e2e-TX-globalSearch-filters-filterByUser">
|
||||
{{ i18n.t('search.filters.by_user') }}
|
||||
<i class="sn-icon sn-icon-info" :title="i18n.t('search.filters.by_user_info')"></i>
|
||||
</div>
|
||||
|
@ -43,12 +52,15 @@
|
|||
:optionRenderer="userRenderer"
|
||||
:labelRenderer="userRenderer"
|
||||
:clearable="true"
|
||||
:searchable="true"
|
||||
:with-checkboxes="true"
|
||||
:multiple="true"
|
||||
@change="(v) => {selectedUsers = v}" />
|
||||
<div class="flex items-center gap-2">
|
||||
:placeholder="i18n.t('search.filters.by_user_placeholder')"
|
||||
@change="selectUsers"
|
||||
e2eValue="e2e-DC-globalSearch-filters-users" />
|
||||
<div class="flex items-center gap-2" data-e2e="e2e-TX-globalSearch-filters-includeArchived">
|
||||
<div class="sci-checkbox-container">
|
||||
<input type="checkbox" v-model="includeArchived" class="sci-checkbox" />
|
||||
<input type="checkbox" v-model="includeArchived" class="sci-checkbox" data-e2e="e2e-CB-globalSearch-filters-includeArchived"/>
|
||||
<span class="sci-checkbox-label"></span>
|
||||
</div>
|
||||
{{ i18n.t('search.filters.include_archived') }}
|
||||
|
@ -56,9 +68,9 @@
|
|||
</div>
|
||||
<hr class="mb-6">
|
||||
<div class="flex items-center gap-6">
|
||||
<button class="btn btn-light" @click="clearFilters">{{ i18n.t('search.filters.clear') }}</button>
|
||||
<button class="btn btn-secondary ml-auto" @click="$emit('cancel')">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="search" >{{ i18n.t('general.search') }}</button>
|
||||
<button class="btn btn-light" @click="clearFilters" data-e2e="e2e-BT-globalSearch-filters-clearFilters">{{ i18n.t('search.filters.clear') }}</button>
|
||||
<button class="btn btn-secondary ml-auto" @click="$emit('cancel')" data-e2e="e2e-BT-globalSearch-filters-cancel">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" @click="search" data-e2e="e2e-BT-globalSearch-filters-search">{{ i18n.t('general.search') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -171,6 +183,16 @@ export default {
|
|||
this.users = response.data.data.map((user) => ([parseInt(user.id, 10), user.attributes.name, { avatar_url: user.attributes.avatar_url }]));
|
||||
});
|
||||
},
|
||||
selectTeams(teams) {
|
||||
if (Array.isArray(teams)) {
|
||||
this.selectedTeams = teams;
|
||||
}
|
||||
},
|
||||
selectUsers(users) {
|
||||
if (Array.isArray(users)) {
|
||||
this.selectedUsers = users;
|
||||
}
|
||||
},
|
||||
clearFilters() {
|
||||
this.createdAt = {
|
||||
on: null,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<SelectDropdown class="!w-40"
|
||||
:options="dateOptions"
|
||||
:value="selectedOption"
|
||||
:data-e2e="`e2e-DD-${e2eValue}`"
|
||||
@change="(v) => {selectedOption = v}" />
|
||||
<div class="grow">
|
||||
<DateTimePicker
|
||||
|
@ -12,7 +13,8 @@
|
|||
size="mb"
|
||||
placeholder="Enter date"
|
||||
:defaultValue="date.on"
|
||||
:clearable="true"/>
|
||||
:clearable="true"
|
||||
:data-e2e="`e2e-DP-${e2eValue}`"/>
|
||||
<DateTimePicker
|
||||
v-if="selectedOption === 'custom'"
|
||||
@change="setFrom"
|
||||
|
@ -21,7 +23,8 @@
|
|||
size="mb"
|
||||
placeholder="From date"
|
||||
:defaultValue="date.from"
|
||||
:clearable="true"/>
|
||||
:clearable="true"
|
||||
:data-e2e="`e2e-DP-${e2eValue}From`"/>
|
||||
<DateTimePicker
|
||||
v-if="selectedOption === 'custom'"
|
||||
@change="setTo"
|
||||
|
@ -29,7 +32,8 @@
|
|||
size="mb"
|
||||
placeholder="To date"
|
||||
:defaultValue="date.to"
|
||||
:clearable="true"/>
|
||||
:clearable="true"
|
||||
:data-e2e="`e2e-DP-${e2eValue}To`"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -44,6 +48,10 @@ export default {
|
|||
date: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
e2eValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form @submit.prevent="submit">
|
||||
<div class="modal-content !pb-2.5">
|
||||
<div class="modal-content !pb-2.5" data-e2e="e2e-MD-globalSearch-filters">
|
||||
<div class="modal-header flex-wrap">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-globalSearch-filters-close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block" id="edit-project-modal-label">
|
||||
<h4 class="modal-title truncate !block" id="edit-project-modal-label" data-e2e="e2e-TX-globalSearch-filters-title">
|
||||
{{ i18n.t('search.filters.title') }}
|
||||
</h4>
|
||||
<div class="basis-full">
|
||||
<div class="basis-full" data-e2e="e2e-TX-globalSearch-filters-subtitle">
|
||||
{{ i18n.t('search.filters.sub_title') }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }">
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }" :data-e2e="'e2e-CO-globalSearch-files'">
|
||||
<template v-if="total && results.length">
|
||||
<div class="flex items-center">
|
||||
<h2 class="flex items-center gap-2 mt-0 mb-4">
|
||||
|
@ -7,9 +7,16 @@
|
|||
{{ i18n.t('search.index.files') }}
|
||||
<span class="text-base" >[{{ total }}]</span>
|
||||
</h2>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort"></SortFlyout>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort" :e2eSortButton="'e2e-BT-globalSearch-files-sort'"></SortFlyout>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_auto_auto_auto_auto_auto] items-center">
|
||||
<TableHeader :selected="selected" :columnNames="[
|
||||
i18n.t('search.index.created_at'),
|
||||
i18n.t('search.index.updated_at'),
|
||||
'',
|
||||
'',
|
||||
i18n.t('search.index.team')
|
||||
]"></TableHeader>
|
||||
<div v-for="(row, index) in preparedResults" :key="row.id" class="contents group">
|
||||
<hr class="col-span-6 w-full m-0" v-if="index > 0">
|
||||
<LinkTemplate :url="row.attributes.parent.url" :icon="row.attributes.icon" :value="row.attributes.file_name"/>
|
||||
|
@ -26,7 +33,7 @@
|
|||
</div>
|
||||
<div v-if="viewAll">
|
||||
<hr class="w-full mb-4 mt-0">
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'AssetsComponent')">View all</button>
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'AssetsComponent')" :data-e2e="'e2e-BT-globalSearch-files-viewAll'">View all</button>
|
||||
</div>
|
||||
</template>
|
||||
<Loader v-if="loading" :loaderRows="loaderRows" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }">
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }" :data-e2e="'e2e-CO-globalSearch-experiments'">
|
||||
<template v-if="total && results.length">
|
||||
<div class="flex items-center">
|
||||
<h2 class="flex items-center gap-2 mt-0 mb-4">
|
||||
|
@ -7,9 +7,15 @@
|
|||
{{ i18n.t('search.index.experiments') }}
|
||||
<span class="text-base" >[{{ total }}]</span>
|
||||
</h2>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort"></SortFlyout>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort" :e2eSortButton="'e2e-BT-globalSearch-experiments-sort'"></SortFlyout>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_110px_auto_auto_auto] items-center">
|
||||
<TableHeader :selected="selected" :columnNames="[
|
||||
i18n.t('search.index.id'),
|
||||
i18n.t('search.index.created_at'),
|
||||
i18n.t('search.index.project'),
|
||||
i18n.t('search.index.team')
|
||||
]"></TableHeader>
|
||||
<div v-for="(row, index) in preparedResults" :key="row.id" class="contents group">
|
||||
<hr class="col-span-5 w-full m-0" v-if="index > 0">
|
||||
<LinkTemplate :url="row.attributes.url" :value="labelName({ name: row.attributes.name, archived: row.attributes.archived})"/>
|
||||
|
@ -21,7 +27,7 @@
|
|||
</div>
|
||||
<div v-if="viewAll">
|
||||
<hr class="w-full mb-4 mt-0">
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'ExperimentsComponent')">View all</button>
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'ExperimentsComponent')" :data-e2e="'e2e-BT-globalSearch-experiments-viewAll'">View all</button>
|
||||
</div>
|
||||
</template>
|
||||
<Loader v-if="loading" :loaderRows="loaderRows" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }">
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }" :data-e2e="'e2e-CO-globalSearch-folders'">
|
||||
<template v-if="total && results.length">
|
||||
<div class="flex items-center">
|
||||
<h2 class="flex items-center gap-2 mt-0 mb-4">
|
||||
|
@ -7,12 +7,19 @@
|
|||
{{ i18n.t('search.index.folders') }}
|
||||
<span class="text-base" >[{{ total }}]</span>
|
||||
</h2>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort"></SortFlyout>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort" :e2eSortButton="'e2e-BT-globalSearch-folders-sort'"></SortFlyout>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_auto_auto_auto] items-center">
|
||||
<div class="grid grid-cols-[auto_110px_auto_auto_auto] items-center">
|
||||
<TableHeader :selected="selected" :columnNames="[
|
||||
i18n.t('search.index.id'),
|
||||
i18n.t('search.index.created_at'),
|
||||
i18n.t('search.index.folder'),
|
||||
i18n.t('search.index.team')
|
||||
]"></TableHeader>
|
||||
<div v-for="(row, index) in preparedResults" :key="row.id" class="contents group">
|
||||
<hr class="col-span-4 w-full m-0" v-if="index > 0">
|
||||
<hr class="col-span-5 w-full m-0" v-if="index > 0">
|
||||
<LinkTemplate :url="row.attributes.url" :value="labelName({ name: row.attributes.name, archived: row.attributes.archived})"/>
|
||||
<CellTemplate :label="i18n.t('search.index.id')" :value="row.attributes.code"/>
|
||||
<CellTemplate :label="i18n.t('search.index.created_at')" :value="row.attributes.created_at"/>
|
||||
<CellTemplate :label="i18n.t('search.index.folder')" :visible="row.attributes.parent_folder"
|
||||
:url="row.attributes.parent_folder?.url" :value="labelName(row.attributes.parent_folder)"/>
|
||||
|
@ -21,7 +28,7 @@
|
|||
</div>
|
||||
<div v-if="viewAll">
|
||||
<hr class="w-full mb-4 mt-0">
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'FoldersComponent')">View all</button>
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'FoldersComponent')" :data-e2e="'e2e-BT-globalSearch-folders-viewAll'">View all</button>
|
||||
</div>
|
||||
</template>
|
||||
<Loader v-if="loading" :loaderRows="loaderRows" />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="h-full py-2 px-4 grid grid-cols-[auto_1fr] min-w-[8rem] items-center gap-1 text-xs group-hover:bg-sn-super-light-grey">
|
||||
<div class="h-full py-2 px-4 grid grid-cols-[auto_1fr] min-w-[6rem] items-center gap-1 text-xs group-hover:bg-sn-super-light-grey">
|
||||
<template v-if="visible">
|
||||
<b class="shrink-0">{{ label }}:</b>
|
||||
<b class="shrink-0 tw-hidden lg:block">{{ label }}:</b>
|
||||
<a v-if="url" :href="url" class="shrink-0 overflow-hidden hover:no-underline">
|
||||
<img v-if="avatar" :src="avatar" class="w-5 h-5 border border-sn-super-light-grey rounded-full mx-1" />
|
||||
<StringWithEllipsis class="w-full" :text="value"></StringWithEllipsis>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<a target="_blank" :href="url"
|
||||
class="h-full py-2 px-4 flex gap-1 items-center font-bold overflow-hidden group-hover:bg-sn-super-light-grey hover:no-underline"
|
||||
class="h-full py-2 px-4 flex gap-1 items-center font-bold overflow-hidden group-hover:bg-sn-super-light-grey min-w-40 hover:no-underline"
|
||||
>
|
||||
<span v-if="icon" :class="icon" class="sn-icon shrink-0"></span>
|
||||
<StringWithEllipsis
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div ref="noSearchResult" class="h-[60vh]">
|
||||
<div class="flex flex-col gap-6 bg-sn-white text-center relative top-1/4">
|
||||
<div><span class=" inline-block sn-icon sn-icon-search"></span></div>
|
||||
<div><span class=" inline-block sn-icon sn-icon-search text-sn-sleepy-grey" style="font-size: 100px !important"></span></div>
|
||||
<div class="">
|
||||
<p class="text-sn-black text-2xl font-semibold">
|
||||
{{ i18n.t('search.index.no_results_text') }}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
position="right"
|
||||
@dtEvent="changeSort"
|
||||
btnIcon="sn-icon sn-icon-sort-down"
|
||||
:e2eSortButton="e2eSortButton"
|
||||
></MenuDropdown>
|
||||
</template>
|
||||
|
||||
|
@ -18,6 +19,10 @@ export default {
|
|||
sort: {
|
||||
type: String,
|
||||
default: 'created_desc'
|
||||
},
|
||||
e2eSortButton: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<div class="contents lg:hidden">
|
||||
<div class="px-4 py-2 bg-white h-full" :class="{'sticky top-[68px] z-10': selected}"></div>
|
||||
<div
|
||||
v-for="column in columnNames"
|
||||
:key="column"
|
||||
class="truncate px-4 py-2 bg-white text-xs font-bold h-full"
|
||||
:class="{'sticky top-[68px] z-10': selected}"
|
||||
>{{ column }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TableHeader',
|
||||
props: {
|
||||
columnNames: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div v-if="!disabled" ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }">
|
||||
<div v-if="!disabled" ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }" :data-e2e="'e2e-CO-globalSearch-labelTemplates'">
|
||||
<template v-if="total && results.length">
|
||||
<div class="flex items-center">
|
||||
<h2 class="flex items-center gap-2 mt-0 mb-4">
|
||||
|
@ -7,9 +7,16 @@
|
|||
{{ i18n.t('search.index.label_templates') }}
|
||||
<span class="text-base" >[{{ total }}]</span>
|
||||
</h2>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort"></SortFlyout>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort" :e2eSortButton="'e2e-BT-globalSearch-labelTemplates-sort'"></SortFlyout>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_110px_auto_auto_auto_auto] items-center">
|
||||
<TableHeader :selected="selected" :columnNames="[
|
||||
i18n.t('search.index.format'),
|
||||
i18n.t('search.index.created_at'),
|
||||
i18n.t('search.index.updated_at'),
|
||||
i18n.t('search.index.created_by'),
|
||||
i18n.t('search.index.team')
|
||||
]"></TableHeader>
|
||||
<div v-for="(row, index) in preparedResults" :key="row.id" class="contents group">
|
||||
<hr class="col-span-6 w-full m-0" v-if="index > 0">
|
||||
<LinkTemplate :url="row.attributes.url" :value="row.attributes.name"/>
|
||||
|
@ -22,7 +29,7 @@
|
|||
</div>
|
||||
<div v-if="viewAll">
|
||||
<hr class="w-full mb-4 mt-0">
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'LabelTemplatesComponent')">View all</button>
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'LabelTemplatesComponent')" :data-e2e="'e2e-BT-globalSearch-labelTemplates-viewAll'">View all</button>
|
||||
</div>
|
||||
</template>
|
||||
<Loader v-if="loading" :loaderRows="loaderRows" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }">
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }" :data-e2e="'e2e-CO-globalSearch-taskProtocols'">
|
||||
<template v-if="total && results.length">
|
||||
<div class="flex items-center">
|
||||
<h2 class="flex items-center gap-2 mt-0 mb-4">
|
||||
|
@ -7,9 +7,17 @@
|
|||
{{ i18n.t('search.index.task_protocols') }}
|
||||
<span class="text-base" >[{{ total }}]</span>
|
||||
</h2>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort"></SortFlyout>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort" :e2eSortButton="'e2e-BT-globalSearch-taskProtocols-sort'"></SortFlyout>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_110px_auto_auto_auto_auto_auto] items-center">
|
||||
<TableHeader :selected="selected" :columnNames="[
|
||||
i18n.t('search.index.id'),
|
||||
i18n.t('search.index.created_at'),
|
||||
i18n.t('search.index.updated_at'),
|
||||
i18n.t('search.index.task'),
|
||||
i18n.t('search.index.experiment'),
|
||||
i18n.t('search.index.team')
|
||||
]"></TableHeader>
|
||||
<div v-for="(row, index) in preparedResults" :key="row.id" class="contents group">
|
||||
<hr class="col-span-7 w-full m-0" v-if="index > 0">
|
||||
<LinkTemplate :url="row.attributes.url" :value="labelName({ name: row.attributes.name, archived: row.attributes.archived})"/>
|
||||
|
@ -23,7 +31,7 @@
|
|||
</div>
|
||||
<div v-if="viewAll">
|
||||
<hr class="w-full mb-4 mt-0">
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'MyModuleProtocolsComponent')">View all</button>
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'MyModuleProtocolsComponent')" :data-e2e="'e2e-BT-globalSearch-taskProtocols-viewAll'">View all</button>
|
||||
</div>
|
||||
</template>
|
||||
<Loader v-if="loading" :loaderRows="loaderRows" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }">
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }" :data-e2e="'e2e-CO-globalSearch-tasks'">
|
||||
<template v-if="total && results.length">
|
||||
<div class="flex items-center">
|
||||
<h2 class="flex items-center gap-2 mt-0 mb-4">
|
||||
|
@ -7,9 +7,16 @@
|
|||
{{ i18n.t('search.index.tasks') }}
|
||||
<span class="text-base" >[{{ total }}]</span>
|
||||
</h2>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort"></SortFlyout>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort" :e2eSortButton="'e2e-BT-globalSearch-tasks-sort'"></SortFlyout>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_110px_auto_auto_auto_auto] items-center">
|
||||
<TableHeader :selected="selected" :columnNames="[
|
||||
i18n.t('search.index.id'),
|
||||
i18n.t('search.index.created_at'),
|
||||
i18n.t('search.index.updated_at'),
|
||||
i18n.t('search.index.experiment'),
|
||||
i18n.t('search.index.team')
|
||||
]"></TableHeader>
|
||||
<div v-for="(row, index) in preparedResults" :key="row.id" class="contents group">
|
||||
<hr class="col-span-6 w-full m-0" v-if="index > 0">
|
||||
<LinkTemplate :url="row.attributes.url" :value="labelName({ name: row.attributes.name, archived: row.attributes.archived})"/>
|
||||
|
@ -22,7 +29,7 @@
|
|||
</div>
|
||||
<div v-if="viewAll">
|
||||
<hr class="w-full mb-4 mt-0">
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'MyModulesComponent')">View all</button>
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'MyModulesComponent')" :data-e2e="'e2e-BT-globalSearch-tasks-viewAll'">View all</button>
|
||||
</div>
|
||||
</template>
|
||||
<Loader v-if="loading" :loaderRows="loaderRows" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }">
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }" :data-e2e="'e2e-CO-globalSearch-projects'">
|
||||
<template v-if="total && results.length">
|
||||
<div class="flex items-center">
|
||||
<h2 class="flex items-center gap-2 mt-0 mb-4">
|
||||
|
@ -7,9 +7,15 @@
|
|||
{{ i18n.t('search.index.projects') }}
|
||||
<span class="text-base" >[{{ total }}]</span>
|
||||
</h2>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort"></SortFlyout>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort" :e2eSortButton="'e2e-BT-globalSearch-projects-sort'"></SortFlyout>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_110px_auto_auto_auto] items-center">
|
||||
<TableHeader :selected="selected" :columnNames="[
|
||||
i18n.t('search.index.id'),
|
||||
i18n.t('search.index.created_at'),
|
||||
i18n.t('search.index.folder'),
|
||||
i18n.t('search.index.team')
|
||||
]"></TableHeader>
|
||||
<div v-for="(row, index) in preparedResults" :key="row.id" class="contents group">
|
||||
<hr class="col-span-5 w-full m-0" v-if="index > 0">
|
||||
<LinkTemplate :url="row.attributes.url" :value="labelName({ name: row.attributes.name, archived: row.attributes.archived})"/>
|
||||
|
@ -22,7 +28,7 @@
|
|||
</div>
|
||||
<div v-if="viewAll">
|
||||
<hr class="w-full mb-4 mt-0">
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'ProjectsComponent')">View all</button>
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'ProjectsComponent')" :data-e2e="'e2e-BT-globalSearch-projects-viewAll'">View all</button>
|
||||
</div>
|
||||
</template>
|
||||
<Loader v-if="loading" :loaderRows="loaderRows" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }">
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }" :data-e2e="'e2e-CO-globalSearch-protocolTemplates'">
|
||||
<template v-if="total && results.length">
|
||||
<div class="flex items-center">
|
||||
<h2 class="flex items-center gap-2 mt-0 mb-4">
|
||||
|
@ -7,9 +7,16 @@
|
|||
{{ i18n.t('search.index.protocol_templates') }}
|
||||
<span class="text-base" >[{{ total }}]</span>
|
||||
</h2>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort"></SortFlyout>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort" :e2eSortButton="'e2e-BT-globalSearch-protocolTemplates-sort'"></SortFlyout>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_110px_auto_auto_auto_auto] items-center">
|
||||
<TableHeader :selected="selected" :columnNames="[
|
||||
i18n.t('search.index.id'),
|
||||
i18n.t('search.index.created_at'),
|
||||
i18n.t('search.index.updated_at'),
|
||||
i18n.t('search.index.created_by'),
|
||||
i18n.t('search.index.team')
|
||||
]"></TableHeader>
|
||||
<div v-for="(row, index) in preparedResults" :key="row.id" class="contents group">
|
||||
<hr class="col-span-6 w-full m-0" v-if="index > 0">
|
||||
<LinkTemplate :url="row.attributes.url" :value="labelName({ name: row.attributes.name, archived: row.attributes.archived})"/>
|
||||
|
@ -22,7 +29,7 @@
|
|||
</div>
|
||||
<div v-if="viewAll">
|
||||
<hr class="w-full mb-4 mt-0">
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'ProtocolsComponent')">View all</button>
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'ProtocolsComponent')" :data-e2e="'e2e-BT-globalSearch-protocolTemplates-viewAll'">View all</button>
|
||||
</div>
|
||||
</template>
|
||||
<Loader v-if="loading" :loaderRows="loaderRows" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }">
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }" :data-e2e="'e2e-CO-globalSearch-reports'">
|
||||
<template v-if="total && results.length">
|
||||
<div class="flex items-center">
|
||||
<h2 class="flex items-center gap-2 mt-0 mb-4">
|
||||
|
@ -7,9 +7,17 @@
|
|||
{{ i18n.t('search.index.reports') }}
|
||||
<span class="text-base" >[{{ total }}]</span>
|
||||
</h2>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort"></SortFlyout>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort" :e2eSortButton="'e2e-BT-globalSearch-reports-sort'"></SortFlyout>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_110px_auto_auto_auto_auto_auto] items-center">
|
||||
<TableHeader :selected="selected" :columnNames="[
|
||||
i18n.t('search.index.id'),
|
||||
i18n.t('search.index.created_at'),
|
||||
i18n.t('search.index.updated_at'),
|
||||
i18n.t('search.index.created_by'),
|
||||
i18n.t('search.index.project'),
|
||||
i18n.t('search.index.team')
|
||||
]"></TableHeader>
|
||||
<div v-for="(row, index) in preparedResults" :key="row.id" class="contents group">
|
||||
<hr class="col-span-7 w-full m-0" v-if="index > 0">
|
||||
<LinkTemplate :url="row.attributes.url" :value="row.attributes.name"/>
|
||||
|
@ -23,7 +31,7 @@
|
|||
</div>
|
||||
<div v-if="viewAll">
|
||||
<hr class="w-full mb-4 mt-0">
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'ReportsComponent')">View all</button>
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'ReportsComponent')" :data-e2e="'e2e-BT-globalSearch-reports-viewAll'">View all</button>
|
||||
</div>
|
||||
</template>
|
||||
<Loader v-if="loading" :loaderRows="loaderRows" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }">
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }" :data-e2e="'e2e-CO-globalSearch-inventoryItems'">
|
||||
<template v-if="total && results.length">
|
||||
<div class="flex items-center">
|
||||
<h2 class="flex items-center gap-2 mt-0 mb-4">
|
||||
|
@ -7,9 +7,16 @@
|
|||
{{ i18n.t('search.index.inventory_items') }}
|
||||
<span class="text-base" >[{{ total }}]</span>
|
||||
</h2>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort"></SortFlyout>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort" :e2eSortButton="'e2e-BT-globalSearch-inventoryItems-sort'"></SortFlyout>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_110px_auto_auto_auto_auto] items-center">
|
||||
<TableHeader :selected="selected" :columnNames="[
|
||||
i18n.t('search.index.id'),
|
||||
i18n.t('search.index.created_at'),
|
||||
i18n.t('search.index.created_by'),
|
||||
i18n.t('search.index.repository'),
|
||||
i18n.t('search.index.team')
|
||||
]"></TableHeader>
|
||||
<div v-for="(row, index) in preparedResults" :key="row.id" class="contents group">
|
||||
<hr class="col-span-6 w-full m-0" v-if="index > 0">
|
||||
<LinkTemplate :url="row.attributes.url" :value="labelName({ name: row.attributes.name, archived: row.attributes.archived})"/>
|
||||
|
@ -22,7 +29,7 @@
|
|||
</div>
|
||||
<div v-if="viewAll">
|
||||
<hr class="w-full mb-4 mt-0">
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'RepositoryRowsComponent')">View all</button>
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'RepositoryRowsComponent')" :data-e2e="'e2e-BT-globalSearch-inventoryItems-viewAll'">View all</button>
|
||||
</div>
|
||||
</template>
|
||||
<Loader v-if="loading" :loaderRows="loaderRows" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }">
|
||||
<div ref="content" class="bg-white rounded" :class="{ 'p-4 mb-4': results.length || loading }" :data-e2e="'e2e-CO-globalSearch-results'">
|
||||
<template v-if="total && results.length">
|
||||
<div class="flex items-center">
|
||||
<h2 class="flex items-center gap-2 mt-0 mb-4">
|
||||
|
@ -7,9 +7,16 @@
|
|||
{{ i18n.t('search.index.task_results') }}
|
||||
<span class="text-base" >[{{ total }}]</span>
|
||||
</h2>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort"></SortFlyout>
|
||||
<SortFlyout v-if="selected" :sort="sort" @changeSort="changeSort" :e2eSortButton="'e2e-BT-globalSearch-results-sort'"></SortFlyout>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_auto_auto_auto_auto_auto] items-center">
|
||||
<TableHeader :selected="selected" :columnNames="[
|
||||
i18n.t('search.index.created_at'),
|
||||
i18n.t('search.index.updated_at'),
|
||||
i18n.t('search.index.task'),
|
||||
i18n.t('search.index.experiment'),
|
||||
i18n.t('search.index.team')
|
||||
]"></TableHeader>
|
||||
<div v-for="(row, index) in preparedResults" :key="row.id" class="contents group">
|
||||
<hr class="col-span-6 w-full m-0" v-if="index > 0">
|
||||
<LinkTemplate :url="row.attributes.url" :value="labelName({ name: row.attributes.name, archived: row.attributes.archived})"/>
|
||||
|
@ -22,7 +29,7 @@
|
|||
</div>
|
||||
<div v-if="viewAll">
|
||||
<hr class="w-full mb-4 mt-0">
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'ResultsComponent')">View all</button>
|
||||
<button class="btn btn-light" @click="$emit('selectGroup', 'ResultsComponent')" :data-e2e="'e2e-BT-globalSearch-results-viewAll'">View all</button>
|
||||
</div>
|
||||
</template>
|
||||
<Loader v-if="loading" :loaderRows="loaderRows" />
|
||||
|
|
|
@ -6,6 +6,7 @@ import ListEnd from './helpers/list_end.vue';
|
|||
import NoSearchResult from './helpers/no_search_result.vue';
|
||||
import CellTemplate from './helpers/cell_template.vue';
|
||||
import LinkTemplate from './helpers/link_template.vue';
|
||||
import TableHeader from './helpers/table_header.vue';
|
||||
/* global GLOBAL_CONSTANTS I18n */
|
||||
|
||||
export default {
|
||||
|
@ -22,7 +23,8 @@ export default {
|
|||
NoSearchResult,
|
||||
ListEnd,
|
||||
CellTemplate,
|
||||
LinkTemplate
|
||||
LinkTemplate,
|
||||
TableHeader
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -86,11 +88,13 @@ export default {
|
|||
handleScroll() {
|
||||
if (this.loading || !this.selected) return;
|
||||
|
||||
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||
if (this.results.length < this.total) {
|
||||
this.loadData();
|
||||
this.$nextTick(() => {
|
||||
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 50) {
|
||||
if (this.results.length < this.total) {
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
changeSort(sort) {
|
||||
this.sort = sort;
|
||||
|
@ -110,8 +114,10 @@ export default {
|
|||
loadData() {
|
||||
if (this.query.length < 2) return;
|
||||
|
||||
if (this.loading && this.page) return;
|
||||
if (this.loading && this.page && !(this.selected && !this.fullDataLoaded)) return;
|
||||
|
||||
const fullView = this.selected;
|
||||
const currentPage = this.page;
|
||||
this.loading = true;
|
||||
axios.get(this.searchUrl, {
|
||||
params: {
|
||||
|
@ -119,21 +125,28 @@ export default {
|
|||
sort: this.sort,
|
||||
filters: this.filters,
|
||||
group: this.group,
|
||||
preview: !this.selected,
|
||||
page: this.page
|
||||
preview: !fullView,
|
||||
page: currentPage
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (this.selected) this.fullDataLoaded = true;
|
||||
this.results = this.results.concat(response.data.data);
|
||||
this.total = response.data.meta.total;
|
||||
this.disabled = response.data.meta.disabled;
|
||||
this.loading = false;
|
||||
this.page = response.data.meta.next_page;
|
||||
|
||||
if (this.selected === fullView && this.page === currentPage) {
|
||||
this.results = this.results.concat(response.data.data);
|
||||
this.total = response.data.meta.total;
|
||||
this.disabled = response.data.meta.disabled;
|
||||
this.loading = false;
|
||||
this.page = response.data.meta.next_page;
|
||||
|
||||
this.handleScroll();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
this.$emit('updated');
|
||||
if (this.selected === fullView) {
|
||||
this.loading = false;
|
||||
this.$emit('updated');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -274,6 +274,7 @@ export default {
|
|||
this.reloadingTable = true;
|
||||
this.editModalObject = null;
|
||||
this.moveModalObject = null;
|
||||
this.updateNavigator(true);
|
||||
},
|
||||
updateNavigator(withExpanedChildren = false) {
|
||||
window.navigatorContainer.reloadNavigator(withExpanedChildren);
|
||||
|
@ -302,12 +303,14 @@ export default {
|
|||
edit(_e, rows) {
|
||||
[this.editModalObject] = rows;
|
||||
},
|
||||
move(_e, rows) {
|
||||
move(event, rows) {
|
||||
[this.moveModalObject] = rows;
|
||||
this.moveModalObject.movePath = event.path;
|
||||
},
|
||||
duplicate(event, rows) {
|
||||
axios.post(event.path, { my_module_ids: rows.map((row) => row.id) }).then(() => {
|
||||
this.reloadingTable = true;
|
||||
this.updateNavigator(true);
|
||||
}).catch((error) => {
|
||||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
|
|
|
@ -52,9 +52,8 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
submit() {
|
||||
axios.post(this.my_module.urls.move, {
|
||||
to_experiment_id: this.targetExperiment,
|
||||
my_module_ids: [this.my_module.id]
|
||||
axios.post(this.my_module.movePath, {
|
||||
to_experiment_id: this.targetExperiment
|
||||
}).then((response) => {
|
||||
this.$emit('move');
|
||||
HelperModule.flashAlertMsg(response.data.message, 'success');
|
||||
|
|
|
@ -105,7 +105,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
validName() {
|
||||
return this.name.length > 0;
|
||||
return this.name.length > 1;
|
||||
},
|
||||
formattedTags() {
|
||||
return this.allTags.map((tag) => (
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<div class="mb-4">
|
||||
<h5>{{ i18n.t("experiments.canvas.modal_manage_tags.project_tags", { project: this.projectName }) }}</h5>
|
||||
</div>
|
||||
<div class="max-h-80 overflow-y-auto" v-click-outside="finishEditMode">
|
||||
<div ref="scrollContainer" class="max-h-80 overflow-y-auto" v-click-outside="finishEditMode">
|
||||
<template v-for="tag in sortedAllTags" :key="tag.id">
|
||||
<div
|
||||
class="flex items-center gap-3 px-3 py-2.5 group"
|
||||
|
@ -203,6 +203,8 @@ export default {
|
|||
startEditMode(tag) {
|
||||
if (!this.canManage) return;
|
||||
|
||||
const scrollPosition = this.$refs.scrollContainer.scrollTop;
|
||||
|
||||
this.finishEditMode();
|
||||
|
||||
tag.initialName = tag.attributes.name;
|
||||
|
@ -210,6 +212,7 @@ export default {
|
|||
this.tagToUpdate = tag;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.modal.querySelector('input').focus();
|
||||
this.$refs.scrollContainer.scrollTop = scrollPosition;
|
||||
});
|
||||
},
|
||||
finishEditMode(e, tag = null) {
|
||||
|
@ -316,6 +319,7 @@ export default {
|
|||
this.creatingTag = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.newTagNameInput.focus();
|
||||
this.setRandomColor();
|
||||
});
|
||||
},
|
||||
cancelCreating(e) {
|
||||
|
@ -323,6 +327,11 @@ export default {
|
|||
|
||||
this.creatingTag = false;
|
||||
this.newTag = { name: null, color: null };
|
||||
},
|
||||
setRandomColor() {
|
||||
if (!this.newTag.color) {
|
||||
this.newTag.color = this.tagsColors[Math.floor(Math.random() * this.tagsColors.length)];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,25 +1,31 @@
|
|||
<template>
|
||||
<GeneralDropdown ref="container" :canOpen="canOpen" :fieldOnlyOpen="true" @close="filtersOpened = false; flyoutOpened = false" @open="flyoutOpened = true">
|
||||
<template v-slot:field>
|
||||
<div class="sci--navigation--top-menu-search left-icon sci-input-container-v2" :class="{'disabled' : !currentTeam}" :title="i18n.t('nav.search')">
|
||||
<input ref="searchField" type="text" class="!pr-20" v-model="searchQuery" @keydown="focusHistoryItem"
|
||||
<div class="sci--navigation--top-menu-search left-icon sci-input-container-v2"
|
||||
:class="{'disabled' : !currentTeam, 'error': invalidQuery}" :title="i18n.t('nav.search')"
|
||||
:data-e2e="'e2e-IF-topMenu-search'">
|
||||
<input ref="searchField" type="text" class="!pr-20" v-model="searchQuery"
|
||||
:class="{'active': flyoutOpened}"
|
||||
@focus="openHistory" :placeholder="i18n.t('nav.search')" @keyup.enter="saveQuery"/>
|
||||
@keydown="focusHistoryItem"
|
||||
@keydown.down="focusQuickSearchResults"
|
||||
@keydown.escape="closeFlyout"
|
||||
@focus="openHistory" :placeholder="i18n.t('nav.search')" @keydown.enter="saveQuery"/>
|
||||
<i class="sn-icon sn-icon-search"></i>
|
||||
<div v-if="this.searchQuery.length > 1" class="flex items-center gap-1 absolute right-2 top-1.5">
|
||||
<div class="btn btn-light icon-btn btn-xs" @click="this.searchQuery = ''; $refs.searchField.focus()">
|
||||
<i class="sn-icon sn-icon-close m-0"></i>
|
||||
</div>
|
||||
<div class="btn btn-light icon-btn btn-xs" :title="i18n.t('search.quick_search.search_options')"
|
||||
:class="{'active': filtersOpened}" @click="filtersOpened = !filtersOpened">
|
||||
<i class="sn-icon sn-icon-search-options m-0"></i>
|
||||
</div>
|
||||
<button class="btn btn-light icon-btn btn-xs" @click="this.searchQuery = ''; $refs.searchField.focus()" :data-e2e="'e2e-BT-topMenu-searchClear'">
|
||||
<i class="sn-icon sn-icon-close !m-0" :title="i18n.t('nav.clear')"></i>
|
||||
</button>
|
||||
<button class="btn btn-light icon-btn btn-xs" :title="i18n.t('search.quick_search.search_options')"
|
||||
:class="{'active': filtersOpened}" @click="filtersOpened = !filtersOpened" :data-e2e="'e2e-BT-topMenu-searchFilters'">
|
||||
<i class="sn-icon sn-icon-search-options !m-0"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:flyout >
|
||||
<SearchFilters
|
||||
class="px-3.5"
|
||||
ref="filters"
|
||||
v-if="filtersOpened"
|
||||
:teamsUrl="teamsUrl"
|
||||
:usersUrl="usersUrl"
|
||||
|
@ -28,39 +34,43 @@
|
|||
:searchQuery="searchQuery"
|
||||
@cancel="filtersOpened = false"
|
||||
></SearchFilters>
|
||||
<div v-else-if="showHistory" class="max-w-[600px]">
|
||||
<div v-else-if="showHistory" class="max-w-[600px]" data-e2e="e2e-DD-topMenu-searchHistory">
|
||||
<div v-for="(query, i) in reversedPreviousQueries" @click="setQuery(query)" :key="i"
|
||||
ref="historyItems"
|
||||
tabindex="1"
|
||||
@keydown="focusHistoryItem"
|
||||
@keydown.enter="setQuery(query)"
|
||||
class="flex px-3 h-11 items-center gap-2 hover:bg-sn-super-light-grey cursor-pointer">
|
||||
class="flex px-3 min-h-11 items-center gap-2 hover:bg-sn-super-light-grey cursor-pointer">
|
||||
<i class="sn-icon sn-icon-history-search"></i>
|
||||
{{ query }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="w-[600px]">
|
||||
<div v-else class="w-[600px]" data-e2e="e2e-FO-topMenu-quickSearch">
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="btn btn-secondary btn-xs"
|
||||
ref="experimentGroup"
|
||||
:class="{'active': quickFilter === 'experiments'}"
|
||||
@click="setQuickFilter('experiments')">
|
||||
@click="setQuickFilter('experiments')"
|
||||
:data-e2e="'e2e-BT-topMenu-quickSearch-experiments'">
|
||||
{{ i18n.t('search.quick_search.experiments') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-xs"
|
||||
:class="{'active': quickFilter === 'my_modules'}"
|
||||
@click="setQuickFilter('my_modules')">
|
||||
@click="setQuickFilter('my_modules')"
|
||||
:data-e2e="'e2e-BT-topMenu-quickSearch-tasks'">
|
||||
{{ i18n.t('search.quick_search.tasks') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-xs"
|
||||
:class="{'active': quickFilter === 'results'}"
|
||||
@click="setQuickFilter('results')">
|
||||
@click="setQuickFilter('results')"
|
||||
:data-e2e="'e2e-BT-topMenu-quickSearch-taskResults'">
|
||||
{{ i18n.t('search.quick_search.results') }}
|
||||
</button>
|
||||
</div>
|
||||
<hr class="my-2">
|
||||
<a v-if="!loading" v-for="(result, i) in results" :key="i"
|
||||
:href="getUrl(result.attributes)"
|
||||
class="px-3 py-2 hover:bg-sn-super-light-grey cursor-pointer
|
||||
class="px-3 py-2 hover:bg-sn-super-light-grey cursor-pointer focus:no-underline
|
||||
text-sn-black hover:no-underline active:no-underline hover:text-black block"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
|
@ -103,15 +113,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<hr class="my-2">
|
||||
<div class="btn btn-light" @click="searchValue">
|
||||
<button class="btn btn-light truncate !block leading-10 max-w-[600px]" @click="searchValue" :data-e2e="'e2e-BT-topMenu-quickSearch-allSearchResults'">
|
||||
{{ i18n.t('search.quick_search.all_results', {query: searchQuery}) }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</GeneralDropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global HelperModule GLOBAL_CONSTANTS */
|
||||
|
||||
import GeneralDropdown from '../shared/general_dropdown.vue';
|
||||
import StringWithEllipsis from '../shared/string_with_ellipsis.vue';
|
||||
import SearchFilters from '../global_search/filters.vue';
|
||||
|
@ -157,6 +169,9 @@ export default {
|
|||
},
|
||||
currentTeamName() {
|
||||
return document.querySelector('body').dataset.currentTeamName;
|
||||
},
|
||||
invalidQuery() {
|
||||
return this.searchQuery.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -166,6 +181,9 @@ export default {
|
|||
if (this.searchQuery.length > 1) {
|
||||
this.fetchQuickSearchResults();
|
||||
}
|
||||
},
|
||||
filtersOpened() {
|
||||
if (this.filtersOpened) this.$nextTick(() => { this.focusFilters() });
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -282,7 +300,7 @@ export default {
|
|||
this.fetchQuickSearchResults();
|
||||
},
|
||||
fetchQuickSearchResults() {
|
||||
if (this.loading) return;
|
||||
if (this.loading || this.invalidQuery) return;
|
||||
|
||||
this.loading = true;
|
||||
|
||||
|
@ -305,27 +323,46 @@ export default {
|
|||
});
|
||||
},
|
||||
searchValue() {
|
||||
window.open(`${this.searchUrl}?q=${this.searchQuery}&teams[]=${this.currentTeam}`, '_self');
|
||||
if (this.searchQuery.length > GLOBAL_CONSTANTS.NAME_MAX_LENGTH) {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('general.query.length_too_long', { max_length: GLOBAL_CONSTANTS.NAME_MAX_LENGTH }), 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
window.open(`${this.searchUrl}?q=${this.searchQuery}&teams[]=${this.currentTeam}&include_archived=true`, '_self');
|
||||
},
|
||||
focusHistoryItem(event) {
|
||||
if (this.focusedHistoryItem === null && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
|
||||
this.focusedHistoryItem = 0;
|
||||
this.$refs.historyItems[this.focusedHistoryItem].focus();
|
||||
this.$refs.historyItems[this.focusedHistoryItem]?.focus();
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
this.focusedHistoryItem += 1;
|
||||
if (this.focusedHistoryItem >= this.$refs.historyItems.length) {
|
||||
this.focusedHistoryItem = 0;
|
||||
}
|
||||
this.$refs.historyItems[this.focusedHistoryItem].focus();
|
||||
this.$refs.historyItems[this.focusedHistoryItem]?.focus();
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
this.focusedHistoryItem -= 1;
|
||||
if (this.focusedHistoryItem < 0) {
|
||||
this.focusedHistoryItem = this.$refs.historyItems.length - 1;
|
||||
}
|
||||
this.$refs.historyItems[this.focusedHistoryItem].focus();
|
||||
this.$refs.historyItems[this.focusedHistoryItem]?.focus();
|
||||
}
|
||||
},
|
||||
focusQuickSearchResults(e) {
|
||||
this.$refs.experimentGroup?.focus();
|
||||
e.preventDefault();
|
||||
},
|
||||
focusFilters() {
|
||||
const { filters } = this.$refs;
|
||||
|
||||
if (filters) {
|
||||
filters.$refs.groupButtons[0].focus();
|
||||
}
|
||||
},
|
||||
closeFlyout() {
|
||||
this.$refs.container.isOpen = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
</div>
|
||||
<QuickSearch
|
||||
v-if="user"
|
||||
:key="globalSearchKey"
|
||||
:class="{'hidden': hideSearch}"
|
||||
:quickSearchUrl="quickSearchUrl"
|
||||
:searchUrl="searchUrl"
|
||||
|
@ -109,6 +110,7 @@ export default {
|
|||
userMenu: null,
|
||||
unseenNotificationsCount: 0,
|
||||
hideSearch: false,
|
||||
globalSearchKey: 0
|
||||
};
|
||||
},
|
||||
created() {
|
||||
|
@ -120,6 +122,7 @@ export default {
|
|||
this.checkUnseenNotifications();
|
||||
this.refreshCurrentTeam();
|
||||
this.hideSearch = !!document.getElementById('GlobalSearch');
|
||||
this.globalSearchKey += 1;
|
||||
});
|
||||
|
||||
// Track name update in user profile settings
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
:confirmText="i18n.t('projects.export_projects.export_button')"
|
||||
ref="exportModal"
|
||||
></ConfirmationModal>
|
||||
<ExportLimitExceededModal v-if="exportLimitExceded" :description="exportDescription" @close="exportLimitExceded = false"/>
|
||||
<EditProjectModal v-if="editProject" :userRolesUrl="userRolesUrl"
|
||||
:project="editProject" @close="editProject = null" @update="updateTable(); updateNavigator()" />
|
||||
<EditFolderModal v-if="editFolder" :folder="editFolder"
|
||||
|
@ -76,6 +77,7 @@ import NewProjectModal from './modals/new.vue';
|
|||
import NewFolderModal from './modals/new_folder.vue';
|
||||
import MoveModal from './modals/move.vue';
|
||||
import AccessModal from '../shared/access_modal/modal.vue';
|
||||
import ExportLimitExceededModal from './modals/export_limit_exceeded_modal.vue';
|
||||
|
||||
export default {
|
||||
name: 'ProjectsList',
|
||||
|
@ -89,7 +91,8 @@ export default {
|
|||
NewProjectModal,
|
||||
NewFolderModal,
|
||||
MoveModal,
|
||||
AccessModal
|
||||
AccessModal,
|
||||
ExportLimitExceededModal
|
||||
},
|
||||
props: {
|
||||
dataSource: { type: String, required: true },
|
||||
|
@ -114,6 +117,7 @@ export default {
|
|||
editFolder: null,
|
||||
objectsToMove: null,
|
||||
reloadingTable: false,
|
||||
exportLimitExceded: false,
|
||||
folderDeleteDescription: '',
|
||||
exportDescription: ''
|
||||
};
|
||||
|
@ -306,6 +310,7 @@ export default {
|
|||
this.newFolder = false;
|
||||
this.objectsToMove = null;
|
||||
this.reloadingTable = true;
|
||||
this.exportLimitExceded = false;
|
||||
},
|
||||
updateNavigator(withExpanedChildren = false) {
|
||||
window.navigatorContainer.reloadNavigator(withExpanedChildren);
|
||||
|
@ -328,7 +333,7 @@ export default {
|
|||
async exportProjects(event, rows) {
|
||||
if (event.number_of_projects === 0) {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('projects.export_projects.zero_projects_flash'), 'danger');
|
||||
} else {
|
||||
} else if (event.number_of_request_left > 0) {
|
||||
this.exportDescription = event.message;
|
||||
const ok = await this.$refs.exportModal.show();
|
||||
if (ok) {
|
||||
|
@ -342,6 +347,9 @@ export default {
|
|||
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.exportDescription = event.message;
|
||||
this.exportLimitExceded = true;
|
||||
}
|
||||
},
|
||||
move(event, rows) {
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block" id="edit-project-modal-label">
|
||||
{{ i18n.t('repositories.index.modal_export_limit_exceeded.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body" v-html="description"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import modalMixin from '../../shared/modal_mixin';
|
||||
|
||||
export default {
|
||||
name: 'ExportLimitExceededModal',
|
||||
props: {
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [modalMixin]
|
||||
};
|
||||
</script>
|
|
@ -2,28 +2,30 @@
|
|||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form @submit.prevent="submit">
|
||||
<div class="modal-content">
|
||||
<div class="modal-content" data-e2e="e2e-MD-renameInventory">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" data-e2e="e2e-BT-renameInventoryModal-close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block" id="edit-project-modal-label" :title="repository.name">
|
||||
<h4 class="modal-title truncate !block" id="edit-project-modal-label" :title="repository.name" data-e2e="e2e-TX-renameInventoryModal-title">
|
||||
{{ i18n.t('repositories.index.modal_rename.title_html', {name: repository.name }) }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-6">
|
||||
<label class="sci-label">{{ i18n.t("repositories.index.modal_rename.name") }}</label>
|
||||
<label class="sci-label" data-e2e="e2e-TX-renameInventoryModal-inputLabel">{{ i18n.t("repositories.index.modal_rename.name") }}</label>
|
||||
<div class="sci-input-container-v2" :class="{'error': error}" :data-error="error">
|
||||
<input type="text" v-model="name" class="sci-input-field"
|
||||
<input type="text" v-model="name"
|
||||
class="sci-input-field"
|
||||
autofocus="true" ref="input"
|
||||
data-e2e="e2e-IF-renameInventoryModal-name"
|
||||
:placeholder="i18n.t('repositories.index.modal_rename.name_placeholder')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" data-e2e="e2e-BT-renameInventoryModal-cancel">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" type="submit" data-e2e="e2e-BT-renameInventoryModal-save">
|
||||
{{ i18n.t('repositories.index.modal_rename.rename') }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
confirmClass="btn btn-danger"
|
||||
:confirmText="i18n.t('repositories.index.modal_delete.delete')"
|
||||
ref="deleteModal"
|
||||
:e2eAttributes="deleteModal.e2eAttributes"
|
||||
></ConfirmationModal>
|
||||
<ExportRepositoryModal
|
||||
v-if="exportRepository"
|
||||
|
@ -114,6 +115,17 @@ export default {
|
|||
shareRepository: null,
|
||||
exportAction: null,
|
||||
deleteModal: {
|
||||
title: '',
|
||||
description: '',
|
||||
e2eAttributes: {
|
||||
modalName: '',
|
||||
title: '',
|
||||
close: '',
|
||||
cancel: '',
|
||||
confirm: ''
|
||||
}
|
||||
},
|
||||
exportModal: {
|
||||
title: '',
|
||||
description: ''
|
||||
}
|
||||
|
@ -220,10 +232,17 @@ export default {
|
|||
},
|
||||
async deleteRepository(event, rows) {
|
||||
const [repository] = rows;
|
||||
this.deleteModal.e2eAttributes = {
|
||||
modalName: 'e2e-MD-deleteInventory',
|
||||
title: 'e2e-TX-deleteInventoryModal-title',
|
||||
close: 'e2e-BT-deleteInventoryModal-close',
|
||||
cancel: 'e2e-BT-deleteInventoryModal-cancel',
|
||||
confirm: 'e2e-BT-deleteInventoryModal-delete'
|
||||
};
|
||||
this.deleteModal.title = this.i18n.t('repositories.index.modal_delete.title_html', { name: repository.name });
|
||||
this.deleteModal.description = `
|
||||
<p>${this.i18n.t('repositories.index.modal_delete.message_html', { name: repository.name })}</p>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<p data-e2e="e2e-TX-deleteInventoryModal-info">${this.i18n.t('repositories.index.modal_delete.message_html', { name: repository.name })}</p>
|
||||
<div class="alert alert-danger" role="alert" data-e2e="e2e-TX-deleteInventoryModal-warning">
|
||||
<span class="fas fa-exclamation-triangle"></span>
|
||||
${this.i18n.t('repositories.index.modal_delete.alert_heading')}
|
||||
<ul>
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
type="text"
|
||||
name="from"
|
||||
v-model="from"
|
||||
:data-e2e="`e2e-IF-invInventoryFilterCO-inputFrom${this.filter.column.id}`"
|
||||
:placeholder= "this.i18n.t('repositories.show.repository_filter.filters.types.RepositoryStockValue.from_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
@ -37,6 +38,7 @@
|
|||
type="text"
|
||||
name="to"
|
||||
v-model="to"
|
||||
:data-e2e="`e2e-IF-invInventoryFilterCO-inputTo${this.filter.column.id}`"
|
||||
:placeholder= "this.i18n.t('repositories.show.repository_filter.filters.types.RepositoryStockValue.to_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
@ -48,7 +50,7 @@
|
|||
:selectedValue="this.stock_unit"
|
||||
:options="this.prepareUnitOptions()"
|
||||
:selectorId="`StockUnitSelector${this.filter.id}`"
|
||||
:data-e2e="`e2e-DD-invInventoryFilterCO-input${this.filter.column.id}`"
|
||||
:data-e2e="`e2e-DD-invInventoryFilterCO-unit${this.filter.column.id}`"
|
||||
@dropdown:changed="updateStockUnit"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -460,6 +460,8 @@ export default {
|
|||
},
|
||||
created() {
|
||||
window.repositoryItemSidebarComponent = this;
|
||||
|
||||
this.handleItemLandingPageLink();
|
||||
},
|
||||
computed: {
|
||||
repositoryRowName() {
|
||||
|
@ -686,6 +688,12 @@ export default {
|
|||
} finally {
|
||||
this.selectedToUnlink = null;
|
||||
}
|
||||
},
|
||||
handleItemLandingPageLink() {
|
||||
const itemLandingPageLink = document.getElementById('itemLandingPagelink');
|
||||
if (itemLandingPageLink) {
|
||||
this.toggleShowHideSidebar(itemLandingPageLink.getAttribute('href'));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -255,6 +255,7 @@ export default {
|
|||
});
|
||||
},
|
||||
initZebraPrinter() {
|
||||
this.printers = this.printers.filter((printer) => !printer.id.startsWith('zebra'));
|
||||
this.zebraPrinters = zebraPrint.init($('#LabelPrinterSelector'), {
|
||||
clearSelectorOnFirstDevice: false,
|
||||
appendDevice: (device) => {
|
||||
|
|
|
@ -1,76 +1,80 @@
|
|||
<template>
|
||||
<div ref="modal" @keydown.esc="cancel" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-dialog" role="document" :data-e2e="e2eAttributes.modalName">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" :data-e2e="e2eAttributes.close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
<h4 class="modal-title" :data-e2e="e2eAttributes.title">
|
||||
{{ title }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body" v-html="description"></div>
|
||||
<div class="modal-footer">
|
||||
<button :class="cancelClass" @click="cancel">{{ cancelText || i18n.t('general.cancel') }}</button>
|
||||
<button :class="confirmClass" @click="confirm">{{ confirmText || i18n.t('general.confirm') }}</button>
|
||||
<button :class="cancelClass" @click="cancel" :data-e2e="e2eAttributes.cancel">{{ cancelText || i18n.t('general.cancel') }}</button>
|
||||
<button :class="confirmClass" @click="confirm" :data-e2e="e2eAttributes.confirm">{{ confirmText || i18n.t('general.confirm') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'confirmationModal',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
cancelText: {
|
||||
type: String
|
||||
},
|
||||
cancelClass: {
|
||||
type: String,
|
||||
default: 'btn btn-secondary'
|
||||
},
|
||||
confirmText: {
|
||||
type: String
|
||||
},
|
||||
confirmClass: {
|
||||
type: String,
|
||||
default: 'btn btn-primary'
|
||||
}
|
||||
<script>
|
||||
export default {
|
||||
name: 'confirmationModal',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
mounted() {
|
||||
$(this.$refs.modal).on('hidden.bs.modal', () => {
|
||||
this.resolvePromise(false)
|
||||
})
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
resolvePromise: null,
|
||||
rejectPromise: null
|
||||
}
|
||||
cancelText: {
|
||||
type: String
|
||||
},
|
||||
methods: {
|
||||
show: function() {
|
||||
$(this.$refs.modal).modal('show');
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolvePromise = resolve
|
||||
this.rejectPromise = reject
|
||||
})
|
||||
},
|
||||
confirm() {
|
||||
this.resolvePromise(true)
|
||||
$(this.$refs.modal).modal('hide');
|
||||
},
|
||||
cancel() {
|
||||
this.resolvePromise(false)
|
||||
$(this.$refs.modal).modal('hide');
|
||||
}
|
||||
cancelClass: {
|
||||
type: String,
|
||||
default: 'btn btn-secondary'
|
||||
},
|
||||
confirmText: {
|
||||
type: String
|
||||
},
|
||||
confirmClass: {
|
||||
type: String,
|
||||
default: 'btn btn-primary'
|
||||
},
|
||||
e2eAttributes: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
$(this.$refs.modal).on('hidden.bs.modal', () => {
|
||||
this.resolvePromise(false);
|
||||
});
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
resolvePromise: null,
|
||||
rejectPromise: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
$(this.$refs.modal).modal('show');
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
},
|
||||
confirm() {
|
||||
this.resolvePromise(true);
|
||||
$(this.$refs.modal).modal('hide');
|
||||
},
|
||||
cancel() {
|
||||
this.resolvePromise(false);
|
||||
$(this.$refs.modal).modal('hide');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -44,8 +44,7 @@
|
|||
<Teleport to="body">
|
||||
<RenameAttachmentModal
|
||||
v-if="renameModal"
|
||||
:url_path="attachment.attributes.urls.rename"
|
||||
:fileName="attachment.attributes.file_name"
|
||||
:attachment="attachment"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@close="renameModal = false"
|
||||
/>
|
||||
|
|
|
@ -30,41 +30,14 @@
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-attachment-action-buttons">
|
||||
<!-- open -->
|
||||
<OpenMenu
|
||||
:attachment="attachment"
|
||||
:multipleOpenOptions="multipleOpenOptions"
|
||||
@menu-dropdown-toggle="toggleMenuDropdown"
|
||||
>
|
||||
</OpenMenu>
|
||||
|
||||
<!-- move -->
|
||||
<a v-if="attachment.attributes.urls.move"
|
||||
@click.prevent.stop="showMoveModal"
|
||||
class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.move')">
|
||||
<i class="sn-icon sn-icon-move"></i>
|
||||
</a>
|
||||
|
||||
<!-- download -->
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.download')"
|
||||
:href="attachment.attributes.urls.download" data-turbolinks="false">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
</a>
|
||||
|
||||
<!-- more options -->
|
||||
<ContextMenu
|
||||
:attachment="attachment"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@menu-toggle="toggleContextMenu"
|
||||
/>
|
||||
</div>
|
||||
<ContextMenu
|
||||
:attachment="attachment"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="attachment.attributes.wopi">
|
||||
<div v-if="showWopi"
|
||||
|
|
|
@ -1,73 +1,42 @@
|
|||
<template>
|
||||
<div class="list-attachment-container asset hover:bg-sn-super-light-grey"
|
||||
:class="[{'menu-dropdown-open': isMenuDropdownOpen}, {'context-menu-open': isContextMenuOpen }]"
|
||||
:data-asset-id="attachment.id">
|
||||
<div id="icon-with-filename" class="h-6 my-auto">
|
||||
<i class="text-sn-grey asset-icon sn-icon mb-1" :class="attachment.attributes.icon"></i>
|
||||
<a :href="attachment.attributes.urls.blob"
|
||||
class="file-preview-link file-name"
|
||||
:id="`modal_link${attachment.id}`"
|
||||
data-no-turbolink="true"
|
||||
:data-id="attachment.id"
|
||||
:data-gallery-view-id="parentId"
|
||||
:data-preview-url="attachment.attributes.urls.preview"
|
||||
>
|
||||
<span class="attachment-name" data-toggle="tooltip"
|
||||
data-placement="bottom">
|
||||
{{ attachment.attributes.file_name }}
|
||||
</span>
|
||||
</a>
|
||||
<div v-if="attachment.attributes.medium_preview !== null" class="attachment-image-tooltip bg-white sn-shadow-menu-sm">
|
||||
<img :src="this.imageLoadError ? attachment.attributes.urls.blob : attachment.attributes.medium_preview" @error="ActiveStoragePreviews.reCheckPreview"
|
||||
@load="ActiveStoragePreviews.showPreview"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="file-metadata" class="file-metadata">
|
||||
<span class="my-auto">
|
||||
{{ attachment.attributes.updated_at_formatted }}
|
||||
</span>
|
||||
<span class="my-auto">
|
||||
{{ attachment.attributes.file_size_formatted }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row" id="action-buttons">
|
||||
|
||||
<!-- open -->
|
||||
<OpenMenu
|
||||
:attachment="attachment"
|
||||
:multipleOpenOptions="multipleOpenOptions"
|
||||
@menu-dropdown-toggle="toggleMenuDropdown"
|
||||
>
|
||||
</OpenMenu>
|
||||
|
||||
<!-- move -->
|
||||
<a v-if="attachment.attributes.urls.move"
|
||||
@click.prevent.stop="showMoveModal"
|
||||
class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.move')">
|
||||
<i class="sn-icon sn-icon-move"></i>
|
||||
</a>
|
||||
|
||||
<!-- download -->
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.download')"
|
||||
:href="attachment.attributes.urls.download" data-turbolinks="false">
|
||||
<i class="sn-icon sn-icon-export"></i>
|
||||
</a>
|
||||
|
||||
<!-- more options -->
|
||||
<ContextMenu
|
||||
:attachment="attachment"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@menu-toggle="toggleContextMenu"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="list-attachment-container asset"
|
||||
:data-asset-id="attachment.id"
|
||||
>
|
||||
<i class="text-sn-grey asset-icon sn-icon" :class="attachment.attributes.icon"></i>
|
||||
<a :href="attachment.attributes.urls.blob"
|
||||
class="file-preview-link file-name"
|
||||
:id="`modal_link${attachment.id}`"
|
||||
data-no-turbolink="true"
|
||||
:data-id="attachment.id"
|
||||
:data-gallery-view-id="parentId"
|
||||
:data-preview-url="attachment.attributes.urls.preview"
|
||||
>
|
||||
<span class="attachment-name" data-toggle="tooltip"
|
||||
data-placement="bottom">
|
||||
{{ attachment.attributes.file_name }}
|
||||
</span>
|
||||
</a>
|
||||
<div v-if="attachment.attributes.medium_preview !== null" class="attachment-image-tooltip bg-white sn-shadow-menu-sm">
|
||||
<img :src="this.imageLoadError ? attachment.attributes.urls.blob : attachment.attributes.medium_preview" @error="ActiveStoragePreviews.reCheckPreview"
|
||||
@load="ActiveStoragePreviews.showPreview"/>
|
||||
</div>
|
||||
<div class="file-metadata">
|
||||
<span>
|
||||
{{ i18n.t('assets.placeholder.modified_label') }}
|
||||
{{ attachment.attributes.updated_at_formatted }}
|
||||
</span>
|
||||
<span>
|
||||
{{ i18n.t('assets.placeholder.size_label', {size: attachment.attributes.file_size_formatted}) }}
|
||||
</span>
|
||||
</div>
|
||||
<ContextMenu
|
||||
:attachment="attachment"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
/>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<moveAssetModal
|
||||
|
|
|
@ -126,6 +126,18 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ContextMenu
|
||||
v-show="showOptions"
|
||||
:attachment="attachment"
|
||||
@attachment:viewMode="updateViewMode"
|
||||
@attachment:delete="deleteAttachment"
|
||||
@attachment:moved="attachmentMoved"
|
||||
@attachment:uploaded="reloadAttachments"
|
||||
@attachment:changed="$emit('attachment:changed', $event)"
|
||||
@attachment:update="$emit('attachment:update', $event)"
|
||||
@menu-visibility-changed="handleMenuVisibilityChange"
|
||||
:withBorder="true"
|
||||
/>
|
||||
<Teleport to="body">
|
||||
<deleteAttachmentModal
|
||||
v-if="deleteModal"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div ref="modal" @keydown.esc="close" class="modal" id="renameAttachmentModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-md" role="document">
|
||||
<div class="modal-dialog modal-sm" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
|
||||
|
@ -9,9 +9,14 @@
|
|||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ i18n.t('assets.from_clipboard.file_name')}}</p>
|
||||
<div class="sci-input-container" :class="{ 'error': error }" :data-error-text="error">
|
||||
<input ref="input" v-model="name" type="text" class="sci-input-field" @keyup.enter="renameAttachment(name)" required="true" />
|
||||
<label class="sci-label">
|
||||
{{ i18n.t('assets.from_clipboard.file_name')}}
|
||||
</label>
|
||||
<div class="flex gap-1 items-center">
|
||||
<div class="sci-input-container" :class="{ 'error': error }" :data-error-text="error">
|
||||
<input ref="input" v-model="name" type="text" class="sci-input-field" @keyup.enter="renameAttachment(name)" required="true" />
|
||||
</div>
|
||||
<div v-if="this.attachment.attributes.file_extension" class="shrink-0">.{{ this.attachment.attributes.file_extension }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
@ -31,14 +36,10 @@ export default {
|
|||
name: 'RenameAttachmentModal',
|
||||
mixins: [modalMixin],
|
||||
props: {
|
||||
url_path: {
|
||||
type: String,
|
||||
attachment: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -47,7 +48,7 @@ export default {
|
|||
};
|
||||
},
|
||||
created() {
|
||||
this.name = this.fileName;
|
||||
this.name = this.attachment.attributes.file_name_without_extension;
|
||||
},
|
||||
watch: {
|
||||
name() {
|
||||
|
@ -71,10 +72,17 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
const payload = { asset: { name: newName } };
|
||||
let fileName = '';
|
||||
|
||||
if (this.attachment.attributes.file_extension) {
|
||||
fileName = `${newName}.${this.attachment.attributes.file_extension}`;
|
||||
} else {
|
||||
fileName = newName;
|
||||
}
|
||||
|
||||
const payload = { asset: { name: fileName } };
|
||||
try {
|
||||
const response = await axios.patch(this.url_path, payload);
|
||||
const response = await axios.patch(this.attachment.attributes.urls.rename, payload);
|
||||
this.$emit('attachment:update', response.data.data);
|
||||
this.close();
|
||||
} catch (error) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="inline" v-if="listItems.length > 0 || alwaysShow" v-click-outside="closeMenu" >
|
||||
<button ref="field" :class="btnClasses" :title="title" @click="isOpen = !isOpen">
|
||||
<div class="relative" v-if="listItems.length > 0 || alwaysShow" v-click-outside="closeMenu" >
|
||||
<button ref="field" :class="btnClasses" :title="title" @click="isOpen = !isOpen" :data-e2e="e2eSortButton">
|
||||
<i v-if="btnIcon" :class="btnIcon"></i>
|
||||
{{ btnText }}
|
||||
<i v-if="caret && isOpen" class="sn-icon sn-icon-up"></i>
|
||||
|
@ -79,7 +79,8 @@ export default {
|
|||
btnIcon: { type: String, required: false },
|
||||
caret: { type: Boolean, default: false },
|
||||
alwaysShow: { type: Boolean, default: false },
|
||||
title: { type: String, default: '' }
|
||||
title: { type: String, default: '' },
|
||||
e2eSortButton: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
<div v-click-outside="close"
|
||||
@focus="open"
|
||||
@keydown="keySelectOptions($event)"
|
||||
tabindex="0" class="w-full focus:outline-none "
|
||||
tabindex="0" class="w-full focus:outline-none"
|
||||
:data-e2e="e2eValue"
|
||||
>
|
||||
<div
|
||||
ref="field"
|
||||
|
@ -26,8 +27,9 @@
|
|||
ref="search"
|
||||
v-else
|
||||
v-model="query"
|
||||
:placeholder="placeholderRender"
|
||||
@keyup="fetchOptions"
|
||||
:placeholder="label || placeholder || this.i18n.t('general.select_dropdown.placeholder')"
|
||||
@change.stop
|
||||
class="w-full border-0 outline-none pl-0 placeholder:text-sn-grey" />
|
||||
</template>
|
||||
<div v-else class="flex items-center gap-1 flex-wrap">
|
||||
|
@ -43,6 +45,7 @@
|
|||
:placeholder="tags.length > 0 ? '' : (placeholder || this.i18n.t('general.select_dropdown.placeholder'))"
|
||||
:style="{ width: searchInputSize }"
|
||||
:class="{ 'pl-2': tags.length > 0 }"
|
||||
@change.stop
|
||||
class="border-0 outline-none pl-0 py-1 placeholder:text-sn-grey" />
|
||||
<div v-else-if="tags.length == 0" class="text-sn-grey truncate">
|
||||
{{ placeholder || this.i18n.t('general.select_dropdown.placeholder') }}
|
||||
|
@ -125,7 +128,8 @@ export default {
|
|||
searchable: { type: Boolean, default: false },
|
||||
clearable: { type: Boolean, default: false },
|
||||
tagsView: { type: Boolean, default: false },
|
||||
urlParams: { type: Object, default: () => ({}) }
|
||||
urlParams: { type: Object, default: () => ({}) },
|
||||
e2eValue: { type: String, default: '' }
|
||||
},
|
||||
directives: {
|
||||
'click-outside': vOnClickOutside
|
||||
|
@ -144,6 +148,13 @@ export default {
|
|||
},
|
||||
mixins: [FixedFlyoutMixin],
|
||||
computed: {
|
||||
placeholderRender() {
|
||||
if (this.searchable && this.labelRenderer && this.label) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.label || this.placeholder || this.i18n.t('general.select_dropdown.placeholder');
|
||||
},
|
||||
sizeClass() {
|
||||
switch (this.size) {
|
||||
case 'xs':
|
||||
|
@ -301,8 +312,6 @@ export default {
|
|||
});
|
||||
},
|
||||
setValue(value) {
|
||||
this.query = '';
|
||||
|
||||
if (this.multiple) {
|
||||
if (this.newValue.includes(value)) {
|
||||
this.newValue = this.newValue.filter((v) => v !== value);
|
||||
|
|
|
@ -73,9 +73,10 @@ class Asset < ApplicationRecord
|
|||
.where(results: { id: Result.search(user, include_archived, nil, teams) })
|
||||
.pluck(:id)
|
||||
|
||||
assets_in_inventories = Asset.joins(
|
||||
repository_cell: { repository_column: :repository }
|
||||
).where(repositories: { team: teams }).pluck(:id)
|
||||
assets_in_inventories = Asset.joins(repository_cell: { repository_column: :repository })
|
||||
.where(repositories: { team: teams })
|
||||
.where.not(repositories: { type: 'RepositorySnapshot' })
|
||||
.pluck(:id)
|
||||
|
||||
assets = distinct.where('assets.id IN (?) OR assets.id IN (?) OR assets.id IN (?)',
|
||||
assets_in_steps, assets_in_results, assets_in_inventories)
|
||||
|
|
|
@ -10,7 +10,7 @@ class AssetSyncToken < ApplicationRecord
|
|||
validates :token, uniqueness: true, presence: true
|
||||
|
||||
def version_token
|
||||
asset.file.checksum
|
||||
OpenSSL::Digest::SHA256.base64digest(asset.file.checksum + asset.file_name)
|
||||
end
|
||||
|
||||
def token_valid?
|
||||
|
|
|
@ -36,6 +36,20 @@ module SearchableByNameModel
|
|||
|
||||
sql_q.limit(options[:limit] || Constants::SEARCH_LIMIT)
|
||||
end
|
||||
|
||||
def self.search_by_search_fields_with_boolean(user, teams = [], query = nil, search_fields = [], options = {})
|
||||
return if user.blank? || teams.blank?
|
||||
|
||||
sanitized_query = ActiveRecord::Base.sanitize_sql_like(query.to_s)
|
||||
sql_q = if options[:fetch_latest_versions]
|
||||
viewable_by_user(user, teams, options)
|
||||
.where_attributes_like_boolean(search_fields, sanitized_query, options)
|
||||
else
|
||||
viewable_by_user(user, teams).where_attributes_like_boolean(search_fields, sanitized_query, options)
|
||||
end
|
||||
|
||||
sql_q.limit(options[:limit] || Constants::SEARCH_LIMIT)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/BlockLength
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
module SearchableModel
|
||||
extend ActiveSupport::Concern
|
||||
DATA_VECTOR_ATTRIBUTES = ['asset_text_data.data_vector', 'tables.data_vector'].freeze
|
||||
|
||||
included do
|
||||
# Helper function for relations that
|
||||
|
@ -102,48 +103,32 @@ module SearchableModel
|
|||
scope :where_attributes_like_boolean, lambda { |attributes, query, options = {}|
|
||||
return unless query
|
||||
|
||||
attrs = normalized_attributes(attributes)
|
||||
where_array = []
|
||||
value_array = {}
|
||||
current_phrase = ''
|
||||
exact_match = false
|
||||
negate = false
|
||||
index = 0
|
||||
normalized_attrs = normalized_attributes(attributes)
|
||||
query_clauses = []
|
||||
value_hash = {}
|
||||
|
||||
query.split.each do |phrase|
|
||||
phrase = phrase.strip
|
||||
if phrase.start_with?('"') && phrase.ends_with?('"')
|
||||
create_query(attrs, index, negate, where_array, value_array, phrase[1..-2], true)
|
||||
negate = false
|
||||
elsif phrase.start_with?('"')
|
||||
exact_match = true
|
||||
current_phrase = phrase[1..]
|
||||
elsif exact_match && phrase.ends_with?('"')
|
||||
exact_match = false
|
||||
create_query(attrs, index, negate, where_array, value_array, "#{current_phrase} #{phrase[0..-2]}", true)
|
||||
current_phrase = ''
|
||||
negate = false
|
||||
elsif exact_match
|
||||
current_phrase = "#{current_phrase} #{phrase}"
|
||||
elsif phrase.casecmp('and').zero?
|
||||
next
|
||||
elsif phrase.casecmp('not').zero?
|
||||
negate = true
|
||||
elsif phrase.casecmp('or').zero?
|
||||
where_array[-1] = "#{where_array.last[0..-5]} OR "
|
||||
extract_phrases(query).each_with_index do |phrase, index|
|
||||
if options[:with_subquery]
|
||||
subquery_result = if phrase[:negate]
|
||||
options[:raw_input].where.not(id: search_subquery(phrase[:query], options[:raw_input]))
|
||||
else
|
||||
options[:raw_input].where(id: search_subquery(phrase[:query], options[:raw_input]))
|
||||
end
|
||||
query_clauses = if index.zero?
|
||||
where(id: subquery_result)
|
||||
elsif phrase[:current_operator] == 'or'
|
||||
query_clauses.or(subquery_result)
|
||||
else
|
||||
query_clauses.and(subquery_result)
|
||||
end
|
||||
else
|
||||
create_query(attrs, index, negate, where_array, value_array, "%#{phrase}%")
|
||||
negate = false
|
||||
phrase[:current_operator] = '' if index.zero?
|
||||
create_query_clause(normalized_attrs, index, phrase[:negate], query_clauses,
|
||||
value_hash, phrase[:query], phrase[:current_operator])
|
||||
end
|
||||
index += 1
|
||||
end
|
||||
|
||||
if current_phrase.present?
|
||||
current_phrase = current_phrase[0..-2] if current_phrase.ends_with?('"')
|
||||
create_query(attrs, index, negate, where_array, value_array, current_phrase, true)
|
||||
end
|
||||
|
||||
where(where_array.join[0..-5], value_array)
|
||||
options[:with_subquery] ? query_clauses : where(query_clauses.join, value_hash)
|
||||
}
|
||||
|
||||
def self.normalized_attributes(attributes)
|
||||
|
@ -163,35 +148,76 @@ module SearchableModel
|
|||
attrs
|
||||
end
|
||||
|
||||
def self.create_query(attrs, index, negate, where_array, value_array, phrase, exact_match=false)
|
||||
like = exact_match ? '~' : 'ILIKE'
|
||||
phrase = "\\m#{phrase}\\M" if exact_match
|
||||
def self.extract_phrases(query)
|
||||
extracted_phrases = []
|
||||
negate = false
|
||||
current_operator = ''
|
||||
|
||||
where_clause = (attrs.map.with_index do |a, i|
|
||||
i = (index * attrs.count) + i
|
||||
if %w(repository_rows.id repository_number_values.data).include?(a)
|
||||
"#{a} IS NOT NULL AND (((#{a})::text) #{like} :t#{i}) OR "
|
||||
elsif defined?(model::PREFIXED_ID_SQL) && a == model::PREFIXED_ID_SQL
|
||||
"#{a} IS NOT NULL AND (#{a} #{like} :t#{i}) OR "
|
||||
elsif a == 'asset_text_data.data_vector'
|
||||
"asset_text_data.data_vector @@ plainto_tsquery(:t#{i})) OR"
|
||||
query.scan(/"[^"]+"|\S+/) do |phrase|
|
||||
phrase = phrase.to_s.strip
|
||||
|
||||
case phrase.downcase
|
||||
when *%w(and or)
|
||||
current_operator = phrase.downcase
|
||||
when 'not'
|
||||
negate = true
|
||||
else
|
||||
"#{a} IS NOT NULL AND ((trim_html_tags(#{a})) #{like} :t#{i}) OR "
|
||||
extracted_phrases << { query: phrase,
|
||||
negate: negate,
|
||||
current_operator: current_operator.presence || 'and' }
|
||||
current_operator = ''
|
||||
negate = false
|
||||
end
|
||||
end
|
||||
|
||||
extracted_phrases
|
||||
end
|
||||
|
||||
def self.create_query_clause(attrs, index, negate, query_clauses, value_hash, phrase, current_operator)
|
||||
phrase = sanitize_sql_like(phrase)
|
||||
exact_match = phrase =~ /^".*"$/
|
||||
like = exact_match ? '~' : 'ILIKE'
|
||||
|
||||
where_clause = (attrs.map.with_index do |attribute, i|
|
||||
i = (index * attrs.count) + i
|
||||
if %w(repository_rows.id repository_number_values.data).include?(attribute)
|
||||
"#{attribute} IS NOT NULL AND (((#{attribute})::text) #{like} :t#{i}) OR "
|
||||
elsif defined?(model::PREFIXED_ID_SQL) && attribute == model::PREFIXED_ID_SQL
|
||||
"#{attribute} IS NOT NULL AND (#{attribute} #{like} :t#{i}) OR "
|
||||
elsif DATA_VECTOR_ATTRIBUTES.include?(attribute)
|
||||
"#{attribute} @@ to_tsquery(:t#{i}) OR "
|
||||
else
|
||||
"#{attribute} IS NOT NULL AND ((trim_html_tags(#{attribute})) #{like} :t#{i}) OR "
|
||||
end
|
||||
end).join[0..-5]
|
||||
|
||||
where_array << if negate
|
||||
"NOT (#{where_clause}) AND "
|
||||
else
|
||||
"(#{where_clause}) AND "
|
||||
end
|
||||
query_clauses << if negate
|
||||
" #{current_operator} NOT (#{where_clause})"
|
||||
else
|
||||
"#{current_operator} (#{where_clause})"
|
||||
end
|
||||
|
||||
value_array.merge!(
|
||||
(attrs.map.with_index do |_, i|
|
||||
value_hash.merge!(
|
||||
(attrs.map.with_index do |attribute, i|
|
||||
i = (index * attrs.count) + i
|
||||
["t#{i}".to_sym, phrase]
|
||||
|
||||
new_phrase = exact_match ? phrase[1..-2] : phrase
|
||||
if DATA_VECTOR_ATTRIBUTES.include?(attribute)
|
||||
new_phrase = Regexp.escape(new_phrase.gsub(/[!()&|:<]/, ' ').strip).split(/\s+/)
|
||||
new_phrase.map! { |t| "#{t}:*" } unless exact_match
|
||||
new_phrase = new_phrase.join('&').tr('\'', '"')
|
||||
else
|
||||
new_phrase = Regexp.escape(new_phrase)
|
||||
new_phrase = exact_match ? "(^|\\s)#{new_phrase}(\\s|$)" : "%#{new_phrase}%"
|
||||
end
|
||||
|
||||
["t#{i}".to_sym, new_phrase]
|
||||
end).to_h
|
||||
)
|
||||
end
|
||||
|
||||
def self.search_subquery(query, raw_input)
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@ module TinyMceImages
|
|||
before_save :clean_tiny_mce_image_urls
|
||||
after_create :ensure_extracted_image_object_references
|
||||
|
||||
def prepare_for_report(field)
|
||||
def prepare_for_report(field, export_all: false)
|
||||
description = self[field]
|
||||
|
||||
# Check tinymce for old format
|
||||
|
@ -29,15 +29,17 @@ module TinyMceImages
|
|||
)[0]
|
||||
next unless tm_asset_to_update
|
||||
|
||||
tm_asset = tm_asset.image.representation(resize_to_limit: Constants::LARGE_PIC_FORMAT).processed
|
||||
unless export_all
|
||||
tm_asset = tm_asset.image.representation(resize_to_limit: Constants::LARGE_PIC_FORMAT).processed
|
||||
|
||||
width_attr = tm_asset_to_update.attributes['width']
|
||||
height_attr = tm_asset_to_update.attributes['height']
|
||||
width_attr = tm_asset_to_update.attributes['width']
|
||||
height_attr = tm_asset_to_update.attributes['height']
|
||||
|
||||
if width_attr && height_attr && (width_attr.value.to_i >= Constants::LARGE_PIC_FORMAT[0] ||
|
||||
height_attr.value.to_i >= Constants::LARGE_PIC_FORMAT[1])
|
||||
width_attr.value = tm_asset.image.blob.metadata['width'].to_s
|
||||
height_attr.value = tm_asset.image.blob.metadata['height'].to_s
|
||||
if width_attr && height_attr && (width_attr.value.to_i >= Constants::LARGE_PIC_FORMAT[0] ||
|
||||
height_attr.value.to_i >= Constants::LARGE_PIC_FORMAT[1])
|
||||
width_attr.value = tm_asset.image.blob.metadata['width'].to_s
|
||||
height_attr.value = tm_asset.image.blob.metadata['height'].to_s
|
||||
end
|
||||
end
|
||||
|
||||
tm_asset_to_update.attributes['src'].value = convert_to_base64(tm_asset.image)
|
||||
|
|
|
@ -73,7 +73,7 @@ class Experiment < ApplicationRecord
|
|||
.where(user_assignments: { team: teams })
|
||||
.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options)
|
||||
|
||||
new_query = new_query.active unless include_archived
|
||||
new_query = new_query.joins(:project).active.where(projects: { archived: false }) unless include_archived
|
||||
|
||||
new_query
|
||||
end
|
||||
|
|
|
@ -56,6 +56,7 @@ class MyModule < ApplicationRecord
|
|||
belongs_to :changing_from_my_module_status, optional: true, class_name: 'MyModuleStatus'
|
||||
delegate :my_module_status_flow, to: :my_module_status, allow_nil: true
|
||||
has_many :results, inverse_of: :my_module, dependent: :destroy
|
||||
has_many :results_include_discarded, -> { with_discarded }, class_name: 'Result', inverse_of: :my_module
|
||||
has_many :my_module_tags, inverse_of: :my_module, dependent: :destroy
|
||||
has_many :tags, through: :my_module_tags, dependent: :destroy
|
||||
has_many :task_comments, foreign_key: :associated_id, dependent: :destroy
|
||||
|
@ -119,7 +120,11 @@ class MyModule < ApplicationRecord
|
|||
.where(user_assignments: { team: teams })
|
||||
.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options)
|
||||
|
||||
new_query = new_query.active unless include_archived
|
||||
unless include_archived
|
||||
new_query = new_query.joins(experiment: :project)
|
||||
.active
|
||||
.where(experiments: { archived: false }, projects: { archived: false })
|
||||
end
|
||||
new_query
|
||||
end
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class ProjectFolder < ApplicationRecord
|
||||
ID_PREFIX = 'PF'
|
||||
include PrefixedIdModel
|
||||
SEARCHABLE_ATTRIBUTES = ['project_folders.name', PREFIXED_ID_SQL].freeze
|
||||
|
||||
include ArchivableModel
|
||||
include SearchableModel
|
||||
|
@ -43,7 +44,7 @@ class ProjectFolder < ApplicationRecord
|
|||
teams = options[:teams] || current_team || user.teams.select(:id)
|
||||
|
||||
new_query = distinct.viewable_by_user(user, teams)
|
||||
.where_attributes_like_boolean('project_folders.name', query, options)
|
||||
.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options)
|
||||
new_query = new_query.active unless include_archived
|
||||
|
||||
new_query
|
||||
|
|
|
@ -6,7 +6,7 @@ class Protocol < ApplicationRecord
|
|||
include ArchivableModel
|
||||
include PrefixedIdModel
|
||||
SEARCHABLE_ATTRIBUTES = ['protocols.name', 'protocols.description', PREFIXED_ID_SQL, 'steps.name',
|
||||
'step_texts.name', 'step_texts.text', 'tables.name',
|
||||
'step_texts.name', 'step_texts.text', 'tables.name', 'tables.data_vector',
|
||||
'checklists.name', 'checklist_items.text', 'comments.message'].freeze
|
||||
REPOSITORY_TYPES = %i(in_repository_published_original in_repository_draft in_repository_published_version).freeze
|
||||
|
||||
|
@ -173,28 +173,41 @@ class Protocol < ApplicationRecord
|
|||
protocol_templates = if options[:options]&.dig(:in_repository).present? || options[:options].blank?
|
||||
templates = latest_available_versions(teams)
|
||||
.with_granted_permissions(user, ProtocolPermissions::READ)
|
||||
.select(:id)
|
||||
templates = templates.active unless include_archived
|
||||
templates
|
||||
templates.select(:id)
|
||||
end || []
|
||||
|
||||
protocol_my_modules = if options[:options]&.dig(:in_repository).blank?
|
||||
protocols = distinct.joins(:my_module)
|
||||
.joins("INNER JOIN user_assignments my_module_user_assignments " \
|
||||
"ON my_module_user_assignments.assignable_type = 'MyModule' " \
|
||||
"AND my_module_user_assignments.assignable_id = my_modules.id")
|
||||
.where(my_module_user_assignments: { user_id: user })
|
||||
.where(team: teams)
|
||||
protocols = protocols.active unless include_archived
|
||||
protocols.pluck(:id)
|
||||
protocols = viewable_by_user_my_module_protocols(user, teams)
|
||||
unless include_archived
|
||||
protocols = protocols.joins(my_module: { experiment: :project })
|
||||
.active
|
||||
.where(my_modules: { archived: false },
|
||||
experiments: { archived: false },
|
||||
projects: { archived: false })
|
||||
end
|
||||
|
||||
protocols.select(:id)
|
||||
end || []
|
||||
|
||||
distinct.left_joins(steps: [:step_texts, { step_tables: :table },
|
||||
{ checklists: :checklist_items }, :step_comments])
|
||||
.where('(protocols.protocol_type IN (?) AND protocols.id IN (?)) OR (protocols.id IN (?))',
|
||||
[Protocol.protocol_types[:unlinked], Protocol.protocol_types[:linked]],
|
||||
protocol_my_modules, protocol_templates)
|
||||
.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options)
|
||||
protocols = Protocol.where('(protocols.protocol_type IN (?) AND protocols.id IN (?)) OR (protocols.id IN (?))',
|
||||
[Protocol.protocol_types[:unlinked], Protocol.protocol_types[:linked]],
|
||||
protocol_my_modules, protocol_templates)
|
||||
|
||||
protocols.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, { with_subquery: true, raw_input: protocols })
|
||||
end
|
||||
|
||||
def self.search_subquery(query, raw_input)
|
||||
raw_input.where_attributes_like_boolean(['protocols.name', 'protocols.description', PREFIXED_ID_SQL], query)
|
||||
.or(raw_input.where(id: Step.left_joins(:step_texts, { step_tables: :table },
|
||||
{ checklists: :checklist_items }, :step_comments)
|
||||
.where(protocol: raw_input)
|
||||
.where_attributes_like_boolean(['steps.name', 'step_texts.name',
|
||||
'step_texts.text', 'tables.name',
|
||||
'tables.data_vector', 'comments.message',
|
||||
'checklists.name', 'checklist_items.text'],
|
||||
query)
|
||||
.select(:protocol_id)))
|
||||
end
|
||||
|
||||
def self.latest_available_versions(teams)
|
||||
|
@ -216,19 +229,37 @@ class Protocol < ApplicationRecord
|
|||
where('protocols.id IN ((?) UNION (?) UNION (?))', original_without_versions, published_versions, new_drafts)
|
||||
end
|
||||
|
||||
def self.viewable_by_user(user, teams)
|
||||
# Team owners see all protocol templates in the team
|
||||
owner_role = UserRole.find_predefined_owner_role
|
||||
protocols = Protocol.where(team: teams)
|
||||
.where(protocol_type: REPOSITORY_TYPES)
|
||||
viewable_as_team_owner = protocols.joins("INNER JOIN user_assignments team_user_assignments " \
|
||||
"ON team_user_assignments.assignable_type = 'Team' " \
|
||||
"AND team_user_assignments.assignable_id = protocols.team_id")
|
||||
.where(team_user_assignments: { user_id: user, user_role_id: owner_role })
|
||||
.select(:id)
|
||||
viewable_as_assigned = protocols.with_granted_permissions(user, ProtocolPermissions::READ).select(:id)
|
||||
def self.viewable_by_user(user, teams, options = {})
|
||||
if options[:fetch_latest_versions]
|
||||
protocol_templates = latest_available_versions(teams)
|
||||
.with_granted_permissions(user, ProtocolPermissions::READ)
|
||||
.select(:id)
|
||||
protocol_my_modules = viewable_by_user_my_module_protocols(user, teams).select(:id)
|
||||
|
||||
where('protocols.id IN ((?) UNION (?))', viewable_as_team_owner, viewable_as_assigned)
|
||||
where('protocols.id IN ((?) UNION (?))', protocol_templates, protocol_my_modules)
|
||||
else
|
||||
# Team owners see all protocol templates in the team
|
||||
owner_role = UserRole.find_predefined_owner_role
|
||||
protocols = Protocol.where(team: teams)
|
||||
.where(protocol_type: REPOSITORY_TYPES)
|
||||
viewable_as_team_owner = protocols.joins("INNER JOIN user_assignments team_user_assignments " \
|
||||
"ON team_user_assignments.assignable_type = 'Team' " \
|
||||
"AND team_user_assignments.assignable_id = protocols.team_id")
|
||||
.where(team_user_assignments: { user_id: user, user_role_id: owner_role })
|
||||
.select(:id)
|
||||
viewable_as_assigned = protocols.with_granted_permissions(user, ProtocolPermissions::READ).select(:id)
|
||||
|
||||
where('protocols.id IN ((?) UNION (?))', viewable_as_team_owner, viewable_as_assigned)
|
||||
end
|
||||
end
|
||||
|
||||
def self.viewable_by_user_my_module_protocols(user, teams)
|
||||
distinct.joins(:my_module)
|
||||
.joins("INNER JOIN user_assignments my_module_user_assignments " \
|
||||
"ON my_module_user_assignments.assignable_type = 'MyModule' " \
|
||||
"AND my_module_user_assignments.assignable_id = my_modules.id")
|
||||
.where(my_module_user_assignments: { user_id: user })
|
||||
.where(team: teams)
|
||||
end
|
||||
|
||||
def self.filter_by_teams(teams = [])
|
||||
|
|
|
@ -5,12 +5,15 @@ class Result < ApplicationRecord
|
|||
include SearchableModel
|
||||
include SearchableByNameModel
|
||||
include ViewableModel
|
||||
include Discard::Model
|
||||
|
||||
default_scope -> { kept }
|
||||
|
||||
auto_strip_attributes :name, nullify: false
|
||||
validates :name, length: { maximum: Constants::NAME_MAX_LENGTH }
|
||||
|
||||
SEARCHABLE_ATTRIBUTES = ['results.name', 'result_texts.name', 'result_texts.text',
|
||||
'tables.name', 'comments.message'].freeze
|
||||
'tables.name', 'tables.data_vector', 'comments.message'].freeze
|
||||
|
||||
enum assets_view_mode: { thumbnail: 0, list: 1, inline: 2 }
|
||||
|
||||
|
@ -48,11 +51,20 @@ class Result < ApplicationRecord
|
|||
"ON my_module_user_assignments.assignable_type = 'MyModule' " \
|
||||
"AND my_module_user_assignments.assignable_id = my_modules.id")
|
||||
.where(my_module_user_assignments: { user_id: user, team_id: teams })
|
||||
.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, options)
|
||||
|
||||
new_query = new_query.active unless include_archived
|
||||
unless include_archived
|
||||
new_query = new_query.joins(my_module: { experiment: :project })
|
||||
.active
|
||||
.where(my_modules: { archived: false },
|
||||
experiments: { archived: false },
|
||||
projects: { archived: false })
|
||||
end
|
||||
|
||||
new_query
|
||||
new_query.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query, { with_subquery: true, raw_input: new_query })
|
||||
end
|
||||
|
||||
def self.search_subquery(query, raw_input)
|
||||
raw_input.where_attributes_like_boolean(SEARCHABLE_ATTRIBUTES, query)
|
||||
end
|
||||
|
||||
def duplicate(my_module, user, result_name: nil)
|
||||
|
|
|
@ -509,6 +509,22 @@ class User < ApplicationRecord
|
|||
.find_by(user_identities: { provider: provider_conf['provider_name'], uid: token_payload[:sub] })
|
||||
end
|
||||
|
||||
def self.from_api_key(api_key)
|
||||
where('api_key_expires_at > ?', Time.current).find_by(api_key: api_key)
|
||||
end
|
||||
|
||||
def regenerate_api_key!
|
||||
update!(
|
||||
api_key: SecureRandom.urlsafe_base64(33),
|
||||
api_key_created_at: Time.current,
|
||||
api_key_expires_at: Constants::API_KEY_EXPIRES_IN.from_now
|
||||
)
|
||||
end
|
||||
|
||||
def revoke_api_key!
|
||||
update!(api_key: nil, api_key_expires_at: nil)
|
||||
end
|
||||
|
||||
def has_linked_account?(provider)
|
||||
user_identities.exists?(provider: provider)
|
||||
end
|
||||
|
@ -614,6 +630,10 @@ class User < ApplicationRecord
|
|||
%w(id due_date age results status archived assigned tags comments)
|
||||
end
|
||||
|
||||
def api_key_enabled?
|
||||
Rails.configuration.x.core_api_key_enabled
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def confirmation_required?
|
||||
|
|
|
@ -9,7 +9,7 @@ class AssetSerializer < ActiveModel::Serializer
|
|||
include ApplicationHelper
|
||||
|
||||
attributes :file_name, :file_extension, :view_mode, :icon, :urls, :updated_at_formatted,
|
||||
:file_size, :medium_preview, :large_preview, :asset_type, :wopi,
|
||||
:file_size, :medium_preview, :large_preview, :asset_type, :wopi, :file_name_without_extension,
|
||||
:wopi_context, :pdf_previewable, :file_size_formatted, :asset_order,
|
||||
:updated_at, :metadata, :image_editable, :image_context, :pdf, :attached, :parent_type,
|
||||
:edit_version_range
|
||||
|
@ -23,6 +23,10 @@ class AssetSerializer < ActiveModel::Serializer
|
|||
object.render_file_name
|
||||
end
|
||||
|
||||
def file_name_without_extension
|
||||
File.basename(object.file_name, '.*')
|
||||
end
|
||||
|
||||
def file_extension
|
||||
File.extname(object.file_name)[1..]
|
||||
end
|
||||
|
|
|
@ -26,7 +26,8 @@ module BreadcrumbsHelper
|
|||
end
|
||||
when Result
|
||||
parent = subject.my_module
|
||||
url = my_module_results_path(subject.my_module)
|
||||
view_mode = subject.archived? ? 'archived' : 'active'
|
||||
url = my_module_results_path(subject.my_module, view_mode:)
|
||||
when ProjectFolder
|
||||
parent = subject.team
|
||||
url = project_folder_path(subject)
|
||||
|
@ -34,8 +35,15 @@ module BreadcrumbsHelper
|
|||
parent = subject.team
|
||||
url = repository_path(subject)
|
||||
when RepositoryRow
|
||||
parent = subject.team
|
||||
url = repository_path(subject.repository)
|
||||
parent = subject.repository
|
||||
params = {
|
||||
id: subject.repository_id,
|
||||
landing_page: true,
|
||||
row_id: subject.id
|
||||
}
|
||||
params[:archived] = true if subject.archived
|
||||
|
||||
url = repository_path(params)
|
||||
when Report
|
||||
parent = subject.team
|
||||
|
||||
|
@ -56,7 +64,7 @@ module BreadcrumbsHelper
|
|||
url = projects_path(team: subject.id)
|
||||
end
|
||||
|
||||
breadcrumbs << { name: subject.name, url: url } if subject.name.present?
|
||||
breadcrumbs << { name: subject.name, url: } if subject.name.present?
|
||||
|
||||
if parent
|
||||
generate_breadcrumbs(parent, breadcrumbs)
|
||||
|
|
|
@ -61,7 +61,6 @@ module Lists
|
|||
assigned_tags: assigned_tags_my_module_my_module_tags_path(object),
|
||||
users_list: search_my_module_user_my_module_path(object, my_module_id: object.id),
|
||||
experiments_to_move: experiments_to_move_experiment_path(object.experiment),
|
||||
move: move_modules_experiment_path(object.experiment),
|
||||
update: my_module_path(object),
|
||||
show_access: access_permissions_my_module_path(object),
|
||||
provisioning_status: provisioning_status_my_module_url(object)
|
||||
|
|
|
@ -31,11 +31,7 @@ module Lists
|
|||
end
|
||||
|
||||
def code
|
||||
if project?
|
||||
object.code
|
||||
else
|
||||
"F#{object.id}" # We don't have proper code for folders, but we need to correct selection
|
||||
end
|
||||
object.code
|
||||
end
|
||||
|
||||
def created_at
|
||||
|
|
|
@ -7,13 +7,13 @@ class QuickSearchSerializer < ActiveModel::Serializer
|
|||
attributes :updated_at, :archived, :breadcrumbs, :code
|
||||
|
||||
def archived
|
||||
@object.archived?
|
||||
@object.respond_to?(:archived_branch?) ? @object.archived_branch? : @object.archived?
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
def code
|
||||
@object.respond_to?(:code) ? @object.code : @object.id
|
||||
@object.code if @object.respond_to?(:code)
|
||||
end
|
||||
|
||||
def updated_at
|
||||
|
|
|
@ -55,9 +55,20 @@ class ActivitiesService
|
|||
child_model = parent_model.reflect_on_association(child).class_name.to_sym
|
||||
next if subjects[child_model]
|
||||
|
||||
subjects[child_model] = parent_model.where(id: subjects[subject_name])
|
||||
.joins(child)
|
||||
.pluck("#{child.to_s.pluralize}.id")
|
||||
|
||||
if subject_name == 'Result'
|
||||
parent_model = parent_model.with_discarded
|
||||
end
|
||||
|
||||
if child == :results
|
||||
subjects[child_model] = parent_model.where(id: subjects[subject_name])
|
||||
.joins(:results_include_discarded)
|
||||
.pluck('results.id')
|
||||
else
|
||||
subjects[child_model] = parent_model.where(id: subjects[subject_name])
|
||||
.joins(child)
|
||||
.pluck("#{child.to_s.pluralize}.id")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -87,8 +87,15 @@ module Lists
|
|||
def filter_project_folder_records(records)
|
||||
records = records.archived if @params[:view_mode] == 'archived'
|
||||
records = records.active if @params[:view_mode] == 'active'
|
||||
records = records.where_attributes_like('project_folders.name', @filters[:query]) if @filters[:query].present?
|
||||
records = records.where_attributes_like('project_folders.name', @params[:search]) if @params[:search].present?
|
||||
if @filters[:query].present?
|
||||
records = records.where_attributes_like(['project_folders.name', ProjectFolder::PREFIXED_ID_SQL],
|
||||
@filters[:query])
|
||||
end
|
||||
|
||||
if @params[:search].present?
|
||||
records = records.where_attributes_like(['project_folders.name', ProjectFolder::PREFIXED_ID_SQL],
|
||||
@params[:search])
|
||||
end
|
||||
records
|
||||
end
|
||||
|
||||
|
|
|
@ -10,14 +10,14 @@ module Lists
|
|||
original_without_versions = @raw_data
|
||||
.where.missing(:published_versions)
|
||||
.in_repository_published_original
|
||||
.select(:id)
|
||||
.select('protocols.id')
|
||||
published_versions = @raw_data
|
||||
.in_repository_published_version
|
||||
.order(:parent_id, version_number: :desc)
|
||||
.select('DISTINCT ON (parent_id) id')
|
||||
.select('DISTINCT ON (protocols.parent_id) protocols.id')
|
||||
new_drafts = @raw_data
|
||||
.where(protocol_type: Protocol.protocol_types[:in_repository_draft], parent_id: nil)
|
||||
.select(:id)
|
||||
.select('protocols.id')
|
||||
|
||||
@records = Protocol.where('protocols.id IN (?) OR protocols.id IN (?) OR protocols.id IN (?)',
|
||||
original_without_versions, published_versions, new_drafts)
|
||||
|
|
|
@ -45,6 +45,19 @@ class MarvinJsService
|
|||
asset
|
||||
end
|
||||
|
||||
def update_file_name(new_name, asset_id, current_user, current_team)
|
||||
asset = current_team.assets.find(asset_id)
|
||||
prepared_name = prepare_name(new_name)
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
asset.last_modified_by = current_user
|
||||
asset.rename_file(prepared_name)
|
||||
asset.save!
|
||||
end
|
||||
|
||||
asset
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def connect_asset(asset, params, current_user)
|
||||
|
|
|
@ -89,7 +89,8 @@ module Toolbars
|
|||
name: 'move',
|
||||
label: I18n.t('experiments.table.toolbar.move'),
|
||||
icon: 'sn-icon sn-icon-move',
|
||||
type: :emit
|
||||
type: :emit,
|
||||
path: move_modules_experiment_path(@my_modules.first.experiment, my_module_ids: @my_modules.pluck(:id))
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -106,11 +106,16 @@ module Toolbars
|
|||
num_of_requests_left = @current_user.exports_left - 1
|
||||
team = @items.first.team
|
||||
|
||||
message = "<p>#{I18n.t('projects.export_projects.modal_text_p1_html',
|
||||
num_projects: num_projects,
|
||||
team: team.name)}</p>
|
||||
<p>#{I18n.t('projects.export_projects.modal_text_p2_html')}</p>"
|
||||
unless limit.zero?
|
||||
message = if limit.zero? || num_of_requests_left.positive?
|
||||
"<p>#{I18n.t('projects.export_projects.modal_text_p1_html',
|
||||
num_projects: num_projects,
|
||||
team: team.name)}</p>
|
||||
<p>#{I18n.t('projects.export_projects.modal_text_p2_html')}</p>"
|
||||
else
|
||||
"<p>#{I18n.t('repositories.index.modal_export_limit_exceeded.error_p1_html', limit: limit)}</p>
|
||||
<p>#{I18n.t('repositories.index.modal_export_limit_exceeded.error_p2_html', limit: limit)}</p>"
|
||||
end
|
||||
unless limit.zero? || !num_of_requests_left.positive?
|
||||
message += "<p><i>#{I18n.t('projects.export_projects.modal_text_p3_html', limit: limit, num: num_of_requests_left)}</i></p>"
|
||||
end
|
||||
|
||||
|
@ -120,6 +125,7 @@ module Toolbars
|
|||
label: I18n.t('projects.export_projects.export_button'),
|
||||
icon: 'sn-icon sn-icon-export',
|
||||
message: message,
|
||||
number_of_request_left: limit.zero? ? 1 : num_of_requests_left,
|
||||
path: export_projects_team_path(team),
|
||||
number_of_projects: num_projects,
|
||||
type: :emit
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
data-module-y="<%= my_module.y %>"
|
||||
data-module-conns="<%= construct_module_connections(my_module) %>"
|
||||
data-module-users-tab-url="<%= designated_users_my_module_user_my_modules_url(my_module_id: my_module.id, format: :json) %>"
|
||||
data-module-tags-url="<%= my_module_tags_experiment_path(my_module.experiment, format: :json) %>">
|
||||
data-module-tags-url="<%= my_module_tags_experiment_path(my_module.experiment, format: :json) %>"
|
||||
data-module-url="<%= my_module_path(my_module, format: :json) %>">
|
||||
|
||||
<div data-view-mode="active">
|
||||
<a class="edit-tags-link pull-right" data-remote="true" href="<%= my_module_tags_edit_url(my_module, format: :json) %>">
|
||||
|
@ -114,7 +115,4 @@
|
|||
<div role="tabpanel" class="tab-pane" id="<%= my_module.id %>_comments" data-contents="comments"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= render partial: 'canvas/full_zoom/tags_modal', locals: { my_module: my_module }%>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,25 +1,14 @@
|
|||
<div id="tagsModalContainer-<%= my_module.id %>" class="vue-tags-modal">
|
||||
<div ref="tagsModal" class="tags-modal-component" id="tagsModalComponent-<%= my_module.id %>"></div>
|
||||
<teleport to="body">
|
||||
<tags-modal v-if="tagsModalOpen"
|
||||
:params="<%=
|
||||
{
|
||||
id: my_module.id,
|
||||
permissions: {
|
||||
manage_tags: can_manage_my_module_tags?(my_module)
|
||||
},
|
||||
urls: {
|
||||
assigned_tags: assigned_tags_my_module_my_module_tags_path(my_module),
|
||||
assign_tags: my_module_my_module_tags_path(my_module)
|
||||
}
|
||||
}.to_json
|
||||
%>"
|
||||
:tags-colors="<%= Constants::TAG_COLORS.to_json %>"
|
||||
project-name="<%= my_module.experiment.project.name %>"
|
||||
project-tags-url="<%= project_tags_path(my_module.experiment.project) %>"
|
||||
@close="close"
|
||||
@tags-loaded="syncTags"
|
||||
/>
|
||||
</teleport>
|
||||
</div>
|
||||
<%= javascript_include_tag 'vue_legacy_tags_modal' %>
|
||||
<div id="tagsModalContainer" class="vue-tags-modal">
|
||||
<div ref="tagsModal" class="tags-modal-component" id="tagsModalComponent"></div>
|
||||
<teleport to="body">
|
||||
<tags-modal v-if="tagsModalOpen"
|
||||
:tags-colors="<%= Constants::TAG_COLORS.to_json %>"
|
||||
:params="myModuleParams"
|
||||
project-name="<%= @project.name %>"
|
||||
project-tags-url="<%= project_tags_path(@project) %>"
|
||||
@close="close"
|
||||
@tags-loaded="syncTags"
|
||||
/>
|
||||
</teleport>
|
||||
</div>
|
||||
<%= javascript_include_tag 'vue_legacy_tags_modal' %>
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Manage tags modal -->
|
||||
<%= render partial: 'canvas/full_zoom/tags_modal' %>
|
||||
<%= render partial: "my_modules/modals/manage_module_tags_modal", locals: { my_module: nil } %>
|
||||
<%= javascript_include_tag("my_modules/tags") %>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="select-container">
|
||||
<div class="edit-button-container hidden">
|
||||
<a class="edit-tags-link" data-remote="true" href="<%= my_module_tags_edit_path(@my_module, format: :json) %>">
|
||||
<a class="edit-tags-link" data-remote="true" href="<%= my_module_path(my_module, format: :json) %>">
|
||||
<i class="sn-icon sn-icon-settings"></i>
|
||||
<span class="hidden-xs"><%= t("my_modules.details.manage_tags") %></span>
|
||||
</a>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<% if my_module.description.present? %>
|
||||
<%= custom_auto_link(my_module.prepare_for_report(:description),
|
||||
<%= custom_auto_link(my_module.prepare_for_report(:description, export_all: export_all),
|
||||
team: current_team,
|
||||
simple_format: false,
|
||||
base64_encoded_imgs: true) %>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
<div class="row module-protocol-description">
|
||||
<% if @settings.dig('task', 'protocol', 'description') && protocol.description.present? %>
|
||||
<%= custom_auto_link(protocol.prepare_for_report(:description),
|
||||
<%= custom_auto_link(protocol.prepare_for_report(:description, export_all: export_all),
|
||||
team: current_team,
|
||||
simple_format: false,
|
||||
base64_encoded_imgs: true) %>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="text-container ql-editor">
|
||||
<%= custom_auto_link(result_text.prepare_for_report(:text),
|
||||
<%= custom_auto_link(result_text.prepare_for_report(:text, export_all: export_all),
|
||||
team: current_team,
|
||||
simple_format: false,
|
||||
tags: %w(img),
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
<div class="report-element-body">
|
||||
<% if step_text.text.present? %>
|
||||
<%= custom_auto_link(step_text.prepare_for_report(:text),
|
||||
<%= custom_auto_link(step_text.prepare_for_report(:text, export_all: export_all),
|
||||
team: current_team,
|
||||
simple_format: false,
|
||||
tags: %w(img),
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
:search-url="'<%= search_path(format: :json) %>'"
|
||||
teams-url="<%= visible_teams_teams_path %>"
|
||||
users-url="<%= visible_users_teams_path %>"
|
||||
:single-team="<%= current_user.teams.count == 1 %>"
|
||||
current-team="<%= current_team.id %>"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -35,6 +35,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<%= render partial: 'users/registrations/edit_partials/2fa' %>
|
||||
|
||||
<% if current_user.api_key_enabled? %>
|
||||
<%= render partial: 'users/registrations/edit_partials/api_key' %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<div class="mb-8">
|
||||
<h3 id="api-key"><%= t("users.registrations.edit.api_key.title") %></h3>
|
||||
<p>
|
||||
<%= t("users.registrations.edit.api_key.description") %>
|
||||
</p>
|
||||
<% if current_user.api_key %>
|
||||
<div class="api-key-display">
|
||||
<div class="form-group sci-input-container right-icon !w-1/2">
|
||||
<%= password_field_tag :api_key,
|
||||
current_user.api_key,
|
||||
name: 'api_key',
|
||||
class: 'form-control sci-input-field !text-sn-black !font-mono !cursor-text',
|
||||
disabled: 'disabled'
|
||||
%>
|
||||
<i class="sn-icon sn-icon-visibility-show show-password" style="cursor: pointer; z-index: 10"></i>
|
||||
</div>
|
||||
<% if current_user.api_key_expires_at < Time.current %>
|
||||
<p class="">
|
||||
<%= t("users.registrations.edit.api_key.expired") %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex">
|
||||
<% if current_user.api_key %>
|
||||
<%= button_to t("users.registrations.edit.api_key.regenerate"), users_api_key_regenerate_path, class: "btn btn-primary mr-2" %>
|
||||
<%= button_to t("users.registrations.edit.api_key.revoke"), users_api_key_revoke_path, class: "btn btn-danger" %>
|
||||
<% else %>
|
||||
<%= button_to t("users.registrations.edit.api_key.generate"), users_api_key_regenerate_path, class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
|
@ -12,4 +12,6 @@ Rails.application.configure do
|
|||
config.x.core_api_v1_enabled = ENV['CORE_API_V1_ENABLED'] || false
|
||||
|
||||
config.x.core_api_v2_enabled = ENV['CORE_API_V2_ENABLED'] || false
|
||||
|
||||
config.x.core_api_key_enabled = ENV['CORE_API_KEY_ENABLED'] == 'true'
|
||||
end
|
||||
|
|
|
@ -247,6 +247,8 @@ class Constants
|
|||
TWO_FACTOR_RECOVERY_CODE_COUNT = 6
|
||||
TWO_FACTOR_RECOVERY_CODE_LENGTH = 12
|
||||
|
||||
API_KEY_EXPIRES_IN = 1.year
|
||||
|
||||
#=============================================================================
|
||||
# Protocol importers
|
||||
#=============================================================================
|
||||
|
@ -425,7 +427,7 @@ class Constants
|
|||
# Team name for default admin user
|
||||
DEFAULT_PRIVATE_TEAM_NAME = 'My projects'.freeze
|
||||
|
||||
TEMPLATES_PROJECT_NAME = 'Templates'.freeze
|
||||
TEMPLATES_PROJECT_NAME = 'SciNote Examples'.freeze
|
||||
|
||||
# Interval time for polling status state
|
||||
FAST_STATUS_POLLING_INTERVAL = 5000
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module PermissionExtends
|
||||
module TeamPermissions
|
||||
%w(
|
||||
NONE
|
||||
READ
|
||||
MANAGE
|
||||
USERS_MANAGE
|
||||
|
@ -17,6 +18,7 @@ module PermissionExtends
|
|||
|
||||
module ProtocolPermissions
|
||||
%w(
|
||||
NONE
|
||||
READ
|
||||
READ_ARCHIVED
|
||||
MANAGE
|
||||
|
@ -27,6 +29,7 @@ module PermissionExtends
|
|||
|
||||
module ReportPermissions
|
||||
%w(
|
||||
NONE
|
||||
READ
|
||||
MANAGE
|
||||
USERS_MANAGE
|
||||
|
@ -35,6 +38,7 @@ module PermissionExtends
|
|||
|
||||
module ProjectPermissions
|
||||
%w(
|
||||
NONE
|
||||
READ
|
||||
READ_ARCHIVED
|
||||
MANAGE
|
||||
|
@ -52,6 +56,7 @@ module PermissionExtends
|
|||
|
||||
module ExperimentPermissions
|
||||
%w(
|
||||
NONE
|
||||
READ
|
||||
READ_ARCHIVED
|
||||
MANAGE
|
||||
|
@ -65,6 +70,7 @@ module PermissionExtends
|
|||
|
||||
module MyModulePermissions
|
||||
%w(
|
||||
NONE
|
||||
READ
|
||||
READ_ARCHIVED
|
||||
ACTIVITIES_READ
|
||||
|
@ -106,6 +112,7 @@ module PermissionExtends
|
|||
|
||||
module RepositoryPermissions
|
||||
%w(
|
||||
NONE
|
||||
READ
|
||||
READ_ARCHIVED
|
||||
MANAGE
|
||||
|
@ -132,7 +139,7 @@ module PermissionExtends
|
|||
ExperimentPermissions.constants.map { |const| ExperimentPermissions.const_get(const) } +
|
||||
MyModulePermissions.constants.map { |const| MyModulePermissions.const_get(const) } +
|
||||
RepositoryPermissions.constants.map { |const| RepositoryPermissions.const_get(const) }
|
||||
)
|
||||
).reject { |p| p.end_with?("_none") }
|
||||
|
||||
NORMAL_USER_PERMISSIONS = [
|
||||
TeamPermissions::PROJECTS_CREATE,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue