Finish experiment table [SCI-9800]

This commit is contained in:
Anton 2023-12-11 15:41:03 +01:00
parent a894d6bd26
commit 985dddef17
16 changed files with 6270 additions and 7159 deletions

View file

@ -7,7 +7,15 @@ module AccessPermissions
before_action :check_read_permissions, only: %i(show)
before_action :check_manage_permissions, only: %i(edit update)
def show; end
def show
render json: @experiment.user_assignments.includes(:user_role, :user).order('users.full_name ASC'),
each_serializer: UserAssignmentSerializer
end
def new
render json: @available_users, each_serializer: UserSerializer
end
def edit; end
@ -36,7 +44,7 @@ module AccessPermissions
log_change_activity
render :experiment_member
render json: {}, status: :ok
end
private

View file

@ -0,0 +1,74 @@
<template>
<div class="p-4 rounded sn-shadow-flyout flex flex-col">
<div class="flex items-center gap-2 mb-2">
<div class="sci-checkbox-container">
<input
type="checkbox"
class="sci-checkbox"
@change="itemSelected"
/>
<label :for="params.id" class="sci-checkbox-label"></label>
</div>
<div>{{ params.code }}</div>
<RowMenuRenderer :params="{data: params, dtComponent: dtComponent}" class="ml-auto"/>
</div>
<a :href="params.urls.show"
:class="{'pointer-events-none text-sn-grey': !params.urls.show}"
class="font-bold mb-4 text-sn-black hover:no-underline hover:text-sn-black">
{{ params.name }}
</a>
<div class="flex">
<div class="grid gap-2 grid-cols-[100px_auto] mt-auto">
<span class="text-sn-grey">{{ i18n.t('experiments.card.start_date') }}</span>
<span class="font-bold">{{ params.created_at }}</span>
<span class="text-sn-grey">{{ i18n.t('experiments.card.modified_date') }}</span>
<span class="font-bold">{{ params.updated_at }}</span>
<template v-if="params.archived_on">
<span class="text-sn-grey">{{ i18n.t('experiments.card.archived_date') }}</span>
<span class="font-bold">{{ params.archived_on }}</span>
</template>
<span class="text-sn-grey">{{ i18n.t('experiments.card.completed_task') }}</span>
<span class="font-bold">{{ i18n.t(
'experiments.card.completed_value', {
completed: params.completed_tasks,
all: params.total_tasks
}
) }}</span>
</div>
<div class="h-20 w-20 p-0.5 bg-sn-light-grey rounded-sm shrink-0 ml-auto">
<img :src="params.workflow_img" class="max-h-18 max-w-[72px]">
</div>
</div>
<div class="py-2">
<div class="w-full h-1 bg-sn-light-grey">
<div class="h-full bg-sn-blue" :style="{
width: params.completed_tasks / params.total_tasks * 100 + '%'
}"></div>
</div>
</div>
<Description :params="{data: params, value: params.description, dtComponent: dtComponent}" />
</div>
</template>
<script>
import RowMenuRenderer from '../shared/datatable/row_menu_renderer.vue';
import CardSelectorMixin from '../shared/datatable/mixins/card_selector.js';
import Description from './renderers/description.vue';
export default {
name: 'ProjectCard',
props: {
params: Object,
dtComponent: Object,
},
components: {
RowMenuRenderer,
Description,
},
mixins: [CardSelectorMixin],
};
</script>

View file

@ -19,8 +19,12 @@
@duplicate="duplicate"
@move="move"
@edit="edit"
@create="create"
@access="access"
>
<template> </template>
<template #card="data">
<ExperimentCard :params="data.params" :dtComponent="data.dtComponent" ></ExperimentCard>
</template>
</DataTable>
<ConfirmationModal
@ -48,6 +52,13 @@
:experiment="editModalObject"
@close="editModalObject = null"
@update="updateTable"/>
<NewModal
v-if="newModalOpen"
:createUrl="createUrl"
@close="newModalOpen = false"
@create="updateTable"/>
<AccessModal v-if="accessModalParams" :params="accessModalParams"
@close="accessModalParams = null" @refresh="this.reloadingTable = true" />
</template>
<script>
@ -63,6 +74,9 @@ import DescriptionModal from './modals/description.vue';
import DuplicateModal from './modals/duplicate.vue';
import MoveModal from './modals/move.vue';
import EditModal from './modals/edit.vue';
import NewModal from './modals/new.vue';
import AccessModal from '../shared/access_modal/modal.vue';
import ExperimentCard from './card.vue';
export default {
name: 'ExperimentsList',
@ -73,6 +87,9 @@ export default {
DuplicateModal,
MoveModal,
EditModal,
NewModal,
AccessModal,
ExperimentCard,
},
props: {
dataSource: { type: String, required: true },
@ -80,9 +97,13 @@ export default {
activePageUrl: { type: String },
archivedPageUrl: { type: String },
currentViewMode: { type: String, required: true },
createUrl: { type: String, required: true },
userRolesUrl: { type: String, required: true },
},
data() {
return {
accessModalParams: null,
newModalOpen: false,
editModalObject: null,
moveModalObject: null,
duplicateModalObject: null,
@ -174,13 +195,27 @@ export default {
type: 'DateRange',
label: this.i18n.t('filters_modal.created_on.label'),
},
{
key: 'created_at',
type: 'DateRange',
label: this.i18n.t('filters_modal.updated_on.label'),
},
];
if (this.currentViewMode === 'archived') {
filters.push({
key: 'archived_on',
type: 'DateRange',
label: this.i18n.t('filters_modal.archived_on.label'),
});
}
return filters;
},
},
methods: {
updateTable() {
this.newModalOpen = false;
this.editModalObject = null;
this.moveModalObject = null;
this.duplicateModalObject = null;
@ -218,6 +253,15 @@ export default {
edit(_e, experiment) {
[this.editModalObject] = experiment;
},
create() {
this.newModalOpen = true;
},
access(event, rows) {
this.accessModalParams = {
object: rows[0],
roles_path: this.userRolesUrl,
};
},
},
};
</script>

View file

@ -0,0 +1,82 @@
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<i class="sn-icon sn-icon-close"></i>
</button>
<h4 class="modal-title truncate !block">
{{ i18n.t('experiments.new.modal_title') }}
</h4>
</div>
<div class="modal-body">
<label class="sci-label">{{ i18n.t('experiments.new.name') }}</label>
<div class="sci-input-container-v2 mb-4">
<input type="text" class="sci-input-field"
v-model="name"
autofocus
:placeholder="i18n.t('experiments.new.name_placeholder')">
</div>
<label class="sci-label">{{ i18n.t('experiments.new.description') }}</label>
<div class="sci-input-container-v2 h-40">
<textarea class="sci-input-field"
ref="description"
v-model="description">
</textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ i18n.t('general.cancel') }}</button>
<button type="button" :disabled="!validName" class="btn btn-primary" @click="submit">
{{ i18n.t('experiments.new.modal_create') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
/* global HelperModule SmartAnnotation */
import axios from '../../../packs/custom_axios.js';
import modalMixin from '../../shared/modal_mixin';
export default {
name: 'NewModal',
props: {
createUrl: String,
},
data() {
return {
name: '',
description: '',
};
},
computed: {
validName() {
return this.name.length > 0;
},
},
mounted() {
SmartAnnotation.init($(this.$refs.description), false);
},
mixins: [modalMixin],
methods: {
submit() {
axios.post(this.createUrl, {
experiment: {
name: this.name,
description: this.$refs.description.value,
},
}).then((response) => {
this.$emit('create');
window.location.replace(response.data.path);
}).catch((error) => {
HelperModule.flashAlertMsg(error.response.data.message, 'danger');
});
},
},
};
</script>

View file

@ -1,20 +1,30 @@
<template>
<div>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
<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(`access_permissions.${params.object.type}.modals.edit_modal.title`, {resource_name: params.object.name}) }}
{{ i18n.t(`access_permissions.${params.object.type}.modals.edit_modal.title`, {
resource_name: params.object.name
}) }}
</h4>
</div>
<div class="modal-body">
<div class="h-[60vh] overflow-y-auto">
<div v-for="userAssignment in manuallyAssignedUsers" :key="userAssignment.id" class="p-2 flex items-center gap-2">
<div v-for="userAssignment in manuallyAssignedUsers"
:key="userAssignment.id"
class="p-2 flex items-center gap-2">
<div>
<img :src="userAssignment.attributes.user.avatar_url" class="rounded-full w-8 h-8">
</div>
<div>{{ userAssignment.attributes.user.name }}</div>
<div>
<div>{{ userAssignment.attributes.user.name }}</div>
<div class="text-xs text-sn-grey">{{ userAssignment.attributes.inherit_message }}</div>
</div>
<MenuDropdown
v-if="!userAssignment.attributes.last_owner"
v-if="!userAssignment.attributes.last_owner && params.object.urls.update_access"
class="ml-auto"
:listItems="rolesFromatted"
:btnText="userAssignment.attributes.user_role.name"
@ -35,8 +45,10 @@
<div>
{{ i18n.t('access_permissions.everyone_else', { team_name: params.object.team }) }}
</div>
<i class="sn-icon sn-icon-info" :title='this.autoAssignedUsers.map((ua) => ua.attributes.user.name).join("\u000d")'></i>
<i class="sn-icon sn-icon-info"
:title='this.autoAssignedUsers.map((ua) => ua.attributes.user.name).join("\u000d")'></i>
<MenuDropdown
v-if="params.object.top_level_assignable && params.object.urls.update_access"
class="ml-auto"
:listItems="rolesFromatted"
:btnText="this.roles.find((role) => role[0] == default_role)[1]"
@ -45,10 +57,14 @@
@setRole="(...args) => this.changeDefaultRole(...args)"
@removeRole="() => this.changeDefaultRole()"
></MenuDropdown>
<div class="ml-auto btn btn-light pointer-events-none" v-else>
{{ this.roles.find((role) => role[0] == default_role)[1] }}
<div class="h-6 w-6"></div>
</div>
</div>
</div>
</div>
<div v-if="params.object.top_level_assignable" class="modal-footer">
<div v-if="params.object.urls.new_access" class="modal-footer">
<button class="btn-light ml-auto btn" @click="$emit('changeMode', 'newView')">
<i class="sn-icon sn-icon-new-task"></i>
{{ i18n.t('access_permissions.grant_access') }}
@ -58,21 +74,23 @@
</template>
<script>
import MenuDropdown from "../../shared/menu_dropdown.vue";
/* global HelperModule */
import MenuDropdown from '../menu_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
export default {
props: {
params: {
type: Object,
required: true
required: true,
},
visible: {
type: Boolean
type: Boolean,
},
default_role: {
type: Number
}
type: Number,
},
},
emits: ['changeMode', 'modified'],
mounted() {
@ -84,31 +102,43 @@ export default {
},
computed: {
rolesFromatted() {
let roles = this.roles.map((role) => {
return {
let roles = [];
if (!this.params.object.top_level_assignable) {
roles.push({
emit: 'setRole',
text: this.i18n.t('access_permissions.reset'),
params: 'reset',
});
}
roles = roles.concat(this.roles.map((role) => (
{
emit: 'setRole',
text: role[1],
params: role[0]
params: role[0],
}
});
)));
roles.push({
dividerBefore: true,
emit: 'removeRole',
text: this.i18n.t('access_permissions.remove_access'),
});
if (this.params.object.top_level_assignable) {
roles.push({
dividerBefore: true,
emit: 'removeRole',
text: this.i18n.t('access_permissions.remove_access'),
});
}
return roles
return roles;
},
manuallyAssignedUsers() {
return this.assignedUsers.filter((user) => {
return user.attributes.assigned === 'manually';
});
return this.assignedUsers.filter((user) => (
user.attributes.assigned === 'manually'
));
},
autoAssignedUsers() {
return this.assignedUsers.filter((user) => {
return user.attributes.assigned === 'automatically';
});
return this.assignedUsers.filter((user) => (
user.attributes.assigned === 'automatically'
));
},
},
data() {
@ -123,56 +153,55 @@ export default {
axios.get(this.params.object.urls.show_access)
.then((response) => {
this.assignedUsers = response.data.data;
})
});
},
getRoles() {
axios.get(this.params.roles_path)
.then((response) => {
this.roles = response.data.data;
})
});
},
changeRole(id, role_id) {
axios.put(this.params.object.urls.show_access, {
changeRole(id, roleId) {
axios.put(this.params.object.urls.update_access, {
user_assignment: {
user_id: id,
user_role_id: role_id
}
}).then((response) => {
user_role_id: roleId,
},
}).then(() => {
this.$emit('modified');
this.getAssignedUsers();
})
});
},
removeRole(id) {
axios.delete(this.params.object.urls.show_access, {
axios.delete(this.params.object.urls.update_access, {
data: {
user_id: id
}
user_id: id,
},
}).then((response) => {
this.$emit('modified');
HelperModule.flashAlertMsg(response.data.message, 'success');
this.getAssignedUsers();
})
});
},
changeDefaultRole(role_id) {
changeDefaultRole(roleId) {
axios.put(this.params.object.urls.default_public_user_role_path, {
project: {
default_public_user_role_id: role_id || ''
}
default_public_user_role_id: roleId || '',
},
}).then((response) => {
this.$emit('modified');
if (!role_id) {
if (!roleId) {
this.$emit('changeVisibility', false, null);
} else {
this.$emit('changeVisibility', true, role_id);
this.$emit('changeVisibility', true, roleId);
}
if (response.data.message) {
HelperModule.flashAlertMsg(response.data.message, 'success');
}
})
});
},
removeDefaultRole() {
},
}
}
},
};
</script>

View file

@ -18,25 +18,24 @@
<script>
import SelectDropdown from "../../shared/select_dropdown.vue";
import axios from '../../../packs/custom_axios.js';
import modal_mixin from "../../shared/modal_mixin";
import SelectDropdown from '../select_dropdown.vue';
import modalMixin from '../modal_mixin';
import editView from './edit.vue';
import newView from './new.vue';
export default {
name: "AccessModal",
name: 'AccessModal',
props: {
params: {
type: Object,
required: true
required: true,
},
},
mixins: [modal_mixin],
mixins: [modalMixin],
components: {
SelectDropdown,
editView,
newView
newView,
},
data() {
return {
@ -63,6 +62,6 @@ export default {
this.visible = status;
this.default_role = role;
},
}
}
},
};
</script>

View file

@ -1,7 +1,9 @@
<template>
<div>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><i class="sn-icon sn-icon-close"></i></button>
<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">
{{ i18n.t('access_permissions.partials.new_assignments_form.title', {resource_name: params.object.name}) }}
<button class="close" @click="$emit('changeMode', 'editView')">
@ -12,7 +14,9 @@
<div class="modal-body">
<div class="mb-4">
<div class="sci-input-container-v2 left-icon">
<input type="text" v-model="query" class="sci-input-field" autofocus="true" :placeholder="i18n.t('access_permissions.partials.new_assignments_form.find_people_html')" />
<input type="text" v-model="query" class="sci-input-field"
autofocus="true"
:placeholder="i18n.t('access_permissions.partials.new_assignments_form.find_people_html')" />
<i class="sn-icon sn-icon-search"></i>
</div>
</div>
@ -53,20 +57,21 @@
</template>
<script>
import MenuDropdown from "../../shared/menu_dropdown.vue";
/* global HelperModule */
import MenuDropdown from '../menu_dropdown.vue';
import axios from '../../../packs/custom_axios.js';
export default {
props: {
params: {
type: Object,
required: true
required: true,
},
visible: {
type: Boolean
type: Boolean,
},
default_role: {
type: Number
type: Number,
},
},
emits: ['changeMode'],
@ -79,19 +84,19 @@ export default {
},
computed: {
rolesFromatted() {
return this.roles.map((role) => {
return {
return this.roles.map((role) => (
{
emit: 'setRole',
text: role[1],
params: role[0]
params: role[0],
}
});
));
},
filteredUsers() {
return this.unAssignedUsers.filter((user) => {
return user.attributes.name.toLowerCase().includes(this.query.toLowerCase());
});
}
return this.unAssignedUsers.filter((user) => (
user.attributes.name.toLowerCase().includes(this.query.toLowerCase())
));
},
},
data() {
return {
@ -105,20 +110,20 @@ export default {
axios.get(this.params.object.urls.new_access)
.then((response) => {
this.unAssignedUsers = response.data.data;
})
});
},
getRoles() {
axios.get(this.params.roles_path)
.then((response) => {
this.roles = response.data.data;
})
});
},
assignRole(id, role_id) {
assignRole(id, roleId) {
axios.post(this.params.object.urls.create_access, {
user_assignment: {
user_id: id,
user_role_id: role_id
}
user_role_id: roleId,
},
})
.then((response) => {
this.$emit('modified');
@ -126,11 +131,10 @@ export default {
this.getUnAssignedUsers();
if (id === 'all') {
this.$emit('changeVisibility', true, role_id);
this.$emit('changeVisibility', true, roleId);
}
})
});
},
}
}
</script>
},
};
</script>

View file

@ -8,7 +8,8 @@ module Lists
include ActionView::Helpers::TextHelper
attributes :name, :code, :created_at, :updated_at, :workflow_img, :description, :completed_tasks,
:total_tasks, :archived_on, :urls, :sa_description
:total_tasks, :archived_on, :urls, :sa_description, :default_public_user_role_id, :team,
:top_level_assignable, :hidden
def created_at
I18n.l(object.created_at, format: :full_date)
@ -22,6 +23,22 @@ module Lists
team: object.project.team)
end
def default_public_user_role_id
object.project.default_public_user_role_id
end
def hidden
object.project.hidden?
end
def top_level_assignable
false
end
def team
object.project.team.name
end
def updated_at
I18n.l(object.updated_at, format: :full_date)
end
@ -43,15 +60,21 @@ module Lists
end
def urls
{
urls_list = {
show: table_experiment_path(object),
actions: actions_toolbar_experiments_path(items: [{ id: object.id }].to_json),
projects_to_clone: projects_to_clone_experiment_path(object),
projects_to_move: projects_to_move_experiment_path(object),
clone: clone_experiment_path(object),
move: move_experiment_path(object),
update: experiment_path(object)
update: experiment_path(object),
show_access: access_permissions_experiment_path(object)
}
if can_manage_project_users?(object.project)
urls_list[:update_access] = access_permissions_experiment_path(object)
end
urls_list
end
def workflow_img

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Lists
class ProjectAndFolderSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
@ -64,8 +66,9 @@ module Lists
project_folder_path(object)
end
urls_list[:show_access] = access_permissions_project_path(object)
if project? && can_manage_project_users?(object)
urls_list[:show_access] = access_permissions_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] =

View file

@ -4,7 +4,15 @@ class UserAssignmentSerializer < ActiveModel::Serializer
include Canaid::Helpers::PermissionsHelper
include Rails.application.routes.url_helpers
attributes :id, :assigned, :assignable_type, :user, :user_role, :last_owner
attributes :id, :assigned, :assignable_type, :user, :user_role, :last_owner, :inherit_message
def assigned
parent_assignment(parent).assigned
end
def parent
object.assignable.permission_parent
end
def user
{
@ -22,6 +30,36 @@ class UserAssignmentSerializer < ActiveModel::Serializer
end
def last_owner
object.last_with_permission?("#{object.assignable.class.name}Permissions".constantize::USERS_MANAGE, assigned: :manually)
parent_assignment(parent).last_with_permission?(ProjectPermissions::USERS_MANAGE, assigned: :manually)
end
def inherit_message
user_assignment_resource_role_name(object.user, object.assignable, inherit = '')
end
private
def parent_assignment(parent)
return object if parent.blank?
case parent
when Project
parent.user_assignments.find_by(user: object.user)
when Experiment
parent_assigned(parent.permission_parent)
end
end
def user_assignment_resource_role_name(user, resource, inherit = '')
user_assignment = resource.user_assignments.find_by(user: user)
return '' if [Project, Protocol].include?(resource.class) && inherit.blank?
if user_assignment.automatically_assigned? && resource.permission_parent.present?
parent = resource.permission_parent
return user_assignment_resource_role_name(user, parent, '_inherit')
end
I18n.t("access_permissions.partials.#{resource.class.to_s.downcase}_tooltip#{inherit}")
end
end

View file

@ -6,6 +6,7 @@ module Lists
@raw_data = raw_data
@params = params
@user = user
@filters = params[:filters] || {}
end
def call

View file

@ -33,12 +33,37 @@ module Lists
end
def filter_records(records)
return records unless @params[:search]
if @filters[:query].present?
records = records.where_attributes_like(
['experiments.name', 'experiments.description', "('EX' || experiments.id)"],
@filters[:query]
)
end
records.where_attributes_like(
['experiments.name', 'experiments.description', "('EX' || experiments.id)"],
@params[:search]
)
if @filters[:created_at_from].present?
records = records.where('experiments.created_at > ?', @filters[:created_at_from])
end
if @filters[:created_at_to].present?
records = records.where('experiments.created_at < ?',
@filters[:created_at_to])
end
if @filters[:updated_on_from].present?
records = records.where('experiments.updated_at > ?', @filters[:updated_on_from])
end
if @filters[:updated_on_to].present?
records = records.where('experiments.updated_at < ?',
@filters[:updated_on_to])
end
if @filters[:archived_on_from].present?
records = records.where('COALESCE(experiments.archived_on, projects.archived_on) > ?',
@filters[:archived_on_from])
end
if @filters[:archived_on_to].present?
records = records.where('COALESCE(experiments.archived_on, projects.archived_on) < ?',
@filters[:archived_on_to])
end
records
end
def sortable_columns

View file

@ -5,11 +5,13 @@
<div id="ExperimentsList" class="fixed-content-body">
<experiments-list
actions-url="<%= actions_toolbar_experiments_path %>"
create-url="<%= project_experiments_path(@project) %>"
data-source="<%= experiments_path(project_id: @project, format: :json) %>"
active-page-url="<%= experiments_path(project_id: @project, view_mode: :active) %>"
archived-page-url="<%= experiments_path(project_id: @project, view_mode: :archived) %>"
current-view-mode="<%= params[:view_mode] || :active %>"
user-roles-url="<%= user_roles_projects_path %>"
/>
</div>
<%= javascript_include_tag 'vue_experiments_list' %>
</div>
</div>

View file

@ -3646,8 +3646,8 @@ en:
reset: "Inherit role"
reset_description: "The inherited role from project or experiment will be applied"
projects: "Project"
projects_tooltip: "This role was set on this project."
projects_tooltip_inherit: "This role was inherited from the project."
project_tooltip: "This role was set on this project."
project_tooltip_inherit: "This role was inherited from the project."
protocol: "Protocol"
protocol_tooltip: "This role was set on this protocol."
protocol_tooltip_inherit: "This role was inherited from the protocol."

View file

@ -49,7 +49,6 @@
"croppie": "^2.6.4",
"css-loader": "^6.7.3",
"decimal.js": "^10.3.1",
"eslint-config-airbnb": "^19.0.4",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^6.2.0",
"glob": "^7.1.2",

12896
yarn.lock

File diff suppressed because it is too large Load diff