mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 14:45:56 +08:00
Merge branch 'develop' into features/sso-improvements
This commit is contained in:
commit
e724b15499
|
@ -1372,6 +1372,9 @@ function bindNewModuleAction(gridDistX, gridDistY) {
|
|||
|
||||
function handleNewNameConfirm(ev) {
|
||||
var input = $("#new-module-name-input");
|
||||
|
||||
input.parent().removeClass("has-error");
|
||||
input.next("span.help-block").remove();
|
||||
// Validate module name
|
||||
var moduleNameValid = textValidator(ev, input,
|
||||
<%= Constants::NAME_MIN_LENGTH %>, <%= Constants::NAME_MAX_LENGTH %>,
|
||||
|
|
|
@ -335,10 +335,9 @@
|
|||
if (data.path) {
|
||||
window.location.replace(data.path);
|
||||
}
|
||||
refreshCurrentView();
|
||||
})
|
||||
.on('ajax:error', '.experiment-action-form', function(ev, data) {
|
||||
$(this).renderFormErrors('experiment', data.responseJSON);
|
||||
HelperModule.flashAlertMsg(data.responseJSON.message, 'danger');
|
||||
});
|
||||
|
||||
window.initActionToolbar();
|
||||
|
|
|
@ -686,6 +686,11 @@ var RepositoryDatatable = (function(global) {
|
|||
targets: 5,
|
||||
class: 'added-on',
|
||||
visible: true
|
||||
},{
|
||||
// Added by column
|
||||
targets: 6,
|
||||
class: 'added-by',
|
||||
visible: true
|
||||
}, {
|
||||
targets: '_all',
|
||||
render: function(data) {
|
||||
|
@ -776,8 +781,8 @@ var RepositoryDatatable = (function(global) {
|
|||
var state = localStorage.getItem(`datatables_repositories_state/${repositoryId}/${viewType}`);
|
||||
|
||||
json.state.start = state !== null ? JSON.parse(state).start : 0;
|
||||
if (json.state.columns[6]) json.state.columns[6].visible = archived;
|
||||
if (json.state.columns[7]) json.state.columns[7].visible = archived;
|
||||
if (json.state.columns[8]) json.state.columns[8].visible = archived;
|
||||
if (json.state.search) delete json.state.search;
|
||||
|
||||
if (json.state.ColSizes && json.state.ColSizes.length > 0) {
|
||||
|
|
|
@ -228,7 +228,7 @@ var RepositoryColumns = (function() {
|
|||
var maxLength = $(TABLE_ID).data('max-dropdown-length');
|
||||
if ($.trim(name).length > maxLength) {
|
||||
return `<div class="modal-tooltip">
|
||||
${truncateLongString(name, maxLength)}
|
||||
<span>${truncateLongString(name, maxLength)}</span>
|
||||
<span class="modal-tooltiptext">${name}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
|
|
@ -14,4 +14,5 @@ const GLOBAL_CONSTANTS = {
|
|||
FILENAME_MAX_LENGTH: <%= Constants::FILENAME_MAX_LENGTH %>,
|
||||
FAST_STATUS_POLLING_INTERVAL: <%= Constants::FAST_STATUS_POLLING_INTERVAL %>,
|
||||
SLOW_STATUS_POLLING_INTERVAL: <%= Constants::SLOW_STATUS_POLLING_INTERVAL %>,
|
||||
ASSET_SYNC_URL: '<%= Constants::ASSET_SYNC_URL %>',
|
||||
};
|
||||
|
|
|
@ -148,7 +148,6 @@ var filterDropdown = (function() {
|
|||
initCloseButton();
|
||||
initDateTimePickerComponent();
|
||||
initSearchField(filtersEnabledFunction);
|
||||
this.toggleFilterMark($filterContainer, filtersEnabled)
|
||||
return $filterContainer;
|
||||
},
|
||||
toggleFilterMark: function(filterContainer, filtersEnabledArg) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable no-undef */
|
||||
/* global I18n */
|
||||
/* global HelperModule I18n */
|
||||
/* eslint-disable no-unused-vars, no-use-before-define */
|
||||
|
||||
/* config = {
|
||||
|
@ -218,6 +218,8 @@ var zebraPrint = (function() {
|
|||
updateProgressModalData(progressModal, printData.printer_name, PRINTER_STATUS_ERROR, PRINTER_STATUS_ERROR);
|
||||
}
|
||||
});
|
||||
}).fail(() => {
|
||||
HelperModule.flashAlertMsg(I18n.t('repository_row.modal_print_label.general_error'), 'danger');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -335,8 +335,8 @@
|
|||
}
|
||||
|
||||
&::before {
|
||||
@include font-small;
|
||||
bottom: -15px;
|
||||
font-size: x-small;
|
||||
bottom: -18px;
|
||||
color: $brand-danger;
|
||||
content: attr(data-error-text);
|
||||
left: 0;
|
||||
|
|
|
@ -108,6 +108,12 @@
|
|||
z-index: 99999999;
|
||||
}
|
||||
|
||||
.modal-tooltip > span:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-list-el {
|
||||
align-items: center;
|
||||
background: $color-white;
|
||||
|
@ -120,6 +126,7 @@
|
|||
|
||||
.manage-controls {
|
||||
display: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text {
|
||||
|
|
|
@ -236,3 +236,7 @@
|
|||
.sa-link {
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.atwho-inserted {
|
||||
line-height: 16px;
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ module Api
|
|||
end
|
||||
|
||||
asset.save!(context: :on_api_upload)
|
||||
asset.post_process_file
|
||||
asset.post_process_file(@team)
|
||||
|
||||
render jsonapi: asset,
|
||||
serializer: AssetSerializer,
|
||||
|
|
|
@ -11,15 +11,6 @@ module Api
|
|||
|
||||
class FilterParamError < StandardError; end
|
||||
|
||||
class MutuallyExclusiveParamsError < StandardError
|
||||
attr_reader :first_param, :second_param
|
||||
|
||||
def initialize(first_param, second_param)
|
||||
@first_param = first_param
|
||||
@second_param = second_param
|
||||
end
|
||||
end
|
||||
|
||||
class PermissionError < StandardError
|
||||
attr_reader :klass, :mode
|
||||
|
||||
|
@ -37,14 +28,6 @@ module Api
|
|||
:bad_request)
|
||||
end
|
||||
|
||||
rescue_from MutuallyExclusiveParamsError do |e|
|
||||
render_error(I18n.t('api.core.errors.mutually_exclusive_params_error.title'),
|
||||
I18n.t('api.core.errors.mutually_exclusive_params_error.detail',
|
||||
first_param: e.first_param,
|
||||
second_param: e.second_param),
|
||||
:bad_request)
|
||||
end
|
||||
|
||||
rescue_from FilterParamError do |e|
|
||||
logger.error e.message
|
||||
logger.error e.backtrace.join("\n")
|
||||
|
|
|
@ -113,6 +113,7 @@ module Api
|
|||
blob = create_blob_from_params
|
||||
asset = Asset.create!(file: blob, team: @team)
|
||||
end
|
||||
asset.post_process_file(@team)
|
||||
ResultAsset.create!(asset: asset, result: @result)
|
||||
end
|
||||
end
|
||||
|
@ -128,6 +129,7 @@ module Api
|
|||
blob = create_blob_from_params
|
||||
asset.update!(file: blob)
|
||||
end
|
||||
asset.post_process_file(@team)
|
||||
new_checksum = asset.file.blob.checksum
|
||||
end
|
||||
@asset_result_updated = old_checksum != new_checksum
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V2
|
||||
class InventoryItemChildRelationshipsController < BaseController
|
||||
before_action :load_team, :load_inventory, :load_inventory_item
|
||||
before_action :load_child_connection, only: %w(show destroy)
|
||||
before_action :check_manage_permission, only: %w(create destroy)
|
||||
|
||||
def index
|
||||
child_connections = timestamps_filter(@inventory_item.child_connections).page(params.dig(:page, :number))
|
||||
.per(params.dig(:page, :size))
|
||||
render jsonapi: child_connections, each_serializer: InventoryItemRelationshipSerializer, include: include_params
|
||||
end
|
||||
|
||||
def show
|
||||
render jsonapi: @child_connection, serializer: InventoryItemRelationshipSerializer, include: include_params
|
||||
end
|
||||
|
||||
def create
|
||||
inventory_item_to_link = RepositoryRow.where(repository: Repository.accessible_by_teams(@team))
|
||||
.find(connection_params[:child_id])
|
||||
child_connection = @inventory_item.child_connections.create!(
|
||||
child: inventory_item_to_link,
|
||||
created_by: current_user,
|
||||
last_modified_by: current_user
|
||||
)
|
||||
|
||||
render jsonapi: child_connection, serializer: InventoryItemRelationshipSerializer, status: :created
|
||||
end
|
||||
|
||||
def destroy
|
||||
@child_connection.destroy!
|
||||
render body: nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_child_connection
|
||||
@child_connection = @inventory_item.child_connections.find(params.require(:id))
|
||||
end
|
||||
|
||||
def check_manage_permission
|
||||
raise PermissionError.new(Repository, :manage) unless can_connect_repository_rows?(@inventory)
|
||||
end
|
||||
|
||||
def connection_params
|
||||
raise TypeError unless params.require(:data).require(:type) == 'inventory_item_relationships'
|
||||
|
||||
params.require(:data).require(:attributes).permit(:child_id)
|
||||
end
|
||||
|
||||
def permitted_includes
|
||||
%w(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V2
|
||||
class InventoryItemParentRelationshipsController < BaseController
|
||||
before_action :load_team, :load_inventory, :load_inventory_item
|
||||
before_action :load_parent_connection, only: %w(show destroy)
|
||||
before_action :check_manage_permission, only: %w(create destroy)
|
||||
|
||||
def index
|
||||
parent_connections = timestamps_filter(@inventory_item.parent_connections).page(params.dig(:page, :number))
|
||||
.per(params.dig(:page, :size))
|
||||
render jsonapi: parent_connections,
|
||||
each_serializer: InventoryItemRelationshipSerializer,
|
||||
include: include_params
|
||||
end
|
||||
|
||||
def show
|
||||
render jsonapi: @parent_connection, serializer: InventoryItemRelationshipSerializer, include: include_params
|
||||
end
|
||||
|
||||
def create
|
||||
inventory_item_to_link = RepositoryRow.where(repository: Repository.accessible_by_teams(@team))
|
||||
.find(connection_params[:parent_id])
|
||||
parent_connection = @inventory_item.parent_connections.create!(
|
||||
parent: inventory_item_to_link,
|
||||
created_by: current_user,
|
||||
last_modified_by: current_user
|
||||
)
|
||||
|
||||
render jsonapi: parent_connection, serializer: InventoryItemRelationshipSerializer, status: :created
|
||||
end
|
||||
|
||||
def destroy
|
||||
@parent_connection.destroy!
|
||||
render body: nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_parent_connection
|
||||
@parent_connection = @inventory_item.parent_connections.find(params.require(:id))
|
||||
end
|
||||
|
||||
def check_manage_permission
|
||||
raise PermissionError.new(Repository, :manage) unless can_connect_repository_rows?(@inventory)
|
||||
end
|
||||
|
||||
def connection_params
|
||||
raise TypeError unless params.require(:data).require(:type) == 'inventory_item_relationships'
|
||||
|
||||
params.require(:data).require(:attributes).permit(:parent_id)
|
||||
end
|
||||
|
||||
def permitted_includes
|
||||
%w(parent)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,65 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V2
|
||||
class InventoryItemRelationshipsController < BaseController
|
||||
before_action :load_team, :load_inventory, :load_inventory_item
|
||||
before_action :check_manage_permission, only: %w(create destroy)
|
||||
before_action :load_create_params, only: :create
|
||||
|
||||
def create
|
||||
parent = @relation == :parent ? @inventory_item : @inventory_item_to_link
|
||||
child = @relation == :child ? @inventory_item : @inventory_item_to_link
|
||||
|
||||
@connection = RepositoryRowConnection.create!(
|
||||
parent_id: parent,
|
||||
child_id: child,
|
||||
created_by: current_user,
|
||||
last_modified_by: current_user
|
||||
)
|
||||
|
||||
render jsonapi: @connection, serializer: InventoryItemRelationshipSerializer, status: :created
|
||||
end
|
||||
|
||||
def destroy
|
||||
@connection = @inventory_item.parent_connections
|
||||
.or(@inventory_item.child_connections)
|
||||
.find(params.require(:id))
|
||||
@connection.destroy!
|
||||
render body: nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_manage_permission
|
||||
raise PermissionError.new(Repository, :manage) unless can_manage_repository?(@inventory)
|
||||
end
|
||||
|
||||
def load_create_params
|
||||
if connection_params[:parent_id].present? && connection_params[:child_id].present?
|
||||
raise MutuallyExclusiveParamsError.new(:parent_id, :child_id)
|
||||
end
|
||||
|
||||
if connection_params[:parent_id].present?
|
||||
@relation = :parent
|
||||
@inventory_item_to_link = RepositoryRow.find(connection_params[:parent_id])
|
||||
elsif connection_params[:child_id].present?
|
||||
@relation = :child
|
||||
@inventory_item_to_link = RepositoryRow.find(connection_params[:child_id])
|
||||
end
|
||||
|
||||
raise ActiveRecord::RecordNotFound unless @inventory_item_to_link
|
||||
end
|
||||
|
||||
def connection_params
|
||||
raise TypeError unless params.require(:data).require(:type) == 'inventory_item_relationships'
|
||||
|
||||
params.require(:data).require(:attributes).permit(%i(parent_id child_id))
|
||||
end
|
||||
|
||||
def permitted_includes
|
||||
%w(parent child)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
160
app/controllers/asset_sync_controller.rb
Normal file
160
app/controllers/asset_sync_controller.rb
Normal file
|
@ -0,0 +1,160 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AssetSyncController < ApplicationController
|
||||
skip_before_action :authenticate_user!, only: %i(update download)
|
||||
skip_before_action :verify_authenticity_token, only: %i(update download)
|
||||
before_action :authenticate_asset_sync_token!, only: %i(update download)
|
||||
before_action :check_asset_sync
|
||||
|
||||
def show
|
||||
asset = Asset.find_by(id: params[:asset_id])
|
||||
|
||||
render_error(:forbidden) and return unless asset && can_manage_asset?(asset)
|
||||
|
||||
asset_sync_token = current_user.asset_sync_tokens.find_or_create_by(asset_id: params[:asset_id])
|
||||
|
||||
unless asset_sync_token.token_valid?
|
||||
asset_sync_token = current_user.asset_sync_tokens.create(asset_id: params[:asset_id])
|
||||
end
|
||||
|
||||
render json: AssetSyncTokenSerializer.new(asset_sync_token).as_json
|
||||
end
|
||||
|
||||
def download
|
||||
redirect_to(@asset.file.url, allow_other_host: true)
|
||||
end
|
||||
|
||||
def update
|
||||
if @asset_sync_token.conflicts?(request.headers['VersionToken'])
|
||||
conflict_response = AssetSyncTokenSerializer.new(conflicting_asset_copy_token).as_json
|
||||
error_message = { message: I18n.t('assets.conflict_error', filename: @asset.file.filename) }
|
||||
render json: conflict_response.merge(error_message), status: :conflict
|
||||
return
|
||||
end
|
||||
|
||||
@asset.file.attach(io: request.body, filename: @asset.file.filename)
|
||||
@asset.update(last_modified_by: current_user)
|
||||
|
||||
log_activity
|
||||
|
||||
render json: AssetSyncTokenSerializer.new(@asset_sync_token).as_json
|
||||
end
|
||||
|
||||
def api_url
|
||||
render plain: Constants::ASSET_SYNC_URL
|
||||
end
|
||||
|
||||
def log_activity
|
||||
assoc ||= @asset.step
|
||||
assoc ||= @asset.result
|
||||
|
||||
case assoc
|
||||
when Step
|
||||
if assoc.protocol.in_module?
|
||||
log_step_activity(
|
||||
:edit_task_step_file_locally,
|
||||
assoc,
|
||||
assoc.my_module.project,
|
||||
my_module: assoc.my_module.id,
|
||||
file: @asset.file_name,
|
||||
user: current_user.id,
|
||||
step_position_original: @asset.step.position + 1,
|
||||
step: assoc.id
|
||||
)
|
||||
else
|
||||
log_step_activity(
|
||||
:edit_protocol_template_file_locally,
|
||||
assoc,
|
||||
nil,
|
||||
{
|
||||
file: @asset.file_name,
|
||||
user: current_user.id,
|
||||
step_position_original: @asset.step.position + 1,
|
||||
step: assoc.id
|
||||
}
|
||||
)
|
||||
end
|
||||
when Result
|
||||
log_result_activity(
|
||||
:edit_task_result_file_locally,
|
||||
assoc,
|
||||
file: @asset.file_name,
|
||||
user: current_user.id,
|
||||
result: Result.first.id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_error(status, filename = nil, message = nil)
|
||||
message ||= if filename.present?
|
||||
I18n.t('assets.default_error_with_filename', filename: filename)
|
||||
else
|
||||
I18n.t('assets.default_error')
|
||||
end
|
||||
|
||||
render json: { message: message }, status: status
|
||||
end
|
||||
|
||||
def conflicting_asset_copy_token
|
||||
Asset.transaction do
|
||||
new_asset = @asset.dup
|
||||
new_asset.save
|
||||
new_asset.file.attach(
|
||||
io: request.body,
|
||||
filename: "#{@asset.file.filename.base} (#{t('general.copy')}).#{@asset.file.filename.extension}"
|
||||
)
|
||||
|
||||
case @asset.parent
|
||||
when Step
|
||||
StepAsset.create!(step: @asset.step, asset: new_asset)
|
||||
when Result
|
||||
ResultAsset.create!(result: @asset.result, asset: new_asset)
|
||||
end
|
||||
|
||||
current_user.asset_sync_tokens.create!(asset_id: new_asset.id)
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_asset_sync_token!
|
||||
@asset_sync_token = AssetSyncToken.find_by(token: request.headers['Authentication'])
|
||||
|
||||
render_error(:unauthorized) and return unless @asset_sync_token&.token_valid?
|
||||
|
||||
@asset = @asset_sync_token.asset
|
||||
@current_user = @asset_sync_token.user
|
||||
|
||||
render_error(:forbidden, @asset.file.filename) and return unless can_manage_asset?(@asset)
|
||||
end
|
||||
|
||||
def log_step_activity(type_of, step, project = nil, message_items = {})
|
||||
default_items = { step: step.id,
|
||||
step_position: { id: step.id, value_for: 'position_plus_one' } }
|
||||
message_items = default_items.merge(message_items)
|
||||
|
||||
Activities::CreateActivityService
|
||||
.call(activity_type: type_of,
|
||||
owner: current_user,
|
||||
subject: step.protocol,
|
||||
team: step.protocol.team,
|
||||
project: project,
|
||||
message_items: message_items)
|
||||
end
|
||||
|
||||
def log_result_activity(type_of, result, message_items)
|
||||
Activities::CreateActivityService
|
||||
.call(activity_type: type_of,
|
||||
owner: current_user,
|
||||
subject: result,
|
||||
team: result.my_module.team,
|
||||
project: result.my_module.project,
|
||||
message_items: {
|
||||
result: result.id
|
||||
}.merge(message_items))
|
||||
end
|
||||
|
||||
def check_asset_sync
|
||||
render_404 if ENV['ASSET_SYNC_URL'].blank?
|
||||
end
|
||||
end
|
|
@ -187,6 +187,7 @@ class AssetsController < ApplicationController
|
|||
return render_403 unless can_read_team?(@asset.team)
|
||||
|
||||
@asset.file.attach(io: params.require(:image), filename: orig_file_name)
|
||||
@asset.last_modified_by = current_user
|
||||
@asset.save!
|
||||
create_edit_image_activity(@asset, current_user, :finish_editing)
|
||||
# release previous image space
|
||||
|
|
4
app/controllers/design_elements_controller.rb
Normal file
4
app/controllers/design_elements_controller.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
class DesignElementsController < ApplicationController
|
||||
def index
|
||||
end
|
||||
end
|
|
@ -8,19 +8,21 @@ class ExperimentsController < ApplicationController
|
|||
include Rails.application.routes.url_helpers
|
||||
include Breadcrumbs
|
||||
|
||||
before_action :load_project, only: %i(new create archive_group restore_group)
|
||||
before_action :load_project, only: %i(new create archive_group restore_group move)
|
||||
before_action :load_experiment, except: %i(new create archive_group restore_group
|
||||
inventory_assigning_experiment_filter actions_toolbar)
|
||||
before_action :check_read_permissions, except: %i(edit archive clone move new
|
||||
inventory_assigning_experiment_filter actions_toolbar
|
||||
move move_modal)
|
||||
before_action :load_experiments, only: %i(move_modal move)
|
||||
before_action :check_move_permissions, only: %i(move_modal move)
|
||||
before_action :check_read_permissions, except: %i(edit archive clone move move_modal new
|
||||
create archive_group restore_group
|
||||
inventory_assigning_experiment_filter actions_toolbar)
|
||||
before_action :check_canvas_read_permissions, only: %i(canvas)
|
||||
before_action :check_create_permissions, only: %i(new create)
|
||||
before_action :check_create_permissions, only: %i(new create move)
|
||||
before_action :check_manage_permissions, only: %i(edit batch_clone_my_modules)
|
||||
before_action :check_update_permissions, only: %i(update)
|
||||
before_action :check_archive_permissions, only: :archive
|
||||
before_action :check_clone_permissions, only: %i(clone_modal clone)
|
||||
before_action :check_move_permissions, only: %i(move_modal move)
|
||||
before_action :set_inline_name_editing, only: %i(canvas table module_archive)
|
||||
before_action :set_breadcrumbs_items, only: %i(canvas table module_archive)
|
||||
before_action :set_navigator, only: %i(canvas module_archive table)
|
||||
|
@ -254,7 +256,7 @@ class ExperimentsController < ApplicationController
|
|||
|
||||
# POST: clone_experiment(id)
|
||||
def clone
|
||||
project = current_team.projects.find(move_experiment_param)
|
||||
@project = current_team.projects.find(move_experiment_param)
|
||||
return render_403 unless can_create_project_experiments?(project)
|
||||
|
||||
service = Experiments::CopyExperimentAsTemplateService.call(experiment: @experiment,
|
||||
|
@ -274,7 +276,7 @@ class ExperimentsController < ApplicationController
|
|||
|
||||
# GET: move_modal_experiment_path(id)
|
||||
def move_modal
|
||||
@projects = @experiment.movable_projects(current_user)
|
||||
@projects = @experiments.first.movable_projects(current_user)
|
||||
render json: {
|
||||
html: render_to_string(partial: 'move_modal', formats: :html)
|
||||
}
|
||||
|
@ -297,23 +299,28 @@ class ExperimentsController < ApplicationController
|
|||
|
||||
# POST: move_experiment(id)
|
||||
def move
|
||||
service = Experiments::MoveToProjectService
|
||||
.call(experiment_id: @experiment.id,
|
||||
project_id: move_experiment_param,
|
||||
user_id: current_user.id)
|
||||
if service.succeed?
|
||||
flash[:success] = t('experiments.move.success_flash',
|
||||
experiment: @experiment.name)
|
||||
status = :ok
|
||||
view_state = @experiment.current_view_state(current_user)
|
||||
view_type = view_state.state['my_modules']['view_type'] || 'canvas'
|
||||
path = view_mode_redirect_url(view_type)
|
||||
else
|
||||
message = "#{service.errors.values.join('. ')}."
|
||||
status = :unprocessable_entity
|
||||
end
|
||||
@project.transaction do
|
||||
@experiments.each do |experiment|
|
||||
service = Experiments::MoveToProjectService
|
||||
.call(experiment_id: experiment.id,
|
||||
project_id: params[:project_id],
|
||||
user_id: current_user.id)
|
||||
raise StandardError unless service.succeed?
|
||||
end
|
||||
|
||||
render json: { message: message, path: path }, status: status
|
||||
flash[:success] = t('experiments.table.move_success_flash', project: escape_input(@project.name))
|
||||
render json: { message: t('experiments.table.move_success_flash',
|
||||
project: escape_input(@project.name)), path: project_path(@project) }
|
||||
rescue StandardError => e
|
||||
Rails.logger.error(e.message)
|
||||
Rails.logger.error(e.backtrace.join("\n"))
|
||||
render json: {
|
||||
message: t('experiments.table.move_error_flash', project: escape_input(@project.name))
|
||||
}, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def move_modules_modal
|
||||
|
@ -531,6 +538,11 @@ class ExperimentsController < ApplicationController
|
|||
render_404 unless @experiment
|
||||
end
|
||||
|
||||
def load_experiments
|
||||
@experiments = Experiment.preload(user_assignments: %i(user user_role)).where(id: params[:ids])
|
||||
render_404 unless @experiments
|
||||
end
|
||||
|
||||
def load_project
|
||||
@project = Project.find_by(id: params[:project_id])
|
||||
render_404 unless @project
|
||||
|
@ -583,7 +595,7 @@ class ExperimentsController < ApplicationController
|
|||
end
|
||||
|
||||
def check_move_permissions
|
||||
render_403 unless can_move_experiment?(@experiment)
|
||||
render_403 unless @experiments.all? { |e| can_move_experiment?(e) }
|
||||
end
|
||||
|
||||
def set_inline_name_editing
|
||||
|
|
|
@ -91,6 +91,7 @@ class GeneSequenceAssetsController < ApplicationController
|
|||
file.blob.metadata['name'] = params[:sequence_name]
|
||||
file.save!
|
||||
@asset.view_mode = view_mode || @parent.assets_view_mode
|
||||
@asset.last_modified_by = current_user
|
||||
@asset.save!
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,9 +5,9 @@ class ResultOrderableElementsController < ApplicationController
|
|||
before_action :check_manage_permissions
|
||||
|
||||
def reorder
|
||||
params[:result_orderable_element_positions].each do |id, position|
|
||||
result_element = @result.result_orderable_elements.find(id)
|
||||
ActiveRecord::Base.transaction do
|
||||
ActiveRecord::Base.transaction do
|
||||
params[:result_orderable_element_positions].each do |id, position|
|
||||
result_element = @result.result_orderable_elements.find(id)
|
||||
result_element.insert_at(position)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,7 @@ module StepElements
|
|||
checklist_item = @checklist.checklist_items.new(checklist_item_params.merge!(created_by: current_user))
|
||||
new_items = []
|
||||
ActiveRecord::Base.transaction do
|
||||
new_items = checklist_item.save_multiline!
|
||||
new_items = checklist_item.save_multiline!(after_id: params[:after_id])
|
||||
new_items.each do |item|
|
||||
log_activity(
|
||||
"#{@step.protocol.in_module? ? :task : :protocol}_step_checklist_item_added",
|
||||
|
@ -102,9 +102,10 @@ module StepElements
|
|||
end
|
||||
|
||||
def reorder
|
||||
checklist_item = @checklist.checklist_items.find(checklist_item_params[:id])
|
||||
checklist_item = @checklist.checklist_items.find(params[:id])
|
||||
ActiveRecord::Base.transaction do
|
||||
checklist_item.insert_at(checklist_item_params[:position])
|
||||
insert_at = (@checklist.checklist_items.find_by(id: params[:after_id])&.position || 0)
|
||||
checklist_item.insert_at(insert_at)
|
||||
end
|
||||
render json: params[:checklist_item_positions], status: :ok
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
|
|
|
@ -9,6 +9,7 @@ module Users
|
|||
|
||||
def index
|
||||
@label_printer_any = LabelPrinter.any?
|
||||
@user_agent = request.user_agent
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -52,6 +52,11 @@ module RepositoryDatatableHelper
|
|||
serialize_repository_cell_value(cell, team, repository, reminders_enabled: reminders_enabled)
|
||||
end
|
||||
|
||||
if repository.repository_columns.stock_type.exists?
|
||||
stock_cell = record.repository_cells.find { |cell| cell.value_type == 'RepositoryStockValue' }
|
||||
row['stock'] = serialize_repository_cell_value(record.repository_stock_cell, team, repository) if stock_cell.present?
|
||||
end
|
||||
|
||||
if has_stock_management
|
||||
stock_cell = record.repository_cells.find { |cell| cell.value_type == 'RepositoryStockValue' }
|
||||
|
||||
|
|
56
app/javascript/packs/vue/design_system/modals.js
Normal file
56
app/javascript/packs/vue/design_system/modals.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||
import { shallowRef } from 'vue';
|
||||
|
||||
import WizardModal from '../../../vue/shared/wizard_modal.vue';
|
||||
import Step1 from './wizard_steps/step_1.vue';
|
||||
import Step2 from './wizard_steps/step_2.vue';
|
||||
import Step3 from './wizard_steps/step_3.vue';
|
||||
import { mountWithTurbolinks } from '../helpers/turbolinks.js';
|
||||
|
||||
const app = createApp({
|
||||
components: {
|
||||
Step1,
|
||||
Step2,
|
||||
Step3
|
||||
},
|
||||
methods: {
|
||||
fireAlert() {
|
||||
alert('Fired!');
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
wizardConfig: {
|
||||
title: 'Wizard steps',
|
||||
subtitle: 'Wizard subtitle description',
|
||||
steps: [
|
||||
{
|
||||
id: 'step1',
|
||||
icon: 'sn-icon sn-icon-open',
|
||||
label: 'Step 1',
|
||||
component: shallowRef(Step1)
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
icon: 'sn-icon sn-icon-edit',
|
||||
label: 'Step 2',
|
||||
component: shallowRef(Step2)
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
icon: 'sn-icon sn-icon-inventory',
|
||||
label: 'Step 3',
|
||||
component: shallowRef(Step3)
|
||||
}
|
||||
]
|
||||
},
|
||||
wizardParams: {
|
||||
text: 'Some text'
|
||||
},
|
||||
showWizard: false
|
||||
};
|
||||
}
|
||||
});
|
||||
app.component('WizardModal', WizardModal);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
mountWithTurbolinks(app, '#modals');
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<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 flex items-center gap-4">
|
||||
Step 1 actions
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body grow">
|
||||
You can add any custom html here or render params like this:
|
||||
|
||||
{{ params }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-light" @click="$emit('close')">Cancel</button>
|
||||
<button class="btn btn-primary" @click="$emit('next')">Next</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
emits: ['close', 'next', 'back'],
|
||||
name: 'Step1',
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
wizardComponent: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<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 flex items-center gap-4">
|
||||
Step 2 actions
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body grow">
|
||||
All steps have access to shared params
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Type something"
|
||||
v-model="params.text"/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-light" @click="$emit('back')">Back</button>
|
||||
<button class="btn btn-primary" @click="$emit('next')">Next</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
emits: ['back', 'next', 'close'],
|
||||
name: 'Step2',
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
wizardComponent: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<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 flex items-center gap-4">
|
||||
Step 3 actions
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body grow">
|
||||
Our params - {{ params }}<br>
|
||||
|
||||
If you want emit action use wizardComponent
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<button class="btn btn-danger" @click="wizardComponent.$emit('alert')">Launch alert</button>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-light" @click="$emit('back')">Back</button>
|
||||
<button class="btn btn-primary" @click="$emit('close')">Apply</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
emits: ['back', 'close', 'alert', 'next'],
|
||||
name: 'Step3',
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
wizardComponent: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
8
app/javascript/packs/vue/open_locally_menu.js
Normal file
8
app/javascript/packs/vue/open_locally_menu.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||
import OpenLocallyMenu from '../../vue/shared/content/attachments/open_locally_menu.vue';
|
||||
import { mountWithTurbolinks } from './helpers/turbolinks.js';
|
||||
|
||||
const app = createApp({});
|
||||
app.component('OpenLocallyMenu', OpenLocallyMenu);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
mountWithTurbolinks(app, '#openLocallyMenu');
|
8
app/javascript/packs/vue/scinote_edit_download.js
Normal file
8
app/javascript/packs/vue/scinote_edit_download.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { createApp } from 'vue/dist/vue.esm-bundler.js';
|
||||
import ScinoteEditDownload from '../../vue/shared/scinote_edit_download.vue';
|
||||
import { mountWithTurbolinks } from './helpers/turbolinks.js';
|
||||
|
||||
const app = createApp({});
|
||||
app.component('ScinoteEditDownload', ScinoteEditDownload);
|
||||
app.config.globalProperties.i18n = window.I18n;
|
||||
mountWithTurbolinks(app, '#scinoteEditDownload');
|
|
@ -6,9 +6,9 @@
|
|||
id="repositoryItemRelationshipsModal"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
class="modal ">
|
||||
class="modal">
|
||||
<div class="modal-dialog modal-sm" role="document">
|
||||
<div class="modal-content w-[400px] m-auto">
|
||||
<div class="modal-content w-[400px] m-auto" v-click-outside="handleClickOutside">
|
||||
|
||||
<!-- header -->
|
||||
<div class="modal-header h-[76px] flex !flex-col gap-[6px]">
|
||||
|
@ -167,6 +167,10 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
handleClickOutside() {
|
||||
this.selectedInventoryValue = null;
|
||||
this.resetSelectedItemValues();
|
||||
},
|
||||
fetchInventories() {
|
||||
if (!this.nextInventoriesPage) return;
|
||||
|
||||
|
|
|
@ -180,7 +180,7 @@ export default {
|
|||
this.secondaryNavigation.style.top = '0px';
|
||||
header.style.top = '0px';
|
||||
header.style.boxShadow = 'none';
|
||||
header.style.zIndex = 105;
|
||||
header.style.zIndex = 100;
|
||||
this.headerSticked = false;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
<div class="sci--navigation--notificaitons-flyout">
|
||||
<div class="sci--navigation--notificaitons-flyout-title">
|
||||
{{ i18n.t('nav.notifications.title') }}
|
||||
<i class="sn-icon sn-icon-close" @click="$emit('close')"></i>
|
||||
<a class="ml-auto cursor-pointer text-sm font-normal" :href="this.preferencesUrl" :title="i18n.t('nav.settings')">
|
||||
{{ i18n.t('nav.settings') }}
|
||||
</a>
|
||||
</div>
|
||||
<hr>
|
||||
<perfect-scrollbar ref="scrollContainer" class="sci--navigation--notificaitons-flyout-notifications">
|
||||
|
@ -35,7 +37,8 @@ export default {
|
|||
},
|
||||
props: {
|
||||
notificationsUrl: String,
|
||||
unseenNotificationsCount: Number
|
||||
unseenNotificationsCount: Number,
|
||||
preferencesUrl: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
<NotificationsFlyout
|
||||
v-if="notificationsOpened"
|
||||
:notificationsUrl="notificationsUrl"
|
||||
:preferencesUrl="this.userMenu.find((item) => item.name === i18n.t('users.settings.sidebar.account_nav.preferences'))?.url"
|
||||
:unseenNotificationsCount="unseenNotificationsCount"
|
||||
@update:unseenNotificationsCount="checkUnseenNotifications()"
|
||||
@close="notificationsOpened = false" />
|
||||
|
|
|
@ -159,7 +159,7 @@
|
|||
</div>
|
||||
<div v-if="parentsCount">
|
||||
<details v-for="(parent) in parents" @toggle="updateOpenState(parent.code, $event.target.open)" :key="parent.code" class="flex flex-col font-normal gap-4 group cursor-default">
|
||||
<summary class="flex flex-row gap-3 mb-4 relative">
|
||||
<summary class="flex flex-row gap-3 mb-4 relative group">
|
||||
<img :src="icons.delimiter_path" class="w-3 h-3 cursor-pointer flex-shrink-0 relative top-1"
|
||||
:class="{ 'rotate-90': relationshipDetailsState[parent.code] }" />
|
||||
<span>
|
||||
|
@ -205,8 +205,8 @@
|
|||
</div>
|
||||
<div v-if="childrenCount">
|
||||
<details v-for="(child) in children" :key="child.code" @toggle="updateOpenState(child.code, $event.target.open)"
|
||||
class="flex flex-col font-normal gap-4 group last-of-type:[&>p:last-child]:mb-0">
|
||||
<summary class="flex flex-row gap-3 mb-4 relative"
|
||||
class="flex flex-col font-normal gap-4 group-last-of-type:[&>p:last-child]:mb-0">
|
||||
<summary class="flex flex-row gap-3 mb-4 relative group"
|
||||
:class="{ 'group-last-of-type:mb-0': !relationshipDetailsState[child.code] }">
|
||||
<img :src="icons.delimiter_path" class="w-3 h-3 flex-shrink-0 cursor-pointer relative top-1"
|
||||
:class="{ 'rotate-90': relationshipDetailsState[child.code] }"/>
|
||||
|
@ -239,7 +239,7 @@
|
|||
<div v-if="!repository?.is_snapshot" id="divider" class="w-500 bg-sn-light-grey flex px-8 items-center self-stretch h-px"></div>
|
||||
|
||||
<!-- ASSIGNED -->
|
||||
<section id="assigned-section" class="flex flex-col" ref="assignedSectionRef">
|
||||
<section v-if="!repository?.is_snapshot" id="assigned-section" class="flex flex-col" ref="assignedSectionRef">
|
||||
<div
|
||||
class="flex flex-row text-lg font-semibold w-[350px] mb-6 leading-7 items-center justify-between transition-colors duration-300"
|
||||
ref="assigned-label"
|
||||
|
@ -272,10 +272,7 @@
|
|||
<div class="flex flex-col gap-2">
|
||||
<div v-for="(item, index_assigned) in assigned" :key="`assigned_element_${index_assigned}`">
|
||||
{{ i18n.t(`repositories.item_card.assigned.labels.${item.type}`) }}
|
||||
<a v-if="defaultColumns.archived" :href="item.url" class="text-sn-science-blue hover:text-sn-science-blue hover:no-underline">
|
||||
{{ i18n.t('labels.archived')}} {{ item.value }}
|
||||
</a>
|
||||
<a v-else :href="item.url" class="text-sn-science-blue hover:text-sn-science-blue hover:no-underline">
|
||||
<a :href="item.url" class="text-sn-science-blue hover:text-sn-science-blue hover:no-underline">
|
||||
{{ item.archived ? i18n.t('labels.archived') : '' }} {{ item.value }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -289,7 +286,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<div id="divider" class="w-500 bg-sn-light-grey flex px-8 items-center self-stretch h-px "></div>
|
||||
<div v-if="!repository?.is_snapshot" id="divider" class="w-500 bg-sn-light-grey flex px-8 items-center self-stretch h-px "></div>
|
||||
|
||||
<!-- QR -->
|
||||
<section id="qr-section" ref="QR-label">
|
||||
|
@ -352,35 +349,40 @@ const items = [
|
|||
textId: 'text-item-1',
|
||||
labelAlias: 'information_label',
|
||||
label: 'information-label',
|
||||
sectionId: 'information-section'
|
||||
sectionId: 'information-section',
|
||||
showInSnapshot: true
|
||||
},
|
||||
{
|
||||
id: 'highlight-item-2',
|
||||
textId: 'text-item-2',
|
||||
labelAlias: 'custom_columns_label',
|
||||
label: 'custom-columns-label',
|
||||
sectionId: 'custom-columns-section'
|
||||
sectionId: 'custom-columns-section',
|
||||
showInSnapshot: true
|
||||
},
|
||||
{
|
||||
id: 'highlight-item-3',
|
||||
textId: 'text-item-3',
|
||||
labelAlias: 'relationships_label',
|
||||
label: 'relationships-label',
|
||||
sectionId: 'relationships-section'
|
||||
sectionId: 'relationships-section',
|
||||
showInSnapshot: false
|
||||
},
|
||||
{
|
||||
id: 'highlight-item-4',
|
||||
textId: 'text-item-4',
|
||||
labelAlias: 'assigned_label',
|
||||
label: 'assigned-label',
|
||||
sectionId: 'assigned-section'
|
||||
sectionId: 'assigned-section',
|
||||
showInSnapshot: false
|
||||
},
|
||||
{
|
||||
id: 'highlight-item-5',
|
||||
textId: 'text-item-5',
|
||||
labelAlias: 'QR_label',
|
||||
label: 'QR-label',
|
||||
sectionId: 'qr-section'
|
||||
sectionId: 'qr-section',
|
||||
showInSnapshot: true
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -460,7 +462,7 @@ export default {
|
|||
methods: {
|
||||
filterNavigationItems() {
|
||||
if (this.repository.is_snapshot) {
|
||||
return items.filter((item) => item.id !== 'highlight-item-3');
|
||||
return items.filter((item) => item.showInSnapshot);
|
||||
}
|
||||
return items;
|
||||
},
|
||||
|
@ -546,12 +548,8 @@ export default {
|
|||
},
|
||||
handleOpeningFromBootstrapModal() {
|
||||
const layout = document.querySelector('.sci--layout');
|
||||
const openModals = layout.querySelectorAll('.modal');
|
||||
openModals.forEach((modal) => {
|
||||
if ($(modal).hasClass('in') && !$(modal).hasClass('full-screen')) {
|
||||
$(modal).modal('hide');
|
||||
}
|
||||
});
|
||||
const openModals = layout.querySelectorAll('.modal.in:not(.modal-full-screen)');
|
||||
openModals.forEach((modal) => $(modal).modal('hide'));
|
||||
},
|
||||
loadRepositoryRow(repositoryRowUrl, scrollTop = 0) {
|
||||
this.dataLoading = true;
|
||||
|
|
|
@ -91,6 +91,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
/* global HelperModule */
|
||||
|
||||
import DropdownSelector from '../shared/dropdown_selector.vue';
|
||||
import LabelPreview from '../label_template/components/label_preview.vue';
|
||||
|
||||
|
@ -203,8 +205,14 @@ export default {
|
|||
this.labelTemplateError = null;
|
||||
this.labelTemplateCode = result.label_code;
|
||||
}).fail((result) => {
|
||||
this.labelTemplateError = result.responseJSON.error;
|
||||
this.labelTemplateCode = result.responseJSON.label_code;
|
||||
if (result.responseJSON) {
|
||||
this.labelTemplateError = result.responseJSON.error;
|
||||
this.labelTemplateCode = result.responseJSON.label_code;
|
||||
} else {
|
||||
this.labelTemplateError = null;
|
||||
this.labelTemplateCode = null;
|
||||
HelperModule.flashAlertMsg(this.i18n.t('repository_row.modal_print_label.general_error'), 'danger');
|
||||
}
|
||||
});
|
||||
},
|
||||
submitPrint() {
|
||||
|
@ -233,6 +241,8 @@ export default {
|
|||
$(this.$refs.modal).modal('hide');
|
||||
this.$emit('close');
|
||||
PrintProgressModal.init(data);
|
||||
}).fail(() => {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('repository_row.modal_print_label.general_error'), 'danger');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div class="result-wrapper p-4 mb-4 rounded pr-8 relative"
|
||||
<div ref="resultContainer"
|
||||
class="result-wrapper p-4 mb-4 rounded pr-8 relative"
|
||||
@drop.prevent="dropFile"
|
||||
@dragenter.prevent="dragEnter($event)"
|
||||
@dragover.prevent
|
||||
|
@ -170,7 +171,8 @@ export default {
|
|||
{ text: I18n.t('protocols.steps.insert.well_plate_options.16_x_24'), emit: 'create:table', params: [[16, 24], true] },
|
||||
{ text: I18n.t('protocols.steps.insert.well_plate_options.8_x_12'), emit: 'create:table', params: [[8, 12], true] },
|
||||
{ text: I18n.t('protocols.steps.insert.well_plate_options.6_x_8'), emit: 'create:table', params: [[6, 8], true] },
|
||||
{ text: I18n.t('protocols.steps.insert.well_plate_options.6_x_4'), emit: 'create:table', params: [[6, 4], true] },
|
||||
{ text: I18n.t('protocols.steps.insert.well_plate_options.4_x_6'), emit: 'create:table', params: [[4, 6], true] },
|
||||
{ text: I18n.t('protocols.steps.insert.well_plate_options.3_x_4'), emit: 'create:table', params: [[3, 4], true] },
|
||||
{ text: I18n.t('protocols.steps.insert.well_plate_options.2_x_3'), emit: 'create:table', params: [[2, 3], true] }
|
||||
],
|
||||
editingName: false,
|
||||
|
@ -191,7 +193,7 @@ export default {
|
|||
},
|
||||
watch: {
|
||||
resultToReload() {
|
||||
if (this.resultToReload === this.result.id) {
|
||||
if (Number(this.resultToReload) === Number(this.result.id)) {
|
||||
this.loadElements();
|
||||
this.loadAttachments();
|
||||
}
|
||||
|
@ -434,9 +436,10 @@ export default {
|
|||
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
|
||||
}).done(() => {
|
||||
this.$parent.$nextTick(() => {
|
||||
const children = this.$refs.stepContainer.querySelectorAll('.result-element');
|
||||
const children = this.$refs.resultContainer.querySelectorAll('.result-element');
|
||||
const lastChild = children[children.length - 1];
|
||||
lastChild.$el.scrollIntoView(false);
|
||||
|
||||
lastChild.scrollIntoView(false);
|
||||
window.scrollBy({
|
||||
top: 200,
|
||||
behavior: 'smooth'
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<div class="asset-context-menu" ref="menu">
|
||||
<div class="asset-context-menu"
|
||||
ref="menu"
|
||||
@mouseenter="fetchLocalAppInfo"
|
||||
>
|
||||
<a class="marvinjs-edit-button hidden"
|
||||
v-if="attachment.attributes.asset_type == 'marvinjs' && attachment.attributes.urls.marvin_js_start_edit"
|
||||
ref="marvinjsEditButton"
|
||||
|
@ -30,21 +33,41 @@
|
|||
@open_ove_editor="openOVEditor(attachment.attributes.urls.open_vector_editor_edit)"
|
||||
@open_marvinjs_editor="openMarvinJsEditor"
|
||||
@open_scinote_editor="openScinoteEditor"
|
||||
@open_locally="openLocally"
|
||||
@delete="deleteModal = true"
|
||||
@viewMode="changeViewMode"
|
||||
@move="showMoveModal"
|
||||
@menu-visibility-changed="$emit('menu-visibility-changed', $event)"
|
||||
></MenuDropdown>
|
||||
<deleteAttachmentModal
|
||||
<Teleport to="body">
|
||||
<deleteAttachmentModal
|
||||
v-if="deleteModal"
|
||||
:fileName="attachment.attributes.file_name"
|
||||
@confirm="deleteAttachment"
|
||||
@cancel="deleteModal = false"
|
||||
/>
|
||||
<moveAssetModal v-if="movingAttachment"
|
||||
:parent_type="attachment.attributes.parent_type"
|
||||
:targets_url="attachment.attributes.urls.move_targets"
|
||||
@confirm="moveAttachment($event)" @cancel="closeMoveModal"/>
|
||||
/>
|
||||
<moveAssetModal
|
||||
v-if="movingAttachment"
|
||||
:parent_type="attachment.attributes.parent_type"
|
||||
:targets_url="attachment.attributes.urls.move_targets"
|
||||
@confirm="moveAttachment($event)" @cancel="closeMoveModal"
|
||||
/>
|
||||
<NoPredefinedAppModal
|
||||
v-if="showNoPredefinedAppModal"
|
||||
:fileName="attachment.attributes.file_name"
|
||||
@confirm="showNoPredefinedAppModal = false"
|
||||
/>
|
||||
<UpdateVersionModal
|
||||
v-if="showUpdateVersionModal"
|
||||
@cancel="showUpdateVersionModal = false"
|
||||
/>
|
||||
<editLaunchingApplicationModal
|
||||
v-if="editAppModal"
|
||||
:fileName="attachment.attributes.file_name"
|
||||
:application="this.localAppName"
|
||||
@cancel="editAppModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -52,12 +75,17 @@
|
|||
import deleteAttachmentModal from './delete_modal.vue';
|
||||
import moveAssetModal from '../modal/move.vue';
|
||||
import MoveMixin from './mixins/move.js';
|
||||
import OpenLocallyMixin from './mixins/open_locally.js';
|
||||
import MenuDropdown from '../../menu_dropdown.vue';
|
||||
|
||||
export default {
|
||||
name: 'contextMenu',
|
||||
components: { deleteAttachmentModal, moveAssetModal, MenuDropdown },
|
||||
mixins: [MoveMixin],
|
||||
components: {
|
||||
deleteAttachmentModal,
|
||||
moveAssetModal,
|
||||
MenuDropdown
|
||||
},
|
||||
mixins: [MoveMixin, OpenLocallyMixin],
|
||||
props: {
|
||||
attachment: {
|
||||
type: Object,
|
||||
|
@ -94,13 +122,23 @@ export default {
|
|||
});
|
||||
}
|
||||
if (this.attachment.attributes.asset_type !== 'marvinjs'
|
||||
&& this.attachment.attributes.image_editable
|
||||
&& this.attachment.attributes.urls.start_edit_image) {
|
||||
&& this.attachment.attributes.image_editable
|
||||
&& this.attachment.attributes.urls.start_edit_image) {
|
||||
menu.push({
|
||||
text: this.i18n.t('assets.file_preview.edit_in_scinote'),
|
||||
emit: 'open_scinote_editor'
|
||||
});
|
||||
}
|
||||
if (this.canOpenLocally) {
|
||||
const text = this.localAppName
|
||||
? this.i18n.t('attachments.open_locally_in', { application: this.localAppName })
|
||||
: this.i18n.t('attachments.open_locally');
|
||||
|
||||
menu.push({
|
||||
text,
|
||||
emit: 'open_locally'
|
||||
});
|
||||
}
|
||||
menu.push({
|
||||
text: this.i18n.t('Download'),
|
||||
url: this.attachment.attributes.urls.download,
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import axios from '../../../../../packs/custom_axios.js';
|
||||
import { satisfies } from 'compare-versions';
|
||||
import editLaunchingApplicationModal from '../../modal/edit_launching_application_modal.vue';
|
||||
import NoPredefinedAppModal from '../../modal/no_predefined_app_modal.vue';
|
||||
import UpdateVersionModal from '../../modal/update_version_modal.vue';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
localAppName: null,
|
||||
scinoteEditRunning: false,
|
||||
scinoteEditVersion: null,
|
||||
showNoPredefinedAppModal: false,
|
||||
showUpdateVersionModal: false,
|
||||
editAppModal: false
|
||||
};
|
||||
},
|
||||
components: {
|
||||
editLaunchingApplicationModal,
|
||||
NoPredefinedAppModal,
|
||||
UpdateVersionModal
|
||||
},
|
||||
computed: {
|
||||
attributes() {
|
||||
return this.attachment.attributes;
|
||||
},
|
||||
canOpenLocally() {
|
||||
return this.scinoteEditRunning
|
||||
&& !!this.attributes.urls.open_locally
|
||||
&& this.attributes.asset_type !== 'gene_sequence'
|
||||
&& this.attributes.asset_type !== 'marvinjs';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchLocalAppInfo() {
|
||||
try {
|
||||
const statusResponse = await axios.get(
|
||||
`${this.attributes.urls.open_locally_api}/status`
|
||||
);
|
||||
|
||||
if (statusResponse.status === 200) {
|
||||
this.scinoteEditRunning = true;
|
||||
this.scinoteEditVersion = statusResponse.data.version;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios.get(
|
||||
`${this.attributes.urls.open_locally_api}/default-application/${this.attributes.file_extension}`
|
||||
);
|
||||
|
||||
if (response.data.application.toLowerCase() !== 'pick an app') {
|
||||
this.localAppName = response.data.application;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in request: ', error);
|
||||
}
|
||||
},
|
||||
async openLocally() {
|
||||
if (this.isWrongVersion(this.scinoteEditVersion)) {
|
||||
this.showUpdateVersionModal = true;
|
||||
return;
|
||||
} else if (this.localAppName === null) {
|
||||
this.showNoPredefinedAppModal = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.editAppModal = true;
|
||||
try {
|
||||
const { data } = await axios.get(this.attributes.urls.open_locally);
|
||||
await axios.post(`${this.attributes.urls.open_locally_api}/download`, data);
|
||||
} catch (error) {
|
||||
console.error('Error in request:', error);
|
||||
}
|
||||
},
|
||||
isWrongVersion(version) {
|
||||
const { min, max } = this.attributes.edit_version_range;
|
||||
return !satisfies(version, `${min} - ${max}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
<template>
|
||||
<div class="sn-open-locally-menu" @mouseenter="fetchLocalAppInfo">
|
||||
<div v-if="!canOpenLocally && (attachment.attributes.wopi && attachment.attributes.urls.edit_asset)">
|
||||
<a :href="`${attachment.attributes.urls.edit_asset}`" target="_blank"
|
||||
class="block whitespace-nowrap rounded px-3 py-2.5
|
||||
hover:!text-sn-blue hover:no-underline cursor-pointer hover:bg-sn-super-light-grey">
|
||||
{{ attachment.attributes.wopi_context.button_text }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MenuDropdown
|
||||
v-if="this.menu.length > 1"
|
||||
class="ml-auto"
|
||||
:listItems="this.menu"
|
||||
:btnClasses="`btn btn-light icon-btn`"
|
||||
:position="'right'"
|
||||
:btnText="i18n.t('attachments.open_in')"
|
||||
:caret="true"
|
||||
@open-locally="openLocally"
|
||||
@open-image-editor="openImageEditor"
|
||||
></MenuDropdown>
|
||||
<a v-else-if="menu.length === 1" class="btn btn-light !bg-sn-white" :href="menu[0].url" :target="menu[0].target" @click="this[this.menu[0].emit]()">
|
||||
{{ menu[0].text }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<NoPredefinedAppModal
|
||||
v-if="showNoPredefinedAppModal"
|
||||
:fileName="attachment.attributes.file_name"
|
||||
@confirm="showNoPredefinedAppModal = false"
|
||||
/>
|
||||
<editLaunchingApplicationModal
|
||||
v-if="editAppModal"
|
||||
:fileName="attachment.attributes.file_name"
|
||||
:application="this.localAppName"
|
||||
@cancel="editAppModal = false"
|
||||
/>
|
||||
<UpdateVersionModal
|
||||
v-if="showUpdateVersionModal"
|
||||
@cancel="showUpdateVersionModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OpenLocallyMixin from './mixins/open_locally.js';
|
||||
import MenuDropdown from '../../menu_dropdown.vue';
|
||||
import UpdateVersionModal from '../modal/update_version_modal.vue';
|
||||
|
||||
export default {
|
||||
name: 'OpenLocallyMenu',
|
||||
mixins: [OpenLocallyMixin],
|
||||
components: { MenuDropdown, UpdateVersionModal },
|
||||
props: {
|
||||
attachment: { type: Object, required: true }
|
||||
},
|
||||
created() {
|
||||
this.fetchLocalAppInfo();
|
||||
window.openLocallyMenu = this;
|
||||
},
|
||||
beforeUnmount() {
|
||||
delete window.openLocallyMenuComponent;
|
||||
},
|
||||
computed: {
|
||||
menu() {
|
||||
const menu = [];
|
||||
|
||||
if (this.attachment.attributes.wopi && this.attachment.attributes.urls.edit_asset) {
|
||||
menu.push({
|
||||
text: this.attachment.attributes.wopi_context.button_text,
|
||||
url: this.attachment.attributes.urls.edit_asset,
|
||||
url_target: '_blank'
|
||||
});
|
||||
}
|
||||
|
||||
if (this.attachment.attributes.image_editable && !this.canOpenLocally) {
|
||||
menu.push({
|
||||
text: this.i18n.t('assets.file_preview.edit_in_scinote'),
|
||||
emit: 'openImageEditor'
|
||||
});
|
||||
}
|
||||
|
||||
if (this.canOpenLocally) {
|
||||
const text = this.localAppName
|
||||
? this.i18n.t('attachments.open_locally_in', { application: this.localAppName })
|
||||
: this.i18n.t('attachments.open_locally');
|
||||
|
||||
menu.push({
|
||||
text,
|
||||
emit: 'openLocally'
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openImageEditor() {
|
||||
document.getElementById('editImageButton').click();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,8 +1,9 @@
|
|||
<template>
|
||||
<div class="attachment-container asset"
|
||||
:data-asset-id="attachment.id"
|
||||
@mouseover="showOptions = true"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
v-click-outside="handleClickOutsideThumbnail"
|
||||
>
|
||||
<a :class="{ hidden: showOptions }"
|
||||
:href="attachment.attributes.urls.blob"
|
||||
|
@ -44,20 +45,39 @@
|
|||
{{ attachment.attributes.file_size_formatted }}
|
||||
</div>
|
||||
<div class="absolute bottom-4 w-[184px] grid grid-cols-[repeat(4,_2.5rem)] justify-between">
|
||||
<MenuDropdown
|
||||
v-if="multipleOpenOptions.length > 1"
|
||||
:listItems="multipleOpenOptions"
|
||||
:btnClasses="'btn btn-light icon-btn thumbnail-action-btn'"
|
||||
:position="'left'"
|
||||
:btnIcon="'sn-icon sn-icon-open'"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.open')"
|
||||
@menu-visibility-changed="handleMenuVisibilityChange"
|
||||
@open_locally="openLocally"
|
||||
@open_scinote_editor="openScinoteEditor"
|
||||
></MenuDropdown>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
v-if="this.attachment.attributes.wopi && this.attachment.attributes.urls.edit_asset"
|
||||
v-else-if="canOpenLocally"
|
||||
@click="openLocally"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.open')"
|
||||
>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn"
|
||||
v-else-if="this.attachment.attributes.wopi && this.attachment.attributes.urls.edit_asset"
|
||||
:href="attachment.attributes.urls.edit_asset"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.open')"
|
||||
id="wopi_file_edit_button"
|
||||
:class="attachment.attributes.wopi_context.edit_supported ? '' : 'disabled'"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="sn-icon sn-icon-edit"></i>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn ove-edit-button"
|
||||
v-else-if="attachment.attributes.asset_type == 'gene_sequence' && attachment.attributes.urls.open_vector_editor_edit"
|
||||
@click="openOVEditor(attachment.attributes.urls.open_vector_editor_edit)"
|
||||
>
|
||||
<i class="sn-icon sn-icon-edit"></i>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn marvinjs-edit-button"
|
||||
v-else-if="attachment.attributes.asset_type == 'marvinjs' && attachment.attributes.urls.marvin_js_start_edit"
|
||||
|
@ -67,11 +87,11 @@
|
|||
:data-sketch-name="attachment.attributes.metadata.name"
|
||||
:data-sketch-description="attachment.attributes.metadata.description"
|
||||
>
|
||||
<i class="sn-icon sn-icon-edit"></i>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
<a class="btn btn-light icon-btn thumbnail-action-btn image-edit-button"
|
||||
v-else-if="attachment.attributes.image_editable && attachment.attributes.urls.edit_asset"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.edit')"
|
||||
:title="i18n.t('attachments.thumbnail.buttons.open')"
|
||||
:data-image-id="attachment.id"
|
||||
:data-image-name="attachment.attributes.file_name"
|
||||
:data-image-url="attachment.attributes.urls.asset_file"
|
||||
|
@ -79,7 +99,7 @@
|
|||
:data-image-mime-type="attachment.attributes.image_context && attachment.attributes.image_context.type"
|
||||
:data-image-start-edit-url="attachment.attributes.urls.start_edit_image"
|
||||
>
|
||||
<i class="sn-icon sn-icon-edit"></i>
|
||||
<i class="sn-icon sn-icon-open"></i>
|
||||
</a>
|
||||
<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>
|
||||
|
@ -114,12 +134,34 @@
|
|||
@confirm="deleteAttachment"
|
||||
@cancel="deleteModal = false"
|
||||
/>
|
||||
<moveAssetModal v-if="movingAttachment"
|
||||
:parent_type="attachment.attributes.parent_type"
|
||||
:targets_url="attachment.attributes.urls.move_targets"
|
||||
@confirm="moveAttachment($event)" @cancel="closeMoveModal"/>
|
||||
<moveAssetModal
|
||||
v-if="movingAttachment"
|
||||
:parent_type="attachment.attributes.parent_type"
|
||||
:targets_url="attachment.attributes.urls.move_targets"
|
||||
@confirm="moveAttachment($event)" @cancel="closeMoveModal"
|
||||
/>
|
||||
<NoPredefinedAppModal
|
||||
v-if="showNoPredefinedAppModal"
|
||||
:fileName="attachment.attributes.file_name"
|
||||
@confirm="showNoPredefinedAppModal = false"
|
||||
/>
|
||||
<UpdateVersionModal
|
||||
v-if="showUpdateVersionModal"
|
||||
@cancel="showUpdateVersionModal = false"
|
||||
/>
|
||||
<a class="image-edit-button hidden"
|
||||
v-if="attachment.attributes.asset_type != 'marvinjs'
|
||||
&& attachment.attributes.image_editable
|
||||
&& attachment.attributes.urls.start_edit_image"
|
||||
ref="imageEditButton"
|
||||
:data-image-id="attachment.id"
|
||||
:data-image-name="attachment.attributes.file_name"
|
||||
:data-image-url="attachment.attributes.urls.asset_file"
|
||||
:data-image-quality="attachment.attributes.image_context.quality"
|
||||
:data-image-mime-type="attachment.attributes.image_context.type"
|
||||
:data-image-start-edit-url="attachment.attributes.urls.start_edit_image"
|
||||
></a>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -127,13 +169,21 @@ import AttachmentMovedMixin from './mixins/attachment_moved.js';
|
|||
import ContextMenuMixin from './mixins/context_menu.js';
|
||||
import ContextMenu from './context_menu.vue';
|
||||
import deleteAttachmentModal from './delete_modal.vue';
|
||||
import MenuDropdown from '../../../shared/menu_dropdown.vue';
|
||||
import MoveAssetModal from '../modal/move.vue';
|
||||
import MoveMixin from './mixins/move.js';
|
||||
import OpenLocallyMixin from './mixins/open_locally.js';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
|
||||
export default {
|
||||
name: 'thumbnailAttachment',
|
||||
mixins: [ContextMenuMixin, AttachmentMovedMixin, MoveMixin],
|
||||
components: { ContextMenu, deleteAttachmentModal, MoveAssetModal },
|
||||
mixins: [ContextMenuMixin, AttachmentMovedMixin, MoveMixin, OpenLocallyMixin],
|
||||
components: {
|
||||
ContextMenu,
|
||||
deleteAttachmentModal,
|
||||
MoveAssetModal,
|
||||
MenuDropdown
|
||||
},
|
||||
props: {
|
||||
attachment: {
|
||||
type: Object,
|
||||
|
@ -147,10 +197,44 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
showOptions: false,
|
||||
isMenuOpen: false,
|
||||
deleteModal: false
|
||||
deleteModal: false,
|
||||
isMenuOpen: false
|
||||
};
|
||||
},
|
||||
directives: {
|
||||
'click-outside': vOnClickOutside
|
||||
},
|
||||
computed: {
|
||||
multipleOpenOptions() {
|
||||
const options = [];
|
||||
if (this.attachment.attributes.wopi && this.attachment.attributes.urls.edit_asset) {
|
||||
options.push({
|
||||
text: this.attachment.attributes.wopi_context.button_text,
|
||||
url: this.attachment.attributes.urls.edit_asset,
|
||||
url_target: '_blank'
|
||||
});
|
||||
}
|
||||
if (this.attachment.attributes.asset_type !== 'marvinjs'
|
||||
&& this.attachment.attributes.image_editable
|
||||
&& this.attachment.attributes.urls.start_edit_image) {
|
||||
options.push({
|
||||
text: this.i18n.t('assets.file_preview.edit_in_scinote'),
|
||||
emit: 'open_scinote_editor'
|
||||
});
|
||||
}
|
||||
if (this.canOpenLocally) {
|
||||
const text = this.localAppName
|
||||
? this.i18n.t('attachments.open_locally_in', { application: this.localAppName })
|
||||
: this.i18n.t('attachments.open_locally');
|
||||
|
||||
options.push({
|
||||
text,
|
||||
emit: 'open_locally'
|
||||
});
|
||||
}
|
||||
return options;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
$(this.$nextTick(() => {
|
||||
$('.attachment-preview img')
|
||||
|
@ -159,7 +243,7 @@ export default {
|
|||
}));
|
||||
},
|
||||
watch: {
|
||||
isHovered(newValue) {
|
||||
showOptions(newValue) {
|
||||
// reload thumbnail on mouse out
|
||||
if (newValue) return;
|
||||
|
||||
|
@ -174,15 +258,33 @@ export default {
|
|||
openOVEditor(url) {
|
||||
window.showIFrameModal(url);
|
||||
},
|
||||
openScinoteEditor() {
|
||||
this.$refs.imageEditButton.click();
|
||||
},
|
||||
handleMouseLeave() {
|
||||
if (!this.isMenuOpen) {
|
||||
this.showOptions = false;
|
||||
}
|
||||
},
|
||||
handleMenuVisibilityChange(newValue) {
|
||||
this.isMenuOpen = newValue;
|
||||
this.showOptions = newValue;
|
||||
}
|
||||
async handleMouseEnter() {
|
||||
await this.fetchLocalAppInfo();
|
||||
this.showOptions = true;
|
||||
},
|
||||
handleMenuVisibilityChange({ isMenuOpen, showOptions }) {
|
||||
if (isMenuOpen !== null) {
|
||||
this.isMenuOpen = isMenuOpen;
|
||||
}
|
||||
if (showOptions !== null) {
|
||||
this.showOptions = showOptions;
|
||||
}
|
||||
},
|
||||
handleClickOutsideThumbnail(event) {
|
||||
const isClickInsideModal = event.target.closest('.modal');
|
||||
if (!isClickInsideModal) {
|
||||
this.showOptions = false;
|
||||
this.isMenuOpen = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
@delete="showDeleteModal"
|
||||
></MenuDropdown>
|
||||
</div>
|
||||
<div v-if="element.attributes.orderable.urls.create_item_url || orderedChecklistItems.length > 0" :class="{ 'pointer-events-none': locked }">
|
||||
<div v-if="element.attributes.orderable.urls.create_item_url || checklistItems.length > 0" :class="{ 'pointer-events-none': locked }">
|
||||
<Draggable
|
||||
v-model="checklistItems"
|
||||
:ghostClass="'checklist-item-ghost'"
|
||||
|
@ -63,8 +63,8 @@
|
|||
<div v-if="element.attributes.orderable.urls.create_item_url && !addingNewItem"
|
||||
class="flex items-center gap-1 text-sn-blue cursor-pointer mb-2 mt-1 "
|
||||
tabindex="0"
|
||||
@keyup.enter="addItem(orderedChecklistItems.length + 1)"
|
||||
@click="addItem(orderedChecklistItems.length + 1)">
|
||||
@keyup.enter="addItem(checklistItems[checklistItems.length - 1]?.id)"
|
||||
@click="addItem(checklistItems[checklistItems.length - 1]?.id)">
|
||||
<i class="sn-icon sn-icon-new-task w-6 text-center inline-block"></i>
|
||||
{{ i18n.t('protocols.steps.insert.checklist_item') }}
|
||||
</div>
|
||||
|
@ -81,6 +81,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
/* global HelperModule I18n */
|
||||
|
||||
import Draggable from 'vuedraggable';
|
||||
import DeleteMixin from './mixins/delete.js';
|
||||
import MoveMixin from './mixins/move.js';
|
||||
|
@ -90,6 +93,7 @@ import InlineEdit from '../inline_edit.vue';
|
|||
import ChecklistItem from './checklistItem.vue';
|
||||
import moveElementModal from './modal/move.vue';
|
||||
import MenuDropdown from '../menu_dropdown.vue';
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
|
||||
export default {
|
||||
name: 'Checklist',
|
||||
|
@ -128,7 +132,7 @@ export default {
|
|||
},
|
||||
created() {
|
||||
if (this.isNew) {
|
||||
this.addItem(1);
|
||||
this.addItem();
|
||||
} else {
|
||||
this.loadChecklistItems();
|
||||
}
|
||||
|
@ -139,13 +143,6 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
orderedChecklistItems() {
|
||||
return this.checklistItems.sort((a, b) => a.attributes.position - b.attributes.position || b.id - a.id)
|
||||
.map((item, index) => {
|
||||
item.attributes.position = index + 1;
|
||||
return item;
|
||||
});
|
||||
},
|
||||
locked() {
|
||||
return this.editingName || !this.element.attributes.orderable.urls.update_url;
|
||||
},
|
||||
|
@ -199,20 +196,23 @@ export default {
|
|||
this.update();
|
||||
},
|
||||
postItem(item) {
|
||||
item.attributes.position = item.attributes.position - 1;
|
||||
$.post(this.element.attributes.orderable.urls.create_item_url, item).done((result) => {
|
||||
this.loadChecklistItems(result.data[result.data.length - 1].attributes.position);
|
||||
}).fail((e) => {
|
||||
const position = this.checklistItems.findIndex((i) => i.id === item.id);
|
||||
let afterId = null;
|
||||
if (position > 0) {
|
||||
afterId = this.checklistItems[position - 1].id;
|
||||
}
|
||||
axios.post(this.element.attributes.orderable.urls.create_item_url, {
|
||||
attributes: item.attributes,
|
||||
after_id: afterId
|
||||
}).then((result) => {
|
||||
this.loadChecklistItems(result.data.data[result.data.data.length - 1].id);
|
||||
}).catch(() => {
|
||||
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
|
||||
});
|
||||
|
||||
// Fake element during loading
|
||||
item.id = `new${Math.floor(Math.random() * 1000000000)}`;
|
||||
this.checklistItems.push(item);
|
||||
},
|
||||
saveItem(item, key) {
|
||||
if (item.id > 0) {
|
||||
const insertAfter = key === 'Enter' ? item.attributes.position : null;
|
||||
const insertAfter = key === 'Enter' ? item.id : null;
|
||||
$.ajax({
|
||||
url: item.attributes.urls.update_url,
|
||||
type: 'PATCH',
|
||||
|
@ -220,7 +220,7 @@ export default {
|
|||
success: () => {
|
||||
this.loadChecklistItems(insertAfter);
|
||||
},
|
||||
error: (xhr) => setFlashErrors(xhr.responseJSON.errors)
|
||||
error: (xhr) => this.setFlashErrors(xhr.responseJSON.errors)
|
||||
});
|
||||
} else {
|
||||
this.postItem(item, key);
|
||||
|
@ -240,20 +240,23 @@ export default {
|
|||
});
|
||||
},
|
||||
addItem(insertAfter) {
|
||||
this.checklistItems.push(
|
||||
const afterIndex = this.checklistItems.findIndex((i) => i.id === insertAfter);
|
||||
this.checklistItems.splice(
|
||||
afterIndex + 1,
|
||||
0,
|
||||
{
|
||||
id: `new${Math.floor(Math.random() * 1000000000)}`,
|
||||
attributes: {
|
||||
text: '',
|
||||
checked: false,
|
||||
position: insertAfter,
|
||||
isNew: true
|
||||
isNew: true,
|
||||
with_paragraphs: false
|
||||
}
|
||||
}
|
||||
);
|
||||
this.checklistItems = this.orderedChecklistItems;
|
||||
},
|
||||
removeItem(position) {
|
||||
this.checklistItems = this.orderedChecklistItems.filter((item) => item.attributes.position !== position);
|
||||
this.checklistItems = this.checklistItems.filter((item) => item.attributes.position !== position);
|
||||
},
|
||||
startReorder() {
|
||||
this.reordering = true;
|
||||
|
@ -265,21 +268,26 @@ export default {
|
|||
&& Number.isInteger(event.oldIndex)
|
||||
&& event.newIndex !== event.oldIndex
|
||||
) {
|
||||
const position = this.orderedChecklistItems[event.newIndex]?.attributes.position;
|
||||
const id = this.checklistItems[event.oldIndex]?.id;
|
||||
this.checklistItems[event.oldIndex].attributes.position = position + (event.newIndex > event.oldIndex ? 1 : -1);
|
||||
this.saveItemOrder(id, position);
|
||||
let afterId = null;
|
||||
if (event.newIndex > 0) {
|
||||
if (event.newIndex > event.oldIndex) {
|
||||
afterId = this.checklistItems[event.newIndex - 1].id;
|
||||
} else {
|
||||
afterId = this.checklistItems[event.newIndex + 1].id;
|
||||
}
|
||||
}
|
||||
const id = this.checklistItems[event.newIndex]?.id;
|
||||
this.saveItemOrder(id, afterId);
|
||||
}
|
||||
},
|
||||
saveItemOrder(id, position) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: this.element.attributes.orderable.urls.reorder_url,
|
||||
data: JSON.stringify({ attributes: { id, position } }),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
error: (xhr) => this.setFlashErrors(xhr.responseJSON.errors),
|
||||
success: () => this.loadChecklistItems()
|
||||
saveItemOrder(id, afterId) {
|
||||
axios.post(this.element.attributes.orderable.urls.reorder_url, {
|
||||
id,
|
||||
after_id: afterId
|
||||
}).then(() => {
|
||||
this.loadChecklistItems();
|
||||
}).catch((e) => {
|
||||
this.setFlashErrors(e.response.errors);
|
||||
});
|
||||
},
|
||||
setFlashErrors(errors) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="content__checklist-item pl-10 ml-[-2.325rem]">
|
||||
<div class="checklist-item-header flex rounded items-center relative w-full group/checklist-item-header" :class="{ 'locked': locked || editingText, 'editing-name': editingText }">
|
||||
<div class="content__checklist-item pl-10 ml-[-2.325rem] group/checklist-item-header">
|
||||
<div class="checklist-item-header flex rounded items-center relative w-full" :class="{ 'locked': locked || editingText, 'editing-name': editingText }">
|
||||
<div v-if="reorderChecklistItemUrl"
|
||||
class="absolute h-6 cursor-grab justify-center left-[-2.325rem] top-0.5 px-2 tw-hidden text-sn-grey element-grip step-element-grip--draggable"
|
||||
:class="{ 'group-hover/checklist-item-header:flex': (!locked && !editingText && draggable) }"
|
||||
|
@ -8,7 +8,8 @@
|
|||
<i class="sn-icon sn-icon-drag"></i>
|
||||
</div>
|
||||
<div class="flex items-start gap-2 grow" :class="{ 'done': checklistItem.attributes.checked }">
|
||||
<div v-if="!inRepository" class="sci-checkbox-container my-1.5 border-0 border-y border-transparent border-solid" :class="{ 'disabled': !toggleUrl }" :style="toggleUrl && 'pointer-events: initial'">
|
||||
<div v-if="!inRepository" class="sci-checkbox-container my-1.5 border-0 border-y border-transparent border-solid"
|
||||
:class="{ 'disabled': !toggleUrl }" :style="toggleUrl && 'pointer-events: initial'">
|
||||
<input ref="checkbox"
|
||||
type="checkbox"
|
||||
class="sci-checkbox"
|
||||
|
@ -42,9 +43,10 @@
|
|||
@delete="removeItem()"
|
||||
@keypress="keyPressHandler"
|
||||
@blur="onBlurHandler"
|
||||
@paste="pasteHandler"
|
||||
/>
|
||||
<span v-if="!editingText && (!checklistItem.attributes.urls || deleteUrl)" class="absolute right-0 top-0.5 leading-6 tw-hidden group-hover/checklist-item-header:inline-block !text-sn-blue cursor-pointer" @click="showDeleteModal" tabindex="0">
|
||||
<span v-if="!editingText && (!checklistItem.attributes.urls || deleteUrl)"
|
||||
class="absolute right-0 top-0.5 leading-6 tw-hidden group-hover/checklist-item-header:inline-block !text-sn-blue cursor-pointer"
|
||||
@click="showDeleteModal" tabindex="0">
|
||||
<i class="sn-icon sn-icon-delete"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -127,8 +129,7 @@ export default {
|
|||
disableTextEdit() {
|
||||
if (this.checklistItem.attributes.isNew) {
|
||||
if (this.deleting) return;
|
||||
|
||||
this.removeItem();
|
||||
if (this.checklistItem.attributes.text.length === 0) this.removeItem();
|
||||
this.$emit('editEnd');
|
||||
}
|
||||
},
|
||||
|
@ -162,15 +163,9 @@ export default {
|
|||
this.$emit('update', this.checklistItem, withKey);
|
||||
},
|
||||
keyPressHandler(e) {
|
||||
if (
|
||||
((e.shiftKey || e.metaKey) && e.key === 'Enter')
|
||||
|| ((e.ctrlKey || e.metaKey) && e.key === 'v')
|
||||
) {
|
||||
if ((e.shiftKey || e.metaKey) && e.key === 'Enter') {
|
||||
this.checklistItem.attributes.with_paragraphs = true;
|
||||
}
|
||||
},
|
||||
pasteHandler() {
|
||||
this.checklistItem.attributes.with_paragraphs = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<div ref="modal" @keydown.esc="cancel" class="modal" role="dialog" aria-hidden="true" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<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>
|
||||
<h2 class="modal-title">{{ i18n.t('assets.edit_launching_application_modal.title') }}</h2>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p v-html="i18n.t(
|
||||
'assets.edit_launching_application_modal.description',
|
||||
{ file_name: fileName, application: application }
|
||||
)"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type='button' class='btn btn-secondary' @click="cancel">
|
||||
{{ i18n.t('general.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'editLaunchingApplicationModal',
|
||||
props: {
|
||||
fileName: String, application: String,
|
||||
},
|
||||
mounted() {
|
||||
$(this.$refs.modal).modal('show');
|
||||
$(this.$refs.modal).on('hidden.bs.modal', () => {
|
||||
this.$emit('cancel');
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
$(this.$refs.modal).modal('hide');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div ref="modal" @keydown.esc="cancel" class="modal" id="modalNoPredefinedApp" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-md" 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" id="modal-delete-result-element">
|
||||
{{ i18n.t('assets.no_predefined_app_modal.set_up_app') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p v-html="i18n.t('assets.no_predefined_app_modal.body_text_html', { file_name: fileName })"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" @click="confirm">{{ this.i18n.t('assets.no_predefined_app_modal.understand_button') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NoPredefinedAppModal',
|
||||
props: {
|
||||
fileName: String
|
||||
},
|
||||
mounted() {
|
||||
$(this.$refs.modal).modal('show');
|
||||
$(this.$refs.modal).on('hidden.bs.modal', () => {
|
||||
this.$emit('confirm');
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
confirm() {
|
||||
$(this.$refs.modal).modal('hide');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<div ref="modal" @keydown.esc="cancel" class="modal" id="modalUpdateVersion" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-md" 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" id="modal-delete-result-element">
|
||||
{{ i18n.t('assets.update_version_modal.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p v-html="i18n.t('assets.update_version_modal.body_text_html')"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @click="cancel">{{ i18n.t('general.cancel') }}</button>
|
||||
<ScinoteEditDownload
|
||||
:data="userAgent"
|
||||
:isUpdateVersionModal="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScinoteEditDownload from '../../../../vue/shared/scinote_edit_download.vue';
|
||||
|
||||
export default {
|
||||
name: 'UpdateVersionModal',
|
||||
components: {
|
||||
ScinoteEditDownload
|
||||
},
|
||||
props: {
|
||||
fileName: String
|
||||
},
|
||||
computed: {
|
||||
userAgent() {
|
||||
return window.navigator.userAgent;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
$(this.$refs.modal).modal('show');
|
||||
$(this.$refs.modal).on('hidden.bs.modal', () => {
|
||||
this.$emit('cancel');
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
$(this.$refs.modal).modal('hide');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -270,8 +270,8 @@ export default {
|
|||
this.newValue = this.$refs.input.value.trim(); // Fix for smart annotation
|
||||
|
||||
this.editing = false;
|
||||
this.$emit('editingDisabled');
|
||||
this.$emit('update', this.newValue, withKey);
|
||||
this.$emit('editingDisabled');
|
||||
},
|
||||
refreshTexareaHeight() {
|
||||
if (this.editing && !this.singleLine) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="relative" v-if="listItems.length > 0" v-click-outside="closeMenuAndEmit">
|
||||
<div class="relative" v-if="listItems.length > 0" v-click-outside="closeMenu">
|
||||
<button
|
||||
ref="openBtn"
|
||||
:class="btnClasses"
|
||||
|
@ -79,6 +79,7 @@ export default {
|
|||
btnClasses: { type: String, default: 'btn btn-light' },
|
||||
btnText: { type: String, required: false },
|
||||
btnIcon: { type: String, required: false },
|
||||
title: { type: String, default: '' },
|
||||
caret: { type: Boolean, default: false }
|
||||
},
|
||||
data() {
|
||||
|
@ -92,9 +93,10 @@ export default {
|
|||
},
|
||||
watch: {
|
||||
showMenu(newValue) {
|
||||
if (newValue) {
|
||||
this.$emit('menu-visibility-changed', newValue);
|
||||
}
|
||||
this.$emit('menu-visibility-changed', {
|
||||
isMenuOpen: newValue,
|
||||
showOptions: newValue ? true : null
|
||||
});
|
||||
|
||||
if (this.showMenu) {
|
||||
this.openUp = false;
|
||||
|
@ -115,14 +117,6 @@ export default {
|
|||
closeMenu() {
|
||||
this.showMenu = false;
|
||||
},
|
||||
closeMenuAndEmit(event) {
|
||||
const isClickInsideModal = event.target.closest('.modal');
|
||||
|
||||
if (!isClickInsideModal) {
|
||||
this.showMenu = false;
|
||||
this.$emit('menu-visibility-changed', false);
|
||||
}
|
||||
},
|
||||
handleClick(event, item) {
|
||||
if (!item.url) {
|
||||
event.preventDefault();
|
||||
|
|
17
app/javascript/vue/shared/modal_mixin.js
Normal file
17
app/javascript/vue/shared/modal_mixin.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
export default {
|
||||
mounted() {
|
||||
$(this.$refs.modal).modal('show');
|
||||
$(this.$refs.modal).on('hidden.bs.modal', () => {
|
||||
this.$emit('close');
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
$(this.$refs.modal).modal('hide');
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$emit('close');
|
||||
$(this.$refs.modal).modal('hide');
|
||||
}
|
||||
},
|
||||
}
|
118
app/javascript/vue/shared/scinote_edit_download.vue
Normal file
118
app/javascript/vue/shared/scinote_edit_download.vue
Normal file
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<div class="buttons">
|
||||
<template v-if="isWindows">
|
||||
<a :href="getWindowsHref"
|
||||
class="btn btn-primary new-project-btn"
|
||||
:title="i18n.t('users.settings.account.addons.desktop_app.windows_button')"
|
||||
role="button"
|
||||
target="_blank">
|
||||
<span class="hidden-xs">{{ i18n.t('users.settings.account.addons.desktop_app.windows_button') }}</span>
|
||||
</a>
|
||||
<div v-if="showButtonLabel" class="text-xs pt-2 pb-6" style="color: var(--sn-sleepy-grey);">
|
||||
{{ i18n.t('users.settings.account.addons.desktop_app.version', { version: this.responseData[0]['version']}) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isMac">
|
||||
<a :href="getMacHref"
|
||||
class="btn btn-primary new-project-btn"
|
||||
:title="i18n.t('users.settings.account.addons.desktop_app.macos_button')"
|
||||
role="button"
|
||||
target="_blank">
|
||||
<span class="hidden-xs">{{ i18n.t('users.settings.account.addons.desktop_app.macos_button') }}</span>
|
||||
</a>
|
||||
<div v-if="showButtonLabel" class="text-xs pt-2 pb-6" style="color: var(--sn-sleepy-grey);">
|
||||
{{ i18n.t('users.settings.account.addons.desktop_app.version', { version: this.responseData[1]['version']}) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex">
|
||||
<div>
|
||||
<a :href="getWindowsHref"
|
||||
class="btn btn-primary new-project-btn"
|
||||
:title="i18n.t('users.settings.account.addons.desktop_app.windows_button')"
|
||||
role="button"
|
||||
target="_blank">
|
||||
<span class="hidden-xs">{{ i18n.t('users.settings.account.addons.desktop_app.windows_button') }}</span>
|
||||
</a>
|
||||
<div v-if="showButtonLabel" class="text-xs pt-2 pb-6" style="color: var(--sn-sleepy-grey);">
|
||||
{{ i18n.t('users.settings.account.addons.desktop_app.version',
|
||||
{ version: this.responseData[0]['version']})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-2">
|
||||
<a :href="getMacHref"
|
||||
class="btn btn-primary new-project-btn"
|
||||
:title="i18n.t('users.settings.account.addons.desktop_app.macos_button')"
|
||||
role="button"
|
||||
target="_blank">
|
||||
<span class="hidden-xs">{{ i18n.t('users.settings.account.addons.desktop_app.macos_button') }}</span>
|
||||
</a>
|
||||
<p v-if="showButtonLabel" class="text-xs pt-2 pb-6" style="color: var(--sn-sleepy-grey);">
|
||||
{{ i18n.t('users.settings.account.addons.desktop_app.version',
|
||||
{ version: this.responseData[1]['version']})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a :href="'https://knowledgebase.scinote.net/en/knowledge/how-to-use-scinote-edit'"
|
||||
:title="i18n.t('users.settings.account.addons.more_info')"
|
||||
class="text-sn-blue"
|
||||
target="_blank">
|
||||
<span class="sn-icon sn-icon-open"></span>
|
||||
{{ i18n.t('users.settings.account.addons.more_info') }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ScinoteEditDownload',
|
||||
props: {
|
||||
data: { type: String, required: true },
|
||||
isUpdateVersionModal: { type: Boolean, required: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userAgent: this.data,
|
||||
responseData: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isWindows() {
|
||||
return /Windows/.test(this.userAgent);
|
||||
},
|
||||
isMac() {
|
||||
return /Mac OS/.test(this.userAgent);
|
||||
},
|
||||
showButtonLabel() {
|
||||
return this.responseData && this.responseData.length > 0 && !this.isUpdateVersionModal;
|
||||
},
|
||||
getWindowsHref() {
|
||||
return this.responseData && this.responseData.length > 0 ? this.responseData[0].url : '#';
|
||||
},
|
||||
getMacHref() {
|
||||
return this.responseData && this.responseData.length > 0 ? this.responseData[1].url : '#';
|
||||
}
|
||||
},
|
||||
created() {
|
||||
window.scinoteEditDownload = this;
|
||||
this.fetchData();
|
||||
},
|
||||
beforeUnmount() {
|
||||
delete window.scinoteEditDownloadComponent;
|
||||
},
|
||||
methods: {
|
||||
fetchData() {
|
||||
$.get('https://extras.scinote.net/scinote-edit/latest.json', (result) => {
|
||||
this.responseData = result;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
73
app/javascript/vue/shared/wizard_modal.vue
Normal file
73
app/javascript/vue/shared/wizard_modal.vue
Normal file
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog !w-[900px]" role="document">
|
||||
<div class="modal-content !p-0 grid grid-cols-3">
|
||||
<div class="bg-sn-super-light-grey p-6 mb-1.5">
|
||||
<h3 class="mb-1.5">{{ config.title }}</h3>
|
||||
<div v-if="config.subtitle" class="text-sn-dark-grey">
|
||||
{{ config.subtitle }}
|
||||
</div>
|
||||
<div class="flex flex-col mt-4">
|
||||
<div v-for="(step, index) in config.steps" :key="step.id">
|
||||
<div v-if="index > 0"
|
||||
class="ml-0.5 left-4 relative h-8 w-0 border border-r-0 border-solid"
|
||||
:class="{
|
||||
'!border-sn-dark-grey': index <= activeStep,
|
||||
'!border-sn-sleepy-grey': index > activeStep
|
||||
}"
|
||||
></div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="rounded bg-white border border-sn-sleepy-grey p-1.5">
|
||||
<i :class="step.icon"></i>
|
||||
</div>
|
||||
<span
|
||||
class="font-bold text-xs"
|
||||
:class="{
|
||||
'text-sn-dark-grey': index <= activeStep,
|
||||
'text-sn-grey': index > activeStep
|
||||
}"
|
||||
>
|
||||
{{ step.label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2 p-6 flex flex-col">
|
||||
<component
|
||||
:is="config.steps[activeStep].component"
|
||||
:params="params"
|
||||
:wizardComponent="this"
|
||||
@close="close"
|
||||
@back="activeStep -= 1"
|
||||
@next="activeStep += 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modalMixin from './modal_mixin';
|
||||
|
||||
export default {
|
||||
name: 'WizardModal',
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return {
|
||||
activeStep: 0
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -44,6 +44,7 @@ class Asset < ApplicationRecord
|
|||
dependent: :nullify
|
||||
has_many :report_elements, inverse_of: :asset, dependent: :destroy
|
||||
has_one :asset_text_datum, inverse_of: :asset, dependent: :destroy
|
||||
has_many :asset_sync_tokens, dependent: :destroy
|
||||
|
||||
scope :sort_assets, lambda { |sort_value = 'new'|
|
||||
sort = case sort_value
|
||||
|
@ -465,6 +466,10 @@ class Asset < ApplicationRecord
|
|||
(result || step)&.my_module
|
||||
end
|
||||
|
||||
def parent
|
||||
step || result || repository_cell
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tempdir
|
||||
|
|
33
app/models/asset_sync_token.rb
Normal file
33
app/models/asset_sync_token.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AssetSyncToken < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :asset
|
||||
|
||||
after_initialize :generate_token
|
||||
after_initialize :set_default_expiration
|
||||
|
||||
validates :token, uniqueness: true, presence: true
|
||||
|
||||
def version_token
|
||||
asset.file.checksum
|
||||
end
|
||||
|
||||
def token_valid?
|
||||
!revoked_at? && expires_at > Time.current
|
||||
end
|
||||
|
||||
def conflicts?(token)
|
||||
asset.locked? || version_token != token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_token
|
||||
self.token ||= SecureRandom.urlsafe_base64(32)
|
||||
end
|
||||
|
||||
def set_default_expiration
|
||||
self.expires_at ||= Constants::ASSET_SYNC_TOKEN_EXPIRATION.from_now
|
||||
end
|
||||
end
|
|
@ -27,13 +27,13 @@ class ChecklistItem < ApplicationRecord
|
|||
after_save :touch_checklist
|
||||
after_touch :touch_checklist
|
||||
|
||||
def save_multiline!
|
||||
def save_multiline!(after_id: nil)
|
||||
at_position = checklist.checklist_items.find_by(id: after_id).position if after_id
|
||||
|
||||
if with_paragraphs
|
||||
if new_record?
|
||||
original_position = position
|
||||
self.position = nil
|
||||
save!
|
||||
insert_at(original_position + 1)
|
||||
insert_at(at_position + 1) || 0
|
||||
else
|
||||
save!
|
||||
end
|
||||
|
@ -42,7 +42,7 @@ class ChecklistItem < ApplicationRecord
|
|||
|
||||
items = []
|
||||
if new_record?
|
||||
start_position = position
|
||||
start_position = at_position || 0
|
||||
text.split("\n").compact.each do |line|
|
||||
new_item = checklist.checklist_items.create!(text: line)
|
||||
new_item.insert_at(start_position + 1)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module TinyMceImages
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# rubocop:disable Metrics/BlockLength:
|
||||
included do
|
||||
has_many :tiny_mce_assets,
|
||||
as: :object,
|
||||
|
@ -22,18 +23,13 @@ module TinyMceImages
|
|||
tiny_mce_assets.each do |tm_asset|
|
||||
next unless tm_asset&.image&.attached?
|
||||
|
||||
begin
|
||||
new_tm_asset_src = tm_asset.convert_variant_to_base64(tm_asset.preview)
|
||||
rescue ActiveStorage::FileNotFoundError
|
||||
next
|
||||
end
|
||||
html_description = Nokogiri::HTML(description)
|
||||
tm_asset_to_update = html_description.css(
|
||||
"img[data-mce-token=\"#{Base62.encode(tm_asset.id)}\"]"
|
||||
)[0]
|
||||
next unless tm_asset_to_update
|
||||
|
||||
tm_asset_to_update.attributes['src'].value = new_tm_asset_src
|
||||
tm_asset_to_update.attributes['src'].value = tm_asset.convert_to_base64
|
||||
description = html_description.css('body').inner_html.to_s
|
||||
end
|
||||
description
|
||||
|
@ -225,4 +221,5 @@ module TinyMceImages
|
|||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/BlockLength:
|
||||
end
|
||||
|
|
|
@ -58,6 +58,9 @@ class TeamSharedObject < ApplicationRecord
|
|||
end
|
||||
|
||||
def unlink_unshared_items
|
||||
# We keep all the other teams shared with and the repository's own team
|
||||
teams_ids = shared_object.teams_shared_with.where.not(id: team).pluck(:id)
|
||||
teams_ids << shared_object.team_id
|
||||
repository_rows_ids = shared_object.repository_rows.select(:id)
|
||||
rows_to_unlink = RepositoryRow.joins("LEFT JOIN repository_row_connections \
|
||||
ON repository_rows.id = repository_row_connections.parent_id \
|
||||
|
@ -67,11 +70,17 @@ class TeamSharedObject < ApplicationRecord
|
|||
repository_rows_ids,
|
||||
repository_rows_ids)
|
||||
.joins(:repository)
|
||||
.where(repositories: { team: team })
|
||||
.where.not(repositories: { team: teams_ids })
|
||||
.select(:id)
|
||||
|
||||
RepositoryRowConnection.where(parent: rows_to_unlink, child: repository_rows_ids)
|
||||
.destroy_all
|
||||
RepositoryRowConnection.where(parent: repository_rows_ids, child: rows_to_unlink)
|
||||
RepositoryRowConnection.where("(repository_row_connections.parent_id IN (?) \
|
||||
AND repository_row_connections.child_id IN (?)) \
|
||||
OR (repository_row_connections.parent_id IN (?) \
|
||||
AND repository_row_connections.child_id IN (?))",
|
||||
repository_rows_ids,
|
||||
rows_to_unlink,
|
||||
rows_to_unlink,
|
||||
repository_rows_ids)
|
||||
.destroy_all
|
||||
end
|
||||
end
|
||||
|
|
|
@ -203,6 +203,14 @@ class TinyMceAsset < ApplicationRecord
|
|||
image&.blob
|
||||
end
|
||||
|
||||
def convert_to_base64
|
||||
encoded_data = Base64.strict_encode64(image.download)
|
||||
"data:#{image.blob.content_type};base64,#{encoded_data}"
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e.message
|
||||
"data:#{image.blob.content_type};base64,"
|
||||
end
|
||||
|
||||
def duplicate_file(to_asset)
|
||||
return unless image.attached?
|
||||
|
||||
|
|
|
@ -317,6 +317,7 @@ class User < ApplicationRecord
|
|||
has_many :access_tokens, class_name: 'Doorkeeper::AccessToken',
|
||||
foreign_key: :resource_owner_id,
|
||||
dependent: :delete_all
|
||||
has_many :asset_sync_tokens, dependent: :destroy
|
||||
|
||||
has_many :hidden_repository_cell_reminders, dependent: :destroy
|
||||
|
||||
|
|
|
@ -31,4 +31,8 @@ Canaid::Permissions.register_for(Asset) do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
can :open_asset_locally do |_user, asset|
|
||||
ENV['ASSET_SYNC_URL'].present?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -82,7 +82,8 @@ Canaid::Permissions.register_for(Experiment) do
|
|||
end
|
||||
|
||||
can :move_experiment do |user, experiment|
|
||||
experiment.permission_granted?(user, ExperimentPermissions::MANAGE)
|
||||
experiment.permission_granted?(user, ExperimentPermissions::MANAGE) &&
|
||||
can_manage_all_experiment_my_modules?(experiment)
|
||||
end
|
||||
|
||||
can :designate_users_to_new_task do |user, experiment|
|
||||
|
|
|
@ -8,10 +8,10 @@ class AssetSerializer < ActiveModel::Serializer
|
|||
include InputSanitizeHelper
|
||||
include ApplicationHelper
|
||||
|
||||
attributes :file_name, :view_mode, :icon, :urls, :updated_at_formatted,
|
||||
attributes :file_name, :file_extension, :view_mode, :icon, :urls, :updated_at_formatted,
|
||||
:file_size, :medium_preview, :large_preview, :asset_type, :wopi,
|
||||
:wopi_context, :pdf_previewable, :file_size_formatted, :asset_order,
|
||||
:updated_at, :metadata, :image_editable, :image_context, :pdf, :attached, :parent_type
|
||||
:updated_at, :metadata, :image_editable, :image_context, :pdf, :attached, :parent_type, :edit_version_range
|
||||
|
||||
def icon
|
||||
file_fa_icon_class(object)
|
||||
|
@ -21,12 +21,16 @@ class AssetSerializer < ActiveModel::Serializer
|
|||
object.render_file_name
|
||||
end
|
||||
|
||||
def file_extension
|
||||
File.extname(object.file_name)[1..]
|
||||
end
|
||||
|
||||
def updated_at
|
||||
object.updated_at.to_i
|
||||
end
|
||||
|
||||
def updated_at_formatted
|
||||
I18n.l(object.updated_at, format: :full_date) if object.updated_at
|
||||
I18n.l(object.updated_at, format: :full_with_comma) if object.updated_at
|
||||
end
|
||||
|
||||
def parent_type
|
||||
|
@ -113,10 +117,14 @@ class AssetSerializer < ActiveModel::Serializer
|
|||
end
|
||||
end
|
||||
|
||||
def edit_version_range
|
||||
{ min: Constants::MIN_SCINOTE_EDIT_VERSION, max: Constants::MAX_SCINOTE_EDIT_VERSION }
|
||||
end
|
||||
|
||||
def urls
|
||||
urls = {
|
||||
preview: asset_file_preview_path(object),
|
||||
download: (rails_blob_path(object.file, disposition: 'attachment') if attached),
|
||||
download: (asset_download_path(object) if attached),
|
||||
load_asset: load_asset_path(object),
|
||||
asset_file: asset_file_url_path(object),
|
||||
marvin_js: marvin_js_asset_path(object),
|
||||
|
@ -135,6 +143,12 @@ class AssetSerializer < ActiveModel::Serializer
|
|||
)
|
||||
end
|
||||
urls[:open_vector_editor_edit] = edit_gene_sequence_asset_path(object.id) if can_manage_asset?(user, object)
|
||||
|
||||
if can_manage_asset?(user, object) && can_open_asset_locally?(user, object)
|
||||
urls[:open_locally] = asset_sync_show_path(object)
|
||||
urls[:open_locally_api] = Constants::ASSET_SYNC_URL
|
||||
end
|
||||
|
||||
urls[:wopi_action] = object.get_action_url(user, 'embedview') if wopi && can_manage_asset?(user, object)
|
||||
urls[:blob] = rails_blob_path(object.file, disposition: 'attachment') if object.file.attached?
|
||||
|
||||
|
|
19
app/serializers/asset_sync_token_serializer.rb
Normal file
19
app/serializers/asset_sync_token_serializer.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AssetSyncTokenSerializer < ActiveModel::Serializer
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
attributes :url, :asset_id, :filename, :token, :asset_id, :version_token, :checksum
|
||||
|
||||
def checksum
|
||||
object.asset.file.checksum
|
||||
end
|
||||
|
||||
def url
|
||||
asset_sync_download_url(asset_id: object.asset)
|
||||
end
|
||||
|
||||
def filename
|
||||
object.asset.file.filename
|
||||
end
|
||||
end
|
|
@ -14,10 +14,6 @@ class ActivitiesService
|
|||
|
||||
if filters[:subjects].present?
|
||||
subjects_with_children = load_subjects_children(filters[:subjects])
|
||||
if subjects_with_children['Project']
|
||||
query = query.where('project_id IN (?)', subjects_with_children['Project'])
|
||||
subjects_with_children = subjects_with_children.except('Project')
|
||||
end
|
||||
where_condition = subjects_with_children.to_h.map { '(subject_type = ? AND subject_id IN(?))' }.join(' OR ')
|
||||
where_arguments = subjects_with_children.to_h.flatten
|
||||
if subjects_with_children[:my_module]
|
||||
|
|
|
@ -17,7 +17,7 @@ module LabelTemplates
|
|||
errors = []
|
||||
keys = @label_template.content.scan(/(?<=\{\{).*?(?=\}\})/).uniq
|
||||
label = keys.reduce(@label_template.content.dup) do |rendered_content, key|
|
||||
rendered_content.gsub!(/\{\{#{key}\}\}/, fetch_value(key))
|
||||
rendered_content.gsub!(Regexp.new(Regexp.escape("{{#{key}}}")), fetch_value(key))
|
||||
rescue LabelTemplates::ColumnNotFoundError,
|
||||
LabelTemplates::LogoNotFoundError,
|
||||
LabelTemplates::LogoParamsError => e
|
||||
|
|
|
@ -28,7 +28,7 @@ class MarvinJsService
|
|||
connect_asset(asset, params, current_user)
|
||||
end
|
||||
|
||||
def update_sketch(params, _current_user, current_team)
|
||||
def update_sketch(params, current_user, current_team)
|
||||
if params[:object_type] == 'TinyMceAsset'
|
||||
asset = current_team.tiny_mce_assets.find(Base62.decode(params[:id]))
|
||||
attachment = asset&.image
|
||||
|
@ -40,6 +40,7 @@ class MarvinJsService
|
|||
|
||||
file = generate_image(params)
|
||||
attach_file(attachment, file, params)
|
||||
asset.update(last_modified_by: current_user) if asset.is_a?(Asset)
|
||||
asset.post_process_file(current_team) if asset.class == Asset
|
||||
asset
|
||||
end
|
||||
|
|
|
@ -87,18 +87,14 @@ module Toolbars
|
|||
end
|
||||
|
||||
def move_action
|
||||
return unless @single
|
||||
|
||||
experiment = @experiments.first
|
||||
|
||||
return unless can_move_experiment?(experiment)
|
||||
return unless @experiments.all? { |experiment| can_move_experiment?(experiment) }
|
||||
|
||||
{
|
||||
name: 'move',
|
||||
label: I18n.t('experiments.toolbar.move_button'),
|
||||
icon: 'sn-icon sn-icon-move',
|
||||
button_class: 'move-experiments-btn',
|
||||
path: move_modal_experiments_path(id: experiment.id),
|
||||
path: move_modal_experiments_path(ids: @experiments.map(&:id)),
|
||||
type: 'remote-modal'
|
||||
}
|
||||
end
|
||||
|
|
|
@ -88,11 +88,7 @@ module Toolbars
|
|||
end
|
||||
|
||||
def move_action
|
||||
return unless @single
|
||||
|
||||
my_module = @my_modules.first
|
||||
|
||||
return unless can_move_my_module?(my_module)
|
||||
return unless @my_modules.all? { |my_module| can_move_my_module?(my_module) }
|
||||
|
||||
{
|
||||
name: 'move',
|
||||
|
|
16
app/views/design_elements/_modals.html.erb
Normal file
16
app/views/design_elements/_modals.html.erb
Normal file
|
@ -0,0 +1,16 @@
|
|||
<div>
|
||||
<h1>Modals</h1>
|
||||
<div id="modals" class="flex items-center gap-4 flex-wrap mt-6">
|
||||
<button @click="showWizard = true" class="btn btn-primary">Show Wizard Modal</button>
|
||||
<wizard-modal
|
||||
v-if="showWizard"
|
||||
@close="showWizard = false"
|
||||
@alert="fireAlert"
|
||||
:params="wizardParams"
|
||||
:config="wizardConfig"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<%= javascript_include_tag 'vue_design_system_modals' %>
|
1
app/views/design_elements/index.html.erb
Normal file
1
app/views/design_elements/index.html.erb
Normal file
|
@ -0,0 +1 @@
|
|||
<%= render partial: 'modals' %>
|
|
@ -1,10 +1,10 @@
|
|||
<div class="modal move-experiment-modal"
|
||||
id="move-experiment-modal-<%= @experiment.id %>"
|
||||
id="move-experiment-modal-<%= params[:ids] %>"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
aria-labelledby="move-experiment-modal-label">
|
||||
<%= form_with model: @experiment,
|
||||
url: move_experiment_path(@experiment),
|
||||
url: move_experiments_path(ids: params[:ids]),
|
||||
method: :post,
|
||||
data: { remote: true },
|
||||
html: { class: 'experiment-action-form' } do |f| %>
|
||||
|
@ -12,11 +12,11 @@
|
|||
<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" id="move-experiment-modal-label"><%= t("experiments.move.modal_title", experiment: @experiment.name ) %></h5>
|
||||
<h4 class="modal-title" id="move-experiment-modal-label"><%= t("experiments.move.modal_title") %></h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><small><%= t("experiments.move.notice") %></small></p>
|
||||
<% if @projects.any? && can_manage_all_experiment_my_modules?(@experiment) %>
|
||||
<% if @projects.any? && @experiments.all? { |experiment| can_move_experiment?(experiment) } %>
|
||||
<%= f.select :project_id, options_for_select(@projects.collect { |p| [ p.name, p.id ] }),
|
||||
{ label: t("experiments.move.target_project") }, { class: "form-control selectpicker", "data-role" => "clear" } %>
|
||||
<% else %>
|
||||
|
@ -24,15 +24,18 @@
|
|||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<% if @projects.blank? %>
|
||||
<%= t("experiments.move.no_projects") %>
|
||||
<% elsif !can_manage_all_experiment_my_modules?(@experiment) %>
|
||||
<% elsif !@experiments.all? { |experiment| can_move_experiment?(experiment) } %>
|
||||
<%= t("experiments.move.task_permission") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% params[:ids].each do |id| %>
|
||||
<%= f.hidden_field :ids, multiple: true, value: id %>
|
||||
<% end %>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal"><%=t "general.cancel" %></button>
|
||||
<% if @projects.any? && can_manage_all_experiment_my_modules?(@experiment) %>
|
||||
<% if @projects.any? && @experiments.all? { |experiment| can_manage_all_experiment_my_modules?(experiment) } %>
|
||||
<%= f.submit t("experiments.move.modal_submit"), class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="modal custom-z-index" id="myModuleRepositoryFullViewModal" data-keyboard="false" role="dialog" >
|
||||
<div class="modal modal-full-screen custom-z-index" id="myModuleRepositoryFullViewModal" data-keyboard="false" role="dialog" >
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
|
|
|
@ -33,7 +33,10 @@
|
|||
</div>
|
||||
<div class="protocol-actions">
|
||||
<% if can_publish_protocol_in_repository?(draft) %>
|
||||
<%= button_to publish_protocol_path(draft), class: "btn btn-light publish-draft", method: :post do %>
|
||||
<%= button_to publish_protocol_path(draft),
|
||||
params: { view: 'show' },
|
||||
class: 'btn btn-light publish-draft',
|
||||
method: :post do %>
|
||||
<%= image_tag 'icon_small/publish.svg' %>
|
||||
<%= t('protocols.index.versions.publish') %>
|
||||
<% end %>
|
||||
|
|
|
@ -3,13 +3,9 @@
|
|||
<div class="sci-btn-group">
|
||||
<% if can_edit && !preview %>
|
||||
<% if wopi_enabled? && wopi_file?(asset) %>
|
||||
<% edit_supported, title = wopi_file_edit_button_status(asset) %>
|
||||
<%= render partial: 'assets/wopi/file_wopi_controls',
|
||||
locals: {
|
||||
asset: asset,
|
||||
edit_supported: edit_supported,
|
||||
title: title
|
||||
} %>
|
||||
<div id="openLocallyMenu" data-behaviour="vue">
|
||||
<open-locally-menu :attachment="<%= { attributes: AssetSerializer.new(asset, scope: { user: current_user }).as_json }.to_json %>" />
|
||||
</div>
|
||||
<% elsif asset.file.metadata[:asset_type] == 'marvinjs' %>
|
||||
<button class="btn btn-light marvinjs-edit-button"
|
||||
data-sketch-id="<%= asset.id %>"
|
||||
|
@ -31,7 +27,7 @@
|
|||
</button>
|
||||
|
||||
<% elsif asset.editable_image? %>
|
||||
<button class="btn btn-light image-edit-button"
|
||||
<button id="editImageButton" class="btn btn-light image-edit-button hidden"
|
||||
data-image-id="<%= asset.id %>"
|
||||
data-image-name="<%= asset.file_name %>"
|
||||
data-image-url="<%= asset_file_url_path(asset) %>"
|
||||
|
@ -43,6 +39,9 @@
|
|||
<%= t('assets.file_preview.edit_in_scinote') %>
|
||||
</button>
|
||||
<% end %>
|
||||
<div id="openLocallyMenu" data-behaviour="vue">
|
||||
<open-locally-menu :attachment="<%= { attributes: AssetSerializer.new(asset, scope: { user: current_user }).as_json }.to_json %>" />
|
||||
</div>
|
||||
<% end %>
|
||||
<a class="btn btn-light file-download-link" href="<%= rails_blob_path(asset.file, disposition: 'attachment') %>" data-turbolinks="false">
|
||||
<span class="sn-icon sn-icon-export"></span> <%= t('Download')%>
|
||||
|
@ -110,3 +109,4 @@
|
|||
<% end %>
|
||||
|
||||
<%= javascript_include_tag 'shared/file_preview', nonce: true %>
|
||||
<%= javascript_include_tag "vue_open_locally_menu", nonce: true %>
|
||||
|
|
|
@ -3,66 +3,109 @@
|
|||
|
||||
<%= render partial: 'users/settings/sidebar' %>
|
||||
<div class="tab-content user-account-addons">
|
||||
<div class="tab-pane content-pane active" role="tabpanel">
|
||||
<div role="tabpanel">
|
||||
|
||||
<div>
|
||||
<h1 class="addons-title"><%= t('users.settings.account.addons.title') %></h1>
|
||||
<h1 id="scinote-addons-title" class="mt-0 pb-1.5 text-sn-black"><%= t('users.settings.account.addons.title') %></h1>
|
||||
|
||||
<div>
|
||||
<h2 class="addons-subtitle" ><%= t('users.settings.account.addons.scinote_addons') %></h2>
|
||||
<div id="scinote-addons-wrapper" class="flex flex-col bg-sn-white p-4 rounded mb-6">
|
||||
<h2 class="my-0 pb-6 text-sn-black" ><%= t('users.settings.account.addons.scinote_addons') %></h2>
|
||||
<div data-hook="settings-addons-container">
|
||||
<em data-hook="settings-addons-no-addons">
|
||||
<%= t('users.settings.account.addons.no_addons') %>
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row printer-settings">
|
||||
<div class="col-xs-12 col-sm-12">
|
||||
<h2 class="addons-subtitle"><%= t('users.settings.account.addons.label_printers') %></h2>
|
||||
<div class="printers-container">
|
||||
<div class="printer">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<%= t('users.settings.account.addons.zebra_printer.title') %>
|
||||
</div>
|
||||
<div class="control">
|
||||
<%= t('users.settings.account.addons.printers.enabled') %>
|
||||
<i class="sn-icon sn-icon-check"></i>
|
||||
</div>
|
||||
|
||||
<%# scinote edit %>
|
||||
<% if ENV['ASSET_SYNC_URL'].present? %>
|
||||
<div class="bg-sn-white rounded p-4 mb-6">
|
||||
<div class="font-bold my-0 pb-2 text-sn-black"><%= t('users.settings.account.addons.desktop_app.title') %></div>
|
||||
<div class="pb-6 text-sn-dark-grey">
|
||||
<%= t('users.settings.account.addons.desktop_app.description') %>
|
||||
</div>
|
||||
<div id="scinoteEditDownload" data-behaviour="vue">
|
||||
<scinote-edit-download data="<%= @user_agent %>">
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# label printers %>
|
||||
<div id="printer-settings" class="flex flex-col gap-6 rounded bg-sn-white mb-6 p-4">
|
||||
<h2 class="my-0 text-sn-black"><%= t('users.settings.account.addons.label_printers') %></h2>
|
||||
|
||||
<%# zebra printer %>
|
||||
<div class="flex flex-row justify-between">
|
||||
<%# left part %>
|
||||
<div id="left-part" class="flex flex-col">
|
||||
<%# title %>
|
||||
<div class="flex flex-row justify-between pb-2">
|
||||
<div class="font-bold text-sn-black">
|
||||
<%= t('users.settings.account.addons.zebra_printer.title') %>
|
||||
</div>
|
||||
<div class="description">
|
||||
<%= t('users.settings.account.addons.zebra_printer.description') %>
|
||||
</div>
|
||||
<%= link_to t('users.settings.account.addons.printers.details'), zebra_settings_path(), class: 'printer-details' %>
|
||||
</div>
|
||||
<%# description text %>
|
||||
<div class="text-sn-dark-grey pb-6">
|
||||
<%= t('users.settings.account.addons.zebra_printer.description') %>
|
||||
</div>
|
||||
<%# button %>
|
||||
<div>
|
||||
<%= link_to t('users.settings.account.addons.printers.details'), zebra_settings_path(), class: 'text-sn-blue' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="printers-container">
|
||||
<div class="printer">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<%= t('users.settings.account.addons.fluics_printer.title') %>
|
||||
</div>
|
||||
<div class="control">
|
||||
<%# right-part %>
|
||||
<div id="right-part" class="flex flex-row min-w-fit items-start">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-sn-black">
|
||||
<%= t('users.settings.account.addons.printers.enabled') %>
|
||||
<i class="sn-icon sn-icon-check"></i>
|
||||
</div>
|
||||
<i class="sn-icon sn-icon-check"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# solid line divider %>
|
||||
<div id="divider" class="w-500 bg-sn-light-grey flex items-center self-stretch h-px"></div>
|
||||
|
||||
<%# fluics printer %>
|
||||
<div class="flex flex-row justify-between">
|
||||
<%# left part %>
|
||||
<div id="left-part" class="flex flex-col">
|
||||
<%# title %>
|
||||
<div class="flex flex-row justify-between pb-2">
|
||||
<div class="font-bold text-sn-black">
|
||||
<%= t('users.settings.account.addons.fluics_printer.title') %>
|
||||
</div>
|
||||
<div class="description">
|
||||
<%= t('users.settings.account.addons.fluics_printer.description') %>
|
||||
</div>
|
||||
</div>
|
||||
<%# description text %>
|
||||
<div class="text-sn-dark-grey pb-6">
|
||||
<%= t('users.settings.account.addons.fluics_printer.description') %>
|
||||
</div>
|
||||
<%# button %>
|
||||
<div>
|
||||
<% if !@label_printer_any && can_manage_label_printers? %>
|
||||
<%= link_to label_printers_path(), class: 'printer-details btn btn-primary' do %>
|
||||
<i class="fas fa-key"></i>
|
||||
<%= t('users.settings.account.addons.printers.enter_api_key') %>
|
||||
<%= t('users.settings.account.addons.printers.set_up') %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to t('users.settings.account.addons.printers.printer_details'), label_printers_path(), class: 'printer-details' %>
|
||||
<%= link_to t('users.settings.account.addons.printers.details'), label_printers_path(), class: 'text-sn-blue' %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<%# right-part %>
|
||||
<div id="right-part" class="flex flex-row min-w-fit items-start">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-sn-black">
|
||||
<%= t('users.settings.account.addons.printers.enabled') %>
|
||||
</div>
|
||||
<i class="sn-icon sn-icon-check"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# Integrations inserted here via deface %>
|
||||
</div>
|
||||
<div class="tab-pane tab-pane-settings" role="tabpanel"></div>
|
||||
</div>
|
||||
|
||||
<%= javascript_include_tag "vue_scinote_edit_download", nonce: true %>
|
||||
|
|
|
@ -428,9 +428,16 @@ class Constants
|
|||
FAST_STATUS_POLLING_INTERVAL = 5000
|
||||
SLOW_STATUS_POLLING_INTERVAL = 10000
|
||||
|
||||
ASSET_SYNC_TOKEN_EXPIRATION = 1.year
|
||||
ASSET_SYNC_URL = ENV['ASSET_SYNC_URL'].freeze
|
||||
|
||||
# Grover timeout in ms
|
||||
GROVER_TIMEOUT_MS = 300000
|
||||
|
||||
# SciNote Edit supported versions
|
||||
MIN_SCINOTE_EDIT_VERSION = ENV['MIN_SCINOTE_EDIT_VERSION'].freeze
|
||||
MAX_SCINOTE_EDIT_VERSION = ENV['MAX_SCINOTE_EDIT_VERSION'].freeze
|
||||
|
||||
# ) \ / (
|
||||
# /|\ )\_/( /|\
|
||||
# * / | \ (/\|/\) / | \ *
|
||||
|
|
|
@ -494,16 +494,19 @@ class Extends
|
|||
edit_wopi_file_on_inventory_item: 295,
|
||||
export_inventory_stock_consumption: 296,
|
||||
inventory_item_relationships_linked: 297,
|
||||
inventory_item_relationships_unlinked: 298
|
||||
inventory_item_relationships_unlinked: 298,
|
||||
edit_task_step_file_locally: 299,
|
||||
edit_protocol_template_file_locally: 300,
|
||||
edit_task_result_file_locally: 301
|
||||
}
|
||||
|
||||
ACTIVITY_GROUPS = {
|
||||
projects: [*0..7, 32, 33, 34, 95, 108, 65, 109, *158..162, 241, 242, 243],
|
||||
task_results: [23, 26, 25, 42, 24, 40, 41, 99, 110, 122, 116, 128, 169, 172, 178, *246..248, *257..273, *284..291],
|
||||
task_results: [23, 26, 25, 42, 24, 40, 41, 99, 110, 122, 116, 128, 169, 172, 178, *246..248, *257..273, *284..291, 301],
|
||||
task: [8, 58, 9, 59, *10..14, 35, 36, 37, 53, 54, *60..63, 138, 139, 140, 64, 66, 106, 126, 120, 132,
|
||||
148, 166],
|
||||
task_protocol: [15, 22, 16, 18, 19, 20, 21, 17, 38, 39, 100, 111, 45, 46, 47, 121, 124, 115, 118, 127, 130, 137,
|
||||
168, 171, 177, 184, 185, 188, 189, *192..203, 221, 222, 224, 225, 226, 236, *249..252, *274..278],
|
||||
168, 171, 177, 184, 185, 188, 189, *192..203, 221, 222, 224, 225, 226, 236, *249..252, *274..278, 299],
|
||||
task_inventory: [55, 56, 146, 147, 183],
|
||||
experiment: [*27..31, 57, 141, 165],
|
||||
reports: [48, 50, 49, 163, 164],
|
||||
|
@ -512,7 +515,7 @@ class Extends
|
|||
protocol_repository: [80, 103, 89, 87, 79, 90, 91, 88, 85, 86, 84, 81, 82,
|
||||
83, 101, 112, 123, 125, 117, 119, 129, 131, 170, 173, 179, 187, 186,
|
||||
190, 191, *204..215, 220, 223, 227, 228, 229, *230..235,
|
||||
*237..240, *253..256, *279..283],
|
||||
*237..240, *253..256, *279..283, 300],
|
||||
team: [92, 94, 93, 97, 104, 244, 245],
|
||||
label_templates: [*216..219]
|
||||
}
|
||||
|
@ -604,8 +607,15 @@ class Extends
|
|||
*.nr-data.net
|
||||
www.recaptcha.net/
|
||||
www.gstatic.com/recaptcha/
|
||||
extras.scinote.net
|
||||
https://www.scinote.net
|
||||
)
|
||||
|
||||
if Constants::ASSET_SYNC_URL && EXTERNAL_SERVICES.exclude?(Constants::ASSET_SYNC_URL)
|
||||
asset_sync_url = URI.parse(Constants::ASSET_SYNC_URL)
|
||||
EXTERNAL_SERVICES << "#{asset_sync_url.scheme}://#{asset_sync_url.host}:#{asset_sync_url.port}"
|
||||
end
|
||||
|
||||
COLORED_BACKGROUND_ACTIONS = %w(
|
||||
my_modules/protocols
|
||||
my_modules/signatures
|
||||
|
@ -613,6 +623,7 @@ class Extends
|
|||
results/index
|
||||
protocols/show
|
||||
preferences/index
|
||||
addons/index
|
||||
)
|
||||
|
||||
DEFAULT_USER_NOTIFICATION_SETTINGS = {
|
||||
|
|
|
@ -414,6 +414,10 @@ en:
|
|||
add: "ADD"
|
||||
sort_by: "SORT BY"
|
||||
attachments_view_mode: "ALL ATTACHMENTS VIEW SIZE"
|
||||
open_locally_in: "Open in %{application}"
|
||||
open_in: "Open in"
|
||||
open_locally: "Open locally"
|
||||
open_in: "Open in"
|
||||
new:
|
||||
description: 'New'
|
||||
uploading: 'Uploading'
|
||||
|
@ -423,7 +427,7 @@ en:
|
|||
leaving_warning: 'Your changes will be lost if you navigate away'
|
||||
thumbnail:
|
||||
buttons:
|
||||
edit: "Edit"
|
||||
open: "Open"
|
||||
move: "Move"
|
||||
download: "Download"
|
||||
delete: "Delete"
|
||||
|
@ -1533,7 +1537,7 @@ en:
|
|||
success: 'Successfully duplicated %{count} task(s) as template.'
|
||||
duplicating: 'Duplicating contents...'
|
||||
move:
|
||||
modal_title: 'Move experiment %{experiment}'
|
||||
modal_title: 'Move experiment(s)'
|
||||
notice: 'Moving is possible to projects, where you have permissions to create experiments and tasks.'
|
||||
target_project: 'Target project'
|
||||
label_title: 'Move'
|
||||
|
@ -1544,6 +1548,8 @@ en:
|
|||
no_projects: 'No projects: You don’t have edit access to any other projects.'
|
||||
table:
|
||||
head_title: "%{experiment} | Overview"
|
||||
move_success_flash: "Successfully moved experiment(s) to project %{project}."
|
||||
move_error_flash: "Failed to move experiment(s) to project %{project}."
|
||||
column:
|
||||
id_html: 'ID'
|
||||
task_name_html: 'Task name'
|
||||
|
@ -1823,10 +1829,10 @@ en:
|
|||
destroy:
|
||||
success_flash: "File result successfully deleted."
|
||||
wopi_open_file: "Open in %{app}"
|
||||
wopi_edit_file: "Edit in %{app}"
|
||||
wopi_word: "Word for the web"
|
||||
wopi_excel: "Excel for the web"
|
||||
wopi_powerpoint: "PowerPoint for the web"
|
||||
wopi_edit_file: "Open in %{app}"
|
||||
wopi_word: "MS Word online"
|
||||
wopi_excel: "Excel online"
|
||||
wopi_powerpoint: "PowerPoint online"
|
||||
error_flash: 'Something went wrong! Please try again later.'
|
||||
|
||||
result_tables:
|
||||
|
@ -1904,16 +1910,16 @@ en:
|
|||
alert_line_2: "all references to inventory items will be rendered as invalid."
|
||||
delete: "Delete"
|
||||
modal_rename:
|
||||
title_html: "Edit inventory: %{name}"
|
||||
title_html: "Rename inventory: %{name}"
|
||||
name: "Inventory name"
|
||||
name_placeholder: "My inventory"
|
||||
rename: "Save"
|
||||
modal_copy:
|
||||
title_html: "Copy inventory: %{name}"
|
||||
title_html: "Duplicate inventory: %{name}"
|
||||
name: "New inventory name"
|
||||
description: "Only the structure of the inventory is going to be copied."
|
||||
name_placeholder: "My inventory"
|
||||
copy: "Copy"
|
||||
copy: "Duplicate"
|
||||
modal_export:
|
||||
title: "Export Inventories"
|
||||
description_p1_html:
|
||||
|
@ -2596,6 +2602,7 @@ en:
|
|||
title: "There seems to be no printer available"
|
||||
description: "To learn more about printing labels and label printers please visit our blog."
|
||||
visit_blog: "Visit blog"
|
||||
general_error: "Something went wrong"
|
||||
reminder:
|
||||
clear: "Clear"
|
||||
low_stock_title: "Item running low"
|
||||
|
@ -2804,6 +2811,7 @@ en:
|
|||
printer_details: "Printer details"
|
||||
details: "Details"
|
||||
enter_api_key: "Enter API key"
|
||||
set_up: "Set up"
|
||||
enabled: "Enabled"
|
||||
fluics_printer:
|
||||
title: "FLUICS Print"
|
||||
|
@ -2812,6 +2820,13 @@ en:
|
|||
title: 'Zebra label printers'
|
||||
description: 'Print labels of custom styles and sizes in seconds flat from your PC or Mac via your Zebra printer connected via USB, Internet or LAN.'
|
||||
details: 'Details'
|
||||
desktop_app:
|
||||
title: 'SciNote Edit'
|
||||
description: 'Download the SciNote Edit desktop app to automatically edit files using locally installed software.'
|
||||
macos_button: 'Download for macOS'
|
||||
windows_button: 'Download for Windows'
|
||||
version: 'Version %{version}'
|
||||
more_info: 'More info'
|
||||
label_printer:
|
||||
fluics_printer: "FLUICS Print: Label printers"
|
||||
zebra_printer: 'Zebra label printers'
|
||||
|
@ -3335,6 +3350,7 @@ en:
|
|||
16_x_24: '384 (16 x 24)'
|
||||
8_x_12: '96 (8 x 12)'
|
||||
6_x_8: '48 (6 x 8 )'
|
||||
6_x_4: '24 (6 x 4)'
|
||||
4_x_6: '24 (4 x 6)'
|
||||
3_x_4: '12 (3 x 4)'
|
||||
2_x_3: '6 (2 x 3)'
|
||||
|
@ -3598,8 +3614,8 @@ en:
|
|||
forbidden: 'You do not have permission to add files.'
|
||||
not_found: 'Element not found.'
|
||||
file_preview:
|
||||
edit_in_scinote: "Edit"
|
||||
edit_in_marvinjs: "Edit in Marvin JS"
|
||||
edit_in_scinote: "Open in image editor"
|
||||
edit_in_marvinjs: "Open in Marvin JS"
|
||||
context_menu:
|
||||
set_view_size: "SET PREVIEW SIZE"
|
||||
delete: "Delete"
|
||||
|
@ -3615,6 +3631,19 @@ en:
|
|||
empty_office_file:
|
||||
description: "The file is empty. Please add some data before saving the file."
|
||||
reload: "Reload file"
|
||||
no_predefined_app_modal:
|
||||
body_text_html: "The specified application for accessing the <b>%{file_name}</b> is not preconfigured. To successfully open this file, please set up the appropriate application in your local environment beforehand.</p>"
|
||||
understand_button: "I understand"
|
||||
set_up_app: "Set up an application to open this file"
|
||||
update_version_modal:
|
||||
title: "Update required"
|
||||
body_text_html: "The current version of the SciNote Edit application is no longer supported. To ensure a seamless and secure user experience, we recommend installing to the latest version."
|
||||
edit_launching_application_modal:
|
||||
title: "Launching application"
|
||||
description: "%{file_name} will now open in %{application}. Saved changes in %{application} will automatically be synced in SciNote."
|
||||
conflict_error: "A newer version of the file was already present in the SciNote web app and your file was saved as “%{filename}”. Close this window and re-open file to get the latest changes."
|
||||
default_error: "An error occurred while saving to SciNote. Please save changes to your open document using “Save As…” to avoid losing any working changes."
|
||||
default_error_with_filename: "An error occurred while saving “%{filename}” to SciNote. Please save changes to your open document using “Save As…” to avoid losing any working changes."
|
||||
|
||||
atwho:
|
||||
no_results:
|
||||
|
@ -3808,6 +3837,7 @@ en:
|
|||
edit: "Edit"
|
||||
delete: "Delete"
|
||||
cancel: "Cancel"
|
||||
copy: "Copy"
|
||||
duplicate: "Duplicate"
|
||||
okay: "Okay"
|
||||
back: "Back"
|
||||
|
@ -4036,7 +4066,7 @@ en:
|
|||
new_sequence: "Sequence"
|
||||
new_sequence_file: "New sequence"
|
||||
default_sequence_name: "New sequence"
|
||||
edit_sequence: "Edit in Sequence editor"
|
||||
edit_sequence: "Open in sequence editor"
|
||||
sequence_name_placeholder: "Click here to enter sequence name"
|
||||
editor:
|
||||
tooltips:
|
||||
|
|
|
@ -319,6 +319,9 @@ en:
|
|||
result_text_moved_html: "%{user} moved text <strong>%{text_name}</strong> from result %{result_original} to result %{result_destination}."
|
||||
result_table_moved_html: "%{user} moved table <strong>%{table_name}</strong> from result %{result_original} to result %{result_destination}."
|
||||
move_chemical_structure_on_result_html: "%{user} moved chemical structure <strong>%{file}</strong> from result %{result_original} to result %{result_destination}."
|
||||
edit_task_step_file_locally_html: "%{user} locally edited file %{file} on protocol's step %{step_position_original} %{step} on task %{my_module}"
|
||||
edit_protocol_template_file_locally_html: "%{user} locally edited file %{file} on protocol's step %{step_position_original} %{step} with SciNote Edit in Protocol repository"
|
||||
edit_task_result_file_locally_html: "%{user} locally edited file %{file} on result %{result}"
|
||||
export_inventories_html: "%{user} exported inventory %{inventories}"
|
||||
edit_image_on_inventory_item_html: "%{user} edited image %{asset_name} on inventory item %{repository_row} in inventory %{repository}: %{action}."
|
||||
edit_wopi_file_on_inventory_item_html: "%{user} edited Office online file %{asset_name} on inventory item %{repository_row} in inventory %{repository}: %{action}."
|
||||
|
@ -382,8 +385,8 @@ en:
|
|||
delete_step_comment: "Task step comment deleted"
|
||||
complete_step: "Task step completed"
|
||||
uncomplete_step: "Task step uncompleted"
|
||||
uncomplete_task: "Task uncompleted"
|
||||
complete_task: "Task completed"
|
||||
uncomplete_task: "Task uncompleted (obsolete)"
|
||||
complete_task: "Task completed (obsolete)"
|
||||
assign_repository_record: "Task inventory assigned"
|
||||
unassign_repository_record: "Task inventory unassigned"
|
||||
assign_user_to_project: "User assigned to Project"
|
||||
|
@ -596,6 +599,9 @@ en:
|
|||
result_text_moved: "Result text moved"
|
||||
result_table_moved: "Result table moved"
|
||||
move_chemical_structure_on_result: "Chemical structure on result moved"
|
||||
edit_task_step_file_locally: "File on task step edited locally"
|
||||
edit_protocol_template_file_locally: "File on protocol templates edited locally"
|
||||
edit_task_result_file_locally: "File on task result edited locally"
|
||||
export_inventories: "Inventories exported"
|
||||
edit_image_on_inventory_item: "Inventory item image edited"
|
||||
edit_wopi_file_on_inventory_item: "Inventory item wopi file edited"
|
||||
|
|
|
@ -387,6 +387,8 @@ Rails.application.routes.draw do
|
|||
get 'clone_modal', action: :clone_modal
|
||||
get 'move_modal', action: :move_modal
|
||||
get 'actions_toolbar'
|
||||
get 'move_modal' # return modal with move options
|
||||
post 'move' # move experiment
|
||||
end
|
||||
member do
|
||||
get 'permissions'
|
||||
|
@ -410,8 +412,6 @@ Rails.application.routes.draw do
|
|||
post 'archive' # archive experiment
|
||||
get 'clone_modal' # return modal with clone options
|
||||
post 'clone' # clone experiment
|
||||
get 'move_modal' # return modal with move options
|
||||
post 'move' # move experiment
|
||||
get 'fetch_workflow_img' # Get updated workflow img
|
||||
get 'modules/new', to: 'my_modules#new'
|
||||
post 'modules', to: 'my_modules#create'
|
||||
|
@ -1007,6 +1007,11 @@ Rails.application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
get 'asset_sync/:asset_id', to: 'asset_sync#show', as: :asset_sync_show
|
||||
get 'asset_sync/:asset_id/download', to: 'asset_sync#download', as: :asset_sync_download
|
||||
put 'asset_sync', to: 'asset_sync#update'
|
||||
get '/asset_sync_api_url', to: 'asset_sync#api_url'
|
||||
|
||||
post 'global_activities', to: 'global_activities#index'
|
||||
|
||||
constraints WopiSubdomain do
|
||||
|
@ -1018,4 +1023,8 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
resources :gene_sequence_assets, only: %i(new create edit update)
|
||||
|
||||
if Rails.env.development? || ENV['ENABLE_DESIGN_ELEMENTS'] == 'true'
|
||||
resources :design_elements, only: %i(index)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,7 +36,12 @@ namespace :v2 do
|
|||
path: 'items',
|
||||
only: [],
|
||||
as: :items do
|
||||
resources :inventory_item_relationships, only: %i(create destroy)
|
||||
resources :child_relationships,
|
||||
only: %i(index show create destroy),
|
||||
controller: :inventory_item_child_relationships
|
||||
resources :parent_relationships,
|
||||
only: %i(index show create destroy),
|
||||
controller: :inventory_item_parent_relationships
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ module.exports = {
|
|||
'./app/javascript/**/*.vue',
|
||||
'./app/assets/javascripts/**/*.js',
|
||||
'./app/views/**/*.{erb,haml,html,slim}',
|
||||
'./addons/**/*.{erb,haml,html,slim}',
|
||||
'./addons/**/*.{erb,haml,html,slim,vue}'
|
||||
],
|
||||
corePlugins: {
|
||||
preflight: false
|
||||
|
|
|
@ -48,6 +48,9 @@ const entryList = {
|
|||
vue_user_preferences: './app/javascript/packs/vue/user_preferences.js',
|
||||
vue_components_manage_stock_value_modal: './app/javascript/packs/vue/manage_stock_value_modal.js',
|
||||
vue_legacy_datetime_picker: './app/javascript/packs/vue/legacy/datetime_picker.js',
|
||||
vue_open_locally_menu: './app/javascript/packs/vue/open_locally_menu.js',
|
||||
vue_scinote_edit_download: './app/javascript/packs/vue/scinote_edit_download.js',
|
||||
vue_design_system_modals: './app/javascript/packs/vue/design_system/modals.js'
|
||||
}
|
||||
|
||||
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949
|
||||
|
|
15
db/migrate/20231006141428_create_asset_sync_tokens.rb
Normal file
15
db/migrate/20231006141428_create_asset_sync_tokens.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateAssetSyncTokens < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :asset_sync_tokens do |t|
|
||||
t.references :user, null: false, foreign_key: true
|
||||
t.references :asset, null: false, foreign_key: true
|
||||
t.string :token, index: { unique: true }
|
||||
t.timestamp :expires_at
|
||||
t.timestamp :revoked_at
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
41
db/migrate/20240115140943_fix_missing_team_id_for_assets.rb
Normal file
41
db/migrate/20240115140943_fix_missing_team_id_for_assets.rb
Normal file
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FixMissingTeamIdForAssets < ActiveRecord::Migration[7.0]
|
||||
def up
|
||||
# step assets
|
||||
execute(
|
||||
'UPDATE assets ' \
|
||||
'SET team_id = protocols.team_id ' \
|
||||
'FROM protocols ' \
|
||||
'INNER JOIN steps ON steps.protocol_id = protocols.id ' \
|
||||
'INNER JOIN step_assets ON step_assets.step_id = steps.id ' \
|
||||
'WHERE step_assets.asset_id = assets.id ' \
|
||||
'AND (protocols.team_id != assets.team_id OR assets.team_id IS NULL)'
|
||||
)
|
||||
|
||||
# result assets
|
||||
execute(
|
||||
'UPDATE assets ' \
|
||||
'SET team_id = protocols.team_id ' \
|
||||
'FROM protocols ' \
|
||||
'INNER JOIN my_modules ON my_modules.id = protocols.my_module_id ' \
|
||||
'INNER JOIN results ON results.my_module_id = my_modules.id ' \
|
||||
'INNER JOIN result_assets ON result_assets.result_id = results.id ' \
|
||||
'WHERE result_assets.asset_id = assets.id ' \
|
||||
'AND (protocols.team_id != assets.team_id OR assets.team_id IS NULL)'
|
||||
)
|
||||
|
||||
# repository assets
|
||||
execute(
|
||||
'UPDATE assets ' \
|
||||
'SET team_id = repositories.team_id ' \
|
||||
'FROM repositories ' \
|
||||
'INNER JOIN repository_columns ON repository_columns.repository_id = repositories.id ' \
|
||||
'INNER JOIN repository_cells ON repository_cells.repository_column_id = repository_columns.id ' \
|
||||
'INNER JOIN repository_asset_values ON repository_asset_values.id = repository_cells.value_id ' \
|
||||
'AND repository_cells.value_type = \'RepositoryAssetValue\' ' \
|
||||
'WHERE repository_asset_values.asset_id = assets.id ' \
|
||||
'AND assets.team_id IS NULL'
|
||||
)
|
||||
end
|
||||
end
|
15
db/schema.rb
15
db/schema.rb
|
@ -76,6 +76,19 @@ ActiveRecord::Schema[7.0].define(version: 2024_01_18_094253) do
|
|||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "asset_sync_tokens", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.bigint "asset_id", null: false
|
||||
t.string "token"
|
||||
t.datetime "expires_at", precision: nil
|
||||
t.datetime "revoked_at", precision: nil
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["asset_id"], name: "index_asset_sync_tokens_on_asset_id"
|
||||
t.index ["token"], name: "index_asset_sync_tokens_on_token", unique: true
|
||||
t.index ["user_id"], name: "index_asset_sync_tokens_on_user_id"
|
||||
end
|
||||
|
||||
create_table "asset_text_data", force: :cascade do |t|
|
||||
t.text "data", null: false
|
||||
t.bigint "asset_id", null: false
|
||||
|
@ -1334,6 +1347,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_01_18_094253) do
|
|||
add_foreign_key "activities", "my_modules"
|
||||
add_foreign_key "activities", "projects"
|
||||
add_foreign_key "activities", "users", column: "owner_id"
|
||||
add_foreign_key "asset_sync_tokens", "assets"
|
||||
add_foreign_key "asset_sync_tokens", "users"
|
||||
add_foreign_key "asset_text_data", "assets"
|
||||
add_foreign_key "assets", "users", column: "created_by_id"
|
||||
add_foreign_key "assets", "users", column: "last_modified_by_id"
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
"bootstrap": "^3.4.1",
|
||||
"bootstrap-select": "^1.13.18",
|
||||
"bwip-js": "^3.0.1",
|
||||
"compare-versions": "^6.1.0",
|
||||
"compression-webpack-plugin": "8.0.1",
|
||||
"croppie": "^2.6.4",
|
||||
"css-loader": "^6.7.3",
|
||||
|
|
|
@ -16,7 +16,6 @@ describe ExperimentsController, type: :controller do
|
|||
archive_group: { project_id: 1 },
|
||||
restore_group: { project_id: 1 },
|
||||
clone: { id: 1 },
|
||||
move: { id: 1 },
|
||||
module_archive: { id: 1 },
|
||||
fetch_workflow_img: { id: 1 },
|
||||
sidebar: { id: 1 },
|
||||
|
@ -112,13 +111,13 @@ describe ExperimentsController, type: :controller do
|
|||
it_behaves_like "a controller action with permissions checking", :get, :move_modal do
|
||||
let(:testable) { experiment }
|
||||
let(:permissions) { [ExperimentPermissions::MANAGE] }
|
||||
let(:action_params) { { id: experiment.id } }
|
||||
let(:action_params) { { ids: [experiment.id] } }
|
||||
end
|
||||
|
||||
it_behaves_like "a controller action with permissions checking", :post, :move do
|
||||
let(:testable) { experiment }
|
||||
let(:permissions) { [ExperimentPermissions::MANAGE] }
|
||||
let(:action_params) { { id: experiment.id } }
|
||||
let(:action_params) { { ids: [experiment.id], project_id: project.id } }
|
||||
end
|
||||
|
||||
it_behaves_like "a controller action with permissions checking", :get, :module_archive do
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V2::InventoryItemChildRelationshipsController', type: :request do
|
||||
before :all do
|
||||
@user = create(:user)
|
||||
@normal_user = create(:user, full_name: 'Normal User')
|
||||
@team = create(:team, created_by: @user)
|
||||
@inventory = create(:repository, team: @team, created_by: @user)
|
||||
@other_inventory = create(:repository, team: @team, created_by: @user)
|
||||
@inventory_item = create(:repository_row, repository: @inventory, created_by: @user)
|
||||
@child_inventory_item = create(:repository_row, repository: @other_inventory, created_by: @user)
|
||||
@child_connection = create(:repository_row_connection, parent: @inventory_item, child: @child_inventory_item, created_by: @user)
|
||||
@other_inventory_item = create(:repository_row, repository: @other_inventory, created_by: @user)
|
||||
|
||||
@valid_headers = {
|
||||
'Authorization': 'Bearer ' + generate_token(@user.id),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
it 'Response with correct inventory child relationship items' do
|
||||
hash_body = nil
|
||||
get api_v2_team_inventory_item_child_relationships_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id,
|
||||
include: :child
|
||||
), headers: @valid_headers
|
||||
expect { hash_body = json }.not_to raise_exception
|
||||
expect(hash_body[:data]).to match_array(
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource
|
||||
.new(@inventory_item.child_connections.order(:id),
|
||||
each_serializer: Api::V2::InventoryItemRelationshipSerializer,
|
||||
include: :child)
|
||||
.to_json
|
||||
)['data']
|
||||
)
|
||||
expect(hash_body).to include('included')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
it 'Response with correct inventory child relationship item' do
|
||||
hash_body = nil
|
||||
get api_v2_team_inventory_item_child_relationship_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id,
|
||||
id: @child_connection.id,
|
||||
include: :child
|
||||
), headers: @valid_headers
|
||||
expect { hash_body = json }.not_to raise_exception
|
||||
expect(hash_body[:data]).to match_array(
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource
|
||||
.new(@child_connection,
|
||||
serializer: Api::V2::InventoryItemRelationshipSerializer,
|
||||
include: :child)
|
||||
.to_json
|
||||
)['data']
|
||||
)
|
||||
expect(hash_body).to include('included')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
let(:create_action) do
|
||||
post api_v2_team_inventory_item_child_relationships_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id
|
||||
), params: request_body.to_json, headers: @valid_headers
|
||||
end
|
||||
|
||||
context 'with valid parameters' do
|
||||
context 'using child_id' do
|
||||
let(:request_body) do
|
||||
{
|
||||
data: {
|
||||
type: 'inventory_item_relationships',
|
||||
attributes: {
|
||||
child_id: @other_inventory_item.id
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates a new relationship' do
|
||||
expect { create_action }.to change { RepositoryRowConnection.count }.by(1)
|
||||
expect(response).to have_http_status(201)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non valid parameters' do
|
||||
context 'with missing type' do
|
||||
let(:request_body) { { data: { type: '', attributes: { parent_id: @other_inventory_item.id } } } }
|
||||
|
||||
it 'renders 400 bad request' do
|
||||
create_action
|
||||
expect(response).to have_http_status(400)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with child_id for missing item' do
|
||||
let(:request_body) do
|
||||
{
|
||||
data: {
|
||||
type: 'inventory_item_relationships',
|
||||
attributes: {
|
||||
child_id: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'renders 404 not found' do
|
||||
create_action
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without manage permission' do
|
||||
let(:another_create_action) do
|
||||
post api_v2_team_inventory_item_child_relationships_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id
|
||||
),
|
||||
params: {
|
||||
data: {
|
||||
type: 'inventory_item_relationships',
|
||||
attributes: {
|
||||
child_id: @other_inventory_item.id
|
||||
}
|
||||
}
|
||||
}.to_json,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + generate_token(@normal_user.id),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
end
|
||||
|
||||
it 'renders 403 forbidden' do
|
||||
another_create_action
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
let(:relationship) { create(:repository_row_connection, parent: @inventory_item, child: @other_inventory_item, created_by: @user) }
|
||||
|
||||
let(:destroy_action) do
|
||||
delete api_v2_team_inventory_item_child_relationship_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id,
|
||||
id: relationship.id
|
||||
), headers: @valid_headers
|
||||
end
|
||||
|
||||
context 'with valid ID' do
|
||||
it 'deletes the relationship' do
|
||||
destroy_action
|
||||
expect(response).to have_http_status(200)
|
||||
expect(RepositoryRowConnection.where(id: relationship.id)).to_not exist
|
||||
end
|
||||
end
|
||||
|
||||
context 'without manage permission' do
|
||||
let(:another_destroy_action) do
|
||||
delete api_v2_team_inventory_item_child_relationship_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id,
|
||||
id: relationship.id
|
||||
),
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + generate_token(@normal_user.id),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
end
|
||||
|
||||
it 'renders 403 forbidden' do
|
||||
another_destroy_action
|
||||
expect(response).to have_http_status(403)
|
||||
expect(RepositoryRowConnection.where(id: relationship.id)).to exist
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid ID' do
|
||||
it 'returns 404 not found' do
|
||||
delete api_v2_team_inventory_item_child_relationship_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id,
|
||||
id: -1
|
||||
), headers: @valid_headers
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,208 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V2::InventoryItemParentRelationshipsController', type: :request do
|
||||
before :all do
|
||||
@user = create(:user)
|
||||
@normal_user = create(:user, full_name: 'Normal User')
|
||||
@team = create(:team, created_by: @user)
|
||||
@inventory = create(:repository, team: @team, created_by: @user)
|
||||
@other_inventory = create(:repository, team: @team, created_by: @user)
|
||||
@inventory_item = create(:repository_row, repository: @inventory, created_by: @user)
|
||||
@parent_inventory_item = create(:repository_row, repository: @other_inventory, created_by: @user)
|
||||
@parent_connection = create(:repository_row_connection, child: @inventory_item, parent: @parent_inventory_item, created_by: @user)
|
||||
@other_inventory_item = create(:repository_row, repository: @other_inventory, created_by: @user)
|
||||
|
||||
@valid_headers = {
|
||||
'Authorization': 'Bearer ' + generate_token(@user.id),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
it 'Response with correct inventory parent relationship items' do
|
||||
hash_body = nil
|
||||
get api_v2_team_inventory_item_parent_relationships_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id,
|
||||
include: :parent
|
||||
), headers: @valid_headers
|
||||
expect { hash_body = json }.not_to raise_exception
|
||||
expect(hash_body[:data]).to match_array(
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource
|
||||
.new(@inventory_item.parent_connections.order(:id),
|
||||
each_serializer: Api::V2::InventoryItemRelationshipSerializer,
|
||||
include: :parent)
|
||||
.to_json
|
||||
)['data']
|
||||
)
|
||||
expect(hash_body).to include('included')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
it 'Response with correct inventory parent relationship item' do
|
||||
hash_body = nil
|
||||
get api_v2_team_inventory_item_parent_relationship_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id,
|
||||
id: @parent_connection.id,
|
||||
include: :parent
|
||||
), headers: @valid_headers
|
||||
expect { hash_body = json }.not_to raise_exception
|
||||
expect(hash_body[:data]).to match_array(
|
||||
JSON.parse(
|
||||
ActiveModelSerializers::SerializableResource
|
||||
.new(@parent_connection,
|
||||
serializer: Api::V2::InventoryItemRelationshipSerializer,
|
||||
include: :parent)
|
||||
.to_json
|
||||
)['data']
|
||||
)
|
||||
expect(hash_body).to include('included')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
let(:create_action) do
|
||||
post api_v2_team_inventory_item_parent_relationships_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id
|
||||
), params: request_body.to_json, headers: @valid_headers
|
||||
end
|
||||
|
||||
context 'with valid parameters' do
|
||||
context 'using parent_id' do
|
||||
let(:request_body) do
|
||||
{
|
||||
data: {
|
||||
type: 'inventory_item_relationships',
|
||||
attributes: {
|
||||
parent_id: @other_inventory_item.id
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates a new relationship' do
|
||||
expect { create_action }.to change { RepositoryRowConnection.count }.by(1)
|
||||
expect(response).to have_http_status(201)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non valid parameters' do
|
||||
context 'with missing type' do
|
||||
let(:request_body) { { data: { type: '', attributes: { parent_id: @other_inventory_item.id } } } }
|
||||
|
||||
it 'renders 400 bad request' do
|
||||
create_action
|
||||
expect(response).to have_http_status(400)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with parent_id for missing item' do
|
||||
let(:request_body) do
|
||||
{
|
||||
data: {
|
||||
type: 'inventory_item_relationships',
|
||||
attributes: {
|
||||
parent_id: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'renders 404 not found' do
|
||||
create_action
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without manage permission' do
|
||||
let(:another_create_action) do
|
||||
post api_v2_team_inventory_item_parent_relationships_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id
|
||||
),
|
||||
params: {
|
||||
data: {
|
||||
type: 'inventory_item_relationships',
|
||||
attributes: {
|
||||
parent_id: @other_inventory_item.id
|
||||
}
|
||||
}
|
||||
}.to_json,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + generate_token(@normal_user.id),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
end
|
||||
|
||||
it 'renders 403 forbidden' do
|
||||
another_create_action
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
let(:relationship) { create(:repository_row_connection, child: @inventory_item, parent: @other_inventory_item, created_by: @user) }
|
||||
|
||||
let(:destroy_action) do
|
||||
delete api_v2_team_inventory_item_parent_relationship_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id,
|
||||
id: relationship.id
|
||||
), headers: @valid_headers
|
||||
end
|
||||
|
||||
context 'with valid ID' do
|
||||
it 'deletes the relationship' do
|
||||
destroy_action
|
||||
expect(response).to have_http_status(200)
|
||||
expect(RepositoryRowConnection.where(id: relationship.id)).to_not exist
|
||||
end
|
||||
end
|
||||
|
||||
context 'without manage permission' do
|
||||
let(:another_destroy_action) do
|
||||
delete api_v2_team_inventory_item_parent_relationship_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id,
|
||||
id: relationship.id
|
||||
),
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + generate_token(@normal_user.id),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
end
|
||||
|
||||
it 'renders 403 forbidden' do
|
||||
another_destroy_action
|
||||
expect(response).to have_http_status(403)
|
||||
expect(RepositoryRowConnection.where(id: relationship.id)).to exist
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid ID' do
|
||||
it 'returns 404 not found' do
|
||||
delete api_v2_team_inventory_item_parent_relationship_path(
|
||||
team_id: @team.id,
|
||||
inventory_id: @inventory.id,
|
||||
item_id: @inventory_item.id,
|
||||
id: -1
|
||||
), headers: @valid_headers
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -199,12 +199,12 @@
|
|||
var beforeAutofillInsidePopulate = function(index, direction, data, deltas, iterators, selected) {
|
||||
var instance = this;
|
||||
|
||||
var r = index.row,
|
||||
c = index.col,
|
||||
var rlength = data.length, // rows
|
||||
clength = data[0].length, //cols
|
||||
r = index.row % rlength,
|
||||
c = index.col % clength,
|
||||
value = data[r][c],
|
||||
delta = 0,
|
||||
rlength = data.length, // rows
|
||||
clength = data ? data[0].length : 0; //cols
|
||||
delta = 0;
|
||||
|
||||
if (value[0] === '=') { // formula
|
||||
|
||||
|
|
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.eot
vendored
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.eot
vendored
Binary file not shown.
36
vendor/assets/stylesheets/fonts/SN-icon-font.svg
vendored
36
vendor/assets/stylesheets/fonts/SN-icon-font.svg
vendored
|
@ -5,14 +5,14 @@
|
|||
<json>
|
||||
<![CDATA[
|
||||
{
|
||||
"fontFamily": "SN-icon-font",
|
||||
"designer": "David Praznik",
|
||||
"description": "SN icon font\nFont generated by IcoMoon.",
|
||||
"copyright": "SciNote LLC",
|
||||
"majorVersion": 1,
|
||||
"minorVersion": 14,
|
||||
"description": "SN icon font\nFont generated by IcoMoon.",
|
||||
"designer": "David Praznik",
|
||||
"fontFamily": "SN-icon-font",
|
||||
"fontURL": "https://www.scinote.net",
|
||||
"version": "Version 1.14",
|
||||
"majorVersion": 1,
|
||||
"minorVersion": 17,
|
||||
"version": "Version 1.17",
|
||||
"fontId": "SN-icon-font",
|
||||
"psName": "SN-icon-font",
|
||||
"subFamily": "Regular",
|
||||
|
@ -150,4 +150,28 @@
|
|||
<glyph unicode="" glyph-name="star-filled" data-tags="star-filled" horiz-adv-x="983" d="M778.24 513.942h-217.088l-69.632 228.558-69.632-228.558h-217.088l174.49-124.518-67.174-220.365 179.405 135.168 179.405-135.168-67.174 220.365 174.49 124.518z" />
|
||||
<glyph unicode="" glyph-name="marvinjs" data-tags="marvinjs" horiz-adv-x="983" d="M491.52 777.123l285.639-160.672v-321.343l-285.639-160.674-285.639 160.674v321.343l285.639 160.672zM819.2 640.1l-327.68 184.32-327.68-184.32v-368.642l327.68-184.32 327.68 184.32v368.642zM484.835 688.801l203.817-124.109-22.282-34.736-203.817 124.11 22.282 34.734zM344.375 578.66v-245.762h-42.041v245.762h42.041zM688.652 346.866l-203.817-124.109-22.282 34.734 203.817 124.109 22.282-34.734z" />
|
||||
<glyph unicode="" glyph-name="sequence-editor" data-tags="sequence-editor" horiz-adv-x="983" d="M317.364 734.104c46.285 29.082 99.94 46.285 154.417 49.562 11.469 0.819 20.070 10.24 19.251 21.709-0.819 11.059-9.421 19.661-21.709 19.251-61.438-3.686-121.649-22.938-173.668-55.706s-95.846-78.643-125.747-132.301c-30.31-53.658-46.285-114.687-47.104-176.127s14.336-122.88 43.418-177.357c3.686-6.963 10.65-10.65 18.022-10.65 3.277 0 6.554 0.819 9.83 2.458 9.83 5.325 13.926 17.613 8.602 27.853-25.805 48.333-38.912 102.81-38.502 157.696s15.155 108.952 41.779 156.876c26.624 47.923 65.536 88.474 111.821 117.555l-0.41-0.819zM285.825 344.166c2.867 0 5.734 0.41 8.192 1.638 10.24 4.506 15.155 16.794 10.24 27.034-13.107 29.901-19.251 62.669-17.613 95.437 2.048 32.768 11.469 64.717 28.262 92.568 16.794 28.262 40.141 52.019 67.584 69.222 27.441 17.203 59.39 27.853 91.748 30.31 11.469 0.819 19.661 10.65 18.842 22.118s-11.878 19.661-22.118 18.842c-38.912-3.277-77.003-15.974-110.18-36.454-33.178-20.89-61.030-49.562-81.101-83.149s-31.539-72.088-33.997-111 4.915-78.643 20.89-114.278c3.277-7.782 11.059-12.288 18.842-12.288h0.41zM449.253 385.946c-9.83 5.734-22.118 2.867-28.262-6.963-5.734-9.83-2.867-22.118 6.963-28.262 18.022-11.059 38.912-16.794 59.802-17.613 1.229 0 2.048 0 3.277 0 20.070 0 39.731 4.915 57.344 14.336 9.83 5.325 13.926 17.613 8.602 27.853s-17.613 13.926-27.853 8.602c-12.288-6.554-26.624-9.83-40.55-9.421-13.926 0-27.853 4.506-39.731 11.469h0.41zM386.996 456.397c0 0 2.867 0 4.096 0 9.421 0 18.022 6.554 20.068 16.384 3.277 15.974 11.059 30.31 22.528 42.189 11.469 11.469 26.214 19.251 42.189 22.528 11.059 2.048 18.432 13.106 16.384 24.165s-13.107 18.022-24.166 16.384c-23.757-4.506-45.875-16.384-63.076-33.586-17.203-17.203-29.082-38.912-33.997-63.078-2.048-11.059 4.915-21.709 15.974-24.166v-0.819zM661.836 307.302c3.686-2.458 7.782-4.096 11.878-4.096 6.144 0 12.698 2.867 16.384 8.602 35.226 48.742 51.61 108.544 45.466 168.755-1.229 11.059-10.24 18.842-22.528 18.432-11.059-1.229-19.251-11.059-18.432-22.528 4.915-49.971-8.602-99.942-38.093-140.493-6.554-9.011-4.506-22.118 4.506-28.672h0.819zM474.649 252.006c-36.045 2.867-70.449 15.565-99.94 36.045-9.421 6.554-22.118 4.096-28.672-5.325s-4.096-22.118 5.325-28.672c35.635-24.576 77.003-39.322 120.011-43.008 6.554-0.41 13.107-0.819 19.661-0.819 36.454 0 72.499 8.192 105.677 23.757 10.24 4.915 14.746 17.203 9.83 27.443s-17.203 14.746-27.443 9.83c-32.358-15.565-68.813-22.118-104.448-19.251zM643.404 746.802c57.344-29.901 104.038-75.776 135.168-132.301 3.686-6.963 10.65-10.65 18.022-10.65 3.277 0 6.963 0.819 9.83 2.458 9.83 5.325 13.517 18.022 8.192 27.853-35.226 63.898-87.654 115.098-151.962 149.094-9.83 5.325-22.118 1.229-27.443-8.602s-1.229-22.528 8.602-27.853h-0.41zM837.964 496.538c-11.469-0.819-20.070-10.24-19.251-21.709 3.686-64.307-11.469-127.386-43.418-183.091s-79.462-100.352-136.806-129.024c-57.344-28.672-121.651-39.731-185.139-32.358-63.486 7.373-123.288 33.587-172.44 74.547-8.602 7.373-21.709 6.144-29.082-2.458s-6.144-21.709 2.458-29.082c55.296-46.694 122.47-75.776 194.148-83.968 14.336-1.638 29.082-2.458 43.418-2.458 56.934 0 113.050 13.107 164.659 38.912 64.717 32.358 117.965 82.33 154.010 144.998s53.248 133.53 49.152 206.029c-0.41 11.469-8.602 19.661-21.709 19.251v0.41z" />
|
||||
<glyph unicode="" glyph-name="file-unknown" data-tags="file-unknown" horiz-adv-x="983" d="M225.28 87.138c-28.672 0-52.907 9.9-72.704 29.696-19.797 19.8-29.696 44.032-29.696 72.704v532.482c0 28.672 9.899 52.907 29.696 72.704s44.032 29.696 72.704 29.696h532.48c28.672 0 52.908-9.899 72.704-29.696s29.696-44.032 29.696-72.704v-532.482c0-28.672-9.9-52.904-29.696-72.704-19.796-19.796-44.032-29.696-72.704-29.696h-532.48zM225.28 128.098h532.48c17.068 0 31.568 5.98 43.5 17.94 11.96 11.936 17.94 26.436 17.94 43.5v532.482c0 17.067-5.98 31.567-17.94 43.5-11.932 11.96-26.431 17.94-43.5 17.94h-532.48c-17.067 0-31.567-5.98-43.5-17.94-11.96-11.933-17.94-26.433-17.94-43.5v-532.482c0-17.064 5.98-31.564 17.94-43.5 11.933-11.96 26.433-17.94 43.5-17.94zM458.822 407.138v3.027c0.078 14.041 1.397 25.211 3.957 33.513 2.638 8.298 6.361 14.971 11.17 20.013 4.809 5.12 10.666 9.814 17.572 14.082 4.731 2.945 8.958 6.164 12.685 9.658 3.801 3.568 6.787 7.524 8.958 11.866 2.171 4.424 3.26 9.351 3.26 14.778 0 6.132-1.438 11.444-4.309 15.942-2.867 4.502-6.746 7.991-11.633 10.473-4.813 2.482-10.203 3.723-16.175 3.723-5.509 0-10.744-1.2-15.712-3.604-4.887-2.331-8.958-5.898-12.218-10.707-3.178-4.731-4.964-10.744-5.349-18.035h-41.427c0.389 14.737 3.957 27.070 10.707 37.003 6.824 9.928 15.823 17.376 26.997 22.34 11.248 4.965 23.658 7.447 37.233 7.447 14.819 0 27.853-2.599 39.1-7.796 11.325-5.197 20.132-12.606 26.415-22.225 6.361-9.544 9.54-20.869 9.54-33.98 0-8.843-1.434-16.753-4.305-23.736-2.793-6.984-6.787-13.189-11.985-18.62-5.198-5.427-11.366-10.277-18.502-14.545-6.283-3.879-11.444-7.909-15.475-12.1-3.957-4.19-6.906-9.118-8.843-14.778-1.864-5.587-2.834-12.489-2.912-20.713v-3.027h-38.748zM479.068 334.529c-6.98 0-12.993 2.482-18.035 7.447s-7.565 11.018-7.565 18.153c0 6.984 2.523 12.956 7.565 17.92s11.055 7.447 18.035 7.447c6.906 0 12.878-2.482 17.92-7.447 5.12-4.964 7.68-10.936 7.68-17.92 0-4.731-1.2-9.036-3.604-12.915-2.331-3.879-5.431-6.984-9.31-9.31-3.801-2.249-8.028-3.375-12.685-3.375z" />
|
||||
<glyph unicode="" glyph-name="file-eln" data-tags="file-eln" horiz-adv-x="983" d="M225.28 87.138c-28.672 0-52.907 9.9-72.704 29.696-19.797 19.8-29.696 44.032-29.696 72.704v532.482c0 28.672 9.899 52.907 29.696 72.704s44.032 29.696 72.704 29.696h532.48c28.672 0 52.908-9.899 72.704-29.696s29.696-44.032 29.696-72.704v-532.482c0-28.672-9.9-52.904-29.696-72.704-19.796-19.796-44.032-29.696-72.704-29.696h-532.48zM225.28 128.098h532.48c17.068 0 31.568 5.98 43.5 17.94 11.96 11.936 17.94 26.436 17.94 43.5v532.482c0 17.067-5.98 31.567-17.94 43.5-11.932 11.96-26.431 17.94-43.5 17.94h-532.48c-17.067 0-31.567-5.98-43.5-17.94-11.96-11.933-17.94-26.433-17.94-43.5v-532.482c0-17.064 5.98-31.564 17.94-43.5 11.933-11.96 26.433-17.94 43.5-17.94zM245.76 332.898v204.8h131.243v-31.097h-94.688v-55.603h87.89v-31.097h-87.89v-55.902h95.476v-31.101h-132.031zM414.298 332.898v204.8h36.553v-173.699h88.875v-31.101h-125.428zM737.28 537.698v-204.8h-32.514l-95.085 139.502h-1.675v-139.502h-36.553v204.8h32.711l94.986-139.6h1.774v139.6h36.356z" />
|
||||
<glyph unicode="" glyph-name="arrow-s-down" data-tags="arrow-s-down" horiz-adv-x="983" d="M512 783.46v-578.246l86.802 86.569 28.201-28.201-135.483-135.483-135.483 135.483 28.2 28.201 86.802-86.569v578.246h40.96z" />
|
||||
<glyph unicode="" glyph-name="arrow-s-up" data-tags="arrow-s-up" horiz-adv-x="983" d="M471.040 128.098v578.246l-86.802-86.567-28.2 28.199 135.483 135.483 135.483-135.483-28.201-28.199-86.802 86.567v-578.246h-40.96z" />
|
||||
<glyph unicode="" glyph-name="arrow-s-left" data-tags="arrow-s-left" horiz-adv-x="983" d="M819.2 435.298h-578.245l86.567-86.807-28.199-28.197-135.483 135.483 135.483 135.482 28.199-28.2-86.567-86.802h578.245v-40.96z" />
|
||||
<glyph unicode="" glyph-name="arrow-s-right" data-tags="arrow-s-right" horiz-adv-x="983" d="M163.84 476.258h578.245l-86.569 86.802 28.201 28.2 135.483-135.482-135.483-135.483-28.201 28.197 86.569 86.807h-578.245v40.96z" />
|
||||
<glyph unicode="" glyph-name="add-text" data-tags="add-text" horiz-adv-x="983" d="M512 496.738h-368.64v-40.96h368.64v40.96zM716.8 169.058h-573.44v-40.96h573.44v40.96zM716.8 332.898h-573.44v-40.96h573.44v40.96zM430.080 660.58h-286.72v-40.96h286.72v40.96zM716.8 783.46h-40.96v-122.88h-122.88v-40.96h122.88v-122.882h40.96v122.882h122.88v40.96h-122.88v122.88z" />
|
||||
<glyph unicode="" glyph-name="add-file" data-tags="add-file" horiz-adv-x="983" d="M716.8 885.46h-40.96v-122.88h-122.88v-40.96h122.88v-122.88h40.96v122.88h122.88v40.96h-122.88v122.88zM675.84 91.656c0-6.963-2.458-12.698-7.782-18.022-4.915-5.325-11.059-7.782-18.022-7.782h-440.32c-6.963 0-12.698 2.458-18.022 7.782-5.325 4.915-7.782 11.059-7.782 18.022v604.569c0 6.963 2.458 12.698 7.782 18.022 4.915 5.325 11.059 7.782 18.022 7.782h261.325v40.96h-261.325c-19.251 0-34.816-6.144-47.514-18.842s-18.842-28.672-18.842-47.514v-604.159c0-19.251 6.144-34.816 18.842-47.514s28.672-18.842 47.514-18.842h440.32c19.251 0 34.816 6.144 47.514 18.842s18.842 28.672 18.842 47.514v425.165h-40.96v-425.165l0.41-0.819z" />
|
||||
<glyph unicode="" glyph-name="add-photo-camera" data-tags="add-photo-camera" horiz-adv-x="983" d="M320.306 461.922c-29.491-29.491-44.237-65.536-44.237-108.544s14.746-79.053 44.237-108.544c29.491-29.491 65.536-44.237 108.545-44.237s79.053 14.746 108.544 44.237c29.491 29.491 44.237 65.536 44.237 108.544s-14.746 79.053-44.237 108.544c-29.491 29.491-65.536 44.237-108.544 44.237s-79.054-14.746-108.545-44.237zM540.672 353.378c0-31.949-10.65-58.573-31.949-79.872s-47.923-31.949-79.872-31.949c-31.95 0-58.574 10.65-79.873 31.949s-31.949 47.923-31.949 79.872c0 31.949 10.65 58.573 31.949 79.872s47.923 31.949 79.873 31.949c31.949 0 58.573-10.65 79.872-31.949s31.949-47.923 31.949-79.872zM756.531 133.014c0-7.373-2.458-13.517-6.963-18.022-4.915-4.915-10.65-6.963-18.022-6.963h-604.98c-7.373 0-13.517 2.458-18.022 6.963s-6.963 10.65-6.963 18.022v440.731c0 7.373 2.458 13.517 6.963 18.022 4.915 4.915 10.65 6.963 18.022 6.963h139.264l75.366 81.92h212.993v40.96h-230.606l-75.776-81.92h-121.242c-18.842 0-34.406-6.144-47.104-18.842s-18.842-28.262-18.842-47.104v-441.141c0-18.842 6.144-34.406 18.842-47.104s28.262-18.842 47.104-18.842h604.98c18.842 0 34.406 6.144 47.104 18.842s18.842 28.262 18.842 47.104l2.048 343.245h-40.96l-2.048-343.245v0.41zM799.539 844.9h-43.008v-122.88h-120.832v-40.96h120.832l2.048-122.88h40.96l-2.048 122.88h124.928v40.96h-124.928l2.048 122.88z" />
|
||||
<glyph unicode="" glyph-name="link" data-tags="link" horiz-adv-x="983" d="M512 558.18h-40.96v-204.802h40.96v204.802zM491.52 844.9c-101.99 0-184.32-82.33-184.32-184.32v-143.362h40.96v143.362c0 79.053 64.307 143.36 143.36 143.36s143.36-64.307 143.36-143.36v-143.362h40.96v143.362c0 101.99-82.33 184.32-184.32 184.32zM491.52 66.658c101.99 0 184.32 82.33 184.32 184.32v143.36h-40.96v-143.36c0-79.053-64.307-143.36-143.36-143.36s-143.36 64.307-143.36 143.36v143.36h-40.96v-143.36c0-101.99 82.33-184.32 184.32-184.32z" />
|
||||
<glyph unicode="" glyph-name="link-s" data-tags="link-s" horiz-adv-x="964" d="M389.12 619.62c0 56.525 45.875 102.4 102.4 102.4s102.4-45.875 102.4-102.4v-95.029h40.96v95.029c0 38.912-15.565 74.138-40.96 99.942-26.214 26.624-62.259 43.418-102.4 43.418s-76.186-16.794-102.4-43.418c-25.395-25.805-40.96-61.030-40.96-99.942v-95.029h40.96v95.029zM593.92 291.946c0-56.525-45.875-102.4-102.4-102.4s-102.4 45.875-102.4 102.4v68.813h-40.96v-68.813c0-38.912 15.565-74.138 40.96-99.942 26.214-26.624 62.259-43.418 102.4-43.418s76.186 16.794 102.4 43.418c25.395 25.805 40.96 61.030 40.96 99.942v68.813h-40.96v-68.813zM471.040 565.56v-245.761h40.96v245.761h-40.96z" />
|
||||
<glyph unicode="" glyph-name="unlink" data-tags="unlink" horiz-adv-x="983" d="M493.568 803.94c79.053 0 143.36-64.307 143.36-143.36v-214.222l40.96-40.96v255.182c0 101.99-82.33 184.32-184.32 184.32-69.222 0-128.614-38.502-160.153-95.437l30.31-30.31c22.528 49.562 71.68 84.787 129.843 84.787zM615.629 177.66c-24.986-41.779-70.451-70.042-122.47-70.042-79.053 0-143.36 64.307-143.36 143.36v192.512l-40.96 40.96v-233.472c0-101.99 82.33-184.32 184.32-184.32 63.488 0 118.784 32.768 151.552 81.51l-29.491 29.491h0.41zM627.917 272.687l40.141-40.55 62.669-62.259 29.082 28.672-507.494 507.496-29.082-29.082 77.005-76.595 327.68-327.682z" />
|
||||
<glyph unicode="" glyph-name="unlink-s" data-tags="unlink-s" horiz-adv-x="964" d="M572.69 230.089c-18.842-24.576-47.923-40.55-81.101-40.55-56.525 0-102.399 45.875-102.399 102.4v121.242l-40.96 40.96v-162.202c0-38.912 15.565-74.138 40.96-99.942 26.214-26.624 62.258-43.418 102.399-43.418s76.186 16.794 102.4 43.418c2.867 2.867 4.915 6.144 7.373 9.421l-28.672 28.672zM410.489 681.47c18.842 24.576 47.923 40.55 81.101 40.55 56.525 0 102.4-45.875 102.4-102.4v-121.243l40.96-40.96v162.203c0 38.912-15.565 74.138-40.96 99.942-26.214 26.624-62.259 43.418-102.4 43.418s-76.186-16.794-102.399-43.418c-2.867-2.867-4.915-6.144-7.373-9.421l28.671-28.672zM694.26 281.801l-376.519 376.519-28.963-28.963 376.519-376.519 28.963 28.963z" />
|
||||
<glyph unicode="" glyph-name="link-italic" data-tags="link-italic" horiz-adv-x="983" d="M433.811 542.638l-28.964-28.963 144.815-144.814 28.963 28.963-144.814 144.814zM216.475 731.002c-72.090-72.090-72.090-188.417 0-260.506l101.171-101.171 29.082 29.082-101.581 101.581c-55.706 55.706-55.706 147.047 0 202.753s147.046 55.706 202.751 0l101.581-101.581 29.082 29.082-101.171 101.171c-72.498 71.68-188.824 71.68-260.914-0.41zM766.566 180.908c72.090 72.090 72.090 188.826 0 260.506l-101.171 101.171-29.082-29.082 101.581-101.581c55.706-55.706 55.706-147.046 0-202.752s-147.046-55.706-202.752 0l-101.581 101.581-29.080-29.082 101.17-101.171c72.090-72.090 188.826-72.090 260.506 0l0.41 0.41z" />
|
||||
<glyph unicode="" glyph-name="link-italic-s" data-tags="link-italic-s" horiz-adv-x="964" d="M303.105 499.192c-39.731 39.731-39.731 104.858 0 144.999s104.858 39.731 144.998 0l67.174-67.174 29.082 29.082-67.174 67.174c-27.443 27.443-63.488 41.37-99.942 41.779-37.274 0.41-74.547-13.517-102.81-41.779s-42.189-65.946-41.779-102.81c0-36.045 14.336-72.090 41.779-99.943l67.174-67.174 29.082 29.082-67.174 67.174-0.41-0.41zM679.936 412.357c39.731-39.731 39.731-104.858 0-144.998s-104.858-39.731-144.998 0l-48.333 48.333-29.082-29.082 48.333-48.333c27.443-27.443 63.488-41.37 99.942-41.779 37.274-0.41 74.547 13.517 102.81 41.779s42.189 65.946 41.779 102.81c0 36.045-14.336 72.090-41.779 99.942l-48.333 48.333-29.082-29.082 48.333-48.333 0.41 0.41zM399.537 518.877l173.776-173.777 28.967 28.963-173.781 173.777-28.962-28.963z" />
|
||||
<glyph unicode="" glyph-name="unlink-italic" data-tags="unlink-italic" horiz-adv-x="983" d="M233.881 701.531c55.706 55.706 147.046 55.706 202.752 0l151.552-151.553h57.754l-180.224 180.635c-72.090 72.090-188.826 72.090-260.506 0-49.152-48.742-63.898-118.374-45.875-180.225h43.008c-19.251 50.792-9.011 110.593 31.949 151.553l-0.41-0.41zM763.494 345.588c11.878-47.104 0-99.123-36.864-135.987-55.706-55.706-147.046-55.706-202.752 0l-136.397 136.397h-57.754l165.069-165.069c72.090-72.090 188.826-72.090 260.506 0 45.056 45.056 61.030 106.906 49.562 165.069h-41.37v-0.41zM850.33 420.954v40.96h-717.62v-40.96h717.62z" />
|
||||
<glyph unicode="" glyph-name="unlink-italic-s" data-tags="unlink-italic-s" horiz-adv-x="964" d="M708.608 353.378c4.096-30.31-5.325-62.669-28.672-86.016-39.731-39.731-104.858-39.731-144.998 0l-86.016 86.016h-57.754l114.688-114.688c27.443-27.443 63.488-41.37 99.942-41.779 37.274-0.41 74.547 13.517 102.81 41.779s42.189 65.946 41.779 102.81c0 4.096-0.819 7.782-1.229 11.878h-40.55zM274.433 558.184c-4.096 30.31 5.325 62.669 28.672 86.016 39.731 39.731 104.858 39.731 144.997 0l86.016-86.016h57.754l-114.688 114.688c-27.443 27.443-63.488 41.37-99.941 41.779-37.274 0.41-74.547-13.517-102.81-41.779-28.672-28.262-42.189-65.536-41.779-102.81 0-4.096 0.819-7.782 1.229-11.878h40.55zM757.76 476.258h-532.48v-40.96h532.48v40.96z" />
|
||||
<glyph unicode="" glyph-name="open" data-tags="open" horiz-adv-x="983" d="M777.83 194.445c0-6.963-2.048-12.698-7.373-18.022-4.915-5.325-11.059-7.782-18.022-7.782h-522.24c-6.963 0-12.698 2.458-18.022 7.782-5.325 4.915-7.782 11.059-7.782 18.022v522.65c0 6.963 2.458 12.698 7.782 18.022 4.915 5.325 11.059 7.782 18.022 7.782h220.365v40.96h-220.365c-19.251 0-34.816-6.144-47.514-18.842s-18.842-28.672-18.842-47.514v-522.24c0-19.251 6.144-34.816 18.842-47.514s28.672-18.842 47.514-18.842h522.24c19.251 0 34.816 6.144 47.514 18.842s18.842 28.672 18.842 47.514v220.365h-40.96v-221.184zM398.855 334.152l-28.987 28.987 379.351 379.351h-175.813v40.96h245.76v-245.76h-40.96v175.813l-379.352-379.351z" />
|
||||
<glyph unicode="" glyph-name="history-search" data-tags="history seaerch" d="M512 789.333c-128.853 0-240.64-71.68-298.667-176.64v91.307h-42.667v-170.667h170.667v42.667h-98.987c48.213 100.693 150.613 170.667 269.653 170.667 164.693 0 298.667-133.973 298.667-298.667s-133.973-298.667-298.667-298.667c-134.827 0-249.173 90.027-286.293 213.333h-43.947c37.973-147.2 171.093-256 330.24-256 188.587 0 341.333 152.747 341.333 341.333s-152.747 341.333-341.333 341.333zM676.267 292.267l-29.867-29.867-177.067 177.067v221.867h42.667v-204.8l164.267-164.267z" />
|
||||
<glyph unicode="" glyph-name="item" data-tags="item" d="M682.667 661.333c0-94.257-76.412-170.667-170.667-170.667-94.257 0-170.667 76.41-170.667 170.667s76.41 170.667 170.667 170.667c94.255 0 170.667-76.41 170.667-170.667zM725.333 106.667c-70.694 0-128 57.306-128 128s57.306 128 128 128c70.694 0 128-57.306 128-128s-57.306-128-128-128zM725.333 64c94.255 0 170.667 76.412 170.667 170.667s-76.412 170.667-170.667 170.667c-94.255 0-170.667-76.412-170.667-170.667s76.412-170.667 170.667-170.667zM298.667 106.667c-70.692 0-128 57.306-128 128s57.308 128 128 128c70.692 0 128-57.306 128-128s-57.308-128-128-128zM298.667 64c94.257 0 170.667 76.412 170.667 170.667s-76.41 170.667-170.667 170.667c-94.257 0-170.667-76.412-170.667-170.667s76.41-170.667 170.667-170.667z" />
|
||||
<glyph unicode="" glyph-name="move-arrows" data-tags="move-arrows" d="M330.988 564.239l-81.66-81.675h134.673v-42.667h-134.69l81.677-81.694-30.165-30.165-133.184 133.184 133.184 133.182 30.165-30.165zM693.013 564.239l30.165 30.165 133.184-133.182-133.184-133.184-30.165 30.165 81.677 81.694h-134.69v42.667h134.673l-81.66 81.675zM512.597 520.956l60.352-60.331-60.352-60.352-60.331 60.352 60.331 60.331zM408.982 280.209l81.684-81.668v134.69h42.667v-134.694l81.685 81.673 30.165-30.165-133.184-133.184-133.183 133.184 30.165 30.165zM408.982 642.234l-30.165 30.165 133.183 133.184 133.184-133.184-30.165-30.165-81.685 81.669v-134.674h-42.667v134.673l-81.684-81.668z" />
|
||||
<glyph unicode="" glyph-name="refresh" data-tags="refresh" d="M810.667 283.307c-58.027-104.96-169.813-176.64-298.667-176.64-188.587 0-341.333 152.747-341.333 341.333h42.667c0-164.693 133.973-298.667 298.667-298.667 119.040 0 221.44 69.973 269.653 170.667h-98.987v42.667h170.667v-170.667h-42.667v91.307zM213.333 612.693c58.027 104.96 169.813 176.64 298.667 176.64 188.587 0 341.333-152.747 341.333-341.333h-42.667c0 164.693-133.973 298.667-298.667 298.667-119.040 0-221.44-69.973-269.653-170.667h98.987v-42.667h-170.667v170.667h42.667v-91.307z" />
|
||||
<glyph unicode="" glyph-name="pin" data-tags="pin" d="M411.605 357.077l-99.584 99.541c0 0 0.683 21.333 14.293 39.893 29.141 39.68 86.443 41.813 129.195 33.536l67.371 66.304c-14.933 49.963 71.509 107.648 71.509 107.648 65.152-65.195 130.347-130.389 195.541-195.584-4.736-6.357-9.515-12.715-14.293-19.072-22.912-30.421-54.144-56.107-91.136-49.877l-70.827-68.651c0.981-7.339 0.896-0.64 2.517-24.576 3.072-45.44-13.653-92.245-56.149-113.92l-15.915-7.765-102.357 102.357-155.563-155.605-30.208 30.208 155.605 155.563zM587.392 648.747c-14.677-12.117-30.208-30.933-21.845-44.203l10.453-15.403-106.368-106.411c-42.411 10.795-92.715 13.568-109.995-13.099l192.427-192.683c2.005 1.408 3.925 2.901 5.803 4.48 26.155 23.040 16.299 64.853 10.624 104.533l102.187 102.187c0 0 31.36-20.011 62.635 16.555l-144.853 144.853c-0.384-0.256-0.725-0.555-1.067-0.811z" />
|
||||
<glyph unicode="" glyph-name="pinned" data-tags="pinned" d="M483.341 312.717l-140.803-0.030c0 0-14.602 15.569-18.102 38.319-7.452 48.661 31.558 90.688 67.641 115.068l0.754 94.52c-45.888 24.77-25.554 126.683-25.554 126.683 92.17-0.030 184.369-0.030 276.567-0.030 1.148-7.844 2.261-15.719 3.379-23.593 5.312-37.712 1.387-77.959-29.175-99.712l-1.536-98.624c5.884-4.497 1.084 0.179 19.157-15.599 34.304-29.961 55.573-74.88 40.849-120.256l-5.76-16.747h-144.755l0.030-220.028h-42.722l0.030 220.028zM401.399 643.26c-1.81-18.947 0.513-43.234 15.809-46.703l18.281-3.5 0.030-150.458c-37.62-22.353-75.151-55.966-68.514-87.040l272.311-0.179c0.422 2.415 0.725 4.826 0.939 7.27 2.202 34.786-34.334 57.382-66.406 81.429v144.513c0 0 36.326 8.025 32.585 55.995h-204.854c-0.090-0.453-0.12-0.905-0.181-1.327z" />
|
||||
</font></defs></svg>
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 112 KiB |
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.ttf
vendored
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.ttf
vendored
Binary file not shown.
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.woff
vendored
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.woff
vendored
Binary file not shown.
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.woff2
vendored
BIN
vendor/assets/stylesheets/fonts/SN-icon-font.woff2
vendored
Binary file not shown.
84
vendor/assets/stylesheets/sn-icon-font.css
vendored
84
vendor/assets/stylesheets/sn-icon-font.css
vendored
|
@ -1,11 +1,11 @@
|
|||
@font-face {
|
||||
font-family: 'SN-icon-font';
|
||||
src: url('fonts/SN-icon-font.eot?x922k8');
|
||||
src: url('fonts/SN-icon-font.eot?x922k8#iefix') format('embedded-opentype'),
|
||||
url('fonts/SN-icon-font.woff2?x922k8') format('woff2'),
|
||||
url('fonts/SN-icon-font.ttf?x922k8') format('truetype'),
|
||||
url('fonts/SN-icon-font.woff?x922k8') format('woff'),
|
||||
url('fonts/SN-icon-font.svg?x922k8#SN-icon-font') format('svg');
|
||||
src: url('fonts/SN-icon-font.eot?h4j5vh');
|
||||
src: url('fonts/SN-icon-font.eot?h4j5vh#iefix') format('embedded-opentype'),
|
||||
url('fonts/SN-icon-font.woff2?h4j5vh') format('woff2'),
|
||||
url('fonts/SN-icon-font.ttf?h4j5vh') format('truetype'),
|
||||
url('fonts/SN-icon-font.woff?h4j5vh') format('woff'),
|
||||
url('fonts/SN-icon-font.svg?h4j5vh#SN-icon-font') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
|
@ -398,3 +398,75 @@
|
|||
.sn-icon-sequence-editor:before {
|
||||
content: "\e97b";
|
||||
}
|
||||
.sn-icon-file-unknown:before {
|
||||
content: "\e97c";
|
||||
}
|
||||
.sn-icon-file-eln:before {
|
||||
content: "\e97d";
|
||||
}
|
||||
.sn-icon-arrow-s-down:before {
|
||||
content: "\e97e";
|
||||
}
|
||||
.sn-icon-arrow-s-up:before {
|
||||
content: "\e97f";
|
||||
}
|
||||
.sn-icon-arrow-s-left:before {
|
||||
content: "\e980";
|
||||
}
|
||||
.sn-icon-arrow-s-right:before {
|
||||
content: "\e981";
|
||||
}
|
||||
.sn-icon-add-text:before {
|
||||
content: "\e982";
|
||||
}
|
||||
.sn-icon-add-file:before {
|
||||
content: "\e983";
|
||||
}
|
||||
.sn-icon-add-photo-camera:before {
|
||||
content: "\e984";
|
||||
}
|
||||
.sn-icon-link:before {
|
||||
content: "\e985";
|
||||
}
|
||||
.sn-icon-link-s:before {
|
||||
content: "\e986";
|
||||
}
|
||||
.sn-icon-unlink:before {
|
||||
content: "\e987";
|
||||
}
|
||||
.sn-icon-unlink-s:before {
|
||||
content: "\e988";
|
||||
}
|
||||
.sn-icon-link-italic:before {
|
||||
content: "\e989";
|
||||
}
|
||||
.sn-icon-link-italic-s:before {
|
||||
content: "\e98a";
|
||||
}
|
||||
.sn-icon-unlink-italic:before {
|
||||
content: "\e98b";
|
||||
}
|
||||
.sn-icon-unlink-italic-s:before {
|
||||
content: "\e98c";
|
||||
}
|
||||
.sn-icon-open:before {
|
||||
content: "\e98d";
|
||||
}
|
||||
.sn-icon-history-search:before {
|
||||
content: "\e98e";
|
||||
}
|
||||
.sn-icon-item:before {
|
||||
content: "\e98f";
|
||||
}
|
||||
.sn-icon-move-arrows:before {
|
||||
content: "\e990";
|
||||
}
|
||||
.sn-icon-refresh:before {
|
||||
content: "\e992";
|
||||
}
|
||||
.sn-icon-pin:before {
|
||||
content: "\e993";
|
||||
}
|
||||
.sn-icon-pinned:before {
|
||||
content: "\e994";
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue