Merge branch 'develop' into features/sso-improvements

This commit is contained in:
Oleksii Kriuchykhin 2024-02-01 15:38:07 +01:00
commit e724b15499
98 changed files with 2309 additions and 385 deletions

View file

@ -1 +1 @@
1.29.6
1.30.0

View file

@ -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 %>,

View file

@ -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();

View file

@ -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) {

View file

@ -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>`;
}

View file

@ -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 %>',
};

View file

@ -148,7 +148,6 @@ var filterDropdown = (function() {
initCloseButton();
initDateTimePickerComponent();
initSearchField(filtersEnabledFunction);
this.toggleFilterMark($filterContainer, filtersEnabled)
return $filterContainer;
},
toggleFilterMark: function(filterContainer, filtersEnabledArg) {

View file

@ -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');
});
}
};

View file

@ -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;

View file

@ -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 {

View file

@ -236,3 +236,7 @@
.sa-link {
pointer-events: initial;
}
.atwho-inserted {
line-height: 16px;
}

View file

@ -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,

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -0,0 +1,4 @@
class DesignElementsController < ApplicationController
def index
end
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -9,6 +9,7 @@ module Users
def index
@label_printer_any = LabelPrinter.any?
@user_agent = request.user_agent
end
private

View file

@ -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' }

View 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');

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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');

View 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');

View file

@ -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;

View file

@ -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;
}

View file

@ -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 {

View file

@ -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" />

View file

@ -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;

View file

@ -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');
});
}
});

View file

@ -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'

View file

@ -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,

View file

@ -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}`);
}
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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) {

View file

@ -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;
}
}
};

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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) {

View file

@ -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();

View 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');
}
},
}

View 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>

View 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>

View file

@ -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

View 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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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|

View file

@ -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?

View 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

View file

@ -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]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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',

View 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' %>

View file

@ -0,0 +1 @@
<%= render partial: 'modals' %>

View file

@ -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>

View file

@ -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">

View file

@ -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 %>

View file

@ -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 %>

View file

@ -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 %>

View file

@ -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
# ) \ / (
# /|\ )\_/( /|\
# * / | \ (/\|/\) / | \ *

View file

@ -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 = {

View file

@ -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 dont 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:

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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"

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

Binary file not shown.

View file

@ -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="&#xe979;" 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="&#xe97a;" 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="&#xe97b;" 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="&#xe97c;" 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="&#xe97d;" 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="&#xe97e;" 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="&#xe97f;" 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="&#xe980;" 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="&#xe981;" 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="&#xe982;" 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="&#xe983;" 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="&#xe984;" 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="&#xe985;" 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="&#xe986;" 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="&#xe987;" 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="&#xe988;" 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="&#xe989;" 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="&#xe98a;" 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="&#xe98b;" 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="&#xe98c;" 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="&#xe98d;" 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="&#xe98e;" 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="&#xe98f;" 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="&#xe990;" 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="&#xe992;" 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="&#xe993;" 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="&#xe994;" 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

Binary file not shown.

Binary file not shown.

View file

@ -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";
}