mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-29 16:34:32 +08:00
Merge branch 'features/repository-templates' into develop
This commit is contained in:
commit
4e5f9c738d
46 changed files with 961 additions and 88 deletions
|
@ -117,7 +117,6 @@ var DasboardCurrentTasksWidget = (function() {
|
|||
}
|
||||
}
|
||||
appendTasksList(result, '.current-tasks-list-wrapper');
|
||||
PerfectSb().update_all();
|
||||
|
||||
InfiniteScroll.init('.current-tasks-list-wrapper', {
|
||||
url: $currentTasksList.data('tasksListUrl'),
|
||||
|
|
|
@ -32,8 +32,6 @@ var DasboardRecentWorkWidget = (function() {
|
|||
} else {
|
||||
container.append($('#recent-work-no-results-template').html());
|
||||
}
|
||||
|
||||
PerfectSb().update_all();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global PerfectScrollbar activePSB PerfectSb I18n */
|
||||
/* global I18n */
|
||||
/* eslint-disable no-unused-vars, no-use-before-define */
|
||||
|
||||
/*
|
||||
|
@ -301,7 +301,6 @@ var dropdownSelector = (function() {
|
|||
function generateDropdown(selector, config = {}) {
|
||||
var selectElement = $(selector);
|
||||
var optionContainer;
|
||||
var perfectScroll;
|
||||
var dropdownContainer;
|
||||
var toggleElement;
|
||||
|
||||
|
@ -410,10 +409,6 @@ var dropdownSelector = (function() {
|
|||
}
|
||||
});
|
||||
|
||||
// Initialize scroll bar inside options container
|
||||
perfectScroll = new PerfectScrollbar(dropdownContainer.find('.dropdown-container')[0]);
|
||||
activePSB.push(perfectScroll);
|
||||
|
||||
// Select options container
|
||||
optionContainer = dropdownContainer.find('.dropdown-container');
|
||||
|
||||
|
@ -450,7 +445,6 @@ var dropdownSelector = (function() {
|
|||
if (dropdownContainer.hasClass('open')) {
|
||||
// Each time we open option container we must scroll it
|
||||
dropdownContainer.find('.dropdown-container').scrollTop(0);
|
||||
PerfectSb().update_all();
|
||||
|
||||
// on Open we load new data
|
||||
loadData(selectElement, dropdownContainer);
|
||||
|
@ -652,9 +646,6 @@ var dropdownSelector = (function() {
|
|||
$(`<div class="empty-dropdown">${I18n.t('dropdown_selector.nothing_found')}</div>`).appendTo(container.find('.dropdown-container'));
|
||||
}
|
||||
|
||||
// Update scrollbar
|
||||
PerfectSb().update_all();
|
||||
|
||||
// Check position of option dropdown
|
||||
refreshDropdownSelection(selector, container);
|
||||
|
||||
|
@ -865,7 +856,6 @@ var dropdownSelector = (function() {
|
|||
}].concat(optionsAjax);
|
||||
}
|
||||
loadData(selector, container, optionsAjax);
|
||||
PerfectSb().update_all();
|
||||
});
|
||||
// For local options we convert options element from select to correct array
|
||||
} else if (selector.data('select-by-group')) {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
.recent-work-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 0 8px;
|
||||
position: relative;
|
||||
|
||||
|
|
|
@ -155,6 +155,7 @@
|
|||
|
||||
.activities-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-top: 10px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
@ -284,6 +285,7 @@
|
|||
.filters-container {
|
||||
height: 100%;
|
||||
margin-bottom: 60px;
|
||||
overflow-y: auto;
|
||||
padding: 15px 20px;
|
||||
position: absolute;
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@
|
|||
bottom: calc(100% - 30px);
|
||||
box-shadow: $flyout-shadow;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
position: fixed;
|
||||
transition: .2s;
|
||||
transition-property: top, bottom, box-shadow;
|
||||
|
|
|
@ -40,6 +40,7 @@ class ProtocolsController < ApplicationController
|
|||
unlink
|
||||
unlink_modal
|
||||
delete_steps
|
||||
list_published_protocol_templates
|
||||
)
|
||||
|
||||
before_action :check_manage_with_read_protocol_permissions, only: %i(
|
||||
|
@ -798,6 +799,26 @@ class ProtocolsController < ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
def list_published_protocol_templates
|
||||
protocols = Protocol.latest_available_versions_without_drafts(current_team)
|
||||
.active
|
||||
.with_granted_permissions(current_user, ProtocolPermissions::READ)
|
||||
|
||||
if params[:query].present?
|
||||
protocols = protocols.where_attributes_like(
|
||||
['protocols.name', Protocol::PREFIXED_ID_SQL],
|
||||
params[:query]
|
||||
)
|
||||
end
|
||||
|
||||
protocols = protocols.order('LOWER(protocols.name) asc').page(params[:page])
|
||||
render json: {
|
||||
paginated: true,
|
||||
next_page: protocols.next_page,
|
||||
data: protocols.map { |protocol_template| [protocol_template.id, protocol_template.name] }
|
||||
}
|
||||
end
|
||||
|
||||
def protocol_status_bar
|
||||
render json: {
|
||||
html: render_to_string(partial: 'my_modules/protocols/protocol_status_bar', formats: :html)
|
||||
|
|
|
@ -151,17 +151,31 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def create
|
||||
@repository = Repository.new(
|
||||
team: current_team,
|
||||
created_by: current_user
|
||||
)
|
||||
@repository.assign_attributes(repository_params)
|
||||
Repository.transaction do
|
||||
@repository = Repository.new(team: current_team, created_by: current_user)
|
||||
@repository.assign_attributes(repository_params)
|
||||
|
||||
if @repository.save
|
||||
@repository.save!
|
||||
log_activity(:create_inventory)
|
||||
|
||||
repository_template = current_team.repository_templates.find_by(id: repository_params[:repository_template_id])
|
||||
if repository_template.present?
|
||||
repository_template.column_definitions&.each do |column_attributes|
|
||||
service = RepositoryColumns::CreateColumnService
|
||||
.call(user: current_user, repository: @repository, team: current_team,
|
||||
column_type: column_attributes['column_type'],
|
||||
params: column_attributes['params'].with_indifferent_access)
|
||||
unless service.succeed?
|
||||
render json: service.errors, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
end
|
||||
render json: { message: t('repositories.index.modal_create.success_flash_html', name: @repository.name) }
|
||||
else
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.error e.message
|
||||
render json: @repository.errors, status: :unprocessable_entity
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -548,7 +562,7 @@ class RepositoriesController < ApplicationController
|
|||
end
|
||||
|
||||
def repository_params
|
||||
params.require(:repository).permit(:name)
|
||||
params.require(:repository).permit(:name, :repository_template_id)
|
||||
end
|
||||
|
||||
def import_params
|
||||
|
|
33
app/controllers/repository_templates_controller.rb
Normal file
33
app/controllers/repository_templates_controller.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RepositoryTemplatesController < ApplicationController
|
||||
before_action :check_read_permissions
|
||||
before_action :load_repository_template, only: :list_repository_columns
|
||||
|
||||
def index
|
||||
repository_templates = current_team.repository_templates.order(:id)
|
||||
render json: {
|
||||
data: repository_templates.map { |repository_template| [repository_template.id, repository_template.name] }
|
||||
}
|
||||
end
|
||||
|
||||
def list_repository_columns
|
||||
render json: {
|
||||
name: @repository_template.name,
|
||||
columns: @repository_template.column_definitions&.map do |column|
|
||||
[column.dig('params', 'name'), I18n.t("libraries.manange_modal_column.select.#{RepositoryColumn.data_types.key(column['column_type']).underscore}")]
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_repository_template
|
||||
@repository_template = current_team.repository_templates.find_by(id: params[:id])
|
||||
render_404 unless @repository_template
|
||||
end
|
||||
|
||||
def check_read_permissions
|
||||
render_403 unless can_create_repositories?(current_team)
|
||||
end
|
||||
end
|
|
@ -7,11 +7,11 @@ class StepsController < ApplicationController
|
|||
before_action :load_vars, only: %i(update destroy show toggle_step_state update_view_state
|
||||
update_asset_view_mode elements
|
||||
attachments upload_attachment duplicate)
|
||||
before_action :load_vars_nested, only: %i(create index reorder)
|
||||
before_action :load_vars_nested, only: %i(create index reorder list_protocol_steps add_protocol_steps)
|
||||
before_action :convert_table_contents_to_utf8, only: %i(create update)
|
||||
|
||||
before_action :check_protocol_manage_permissions, only: %i(reorder)
|
||||
before_action :check_view_permissions, only: %i(show index attachments elements)
|
||||
before_action :check_protocol_manage_permissions, only: %i(reorder add_protocol_steps)
|
||||
before_action :check_view_permissions, only: %i(show index attachments elements list_protocol_steps)
|
||||
before_action :check_create_permissions, only: %i(create)
|
||||
before_action :check_manage_permissions, only: %i(update destroy
|
||||
update_view_state update_asset_view_mode upload_attachment)
|
||||
|
@ -267,6 +267,49 @@ class StepsController < ApplicationController
|
|||
}
|
||||
end
|
||||
|
||||
def list_protocol_steps
|
||||
steps = @protocol.steps
|
||||
|
||||
if params[:query].present?
|
||||
steps = steps.where_attributes_like(
|
||||
['steps.name'],
|
||||
params[:query]
|
||||
)
|
||||
end
|
||||
|
||||
steps = steps.order(position: :asc).page(params[:page])
|
||||
render json: {
|
||||
paginated: true,
|
||||
next_page: steps.next_page,
|
||||
data: steps.map { |step| [step.id, step.name] }
|
||||
}
|
||||
end
|
||||
|
||||
def add_protocol_steps
|
||||
Protocol.transaction do
|
||||
selected_protocol = Protocol.find_by(id: params[:selected_protocol])
|
||||
render_403 unless selected_protocol.present? && can_read_protocol_in_repository?(selected_protocol)
|
||||
|
||||
steps = selected_protocol.steps.where(id: params[:steps]).order(position: :asc).map do |step|
|
||||
step.duplicate(@protocol, current_user, original_protocol: selected_protocol)
|
||||
end
|
||||
|
||||
message_items = {
|
||||
protocol: selected_protocol.id,
|
||||
count: steps.count
|
||||
}
|
||||
|
||||
if @protocol.in_module?
|
||||
message_items[:my_module] = @protocol.my_module.id
|
||||
log_activity(:task_steps_loaded_from_template, @my_module.experiment.project, message_items)
|
||||
else
|
||||
log_activity(:protocol_steps_loaded_from_template, nil, message_items)
|
||||
end
|
||||
|
||||
render json: steps, each_serializer: StepSerializer, user: current_user
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_vars
|
||||
|
|
|
@ -81,18 +81,7 @@ class LoadFromRepositoryProtocolsDatatable < CustomDatatable
|
|||
end
|
||||
|
||||
def get_raw_records_base
|
||||
original_without_versions = @team.protocols
|
||||
.left_outer_joins(:published_versions)
|
||||
.where(protocol_type: Protocol.protocol_types[:in_repository_published_original])
|
||||
.where(published_versions: { id: nil })
|
||||
.select(:id)
|
||||
|
||||
published_versions = @team.protocols
|
||||
.where(protocol_type: Protocol.protocol_types[:in_repository_published_version])
|
||||
.order('parent_id, version_number DESC')
|
||||
.select('DISTINCT ON (parent_id) id')
|
||||
|
||||
Protocol.where("protocols.id IN ((#{original_without_versions.to_sql}) UNION (#{published_versions.to_sql}))")
|
||||
Protocol.latest_available_versions_without_drafts(@team)
|
||||
.active
|
||||
.with_granted_permissions(@user, ProtocolPermissions::READ)
|
||||
end
|
||||
|
|
|
@ -46,7 +46,9 @@
|
|||
<ProtocolOptions
|
||||
v-if="protocol.attributes && protocol.attributes.urls"
|
||||
:protocol="protocol"
|
||||
:inRepository="inRepository"
|
||||
@protocol:delete_steps="deleteSteps"
|
||||
@protocol:add_protocol_steps="addSteps"
|
||||
:canDeleteSteps="steps.length > 0 && urls.delete_steps_url !== null"
|
||||
/>
|
||||
<button class="btn btn-light icon-btn" data-toggle="modal" data-target="#print-protocol-modal" tabindex="0">
|
||||
|
@ -128,7 +130,7 @@
|
|||
</div>
|
||||
<div :class="inRepository ? 'protocol-section protocol-steps-section protocol-information' : ''">
|
||||
<div v-if="inRepository" id="protocol-steps" class="protocol-section-header">
|
||||
<div class="protocol-steps-container">
|
||||
<div class="protocol-steps-container w-full flex flex-row items-center justify-between">
|
||||
<a class="protocol-section-caret" role="button" data-toggle="collapse" href="#protocol-steps-container" aria-expanded="false" aria-controls="protocol-steps-container">
|
||||
<i class="sn-icon sn-icon-right"></i>
|
||||
<span id="protocolStepsLabel" class="protocol-section-title" data-e2e="e2e-TX-protocol-templateSteps-title">
|
||||
|
@ -137,6 +139,14 @@
|
|||
</h2>
|
||||
</span>
|
||||
</a>
|
||||
<ProtocolOptions
|
||||
v-if="protocol.attributes && protocol.attributes.urls"
|
||||
:protocol="protocol"
|
||||
:inRepository="inRepository"
|
||||
@protocol:delete_steps="deleteSteps"
|
||||
@protocol:add_protocol_steps="addSteps"
|
||||
:canDeleteSteps="steps.length > 0 && urls.delete_steps_url !== null"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sci-divider my-4" v-if="!inRepository"></div>
|
||||
|
@ -372,6 +382,9 @@ export default {
|
|||
HelperModule.flashAlertMsg(this.i18n.t('errors.general'), 'danger');
|
||||
});
|
||||
},
|
||||
addSteps(steps) {
|
||||
this.steps.push(...steps);
|
||||
},
|
||||
refreshProtocolStatus() {
|
||||
if (this.inRepository) return;
|
||||
// legacy method from app/assets/javascripts/my_modules/protocols.js
|
||||
|
|
119
app/javascript/vue/protocol/modals/add_protocol_steps.vue
Normal file
119
app/javascript/vue/protocol/modals/add_protocol_steps.vue
Normal file
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form @submit.prevent="submit">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title truncate !block" >
|
||||
{{ i18n.t('protocols.steps.modals.add_protocol_steps.title') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-6">{{ i18n.t('protocols.steps.modals.add_protocol_steps.description')}}</p>
|
||||
<div class="mb-6">
|
||||
<label class="sci-label">{{ i18n.t('protocols.steps.modals.add_protocol_steps.protocol_label') }}</label>
|
||||
<SelectDropdown
|
||||
:placeholder="i18n.t('protocols.steps.modals.add_protocol_steps.protocol_placeholder')"
|
||||
:optionsUrl="protocolsUrl"
|
||||
:searchable="true"
|
||||
:value="selectedProtocol"
|
||||
@change="selectedProtocol = $event"
|
||||
></SelectDropdown>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<label class="sci-label">{{ i18n.t('protocols.steps.modals.add_protocol_steps.protocol_steps_label') }}</label>
|
||||
<SelectDropdown
|
||||
:key="selectedProtocol"
|
||||
:disabled="!selectedProtocol"
|
||||
:optionsUrl="protocolStepsUrl"
|
||||
:urlParams="{ selected_protocol_id: selectedProtocol }"
|
||||
:placeholder="i18n.t('protocols.steps.modals.add_protocol_steps.protocol_steps_placeholder')"
|
||||
:multiple="true"
|
||||
:withCheckboxes="true"
|
||||
:searchable="true"
|
||||
:value="selectedSteps"
|
||||
@change="selectedSteps= $event"
|
||||
></SelectDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" :disabled="submitting || !validObject" type="submit">
|
||||
{{ i18n.t('protocols.steps.modals.add_protocol_steps.confirm') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
/* global HelperModule */
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import modalMixin from '../../shared/modal_mixin.js';
|
||||
import SelectDropdown from '../../shared/select_dropdown.vue';
|
||||
import {
|
||||
list_published_protocol_templates_protocol_path,
|
||||
list_protocol_steps_protocol_steps_path
|
||||
} from '../../../routes.js';
|
||||
|
||||
export default {
|
||||
name: 'AddStepsModal',
|
||||
props: {
|
||||
protocol: Object
|
||||
},
|
||||
components: {
|
||||
SelectDropdown
|
||||
},
|
||||
mixins: [modalMixin],
|
||||
data() {
|
||||
return {
|
||||
selectedProtocol: null,
|
||||
selectedRow: null,
|
||||
submitting: false,
|
||||
selectedSteps: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
validObject() {
|
||||
return this.selectedProtocol && this.selectedSteps;
|
||||
},
|
||||
protocolsUrl() {
|
||||
return list_published_protocol_templates_protocol_path(this.protocol.id);
|
||||
},
|
||||
protocolStepsUrl() {
|
||||
if (!this.selectedProtocol) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return list_protocol_steps_protocol_steps_path(this.selectedProtocol);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// move modal to body to avoid z-index issues
|
||||
$('body').append($(this.$refs.modal));
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
this.submitting = true;
|
||||
axios.post(this.protocol.attributes.urls.add_protocol_steps_url, {
|
||||
selected_protocol: this.selectedProtocol,
|
||||
steps: this.selectedSteps
|
||||
}).then((data) => {
|
||||
this.submitting = false;
|
||||
this.$emit('confirm', data.data.data);
|
||||
this.close();
|
||||
HelperModule.flashAlertMsg(this.i18n.t('protocols.steps.modals.add_protocol_steps.success_flash', { count: data.data.data.length }), 'success');
|
||||
}).catch(() => {
|
||||
this.submitting = false;
|
||||
HelperModule.flashAlertMsg(this.i18n.t('protocols.steps.modals.add_protocol_steps.error_flash'), 'danger');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -17,7 +17,7 @@
|
|||
class="dropdown-menu dropdown-menu-right rounded !p-2.5 sn-shadow-menu-sm"
|
||||
aria-labelledby="dropdownProtocolOptions"
|
||||
>
|
||||
<li v-if="protocol.attributes.urls.load_from_repo_url">
|
||||
<li v-if="protocol.attributes.urls.load_from_repo_url && !inRepository">
|
||||
<a class="!px-3 !py-2.5 hover:!bg-sn-super-light-blue !text-sn-blue"
|
||||
ref="loadProtocol"
|
||||
data-action="load-from-repository"
|
||||
|
@ -26,7 +26,17 @@
|
|||
<span>{{ i18n.t("my_modules.protocol.options_dropdown.load_from_repo") }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li v-if="protocol.attributes.urls.add_protocol_steps_url">
|
||||
<a class="!px-3 !py-2.5 hover:!bg-sn-super-light-blue !text-sn-blue"
|
||||
data-turbolinks="false"
|
||||
@click.prevent="openAddStepsModal()"
|
||||
>
|
||||
<span>{{
|
||||
i18n.t("my_modules.protocol.options_dropdown.add_protocol_steps")
|
||||
}}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="!inRepository">
|
||||
<a class="!px-3 !py-2.5 hover:!bg-sn-super-light-blue !text-sn-blue"
|
||||
data-toggle="modal"
|
||||
data-target="#newProtocolModal"
|
||||
|
@ -38,7 +48,7 @@
|
|||
}}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li v-if="!inRepository">
|
||||
<a class="!px-3 !py-2.5 hover:!bg-sn-super-light-blue !text-sn-blue"
|
||||
data-turbolinks="false"
|
||||
:href="protocol.attributes.urls.export_url"
|
||||
|
@ -49,7 +59,7 @@
|
|||
}}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="protocol.attributes.urls.update_protocol_url">
|
||||
<li v-if="protocol.attributes.urls.update_protocol_url && !inRepository">
|
||||
<a class="!px-3 !py-2.5 hover:!bg-sn-super-light-blue !text-sn-blue"
|
||||
ref="updateProtocol"
|
||||
data-action="update-self"
|
||||
|
@ -60,7 +70,7 @@
|
|||
}}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="protocol.attributes.urls.unlink_url">
|
||||
<li v-if="protocol.attributes.urls.unlink_url && !inRepository">
|
||||
<a class="!px-3 !py-2.5 hover:!bg-sn-super-light-blue !text-sn-blue"
|
||||
ref="unlinkProtocol"
|
||||
data-action="unlink"
|
||||
|
@ -71,7 +81,7 @@
|
|||
}}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="protocol.attributes.urls.revert_protocol_url">
|
||||
<li v-if="protocol.attributes.urls.revert_protocol_url && !inRepository">
|
||||
<a class="!px-3 !py-2.5 hover:!bg-sn-super-light-blue !text-sn-blue"
|
||||
ref="revertProtocol"
|
||||
data-action="revert"
|
||||
|
@ -95,19 +105,22 @@
|
|||
</ul>
|
||||
</div>
|
||||
<DeleteStepsModals v-if="stepsDeleting" @confirm="deleteSteps" @close="closeStartStepsDeletingModal" />
|
||||
<AddStepsModal v-if="stepsAdding" :protocol="protocol" @confirm="addSteps" @close="closeAddStepsModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DeleteStepsModals from './modals/delete_steps';
|
||||
import AddStepsModal from './modals/add_protocol_steps';
|
||||
|
||||
export default {
|
||||
|
||||
name: 'ProtocolOptions',
|
||||
components: { DeleteStepsModals },
|
||||
components: { DeleteStepsModals, AddStepsModal },
|
||||
data() {
|
||||
return {
|
||||
stepsDeleting: false
|
||||
stepsDeleting: false,
|
||||
stepsAdding: false
|
||||
};
|
||||
},
|
||||
props: {
|
||||
|
@ -118,12 +131,18 @@ export default {
|
|||
canDeleteSteps: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
inRepository: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Legacy global functions from app/assets/javascripts/my_modules/protocols.js
|
||||
initLoadFromRepository();
|
||||
initLinkUpdate();
|
||||
if (!this.inRepository) {
|
||||
initLoadFromRepository();
|
||||
initLinkUpdate();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openStepsDeletingModal() {
|
||||
|
@ -132,6 +151,12 @@ export default {
|
|||
closeStartStepsDeletingModal() {
|
||||
this.stepsDeleting = false;
|
||||
},
|
||||
openAddStepsModal() {
|
||||
this.stepsAdding = true;
|
||||
},
|
||||
closeAddStepsModal() {
|
||||
this.stepsAdding = false;
|
||||
},
|
||||
loadProtocol() {
|
||||
$.get(
|
||||
`${this.protocol.attributes.urls.load_from_repo_url}?type=recent`
|
||||
|
@ -156,6 +181,9 @@ export default {
|
|||
},
|
||||
deleteSteps() {
|
||||
this.$emit('protocol:delete_steps');
|
||||
},
|
||||
addSteps(steps) {
|
||||
this.$emit('protocol:add_protocol_steps', steps);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -22,10 +22,37 @@
|
|||
:placeholder="i18n.t('repositories.index.modal_create.name_placeholder')" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="sci-label">{{ i18n.t("repositories.index.modal_create.repository_template_label") }}</label>
|
||||
<SelectDropdown
|
||||
:options="repositoryTemplates"
|
||||
:value="repositoryTemplate"
|
||||
:optionRenderer="repositoryTemplateOptionRenderer"
|
||||
@change="repositoryTemplate = $event"
|
||||
@close="showColumnInfo = false"
|
||||
/>
|
||||
<div v-if="showColumnInfo" class="absolute -right-64 w-60 bg-white border border-radius p-4 min-h-[10rem]">
|
||||
<div v-if="loadingHoveredRow" class="flex absolute top-0 left-0 items-center justify-center w-full flex-grow h-full z-10">
|
||||
<img src="/images/medium/loading.svg" alt="Loading" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="flex gap-4 overflow-hidden items-centers">
|
||||
<div class="truncate font-bold">{{ hoveredRow.name }}</div>
|
||||
</div>
|
||||
<template v-if="hoveredRow.columns">
|
||||
<div class="flex items-center gap-0.5 overflow-hidden text-xs" v-for="column in hoveredRow.columns">
|
||||
<span class="truncate shrink-c">{{ column[0] }}</span>
|
||||
<span>-</span>
|
||||
<span class="truncate shrink-0">{{ column[1] }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" data-e2e="e2e-BT-newInventoryModal-cancel">{{ i18n.t('general.cancel') }}</button>
|
||||
<button class="btn btn-primary" type="submit" :disabled="submitting || !validName" data-e2e="e2e-BT-newInventoryModal-create">
|
||||
<button class="btn btn-primary" type="submit" :disabled="submitting || !validName || !repositoryTemplate" data-e2e="e2e-BT-newInventoryModal-create">
|
||||
{{ i18n.t('repositories.index.modal_create.submit') }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -40,9 +67,18 @@
|
|||
|
||||
import axios from '../../../packs/custom_axios.js';
|
||||
import modalMixin from '../../shared/modal_mixin';
|
||||
import SelectDropdown from '../../shared/select_dropdown.vue';
|
||||
|
||||
import {
|
||||
repository_templates_path,
|
||||
list_repository_columns_repository_template_path
|
||||
} from '../../../routes.js';
|
||||
|
||||
export default {
|
||||
name: 'NewRepositoryModal',
|
||||
components: {
|
||||
SelectDropdown
|
||||
},
|
||||
props: {
|
||||
createUrl: String
|
||||
},
|
||||
|
@ -51,21 +87,42 @@ export default {
|
|||
return {
|
||||
name: '',
|
||||
error: null,
|
||||
submitting: false
|
||||
submitting: false,
|
||||
repositoryTemplates: [],
|
||||
repositoryTemplate: null,
|
||||
showColumnInfo: false,
|
||||
hoveredRow: {},
|
||||
loadingHoveredRow: false
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetctRepositoryTemplates();
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('mouseover', this.loadColumnsInfo);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('mouseover', this.loadColumnsInfo);
|
||||
},
|
||||
computed: {
|
||||
repositoryTemplateUrl() {
|
||||
return repository_templates_path();
|
||||
},
|
||||
validName() {
|
||||
return this.name.length >= GLOBAL_CONSTANTS.NAME_MIN_LENGTH;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
listRepositoryTemplateColumnsUrl(repositoryTemplateId) {
|
||||
return list_repository_columns_repository_template_path(repositoryTemplateId);
|
||||
},
|
||||
submit() {
|
||||
this.submitting = true;
|
||||
|
||||
axios.post(this.createUrl, {
|
||||
repository: {
|
||||
name: this.name
|
||||
name: this.name,
|
||||
repository_template_id: this.repositoryTemplate
|
||||
}
|
||||
}).then((response) => {
|
||||
this.error = null;
|
||||
|
@ -76,6 +133,45 @@ export default {
|
|||
this.submitting = false;
|
||||
this.error = error.response.data.name;
|
||||
});
|
||||
},
|
||||
repositoryTemplateOptionRenderer(row) {
|
||||
return `
|
||||
<div class="flex items-center gap-4 w-full">
|
||||
<div class="grow overflow-hidden">
|
||||
<div class="truncate" >${row[1]}</div>
|
||||
</div>
|
||||
<i class="sn-icon sn-icon-info show-items-columns" title="" data-item-id="${row[0]}"></i>
|
||||
</div>`;
|
||||
},
|
||||
fetctRepositoryTemplates() {
|
||||
axios.get(this.repositoryTemplateUrl)
|
||||
.then((response) => {
|
||||
this.repositoryTemplates = response.data.data;
|
||||
[this.repositoryTemplate] = this.repositoryTemplates[0];
|
||||
});
|
||||
},
|
||||
loadColumnsInfo(e) {
|
||||
if (!e.target.classList.contains('show-items-columns')) {
|
||||
this.showColumnInfo = false;
|
||||
this.hoveredRow = {};
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingHoveredRow = true;
|
||||
|
||||
this.showColumnInfo = true;
|
||||
|
||||
axios.get(this.listRepositoryTemplateColumnsUrl(e.target.dataset.itemId))
|
||||
.then((response) => {
|
||||
this.loadingHoveredRow = false;
|
||||
this.hoveredRow = {
|
||||
name: response.data.name,
|
||||
columns: response.data.columns
|
||||
};
|
||||
});
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
<template v-for="item in insertMenu">
|
||||
<button v-if="!item.submenu" @click="$emit(item.emit)" class="btn btn-light">
|
||||
<i :class="item.icon"></i>
|
||||
{{ item.text }}
|
||||
<span class="tw-hidden lg:inline">{{ item.text }}</span>
|
||||
</button>
|
||||
<MenuDropdown
|
||||
:listItems="item.submenu"
|
||||
:btnText="item.text"
|
||||
:btnClasses="'btn btn-light'"
|
||||
:smallScreenCollapse="true"
|
||||
:position="'right'"
|
||||
:caret="true"
|
||||
:btnIcon="item.icon"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div v-if="params.data.comments">
|
||||
<div v-if="params.data.comments" class="w-9 flex justify-end">
|
||||
<span v-if="!params.data.permissions.create_comments && params.data.comments.count === 0">0</span>
|
||||
<a v-else
|
||||
href="#"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="relative" v-if="listItems.length > 0 || alwaysShow" v-click-outside="closeMenu" >
|
||||
<button ref="field" :class="btnClasses" :title="title" @click="isOpen = !isOpen" :data-e2e="dataE2e">
|
||||
<i v-if="btnIcon" :class="btnIcon"></i>
|
||||
{{ btnText }}
|
||||
<span :class="{'tw-hidden lg:inline': smallScreenCollapse}">{{ btnText }}</span>
|
||||
<i v-if="caret && isOpen" class="sn-icon sn-icon-up"></i>
|
||||
<i v-else-if="caret" class="sn-icon sn-icon-down"></i>
|
||||
</button>
|
||||
|
@ -84,7 +84,8 @@ export default {
|
|||
caret: { type: Boolean, default: false },
|
||||
alwaysShow: { type: Boolean, default: false },
|
||||
title: { type: String, default: '' },
|
||||
dataE2e: { type: String, default: '' }
|
||||
dataE2e: { type: String, default: '' },
|
||||
smallScreenCollapse: { type: Boolean, default: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -373,7 +373,7 @@ export default {
|
|||
|
||||
let request = {};
|
||||
|
||||
if (this.ajaxMethod === 'get') {
|
||||
if (this.ajaxMethod.toLowerCase() === 'get') {
|
||||
request = { method: 'get', url: this.optionsUrl, params };
|
||||
} else {
|
||||
request = { method: 'post', url: this.optionsUrl, data: params };
|
||||
|
|
|
@ -144,6 +144,7 @@ class Protocol < ApplicationRecord
|
|||
dependent: :destroy
|
||||
has_many :protocol_keywords, through: :protocol_protocol_keywords
|
||||
has_many :steps, inverse_of: :protocol, dependent: :destroy
|
||||
has_many :original_steps, class_name: 'Step', foreign_key: :original_protocol_id, inverse_of: :original_protocol, dependent: :nullify
|
||||
has_many :users, through: :user_assignments
|
||||
|
||||
def self.search(user,
|
||||
|
@ -218,6 +219,21 @@ class Protocol < ApplicationRecord
|
|||
where('protocols.id IN ((?) UNION ALL (?) UNION ALL (?))', original_without_versions, published_versions, new_drafts)
|
||||
end
|
||||
|
||||
def self.latest_available_versions_without_drafts(teams)
|
||||
team_protocols = where(team: teams)
|
||||
|
||||
original_without_versions = team_protocols
|
||||
.where.missing(:published_versions)
|
||||
.where(protocol_type: Protocol.protocol_types[:in_repository_published_original])
|
||||
.select(:id)
|
||||
published_versions = team_protocols
|
||||
.where(protocol_type: Protocol.protocol_types[:in_repository_published_version])
|
||||
.order(:parent_id, version_number: :desc)
|
||||
.select('DISTINCT ON (parent_id) id')
|
||||
|
||||
where('protocols.id IN ((?) UNION ALL (?))', original_without_versions, published_versions)
|
||||
end
|
||||
|
||||
def self.viewable_by_user(user, teams, options = {})
|
||||
if options[:fetch_latest_versions]
|
||||
protocol_templates = latest_available_versions(teams)
|
||||
|
|
|
@ -22,6 +22,7 @@ class Repository < RepositoryBase
|
|||
class_name: 'User',
|
||||
inverse_of: :restored_repositories,
|
||||
optional: true
|
||||
belongs_to :repository_template, inverse_of: :repositories, optional: true
|
||||
has_many :repository_snapshots,
|
||||
class_name: 'RepositorySnapshot',
|
||||
foreign_key: :parent_id,
|
||||
|
|
221
app/models/repository_template.rb
Normal file
221
app/models/repository_template.rb
Normal file
|
@ -0,0 +1,221 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RepositoryTemplate < ApplicationRecord
|
||||
belongs_to :team, inverse_of: :repository_templates
|
||||
has_many :repositories, inverse_of: :repository_template, dependent: :destroy
|
||||
|
||||
def self.default
|
||||
RepositoryTemplate.new(
|
||||
name: I18n.t('repository_templates.default_template_name'),
|
||||
column_definitions: [],
|
||||
predefined: true
|
||||
)
|
||||
end
|
||||
|
||||
def self.cell_lines
|
||||
RepositoryTemplate.new(
|
||||
name: I18n.t('repository_templates.cell_lines_template_name'),
|
||||
column_definitions: [
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.species') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.organ') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryListValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.morphology'),
|
||||
metadata: { delimiter: I18n.t('repository_templates.repository_list_value_delimiter') },
|
||||
repository_list_items_attributes: [{ data: I18n.t('repository_templates.template_columns.repository_list_value.endothelial') },
|
||||
{ data: I18n.t('repository_templates.template_columns.repository_list_value.epithelial') },
|
||||
{ data: I18n.t('repository_templates.template_columns.repository_list_value.fibroblast') },
|
||||
{ data: I18n.t('repository_templates.template_columns.repository_list_value.lymphoblast') }] }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryListValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.culture_type'),
|
||||
metadata: { delimiter: I18n.t('repository_templates.repository_list_value_delimiter') },
|
||||
repository_list_items_attributes: [{ data: I18n.t('repository_templates.template_columns.repository_list_value.adherent') },
|
||||
{ data: I18n.t('repository_templates.template_columns.repository_list_value.suspension') }] }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryStockValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.stock'),
|
||||
metadata: { decimals: 2 },
|
||||
repository_stock_unit_items_attributes: RepositoryStockUnitItem::DEFAULT_UNITS.map { |unit| { data: unit } } +
|
||||
[{ data: I18n.t('repository_templates.template_columns.stock_units.vials') }] }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryNumberValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.passage_number') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.lot_number') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryDateValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.freezing_date') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.operator') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.yield') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryStatusValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.status'),
|
||||
repository_status_items_attributes: [{ status: I18n.t('repository_templates.template_columns.repository_status_value.frozen'), icon: '❄️' },
|
||||
{ status: I18n.t('repository_templates.template_columns.repository_status_value.in_subculturing'), icon: '🧫' },
|
||||
{ status: I18n.t('repository_templates.template_columns.repository_status_value.out_of_tock'), icon: '❌' }] }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryAssetValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.handling_procedure') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.notes') }
|
||||
}
|
||||
],
|
||||
predefined: true
|
||||
)
|
||||
end
|
||||
|
||||
def self.equipment
|
||||
RepositoryTemplate.new(
|
||||
name: I18n.t('repository_templates.equipment_template_name'),
|
||||
column_definitions: [
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryDateValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.calibration_date'),
|
||||
reminder_value: 1, reminder_unit: 2419200, reminder_message: I18n.t('repository_templates.template_columns.calibration_message') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryStatusValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.availability_status'),
|
||||
repository_status_items_attributes: [{ status: I18n.t('repository_templates.template_columns.repository_status_value.available_for_use'), icon: '🟢' },
|
||||
{ status: I18n.t('repository_templates.template_columns.repository_status_value.in_use'), icon: '🟥' },
|
||||
{ status: I18n.t('repository_templates.template_columns.repository_status_value.out_of_service'), icon: '❌' },
|
||||
{ status: I18n.t('repository_templates.template_columns.repository_status_value.under_maintenance'), icon: '🔧' }] }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryAssetValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.safety_handling_info') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryAssetValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.training_records') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.contact_person') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.contact_phone') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.internal_id') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.manufacturer') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.serial_number') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.notes') }
|
||||
}
|
||||
],
|
||||
predefined: true
|
||||
)
|
||||
end
|
||||
|
||||
def self.chemicals_and_reagents
|
||||
RepositoryTemplate.new(
|
||||
name: I18n.t('repository_templates.chemicals_and_reagents_template_name'),
|
||||
column_definitions: [
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.concentration') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryStockValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.stock'),
|
||||
metadata: { decimals: 2 },
|
||||
repository_stock_unit_items_attributes: RepositoryStockUnitItem::DEFAULT_UNITS.map { |unit| { data: unit } } }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryDateValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.date_opened') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryDateValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.expiration_date'),
|
||||
reminder_value: 1, reminder_unit: 2419200, reminder_message: I18n.t('repository_templates.template_columns.expiration_date_message') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryListValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.storage_conditions'),
|
||||
metadata: { delimiter: I18n.t('repository_templates.repository_list_value_delimiter') },
|
||||
repository_list_items_attributes: [{ data: I18n.t('repository_templates.template_columns.repository_list_value.minus_twenty_celsious') },
|
||||
{ data: I18n.t('repository_templates.template_columns.repository_list_value.two_to_eigth_celsious') },
|
||||
{ data: I18n.t('repository_templates.template_columns.repository_list_value.minus_eigthty') },
|
||||
{ data: I18n.t('repository_templates.template_columns.repository_list_value.ambient') }] }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryListValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.type'),
|
||||
metadata: { delimiter: I18n.t('repository_templates.repository_list_value_delimiter') },
|
||||
repository_list_items_attributes: [{ data: I18n.t('repository_templates.template_columns.repository_list_value.buffer') },
|
||||
{ data: I18n.t('repository_templates.template_columns.repository_list_value.liquid') },
|
||||
{ data: I18n.t('repository_templates.template_columns.repository_list_value.reagent') },
|
||||
{ data: I18n.t('repository_templates.template_columns.repository_list_value.solid') }] }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.purity') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.cas_number') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryAssetValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.safety_sheet') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryListValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.vendor') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.catalog_number') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.lot') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.price') }
|
||||
},
|
||||
{
|
||||
column_type: Extends::REPOSITORY_DATA_TYPES[:RepositoryTextValue],
|
||||
params: { name: I18n.t('repository_templates.template_columns.molecular_weight') }
|
||||
}
|
||||
],
|
||||
predefined: true
|
||||
)
|
||||
end
|
||||
end
|
|
@ -32,6 +32,7 @@ class Step < ApplicationRecord
|
|||
belongs_to :user, inverse_of: :steps
|
||||
belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User', optional: true
|
||||
belongs_to :protocol, inverse_of: :steps
|
||||
belongs_to :original_protocol, class_name: 'Protocol', optional: true, inverse_of: :original_steps
|
||||
delegate :team, to: :protocol
|
||||
has_many :step_orderable_elements, inverse_of: :step, dependent: :destroy
|
||||
has_many :checklists, inverse_of: :step, dependent: :destroy
|
||||
|
@ -118,7 +119,7 @@ class Step < ApplicationRecord
|
|||
step_texts.order(created_at: :asc).first
|
||||
end
|
||||
|
||||
def duplicate(protocol, user, step_position: nil, step_name: nil, include_file_versions: false)
|
||||
def duplicate(protocol, user, step_position: nil, step_name: nil, include_file_versions: false, original_protocol: nil)
|
||||
ActiveRecord::Base.transaction do
|
||||
assets_to_clone = []
|
||||
|
||||
|
@ -126,7 +127,8 @@ class Step < ApplicationRecord
|
|||
name: step_name || name,
|
||||
position: step_position || protocol.steps.length,
|
||||
completed: false,
|
||||
user: user
|
||||
user: user,
|
||||
original_protocol: original_protocol
|
||||
)
|
||||
new_step.save!
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ class Team < ApplicationRecord
|
|||
before_save -> { shareable_links.destroy_all }, if: -> { !shareable_links_enabled? }
|
||||
after_create :generate_template_project
|
||||
after_create :create_default_label_templates
|
||||
after_create :create_default_repository_templates
|
||||
scope :teams_select, -> { select(:id, :name).order(name: :asc) }
|
||||
scope :ordered, -> { order('LOWER(name)') }
|
||||
|
||||
|
@ -44,6 +45,7 @@ class Team < ApplicationRecord
|
|||
has_many :activities, inverse_of: :team, dependent: :destroy
|
||||
has_many :assets, inverse_of: :team, dependent: :destroy
|
||||
has_many :label_templates, dependent: :destroy
|
||||
has_many :repository_templates, inverse_of: :team, dependent: :destroy
|
||||
has_many :team_shared_objects, inverse_of: :team, dependent: :destroy
|
||||
has_many :team_shared_repositories,
|
||||
-> { where(shared_object_type: 'RepositoryBase') },
|
||||
|
@ -210,4 +212,11 @@ class Team < ApplicationRecord
|
|||
ZebraLabelTemplate.default_203dpi.update(team: self, default: false)
|
||||
FluicsLabelTemplate.default.update(team: self, default: true)
|
||||
end
|
||||
|
||||
def create_default_repository_templates
|
||||
RepositoryTemplate.default.update(team: self)
|
||||
RepositoryTemplate.cell_lines.update(team: self)
|
||||
RepositoryTemplate.equipment.update(team: self)
|
||||
RepositoryTemplate.chemicals_and_reagents.update(team: self)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -98,7 +98,8 @@ class ProtocolSerializer < ActiveModel::Serializer
|
|||
version_comment_url: version_comment_url,
|
||||
print_protocol_url: print_protocol_url,
|
||||
versions_modal: versions_modal_protocol_path(object.parent || object),
|
||||
redirect_to_protocols: protocols_path
|
||||
redirect_to_protocols: protocols_path,
|
||||
add_protocol_steps_url: add_protocol_steps_url
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -150,6 +151,12 @@ class ProtocolSerializer < ActiveModel::Serializer
|
|||
export_protocols_path(protocol_ids: object.id, my_module_id: object.my_module.id)
|
||||
end
|
||||
|
||||
def add_protocol_steps_url
|
||||
return unless can_manage_protocol_in_module?(object) || can_manage_protocol_draft_in_repository?(object)
|
||||
|
||||
add_protocol_steps_protocol_steps_path(object)
|
||||
end
|
||||
|
||||
def steps_url
|
||||
return unless can_read_protocol_in_module?(object) || can_read_protocol_in_repository?(object)
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
</div>
|
||||
|
||||
<div class="widget-body">
|
||||
<div class="current-tasks-list-wrapper perfect-scrollbar">
|
||||
<div class="current-tasks-list-wrapper">
|
||||
<div class="current-tasks-list "
|
||||
data-tasks-list-url="<%= dashboard_current_tasks_path %>">
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="widget-body">
|
||||
<div class="recent-work-container perfect-scrollbar" data-url="<%= dashboard_recent_works_path %>" data-e2e="e2e-CO-dashboard-recentWork"></div>
|
||||
<div class="recent-work-container" data-url="<%= dashboard_recent_works_path %>" data-e2e="e2e-CO-dashboard-recentWork"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</div>
|
||||
<div class="ga-bottom">
|
||||
<div class="ga-main">
|
||||
<div class="activities-container perfect-scrollbar">
|
||||
<div class="activities-container">
|
||||
<div class="no-activities-message text-center <%= 'hidden' if @grouped_activities.keys.any? %>">
|
||||
<p><%= t('activities.index.no_activities_message') %></p>
|
||||
</div>
|
||||
|
@ -24,7 +24,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="ga-side">
|
||||
<div class="filters-container perfect-scrollbar">
|
||||
<div class="filters-container">
|
||||
<%= render partial: "side_filters" %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,16 +2,21 @@
|
|||
<button type="button" class="close self-start" data-dismiss="modal" aria-label="Close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title">
|
||||
<% if downstream %>
|
||||
<% if downstream %>
|
||||
<h4 class="modal-title !line-clamp-3" title="<%= t('my_modules.modals.assign_repository_record.title_downstream',
|
||||
repository_name: repository.name) %>">
|
||||
<%= t('my_modules.modals.assign_repository_record.title_downstream',
|
||||
repository_name: repository.name) %>
|
||||
<% else %>
|
||||
repository_name: repository.name) %>
|
||||
</h4>
|
||||
<% else %>
|
||||
<h4 class="modal-title !line-clamp-3" title="<%= t('my_modules.modals.assign_repository_record.title',
|
||||
repository_name: repository.name,
|
||||
my_module_name: my_module.name) %>">
|
||||
<%= t('my_modules.modals.assign_repository_record.title',
|
||||
repository_name: repository.name,
|
||||
my_module_name: my_module.name) %>
|
||||
<% end %>
|
||||
</h4>
|
||||
repository_name: repository.name,
|
||||
my_module_name: my_module.name) %>
|
||||
</h4>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
|
|
|
@ -6,16 +6,21 @@
|
|||
<button type="button" class="close self-start" data-dismiss="modal" aria-label="Close">
|
||||
<i class="sn-icon sn-icon-close"></i>
|
||||
</button>
|
||||
<h4 class="modal-title">
|
||||
<% if downstream %>
|
||||
<%= t('my_modules.modals.update_repository_record.title_downstream',
|
||||
<% if downstream %>
|
||||
<h4 class="modal-title !line-clamp-3" title="<%= t('my_modules.modals.update_repository_record.title_downstream',
|
||||
repository_name: repository.name) %>">
|
||||
<%= t('my_modules.modals.update_repository_record.title_downstream',
|
||||
repository_name: repository.name) %>
|
||||
<% else %>
|
||||
</h4>
|
||||
<% else %>
|
||||
<h4 class="modal-title !line-clamp-3" title="<%= t('my_modules.modals.update_repository_record.title',
|
||||
repository_name: repository.name,
|
||||
my_module_name: my_module.name) %>">
|
||||
<%= t('my_modules.modals.update_repository_record.title',
|
||||
repository_name: repository.name,
|
||||
my_module_name: my_module.name) %>
|
||||
<% end %>
|
||||
</h4>
|
||||
</h4>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<% if downstream %>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="dropdown asset-context-menu">
|
||||
<button class="btn btn-light btn-xs dropdown-toggle icon-btn" type="button" id="dropdownAssetContextMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<button class="btn btn-secondary dropdown-toggle icon-btn" type="button" id="dropdownAssetContextMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<i class="sn-icon sn-icon-more-hori"></i>
|
||||
</button>
|
||||
|
||||
|
|
|
@ -38,10 +38,10 @@
|
|||
<%= link_to shared_asset_download_path(@shareable_link.uuid, asset), class: 'btn btn-light icon-btn thumbnail-action-btn', data: { turbolinks: false } do %>
|
||||
<span class="sn-icon sn-icon-export"></span>
|
||||
<% end %>
|
||||
<% if defined?(show_context) && show_context %>
|
||||
<%= render partial: "shareable_links/my_modules/step_attachments/context_menu", locals: { asset: asset } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if defined?(show_context) && show_context %>
|
||||
<%= render partial: "shareable_links/my_modules/step_attachments/context_menu", locals: { asset: asset } %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -556,7 +556,9 @@ class Extends
|
|||
protocol_step_form_moved: 355,
|
||||
form_block_duplicated: 356,
|
||||
form_duplicated: 357,
|
||||
form_unpublished: 358
|
||||
form_unpublished: 358,
|
||||
task_steps_loaded_from_template: 359,
|
||||
protocol_steps_loaded_from_template: 360
|
||||
}
|
||||
|
||||
ACTIVITY_GROUPS = {
|
||||
|
@ -565,7 +567,7 @@ class Extends
|
|||
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,
|
||||
184, 185, 188, 189, *192..203, 221, 222, 224, 225, 226, 236, *249..252, *274..278, 299, 302, 305, 327, *347..352],
|
||||
184, 185, 188, 189, *192..203, 221, 222, 224, 225, 226, 236, *249..252, *274..278, 299, 302, 305, 327, *347..352, 359],
|
||||
task_inventory: [55, 56, 146, 147, 183],
|
||||
experiment: [*27..31, 57, 141, 165],
|
||||
reports: [48, 50, 49, 163, 164],
|
||||
|
@ -574,7 +576,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, 187, 186,
|
||||
190, 191, *204..215, 220, 223, 227, 228, 229, *230..235,
|
||||
*237..240, *253..256, *279..283, 300, 304, 307, 330, *353..355],
|
||||
*237..240, *253..256, *279..283, 300, 304, 307, 330, *353..355, 360],
|
||||
team: [92, 94, 93, 97, 104, 244, 245],
|
||||
label_templates: [*216..219],
|
||||
storage_locations: [*309..315],
|
||||
|
|
|
@ -1323,6 +1323,76 @@ en:
|
|||
unreachable: "Printer is offline"
|
||||
search: "Checking printer status"
|
||||
|
||||
repository_templates:
|
||||
default_template_name: 'Default template'
|
||||
cell_lines_template_name: 'Cell lines template'
|
||||
equipment_template_name: 'Equipment template'
|
||||
chemicals_and_reagents_template_name: "Chemicals & reagents template"
|
||||
repository_list_value_delimiter: 'return'
|
||||
template_columns:
|
||||
species: 'Species'
|
||||
organ: 'Organ'
|
||||
morphology: 'Morphology'
|
||||
culture_type: 'Culture Type'
|
||||
stock: 'Stock'
|
||||
passage_number: 'Passage Number'
|
||||
lot_number: 'Lot Number'
|
||||
freezing_date: 'Freezing Date'
|
||||
operator: 'Operator'
|
||||
yield: 'Yield'
|
||||
status: 'Status'
|
||||
handling_procedure: 'Handling Procedure'
|
||||
notes: 'Notes'
|
||||
calibration_date: 'Calibration Date'
|
||||
calibration_message: 'Consider recalibration.'
|
||||
availability_status: 'Availability Status'
|
||||
safety_handling_info: 'Safety & Handling Info'
|
||||
training_records: 'Training Records'
|
||||
contact_person: 'Contact Person'
|
||||
contact_phone: 'Contact Phone'
|
||||
internal_id: 'Internal ID'
|
||||
manufacturer: 'Manufacturer'
|
||||
serial_number: 'Serial Number'
|
||||
concentration: 'Concentration'
|
||||
date_opened: 'Date Opened'
|
||||
expiration_date: 'Expiration Date'
|
||||
expiration_date_message: 'Consider replacing the item.'
|
||||
storage_conditions: 'Storage Conditions'
|
||||
type: 'Type'
|
||||
purity: 'Purity'
|
||||
cas_number: 'CAS Number'
|
||||
safety_sheet: 'Safety Sheet'
|
||||
vendor: 'Vendor'
|
||||
catalog_number: 'Catalog Number'
|
||||
lot: 'Lot'
|
||||
price: 'Price'
|
||||
molecular_weight: 'Molecular Weight'
|
||||
repository_list_value:
|
||||
endothelial: 'Endothelial'
|
||||
epithelial: 'Epithelial'
|
||||
fibroblast: 'Fibroblast'
|
||||
lymphoblast: 'Lymphoblast'
|
||||
adherent: 'Adherent'
|
||||
suspension: 'Suspension'
|
||||
minus_twenty_celsious: "-20°C"
|
||||
two_to_eigth_celsious: "2°C to 8°C"
|
||||
minus_eigthty: "-80°C"
|
||||
ambient: 'Ambient'
|
||||
buffer: 'Buffer'
|
||||
liquid: 'Liquid'
|
||||
reagent: 'Reagent'
|
||||
solid: 'Solid'
|
||||
stock_units:
|
||||
vials: 'Vial(s)'
|
||||
repository_status_value:
|
||||
frozen: 'Frozen'
|
||||
in_subculturing: 'In subculturing'
|
||||
out_of_tock: 'Out of stock'
|
||||
available_for_use: 'Available for use'
|
||||
in_use: 'In use'
|
||||
out_of_service: 'Out of service'
|
||||
under_maintenance: 'Under maintenance'
|
||||
|
||||
my_modules:
|
||||
details:
|
||||
title: "Details"
|
||||
|
@ -1387,6 +1457,7 @@ en:
|
|||
update_protocol: "New version update"
|
||||
save_to_repo: "Save as new template"
|
||||
delete_steps: "Delete all steps"
|
||||
add_protocol_steps: "Add protocol steps"
|
||||
description:
|
||||
title: "Edit task %{module} description"
|
||||
label: "Description"
|
||||
|
@ -2178,6 +2249,7 @@ en:
|
|||
name_placeholder: "My inventory"
|
||||
submit: "Create"
|
||||
success_flash_html: "Inventory <strong>%{name}</strong> successfully created."
|
||||
repository_template_label: "Select inventory template"
|
||||
modal_confirm_sharing:
|
||||
title: "Inventory sharing changes"
|
||||
description_1: "You will no longer share this inventory with some of the teams. All unshared inventory items assigned to tasks will be automatically removed and this action is irreversible. Any item relationship links (if they exist) will also be deleted."
|
||||
|
@ -3999,6 +4071,18 @@ en:
|
|||
name_label: 'Well plate name'
|
||||
name_placeholder: 'Plate'
|
||||
dimension_label: 'Dimensions (rows x columns)'
|
||||
add_protocol_steps:
|
||||
title: "Add steps from another template"
|
||||
description: "Select one or more steps from a different protocol template to add them as new steps at the end of your current protocol."
|
||||
confirm: "Add steps"
|
||||
protocol_label: "Protocol"
|
||||
protocol_placeholder: "Select protocol template"
|
||||
protocol_steps_label: "Protocol steps"
|
||||
protocol_steps_placeholder: "Select steps"
|
||||
success_flash:
|
||||
one: "%{count} step successfully added."
|
||||
other: "%{count} steps successfully added."
|
||||
error_flash: "There was an error importing steps. Please try again."
|
||||
options:
|
||||
up_arrow_title: "Move step up"
|
||||
down_arrow_title: "Move step down"
|
||||
|
|
|
@ -372,6 +372,8 @@ en:
|
|||
form_duplicated_html: "%{user} duplicated form %{form} from %{form_old} in Forms."
|
||||
form_block_duplicated_html: "%{user} duplicated form block %{block_name} in form %{form} in Form templates."
|
||||
form_unpublished_html: "%{user} unpublished form %{form}."
|
||||
task_steps_loaded_from_template_html: "%{user} added %{count} steps from template %{protocol} to task %{my_module}."
|
||||
protocol_steps_loaded_from_template_html: "%{user} added %{count} steps from template %{protocol}."
|
||||
activity_name:
|
||||
create_project: "Project created"
|
||||
rename_project: "Project renamed"
|
||||
|
@ -701,6 +703,8 @@ en:
|
|||
form_duplicated: "Form duplicated"
|
||||
form_block_duplicated: "Form block duplicated"
|
||||
form_unpublished: "Form unpublished"
|
||||
task_steps_loaded_from_template: "Task step loaded from template"
|
||||
protocol_steps_loaded_from_template: "Step loaded from template"
|
||||
activity_group:
|
||||
projects: "Projects"
|
||||
task_results: "Task results"
|
||||
|
|
|
@ -659,7 +659,11 @@ Rails.application.routes.draw do
|
|||
|
||||
resources :protocols, only: %i(index show edit create update) do
|
||||
resources :steps, only: [:create] do
|
||||
post :reorder, on: :collection
|
||||
collection do
|
||||
post :reorder
|
||||
get :list_protocol_steps
|
||||
post :add_protocol_steps
|
||||
end
|
||||
end
|
||||
member do
|
||||
post :publish
|
||||
|
@ -697,6 +701,7 @@ Rails.application.routes.draw do
|
|||
post 'delete_steps'
|
||||
get :permissions
|
||||
put :update_version_comment
|
||||
get :list_published_protocol_templates
|
||||
end
|
||||
collection do
|
||||
post 'archive', to: 'protocols#archive'
|
||||
|
@ -841,6 +846,12 @@ Rails.application.routes.draw do
|
|||
get :repositories
|
||||
end
|
||||
|
||||
resources :repository_templates, only: %i(index) do
|
||||
member do
|
||||
get :list_repository_columns
|
||||
end
|
||||
end
|
||||
|
||||
resources :connected_devices, controller: 'users/connected_devices', only: %i(destroy)
|
||||
|
||||
resources :storage_locations, only: %i(index create destroy update show) do
|
||||
|
|
16
db/migrate/20250331114826_create_repository_template.rb
Normal file
16
db/migrate/20250331114826_create_repository_template.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateRepositoryTemplate < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :repository_templates do |t|
|
||||
t.string :name
|
||||
t.jsonb :column_definitions
|
||||
t.references :team, index: true, foreign_key: { to_table: :teams }
|
||||
t.boolean :predefined, null: false, default: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_reference :repositories, :repository_template, null: true, foreign_key: true
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddRepositoryTemplatesToTeams < ActiveRecord::Migration[7.0]
|
||||
def up
|
||||
Team.find_each do |team|
|
||||
RepositoryTemplate.default.update(team: team)
|
||||
RepositoryTemplate.cell_lines.update(team: team)
|
||||
RepositoryTemplate.equipment.update(team: team)
|
||||
RepositoryTemplate.chemicals_and_reagents.update(team: team)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
Repository.update_all(repository_template_id: nil)
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
RepositoryTemplate.destroy_all
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddOriginalProtocolToSteps < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_reference :steps, :original_protocol, null: true, foreign_key: { to_table: :protocols }
|
||||
end
|
||||
end
|
16
db/schema.rb
16
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2025_03_25_124848) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2025_04_02_092301) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "btree_gist"
|
||||
enable_extension "pg_trgm"
|
||||
|
@ -729,10 +729,12 @@ ActiveRecord::Schema[7.0].define(version: 2025_03_25_124848) do
|
|||
t.bigint "restored_by_id"
|
||||
t.string "external_id"
|
||||
t.integer "repository_rows_count", default: 0, null: false
|
||||
t.bigint "repository_template_id"
|
||||
t.index ["archived"], name: "index_repositories_on_archived"
|
||||
t.index ["archived_by_id"], name: "index_repositories_on_archived_by_id"
|
||||
t.index ["discarded_at"], name: "index_repositories_on_discarded_at"
|
||||
t.index ["my_module_id"], name: "index_repositories_on_my_module_id"
|
||||
t.index ["repository_template_id"], name: "index_repositories_on_repository_template_id"
|
||||
t.index ["restored_by_id"], name: "index_repositories_on_restored_by_id"
|
||||
t.index ["team_id", "external_id"], name: "unique_index_repositories_on_external_id", unique: true
|
||||
t.index ["team_id"], name: "index_repositories_on_team_id"
|
||||
|
@ -1026,6 +1028,16 @@ ActiveRecord::Schema[7.0].define(version: 2025_03_25_124848) do
|
|||
t.index ["user_id"], name: "index_repository_table_states_on_user_id"
|
||||
end
|
||||
|
||||
create_table "repository_templates", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.jsonb "column_definitions"
|
||||
t.bigint "team_id"
|
||||
t.boolean "predefined", default: false, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["team_id"], name: "index_repository_templates_on_team_id"
|
||||
end
|
||||
|
||||
create_table "repository_text_values", force: :cascade do |t|
|
||||
t.string "data"
|
||||
t.datetime "created_at", precision: nil
|
||||
|
@ -1585,6 +1597,7 @@ ActiveRecord::Schema[7.0].define(version: 2025_03_25_124848) do
|
|||
add_foreign_key "reports", "projects"
|
||||
add_foreign_key "reports", "users"
|
||||
add_foreign_key "reports", "users", column: "last_modified_by_id"
|
||||
add_foreign_key "repositories", "repository_templates"
|
||||
add_foreign_key "repositories", "users", column: "archived_by_id"
|
||||
add_foreign_key "repositories", "users", column: "created_by_id"
|
||||
add_foreign_key "repositories", "users", column: "restored_by_id"
|
||||
|
@ -1630,6 +1643,7 @@ ActiveRecord::Schema[7.0].define(version: 2025_03_25_124848) do
|
|||
add_foreign_key "repository_stock_values", "users", column: "created_by_id"
|
||||
add_foreign_key "repository_stock_values", "users", column: "last_modified_by_id"
|
||||
add_foreign_key "repository_table_filters", "users", column: "created_by_id"
|
||||
add_foreign_key "repository_templates", "teams"
|
||||
add_foreign_key "repository_text_values", "users", column: "created_by_id"
|
||||
add_foreign_key "repository_text_values", "users", column: "last_modified_by_id"
|
||||
add_foreign_key "result_assets", "assets"
|
||||
|
|
|
@ -8,6 +8,7 @@ describe RepositoriesController, type: :controller do
|
|||
let!(:user) { controller.current_user }
|
||||
let!(:team) { create :team, created_by: user }
|
||||
let(:action) { post :create, params: params, format: :json }
|
||||
let(:repository_template) { create :repository_template, team: team }
|
||||
|
||||
describe 'index' do
|
||||
let(:repository) { create :repository, team: team, created_by: user }
|
||||
|
@ -29,7 +30,7 @@ describe RepositoriesController, type: :controller do
|
|||
end
|
||||
|
||||
describe 'POST create' do
|
||||
let(:params) { { repository: { name: 'My Repository' } } }
|
||||
let(:params) { { repository: { name: 'My Repository', repository_template_id: repository_template.id } } }
|
||||
|
||||
it 'calls create activity for creating inventory' do
|
||||
expect(Activities::CreateActivityService)
|
||||
|
@ -43,6 +44,13 @@ describe RepositoriesController, type: :controller do
|
|||
expect { action }
|
||||
.to(change { Activity.count })
|
||||
end
|
||||
|
||||
it 'returns success response' do
|
||||
expect { action }.to change(Repository, :count).by(1)
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
expect(Repository.order(created_at: :desc).first.repository_template).to eq repository_template
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE destroy' do
|
||||
|
|
49
spec/controllers/repository_templates_controller_spec.rb
Normal file
49
spec/controllers/repository_templates_controller_spec.rb
Normal file
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe RepositoryTemplatesController, type: :controller do
|
||||
login_user
|
||||
|
||||
let!(:user) { controller.current_user }
|
||||
let!(:team) { create :team, created_by: user }
|
||||
|
||||
describe 'index' do
|
||||
|
||||
let(:action) { get :index, format: :json }
|
||||
|
||||
it 'correct JSON format' do
|
||||
action
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
parsed_response = JSON.parse(response.body)
|
||||
expect(parsed_response['data'].count).to eq(4)
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
describe 'list_repository_columns' do
|
||||
let(:repository_template) { team.repository_templates.last }
|
||||
let(:action) { get :list_repository_columns, params: { id: repository_template } ,format: :json }
|
||||
let(:action_invalid) { get :list_repository_columns, params: { id: -1 } ,format: :json }
|
||||
|
||||
it 'correct JSON format' do
|
||||
action
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
|
||||
parsed_response = JSON.parse(response.body)
|
||||
expect(parsed_response['name']).to eq(repository_template.name)
|
||||
expect(parsed_response['columns'].count).to eq(repository_template.column_definitions.count)
|
||||
end
|
||||
|
||||
it 'invalid id' do
|
||||
action_invalid
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
|
||||
end
|
||||
end
|
||||
end
|
7
spec/factories/repository_template.rb
Normal file
7
spec/factories/repository_template.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :repository_template do
|
||||
association :team
|
||||
end
|
||||
end
|
|
@ -24,6 +24,7 @@ describe Repository, type: :model do
|
|||
describe 'Relations' do
|
||||
it { should belong_to :team }
|
||||
it { should belong_to(:created_by).class_name('User') }
|
||||
it { should belong_to(:repository_template).optional }
|
||||
it { should have_many :repository_rows }
|
||||
it { should have_many :repository_table_states }
|
||||
it { should have_many :report_elements }
|
||||
|
|
29
spec/models/repository_template_spec.rb
Normal file
29
spec/models/repository_template_spec.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe RepositoryTemplate, type: :model do
|
||||
let(:repository_template) { build :repository_template }
|
||||
|
||||
it 'is valid' do
|
||||
expect(repository_template).to be_valid
|
||||
end
|
||||
|
||||
it 'should be of class Repository Template' do
|
||||
expect(subject.class).to eq RepositoryTemplate
|
||||
end
|
||||
|
||||
describe 'Database table' do
|
||||
it { should have_db_column :name }
|
||||
it { should have_db_column :team_id }
|
||||
it { should have_db_column :column_definitions }
|
||||
it { should have_db_column :predefined }
|
||||
it { should have_db_column :created_at }
|
||||
it { should have_db_column :updated_at }
|
||||
end
|
||||
|
||||
describe 'Relations' do
|
||||
it { should belong_to :team }
|
||||
it { should have_many(:repositories).dependent(:destroy) }
|
||||
end
|
||||
end
|
|
@ -37,6 +37,7 @@ describe Team, type: :model do
|
|||
it { should have_many(:team_shared_objects).dependent(:destroy) }
|
||||
it { should have_many :shared_repositories }
|
||||
it { should have_many(:shareable_links).dependent(:destroy) }
|
||||
it { should have_many(:repository_templates).dependent(:destroy) }
|
||||
end
|
||||
|
||||
describe 'Validations' do
|
||||
|
@ -87,5 +88,12 @@ describe Team, type: :model do
|
|||
expect(team.shareable_links.count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'repository templates after team create' do
|
||||
it 'create repository templates after team create' do
|
||||
expect_any_instance_of(Team).to receive(:create_default_repository_templates)
|
||||
team.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue