Merge pull request #8247 from aignatov-bio/ai-sci-11561-add-assigned-items-vue-component

Add assigned items vue component [SCI-11561]
This commit is contained in:
aignatov-bio 2025-02-17 13:49:38 +01:00 committed by GitHub
commit 0fe6228bc1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 432 additions and 8 deletions

View file

@ -54,6 +54,10 @@ var MyModuleRepositories = (function() {
}
function reloadRepositoriesList(repositoryId, expand = false) {
window.assignedItemsTable.$refs.assignedItems.loadAssingedRepositories();
/*
var repositoriesContainer = $('#assigned-items-container');
$.get(repositoriesContainer.data('repositories-list-url'), function(result) {
repositoriesContainer.html(result.html);
@ -64,6 +68,7 @@ var MyModuleRepositories = (function() {
$('#assigned-repository-items-container-' + repositoryId).collapse('show');
}
});
*/
}
function tableColumns(tableContainer, skipCheckbox = false) {
@ -608,7 +613,7 @@ var MyModuleRepositories = (function() {
}
function initRepositoryFullView() {
$('#assigned-items-container').on('click', '.action-buttons .full-screen', function(e) {
$('#assignedItems').on('click', '.full-screen', function(e) {
var repositoryNameObject = $(this).closest('.assigned-repository-caret')
.find('.assigned-repository-title');
@ -699,7 +704,7 @@ var MyModuleRepositories = (function() {
}
function initRepositoryAssignView() {
$('.repositories-dropdown-menu').on('click', '.repository', function(e) {
$('#assignedItems').on('click', '.repository-assign', function(e) {
var assignUrlModal = $(this).data('assign-url-modal');
var updateUrlModal = $(this).data('update-url-modal');
FULL_VIEW_MODAL.modal('show');

View file

@ -4,9 +4,9 @@ class MyModuleRepositoriesController < ApplicationController
include ApplicationHelper
before_action :load_my_module, except: :assign_my_modules
before_action :load_repository, except: %i(repositories_dropdown_list repositories_list_html create)
before_action :load_repository, except: %i(repositories_dropdown_list repositories_list_html repositories_list create)
before_action :check_my_module_view_permissions, except: %i(update consume_modal update_consumption assign_my_modules)
before_action :check_repository_view_permissions, except: %i(repositories_dropdown_list repositories_list_html create)
before_action :check_repository_view_permissions, except: %i(repositories_dropdown_list repositories_list_html repositories_list create)
before_action :check_repository_row_consumption_permissions, only: %i(consume_modal update_consumption)
before_action :check_assign_repository_records_permissions, only: %i(update create)
before_action :load_my_modules, only: :assign_my_modules
@ -149,6 +149,11 @@ class MyModuleRepositoriesController < ApplicationController
}
end
def repositories_list
@assigned_repositories = @my_module.readable_live_and_snapshot_repositories_list(current_user)
render json: @assigned_repositories, each_serializer: AssignedRepositorySerializer, scope: {user: current_user, my_module: @my_module }
end
def full_view_table
render json: {
html: render_to_string(
@ -170,7 +175,20 @@ class MyModuleRepositoriesController < ApplicationController
.having('COUNT(my_module_repository_rows.id) > 0 OR repositories.archived = FALSE')
.order(:name)
render json: { html: render_to_string(partial: 'my_modules/repositories/repositories_dropdown_list') }
#render json: { html: render_to_string(partial: 'my_modules/repositories/repositories_dropdown_list') }
render json: {
repositories: @repositories.map do |repository|
{
id: repository.id,
name: repository.name,
rows_count: repository.rows_count,
shared: repository.shared_with?(current_team),
table_url: full_view_table_my_module_repository_path(@my_module, repository),
assign_url: assign_modal_my_module_repository_path(@my_module, repository),
update_url: update_modal_my_module_repository_path(@my_module, repository),
}
end
}
end
def export_repository

View file

@ -84,7 +84,7 @@ module MyModulesHelper
date_time: l(repository.created_at, format: :full))
end
t('my_modules.repository.snapshots.simple_view.live_bottom_label')
I18n.t('my_modules.repository.snapshots.simple_view.live_bottom_label')
end
def assigned_repository_simple_view_name_column_id(repository)

View file

@ -0,0 +1,13 @@
import { createApp } from 'vue/dist/vue.esm-bundler.js';
import PerfectScrollbar from 'vue3-perfect-scrollbar';
import AssignedItems from '../../vue/my_module/assigned_items.vue';
import { mountWithTurbolinks } from './helpers/turbolinks.js';
const app = createApp();
app.component('AssignedItems', AssignedItems);
app.config.globalProperties.i18n = window.I18n;
app.use(PerfectScrollbar);
window.assignedItemsTable = mountWithTurbolinks(app, '#assignedItems', () => {
delete window.assignedItemsTable;
});

View file

@ -0,0 +1,129 @@
<template>
<div class="bg-white px-4 my-4">
<div class="py-4 flex items-center gap-4">
<i ref="openHandler" @click="toggleContainer" class="sn-icon sn-icon-right cursor-pointer"></i>
<h2 class="my-0 flex items-center gap-1">
{{ i18n.t('my_modules.assigned_items.title') }}
<span class="text-sn-grey-500 font-normal text-base">[{{ totalRows }}]</span>
</h2>
<div class="ml-auto">
<!-- Next block just for legacy support, JQuery not good works with Teleport -->
<div class="hidden repository-assign"
v-for="repository in availableRepositories"
:key="repository.id"
:data-table-url="repository.table_url"
:data-assign-url-modal="repository.assign_url"
:data-update-url-modal="repository.update_url"
:data-repository-id="repository.id"
:ref="`repository_${repository.id}`"
></div>
<!-- End of block -->
<GeneralDropdown position="right" @open="loadAvailableRepositories">
<template v-slot:field>
<button class="btn btn-light">
{{ i18n.t('my_modules.assigned_items.assign_from') }}
</button>
</template>
<template v-slot:flyout>
<div v-if="loadingAvailableRepositories" class="flex items-center justify-center w-full h-32">
<img src="/images/medium/loading.svg" alt="Loading" />
</div>
<div v-else v-for="repository in availableRepositories" :key="repository.id">
<div class="px-3 py-2.5 hover:bg-sn-super-light-grey max-w-[320px] cursor-pointer overflow-hidden flex items-center gap-1" @click="openAssignModal(repository.id)">
<i v-if="repository.shared" class="sn-icon sn-icon sn-icon-users shrink-0"></i>
<span class="truncate">{{ repository.name }}</span>
<span v-if="repository.rows_count > 0" class="text-sn-grey-500">
<i class="fas fa-file-signature"></i>
{{ repository.rows_count }}
</span>
</div>
</div>
</template>
</GeneralDropdown>
</div>
</div>
<div ref="repositoriesContainer" class="overflow-hidden transition-all" style="max-height: 0px;">
<div class="pl-[2.375rem] py-2.5 mb-4 flex flex-col gap-4">
<AssignedRepository
v-for="repository in assignedRepositories"
:key="repository.id"
:repository="repository"
@recalculateContainerSize="recalculateContainerSize"
/>
</div>
</div>
</div>
</template>
<script>
import axios from '../../packs/custom_axios.js';
import GeneralDropdown from '../shared/general_dropdown.vue';
import AssignedRepository from './assigned_items/repository.vue';
export default {
name: 'AssignedItems',
props: {
avaialableRepositoriesUrl: String,
assignedRepositoriesUrl: String
},
components: {
GeneralDropdown,
AssignedRepository
},
created() {
this.loadAssingedRepositories();
},
computed: {
totalRows() {
return this.assignedRepositories.reduce((acc, repository) => acc + repository.attributes.assigned_rows_count, 0);
}
},
data() {
return {
availableRepositories: [],
assignedRepositories: [],
loadingAvailableRepositories: false,
sectionOpened: false
};
},
methods: {
recalculateContainerSize(offset = 0) {
const container = this.$refs.repositoriesContainer;
const handler = this.$refs.openHandler;
if (this.sectionOpened) {
container.style.maxHeight = `${container.scrollHeight + offset}px`;
handler.classList.remove('sn-icon-right');
handler.classList.add('sn-icon-down');
} else {
container.style.maxHeight = '0px';
handler.classList.remove('sn-icon-down');
handler.classList.add('sn-icon-right');
}
},
toggleContainer() {
this.sectionOpened = !this.sectionOpened;
this.recalculateContainerSize();
},
loadAssingedRepositories() {
axios.get(this.assignedRepositoriesUrl)
.then((response) => {
this.assignedRepositories = response.data.data;
});
},
openAssignModal(repositoryId) {
const [repository] = this.$refs[`repository_${repositoryId}`];
repository.click();
},
loadAvailableRepositories() {
this.loadingAvailableRepositories = true;
axios.get(this.avaialableRepositoriesUrl)
.then((response) => {
this.availableRepositories = response.data.repositories;
this.loadingAvailableRepositories = false;
this.recalculateContainerSize();
});
}
}
};
</script>

View file

@ -0,0 +1,21 @@
<template>
<a class="hover:no-underline flex items-center gap-1 record-info-link"
:title="params.data[0]"
:href="params.data.recordInfoUrl"
>
<span class="truncate">
{{ params.data[0] }}
</span>
</a>
</template>
<script>
export default {
props: {
params: {
type: Object,
required: true
}
}
};
</script>

View file

@ -0,0 +1,180 @@
<template>
<div ref="container" class="border rounded transition-all overflow-hidden" :style="{height: (sectionOpened ? '448px' : '48px')}">
<div class="flex items-center h-12 px-4 gap-4 assigned-repository-title">
<i ref="openHandler" @click="toggleContainer" class="sn-icon sn-icon-right cursor-pointer"></i>
<h3 class="my-0 flex items-center gap-1 ">
<span class="assigned-repository-title">{{ repository.attributes.name }}</span>
<span class="text-sn-grey-500 font-normal text-base">
[{{ repository.attributes.assigned_rows_count }}]
</span>
</h3>
<button
class="btn btn-light icon-btn ml-auto full-screen"
:data-table-url="repository.attributes.urls.full_view"
>
<i class="sn-icon sn-icon-expand"></i>
</button>
</div>
<div style="height: 400px">
<ag-grid-vue
class="ag-theme-alpine w-full flex-grow h-[340px] z-10"
:columnDefs="columnDefs"
:rowData="preparedAssignedItems"
:rowSelection="false"
:suppressRowTransform="true"
:suppressRowClickSelection="true"
:enableCellTextSelection="true"
@grid-ready="onGridReady"
@sortChanged="setOrder"
>
</ag-grid-vue>
<div class="h-[60px] flex items-center border-transparent border-t border-t-sn-light-grey border-solid grey px-6">
<div>
{{ repository.attributes.footer_label }}
</div>
<div class="ml-auto">
<Pagination
:totalPage="Math.ceil(assignedItems.recordsTotal / perPage)"
:currentPage="page"
@setPage="setPage"
></Pagination>
</div>
</div>
</div>
</div>
</template>
<script>
import { AgGridVue } from 'ag-grid-vue3';
import axios from '../../../packs/custom_axios.js';
import CustomHeader from '../../shared/datatable/tableHeader';
import Pagination from '../../shared/datatable/pagination.vue';
import nameRenderer from './renderers/name.vue';
export default {
name: 'AssignedRepository',
props: {
repository: Object
},
components: {
AgGridVue,
agColumnHeader: CustomHeader,
Pagination,
nameRenderer
},
data: () => ({
assignedItems: {
data: [],
recordsTotal: 0
},
order: { column: 0, dir: 'asc' },
sectionOpened: false,
page: 1,
perPage: 20,
gridApi: null,
columnApi: null,
gridReady: false
}),
created() {
},
computed: {
preparedAssignedItems() {
return this.assignedItems.data;
},
columnDefs() {
const columns = [{
field: '0',
flex: 1,
headerName: this.i18n.t('repositories.table.row_name'),
sortable: true,
cellRenderer: 'nameRenderer',
comparator: () => null
}];
if (this.repository.attributes.has_stock && this.repository.attributes.has_stock_consumption) {
columns.push({
field: 'stock',
headerName: this.repository.attributes.stock_column_name,
sortable: true,
comparator: () => null
});
columns.push({
field: 'consumedStock',
headerName: this.i18n.t('repositories.table.row_consumption'),
sortable: true,
comparator: () => null
});
}
return columns;
}
},
methods: {
recalculateContainerSize() {
const { container, openHandler } = this.$refs;
if (this.sectionOpened) {
container.style.height = '448px';
openHandler.classList.remove('sn-icon-right');
openHandler.classList.add('sn-icon-down');
this.$emit('recalculateContainerSize', 400);
if (this.assignedItems.data.length === 0) this.getRows();
} else {
container.style.height = '48px';
openHandler.classList.remove('sn-icon-down');
openHandler.classList.add('sn-icon-right');
this.$emit('recalculateContainerSize', 0);
}
},
toggleContainer() {
this.sectionOpened = !this.sectionOpened;
this.recalculateContainerSize();
},
setOrder() {
const orderState = this.getOrder(this.columnApi.getColumnState());
const [order] = orderState;
if (order.column === 'stock') {
order.column = 1;
} else if (order.column === 'consumedStock') {
order.column = 2;
} else if (order.column === '0') {
order.column = 0;
}
this.order = order;
this.getRows();
},
getOrder(columnsState) {
if (!columnsState) return null;
return columnsState.filter((column) => column.sort)
.map((column) => ({
column: column.colId,
dir: column.sort
}));
},
onGridReady(params) {
this.gridApi = params.api;
this.columnApi = params.columnApi;
this.gridReady = true;
},
getRows() {
axios.post(this.repository.attributes.urls.assigned_rows, {
assigned: 'assigned_simple',
draw: this.page,
length: this.perPage,
order: [this.order],
search: { value: '', regex: false },
simple_view: true,
start: (this.page - 1) * this.perPage,
view_mode: true
}).then((response) => {
this.assignedItems = response.data;
});
},
setPage(page) {
this.page = page;
this.getRows();
}
}
};
</script>

View file

@ -0,0 +1,48 @@
# frozen_string_literal: true
class AssignedRepositorySerializer < ActiveModel::Serializer
include Canaid::Helpers::PermissionsHelper
include Rails.application.routes.url_helpers
include MyModulesHelper
attributes :id, :name
attribute :assigned_rows_count do
object['assigned_rows_count']
end
attribute :is_snapshot do
object.is_a?(RepositorySnapshot)
end
attribute :has_stock do
object.has_stock_management?
end
attribute :has_stock_consumption do
object.has_stock_consumption?
end
attribute :can_manage_consumption do
can_update_my_module_stock_consumption?(scope[:user], scope[:my_module])
end
attribute :stock_column_name do
object.repository_stock_column.name if object.has_stock_management?
end
attribute :footer_label do
assigned_repository_simple_view_footer_label(object)
end
attribute :name_column_id do
assigned_repository_simple_view_name_column_id(object)
end
attribute :urls do
{
full_view: assigned_repository_full_view_table_path(scope[:my_module], object),
assigned_rows: assigned_repository_simple_view_index_path(scope[:my_module], object)
}
end
end

View file

@ -79,6 +79,13 @@
<%= render partial: "my_module_notes" %>
</div>
</div>
<div id="assignedItems" >
<assigned-items
ref="assignedItems"
avaialable-repositories-url="<%= my_module_repositories_dropdown_list_path(@my_module) %>"
assigned-repositories-url="<%= my_module_repositories_list_path(@my_module) %>"
/>
</div>
<!-- Assigned items -->
<div class="task-section hidden">
<div class="task-section-header">
@ -208,5 +215,6 @@
<%= javascript_include_tag "protocols/new_protocol" %>
<%= javascript_include_tag 'vue_protocol' %>
<%= javascript_include_tag 'vue_my_module_assigned_items' %>
<%= javascript_include_tag 'vue_legacy_tags_modal' %>
<%= javascript_include_tag 'vue_legacy_access_modal' %>

View file

@ -2,7 +2,7 @@
<li class="repository"
data-table-url="<%= full_view_table_my_module_repository_path(@my_module, repository) %>"
data-assign-url-modal="<%= assign_modal_my_module_repository_path(@my_module, repository) %>"
data-update-url-modal="<%= update_modal_my_module_repository_path(@my_module, repository)%>" data-repository-id="<%= repository.id %>"
data-update-url-modal="<%= update_modal_my_module_repository_path(@my_module, repository) %>" data-repository-id="<%= repository.id %>"
data-rows-count="<%= repository.rows_count %>" >
<span class="!px-3 !py-2.5 rounded hover:!bg-sn-super-light-grey !text-sn-blue block cursor-pointer">
<% if repository.shared_with?(current_team) %>

View file

@ -493,6 +493,7 @@ Rails.application.routes.draw do
get :repositories_dropdown_list, controller: :my_module_repositories
get :repositories_list_html, controller: :my_module_repositories
get :repositories_list, controller: :my_module_repositories
resources :repositories, controller: :my_module_repositories, only: %i(update create) do
member do

View file

@ -70,7 +70,8 @@ const entryList = {
vue_storage_locations_table: './app/javascript/packs/vue/storage_locations_table.js',
vue_storage_locations_container: './app/javascript/packs/vue/storage_locations_container.js',
vue_form_show: './app/javascript/packs/vue/forms_show.js',
vue_form_table: './app/javascript/packs/vue/forms_table.js'
vue_form_table: './app/javascript/packs/vue/forms_table.js',
vue_my_module_assigned_items: './app/javascript/packs/vue/my_module_assigned_items.js'
};
// Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949