Merge pull request #8485 from aignatov-bio/ai-sci-11858-add-description-to-project-and-experiment-page

Add description buttons to experiments list and tasks list [SCI-11858]
This commit is contained in:
aignatov-bio 2025-05-06 14:20:21 +02:00 committed by GitHub
commit df4d371d17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 195 additions and 8 deletions

View file

@ -43,6 +43,10 @@ class ExperimentsController < ApplicationController
end
end
def show
render json: @experiment, serializer: Lists::ExperimentSerializer, user: current_user
end
def assigned_users
render json: User.where(id: @experiment.user_assignments.select(:user_id)),
each_serializer: UserSerializer,

View file

@ -16,7 +16,7 @@ class ProjectsController < ApplicationController
helper_method :current_folder
before_action :switch_team_with_param, only: :index
before_action :load_vars, only: %i(update create_tag assigned_users_list)
before_action :load_vars, only: %i(update create_tag assigned_users_list show)
before_action :load_current_folder, only: :index
before_action :check_read_permissions, except: %i(index create update archive_group restore_group
inventory_assigning_project_filter
@ -42,6 +42,10 @@ class ProjectsController < ApplicationController
end
end
def show
render json: @project, serializer: ProjectSerializer, user: current_user
end
def inventory_assigning_project_filter
viewable_experiments = Experiment.viewable_by_user(current_user, current_team)
assignable_my_modules = MyModule.repository_row_assignable_by_user(current_user)

View file

@ -18,6 +18,7 @@
@archive="archive"
@restore="restore"
@showDescription="showDescription"
@showProjectDescription="showProjectDescription = true"
@duplicate="duplicate"
@move="move"
@edit="edit"
@ -37,6 +38,11 @@
:object="descriptionModalObject"
@update="updateDescription"
@close="descriptionModalObject = null"/>
<ProjectDescriptionModal
v-if="project && showProjectDescription"
:object="project.attributes"
@update="updateProjectDescription"
@close="showProjectDescription = false"/>
<DuplicateModal
v-if="duplicateModalObject"
:experiment="duplicateModalObject"
@ -70,6 +76,7 @@ import ConfirmationModal from '../shared/confirmation_modal.vue';
import CompletedTasksRenderer from './renderers/completed_tasks.vue';
import NameRenderer from './renderers/name.vue';
import DescriptionModal from '../shared/datatable/modals/description.vue';
import ProjectDescriptionModal from '../shared/datatable/modals/description.vue';
import DuplicateModal from './modals/duplicate.vue';
import MoveModal from './modals/move.vue';
import ExperimentFormModal from './modals/form.vue';
@ -85,6 +92,7 @@ export default {
DataTable,
ConfirmationModal,
DescriptionModal,
ProjectDescriptionModal,
DuplicateModal,
MoveModal,
ExperimentFormModal,
@ -102,7 +110,8 @@ export default {
currentViewMode: { type: String, required: true },
createUrl: { type: String, required: true },
userRolesUrl: { type: String, required: true },
archived: { type: Boolean }
archived: { type: Boolean },
projectUrl: { type: String, required: true }
},
data() {
return {
@ -112,6 +121,8 @@ export default {
moveModalObject: null,
duplicateModalObject: null,
descriptionModalObject: null,
showProjectDescription: false,
project: null,
reloadingTable: false,
statusesList: [
['not_started', this.i18n.t('experiments.table.column.status.not_started')],
@ -230,6 +241,14 @@ export default {
});
}
left.push({
name: 'showProjectDescription',
icon: 'sn-icon sn-icon-info',
label: this.i18n.t('experiments.toolbar.description_button'),
type: 'emit',
buttonStyle: 'btn btn-light'
});
return {
left,
right: []
@ -278,7 +297,17 @@ export default {
return filters;
}
},
created() {
this.loadProject();
},
methods: {
loadProject() {
axios.get(this.projectUrl).then((response) => {
this.project = response.data.data;
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
},
updateTable() {
this.newModalOpen = false;
this.editModalObject = null;
@ -308,6 +337,15 @@ export default {
this.updateTable();
});
},
updateProjectDescription(description) {
axios.put(this.project.attributes.urls.update, {
project: {
description
}
}).then(() => {
this.loadProject();
});
},
restore(event, rows) {
axios.post(event.path, { experiment_ids: rows.map((row) => row.id) }).then((response) => {
this.reloadingTable = true;

View file

@ -23,6 +23,7 @@
@access="access"
@archive="archive"
@restore="restore"
@showExperimentDescription="showExperimentDescription = true"
@duplicate="duplicate"
@updateDueDate="updateDueDate"
@updateStartDate="updateStartDate"
@ -34,6 +35,11 @@
:projectName="projectName"
:projectTagsUrl="projectTagsUrl"
@close="updateTable" />
<ExperimentDescriptionModal
v-if="experiment && showExperimentDescription"
:object="experiment.attributes"
@update="updateExperimentDescription"
@close="showExperimentDescription = false"/>
<NewModal v-if="newModalOpen"
:createUrl="createUrl"
:projectTagsUrl="projectTagsUrl"
@ -59,6 +65,7 @@
import axios from '../../packs/custom_axios.js';
import DataTable from '../shared/datatable/table.vue';
import ConfirmationModal from '../shared/confirmation_modal.vue';
import ExperimentDescriptionModal from '../shared/datatable/modals/description.vue';
import NameRenderer from './renderers/name.vue';
import ResultsRenderer from './renderers/results.vue';
import StatusRenderer from './renderers/status.vue';
@ -79,6 +86,7 @@ export default {
DataTable,
ConfirmationModal,
DueDateRenderer,
ExperimentDescriptionModal,
StartDateRenderer,
DesignatedUsers,
TagsModal,
@ -106,7 +114,8 @@ export default {
usersFilterUrl: { type: String, required: true },
statusesList: { type: Array, required: true },
projectName: { type: String },
archived: { type: Boolean }
archived: { type: Boolean },
experimentUrl: { type: String, required: true }
},
data() {
return {
@ -117,10 +126,14 @@ export default {
reloadingTable: false,
accessModalParams: null,
columnDefs: [],
filters: []
filters: [],
showExperimentDescription: false,
experiment: null
};
},
created() {
this.loadExperiment();
const columns = [
{
field: 'name',
@ -293,6 +306,14 @@ export default {
});
}
left.push({
name: 'showExperimentDescription',
icon: 'sn-icon sn-icon-info',
label: this.i18n.t('experiments.toolbar.description_button'),
type: 'emit',
buttonStyle: 'btn btn-light'
});
return {
left,
right: []
@ -300,6 +321,13 @@ export default {
}
},
methods: {
loadExperiment() {
axios.get(this.experimentUrl).then((response) => {
this.experiment = response.data.data;
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.error, 'danger');
});
},
updateDueDate(value, params) {
axios.put(params.data.urls.update_due_date, {
my_module: {
@ -309,6 +337,15 @@ export default {
this.updateTable();
});
},
updateExperimentDescription(description) {
axios.put(this.experiment.attributes.urls.update, {
experiment: {
description
}
}).then(() => {
this.loadExperiment();
});
},
updateStartDate(value, params) {
axios.put(params.data.urls.update_start_date, {
my_module: {

View file

@ -8,7 +8,7 @@
:data-e2e="`e2e-BT-topToolbar-${action.name}`"
@click="doAction(action, $event)">
<i :class="action.icon"></i>
{{ action.label }}
<span class="tw-hidden lg:inline">{{ action.label }}</span>
</a>
<MenuDropdown
v-if="action.type === 'menu'"

View file

@ -0,0 +1,101 @@
# frozen_string_literal: true
class ProjectSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
include Canaid::Helpers::PermissionsHelper
include CommentHelper
attributes :name, :code, :created_at, :archived_on, :users, :urls, :hidden, :default_public_user_role_id, :supervised_by,
:comments, :updated_at, :due_date_cell, :start_on_cell, :description, :status, :permissions
def hidden
object.hidden?
end
def supervised_by
{
id: object.supervised_by&.id,
name: object.supervised_by&.name,
avatar: (avatar_path(object.supervised_by, :icon_small) if object.supervised_by)
}
end
def created_at
I18n.l(object.created_at, format: :full_date)
end
def updated_at
I18n.l(object.updated_at, format: :full_date)
end
def archived_on
I18n.l(object.archived_on, format: :full) if object.archived_on
end
def users
object.user_assignments.map do |ua|
{
avatar: avatar_path(ua.user, :icon_small),
full_name: ua.user_name_with_role
}
end
end
def comments
@user = scope[:user] || @instance_options[:user]
{
count: object.comments.count,
count_unseen: count_unseen_comments(object, @user)
}
end
def due_date_cell
{
value: (I18n.l(object.due_date, format: :default) if object.due_date),
value_formatted: (I18n.l(object.due_date, format: :full_date) if object.due_date),
editable: can_manage_project?(@object),
icon: (if object.one_day_prior? && !object.completed?
'sn-icon sn-icon-alert-warning text-sn-alert-brittlebush'
elsif object.overdue? && !object.completed?
'sn-icon sn-icon-alert-warning text-sn-delete-red'
end)
}
end
def start_on_cell
{
value: (I18n.l(object.start_on, format: :default) if object.start_on),
value_formatted: (I18n.l(object.start_on, format: :full_date) if object.start_on),
editable: can_manage_project?(@object)
}
end
def permissions
{
create_comments: can_create_project_comments?(object),
manage_users_assignments: can_manage_project_users?(object),
manage: can_manage_project?(object)
}
end
def urls
urls_list = {}
urls_list[:favorite] = favorite_project_url(object)
urls_list[:unfavorite] = unfavorite_project_url(object)
urls_list[:show_access] = access_permissions_project_path(object)
urls_list[:update] = project_path(object) if can_manage_project?(object)
if can_manage_project_users?(object)
urls_list[:assigned_users] = assigned_users_list_project_path(object)
urls_list[:update_access] = access_permissions_project_path(object)
urls_list[:new_access] = new_access_permissions_project_path(id: object.id)
urls_list[:create_access] = access_permissions_projects_path(id: object.id)
urls_list[:default_public_user_role_path] =
update_default_public_user_role_access_permissions_project_path(object)
end
urls_list
end
end

View file

@ -4,6 +4,7 @@
<%= render partial: 'experiments/index/header' %>
<div id="ExperimentsList" class="fixed-content-body">
<experiments-list
project-url="<%= project_path(@project) %>"
actions-url="<%= actions_toolbar_experiments_path %>"
create-url="<%= project_experiments_path(@project) if can_create_project_experiments?(@project) %>"
data-source="<%= experiments_path(project_id: @project, format: :json) %>"

View file

@ -5,6 +5,7 @@
<div id="MyModulesList" class="fixed-content-body">
<% view_mode = params[:view_mode] || 'active' %>
<my-modules-list
experiment-url="<%= experiment_path(@experiment) %>"
actions-url="<%= actions_toolbar_my_modules_path %>"
create-url="<%= modules_experiment_path(@experiment) if can_manage_experiment?(@experiment) %>"
data-source="<%= my_modules_path(experiment_id: @experiment, format: :json) %>"
@ -23,5 +24,6 @@
:archived="<%= @experiment.archived_branch?%>"
/>
</div>
<%= render 'shared/tiny_mce_packs' %>
<%= javascript_include_tag 'vue_my_modules_list' %>
</div>

View file

@ -1718,6 +1718,7 @@ en:
toolbar:
new_button: "New experiment"
new_button_tooltip: "Create new experiment"
description_button: "Description"
edit_button: "Edit details"
duplicate_button: "Duplicate"
move_button: "Move"

View file

@ -360,7 +360,7 @@ Rails.application.routes.draw do
end
end
resources :projects, except: [:destroy, :new, :show, :edit] do
resources :projects, except: %i(destroy new edit) do
# Activities popup (JSON) for individual project in projects index,
# as well as all activities page for single project (HTML)
resources :project_activities, path: '/activities', only: [:index]
@ -417,9 +417,8 @@ Rails.application.routes.draw do
end
get 'project_folders/:project_folder_id', to: 'projects#index', as: :project_folder_projects
get 'projects/:project_id', to: 'experiments#index'
get 'projects/:project_id/experiments', to: 'experiments#index', as: :experiments
resources :experiments, only: %i(update) do
resources :experiments, only: %i(update show) do
collection do
get 'inventory_assigning_experiment_filter'
get 'clone_modal', action: :clone_modal