mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2026-01-07 00:36:08 +08:00
Connect add relationship modal to backend [SCI-9711] (#6749)
This commit is contained in:
parent
d1e7ab5efb
commit
dceb92021a
8 changed files with 456 additions and 108 deletions
|
|
@ -1,11 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RepositoryRowConnectionsController < ApplicationController
|
||||
before_action :load_repository, except: %i(repositories create)
|
||||
before_action :load_repository, except: %i(repositories)
|
||||
before_action :load_create_vars, only: :create
|
||||
before_action :check_read_permissions, except: :repositories
|
||||
before_action :load_repository_row, except: %i(repositories repository_rows)
|
||||
before_action :check_manage_permissions, except: %i(repositories repository_rows index)
|
||||
before_action :check_manage_permissions, only: %i(create destroy)
|
||||
|
||||
def index
|
||||
parents = @repository_row.parent_connections
|
||||
|
|
@ -36,9 +36,12 @@ class RepositoryRowConnectionsController < ApplicationController
|
|||
end
|
||||
|
||||
def create
|
||||
connection_params[:relation_ids]
|
||||
# Filtter exixting relations from params
|
||||
relation_ids = connection_params[:relation_ids].map(&:to_i) -
|
||||
@repository_row.public_send("#{@relation}_connections").pluck("#{@relation}_id") -
|
||||
[@repository_row.id]
|
||||
RepositoryRowConnection.transaction do
|
||||
@repository.repository_rows.where(id: connection_params[:relation_ids]).find_each do |row|
|
||||
@connection_repository.repository_rows.where(id: relation_ids).find_each do |row|
|
||||
attributes = {
|
||||
created_by: current_user,
|
||||
last_modified_by: current_user,
|
||||
|
|
@ -48,11 +51,18 @@ class RepositoryRowConnectionsController < ApplicationController
|
|||
end
|
||||
@repository_row.save!
|
||||
end
|
||||
|
||||
if @repository_row.valid?
|
||||
relations = @repository_row.public_send("#{@relation}_repository_rows")
|
||||
.select(:id, :name)
|
||||
.preload(:repository)
|
||||
.map do |row|
|
||||
{ id: row.id, name: row.name, code: "#{RepositoryRow::ID_PREFIX}#{row.id}" }
|
||||
{
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
path: repository_repository_row_path(row.repository, row),
|
||||
repository_name: row.repository.name,
|
||||
repository_path: repository_path(row.repository)
|
||||
}
|
||||
end
|
||||
render json: {
|
||||
"#{@relation.pluralize}": relations
|
||||
|
|
@ -73,7 +83,11 @@ class RepositoryRowConnectionsController < ApplicationController
|
|||
.search_by_name_and_id(current_user, current_user.teams, params[:query])
|
||||
.page(params[:page] || 1)
|
||||
.per(Constants::SEARCH_LIMIT)
|
||||
render json: repositories.select(:id, :name).map { |repository| { id: repository.id, name: repository.name } }
|
||||
render json: {
|
||||
data: repositories.select(:id, :name)
|
||||
.map { |repository| { id: repository.id, name: repository.name } },
|
||||
next_page: repositories.next_page
|
||||
}
|
||||
end
|
||||
|
||||
def repository_rows
|
||||
|
|
@ -81,7 +95,11 @@ class RepositoryRowConnectionsController < ApplicationController
|
|||
.search_by_name_and_id(current_user, current_user.teams, params[:query])
|
||||
.page(params[:page] || 1)
|
||||
.per(Constants::SEARCH_LIMIT)
|
||||
render json: repository_rows.select(:id, :name).map { |repository| { id: repository.id, name: repository.name } }
|
||||
render json: {
|
||||
data: repository_rows.select(:id, :name)
|
||||
.map { |repository| { id: repository.id, name: repository.name } },
|
||||
next_page: repository_rows.next_page
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -91,14 +109,14 @@ class RepositoryRowConnectionsController < ApplicationController
|
|||
@relation = 'child' if connection_params[:relation] == 'child'
|
||||
return render_422(t('.invalid_params')) unless @relation
|
||||
|
||||
@repository = Repository.accessible_by_teams(current_team)
|
||||
.active
|
||||
.find_by(id: connection_params[:repository_id])
|
||||
return render_404 unless @repository
|
||||
@connection_repository = Repository.accessible_by_teams(current_team)
|
||||
.find_by(id: connection_params[:connection_repository_id])
|
||||
return render_404 unless @connection_repository
|
||||
return render_403 unless can_manage_repository_rows?(@connection_repository)
|
||||
end
|
||||
|
||||
def load_repository
|
||||
@repository = Repository.accessible_by_teams(current_team).active.find_by(id: params[:repository_id])
|
||||
@repository = Repository.accessible_by_teams(current_team).find_by(id: params[:repository_id])
|
||||
render_404 unless @repository
|
||||
end
|
||||
|
||||
|
|
@ -116,6 +134,6 @@ class RepositoryRowConnectionsController < ApplicationController
|
|||
end
|
||||
|
||||
def connection_params
|
||||
params.require(:repository_row_connection).permit(:repository_id, :relation, relation_ids: [])
|
||||
params.require(:repository_row_connection).permit(:connection_repository_id, :relation, relation_ids: [])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div ref="repositoryItemRelationshipsModal" @keydown.esc="cancel" id="repositoryItemRelationshipsModal" tabindex="-1" role="dialog"
|
||||
<div ref="repositoryItemRelationshipsModal" @keydown.esc="close" id="repositoryItemRelationshipsModal" tabindex="-1" role="dialog"
|
||||
class="modal ">
|
||||
<div class="modal-dialog modal-sm" role="document">
|
||||
<div class="modal-content w-[400px] h-[498px] m-auto">
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
<!-- header title with close icon -->
|
||||
<div class="h-[30px] w-full flex flex-row-reverse">
|
||||
<i id="close-icon" class="sn-icon sn-icon-close ml-auto cursor-pointer my-auto mx-0" data-dismiss="modal"
|
||||
aria-label="Close"></i>
|
||||
aria-label="Close" @click="close"></i>
|
||||
<h4 class="modal-title" id="modal-destroy-team-label">
|
||||
{{ i18n.t('repositories.item_card.repository_item_relationships_modal.header_title') }}
|
||||
</h4>
|
||||
|
|
@ -24,22 +24,22 @@
|
|||
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="flex justify-center h-40 w-auto">
|
||||
<div class="m-auto h-fit w-fit">{{ i18n.t('general.loading') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="modal-body flex flex-col gap-6">
|
||||
<div class="modal-body flex flex-col gap-6">
|
||||
<!-- inventory -->
|
||||
<div class="flex flex-col gap-[7px]">
|
||||
<div class="h-5 whitespace-nowrap overflow-auto">
|
||||
{{ i18n.t('repositories.item_card.repository_item_relationships_modal.inventory_section_title') }}</div>
|
||||
<div class="h-11">
|
||||
<select-search ref="ChangeSelectedInventoryDropdownSelector" @change="changeSelectedInventory"
|
||||
:value="selectedInventoryValue" :withClearButton="false" :withEditCursor="false"
|
||||
:options="inventoryOptions" :isLoading="isLoading"
|
||||
<select-search ref="ChangeSelectedInventoryDropdownSelector" @change="changeSelectedInventory" :value="selectedInventoryValue"
|
||||
:options="inventoryOptions"
|
||||
:isLoading="isLoadingInventories"
|
||||
:lazyLoadEnabled="true"
|
||||
:optionsUrl="inventoriesUrl"
|
||||
:placeholder="i18n.t('repositories.item_card.repository_item_relationships_modal.select_inventory_placeholder')"
|
||||
:noOptionsPlaceholder="i18n.t('repositories.item_card.repository_item_relationships_modal.select_inventory_no_options_placeholder')"
|
||||
:searchPlaceholder="i18n.t('repositories.item_card.repository_item_relationships_modal.select_inventory_placeholder')"
|
||||
@update-options="updateInventories"
|
||||
@reached-end="fetchInventories"
|
||||
></select-search>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -49,14 +49,24 @@
|
|||
<div class="h-5 whitespace-nowrap overflow-auto">
|
||||
{{ i18n.t('repositories.item_card.repository_item_relationships_modal.item_section_title') }}</div>
|
||||
<div class="h-11">
|
||||
<checklist-select :shouldUpdateWithoutValues="true" :withButtons="false" :withEditCursor="false"
|
||||
ref="ChangeSelectedItemChecklistSelector" :options="itemOptions"
|
||||
<ChecklistSearch
|
||||
ref="ChangeSelectedItemChecklistSelector"
|
||||
:shouldUpdateWithoutValues="true"
|
||||
:lazyLoadEnabled="true"
|
||||
:withButtons="false"
|
||||
:withEditCursor="false"
|
||||
optionsClassName="max-h-[300px]"
|
||||
:optionsUrl="inventoryItemsUrl"
|
||||
:options="itemOptions"
|
||||
:placeholder="i18n.t('repositories.item_card.repository_item_relationships_modal.select_item_placeholder')"
|
||||
:noOptionsPlaceholder="i18n.t('repositories.item_card.repository_item_relationships_modal.select_item_no_options_placeholder')"
|
||||
:initialSelectedValues="this.selectedItemValues.map((val) => val.id)" @update="handleUpdate"
|
||||
:initialSelectedValues="selectedItemValues"
|
||||
:shouldUpdateOnToggleClose="true"
|
||||
>
|
||||
</checklist-select>
|
||||
:params="itemParams"
|
||||
@update-options="updateInventoryItems"
|
||||
@update="selectedItemValues = $event"
|
||||
@reached-end="() => fetchInventoryItems(selectedInventoryValue)"
|
||||
></ChecklistSearch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -66,13 +76,14 @@
|
|||
{{ i18n.t('repositories.item_card.repository_item_relationships_modal.relationship_section_title') }}
|
||||
</div>
|
||||
<div class="h-11">
|
||||
<select-search ref="ChangeSelectedRelationshipDropdownSelector" @change="changeSelectedRelationship"
|
||||
:value="selectedRelationshipValue" :withClearButton="false" :withEditCursor="false"
|
||||
:options="relationshipOptions" :isLoading="isLoading"
|
||||
<Select
|
||||
ref="ChangeSelectedRelationshipDropdownSelector"
|
||||
class="hover:border-sn-sleepy-grey"
|
||||
@change="selectedRelationshipValue = $event"
|
||||
:value="selectedRelationshipValue"
|
||||
:options="[['parent', 'Parent'], ['child', 'Child']]"
|
||||
:placeholder="i18n.t('repositories.item_card.repository_item_relationships_modal.select_relationship_placeholder')"
|
||||
:noOptionsPlaceholder="i18n.t('repositories.item_card.repository_item_relationships_modal.select_relationship_no_options_placeholder')"
|
||||
:searchPlaceholder="i18n.t('repositories.item_card.repository_item_relationships_modal.select_relationship_placeholder')"
|
||||
></select-search>
|
||||
></Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -80,11 +91,11 @@
|
|||
<!-- footer -->
|
||||
<div class="modal-footer">
|
||||
<div class="flex justify-end gap-4">
|
||||
<button class="btn btn-secondary w-[78px] h-10 whitespace-nowrap overflow-auto" @click="cancel">
|
||||
<button class="btn btn-secondary w-[78px] h-10 whitespace-nowrap" @click="close">
|
||||
{{ i18n.t('repositories.item_card.repository_item_relationships_modal.cancel_button') }}
|
||||
</button>
|
||||
<button class="btn btn-primary w-[59px] h-10 whitespace-nowrap overflow-auto"
|
||||
:class="{ 'disabled': !shouldEnableAddButton }" @click="add">
|
||||
<button class="btn btn-primary w-[59px] h-10 whitespace-nowrap"
|
||||
:class="{ 'disabled': !shouldEnableAddButton }" @click="() => addRelation(selectedRelationshipValue)">
|
||||
{{ i18n.t('repositories.item_card.repository_item_relationships_modal.add_button') }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -96,12 +107,16 @@
|
|||
|
||||
<script>
|
||||
import SelectSearch from '../shared/select_search.vue';
|
||||
import ChecklistSearch from '../shared/checklist_search.vue';
|
||||
import Select from '../shared/select.vue';
|
||||
import ChecklistSelect from '../shared/checklist_select.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepositoryItemRelationshipsModal',
|
||||
components: {
|
||||
'select-search': SelectSearch,
|
||||
ChecklistSearch,
|
||||
Select,
|
||||
'checklist-select': ChecklistSelect,
|
||||
},
|
||||
created() {
|
||||
|
|
@ -109,13 +124,19 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
isLoadingInventories: false,
|
||||
inventoriesUrl: '',
|
||||
inventoryItemsUrl: '',
|
||||
createConnectionUrl: '',
|
||||
createConnectionUrlValue: '',
|
||||
selectedInventoryValue: null,
|
||||
selectedItemValues: [],
|
||||
selectedRelationshipValue: null,
|
||||
inventoryOptions: [{ 0: 1, 1: 'Participants database' }, { 0: 2, 1: 'Inventory Option 2' }, { 0: 3, 1: 'Inventory Option 3' }, { 0: 4, 1: 'Inventory Option 4' }],
|
||||
itemOptions: [{ id: 1, label: 'SEPA-STASE' }, { id: 2, label: 'GTC' }, { id: 3, label: 'DESTIL-MACRO' }, { id: 4, label: 'ARNESSLIM-L' }],
|
||||
relationshipOptions: [{ 0: 1, 1: 'Parent' }, { 0: 2, 1: 'Child' }],
|
||||
inventoryOptions: [],
|
||||
itemOptions: [],
|
||||
nextInventoriesPage: 1,
|
||||
nextItemsPage: 1,
|
||||
itemParams: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -124,35 +145,99 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
setParentOrChild(parentOrChild) {
|
||||
const lowerCaseParentOrChild = parentOrChild.toLowerCase();
|
||||
const foundRelationshipOption = this.relationshipOptions.find((option) => option[1].toLowerCase() === lowerCaseParentOrChild);
|
||||
this.selectedRelationshipValue = foundRelationshipOption[0];
|
||||
fetchInventories() {
|
||||
if (!this.nextInventoriesPage) return;
|
||||
|
||||
this.loadingInventories = true;
|
||||
$.ajax({
|
||||
url: `${this.inventoriesUrl}?page=${this.nextInventoriesPage}`,
|
||||
success: (result) => {
|
||||
this.inventoryOptions = this.inventoryOptions.concat(result.data.map((val) => [val.id, val.name]));
|
||||
this.loadingInventories = false;
|
||||
this.nextInventoriesPage = result.next_page;
|
||||
},
|
||||
});
|
||||
},
|
||||
handleUpdate(selectedValues) {
|
||||
const updatedItemOptions = this.itemOptions.filter((itemOption) => selectedValues.includes(itemOption.id));
|
||||
this.selectedItemValues = updatedItemOptions;
|
||||
fetchInventoryItems(inventoryValue = null) {
|
||||
if (!inventoryValue || !this.nextItemsPage) return;
|
||||
|
||||
this.loadingItems = true;
|
||||
$.ajax({
|
||||
url: `${this.inventoryItemsUrl}/?page=${this.nextItemsPage}&repository_id=${inventoryValue}`,
|
||||
success: (result) => {
|
||||
this.itemOptions = this.itemOptions.concat(result.data.map((val) => ({ id: val.id, label: val.name })));
|
||||
this.loadingItems = false;
|
||||
this.nextItemsPage = result.next_page;
|
||||
},
|
||||
});
|
||||
},
|
||||
show(parentOrChild) {
|
||||
updateInventories(currentOptions, result) {
|
||||
this.inventoryOptions = currentOptions.concat(result.data?.map((val) => [val.id, val.name]));
|
||||
},
|
||||
updateInventoryItems(currentOptions, result) {
|
||||
this.itemOptions = currentOptions.concat(result.data.map(({ id, name }) => ({ id, label: name })));
|
||||
},
|
||||
show(params = {}) {
|
||||
const { relation, optionUrls, addRelationCallback } = params;
|
||||
$(this.$refs.repositoryItemRelationshipsModal).modal('show');
|
||||
if (parentOrChild) {
|
||||
this.setParentOrChild(parentOrChild);
|
||||
this.inventoriesUrl = optionUrls.inventories_url;
|
||||
this.inventoryItemsUrl = optionUrls.inventory_items_url;
|
||||
this.createConnectionUrl = optionUrls.create_url;
|
||||
this.addRelationCallback = addRelationCallback;
|
||||
if (['parent', 'child'].includes(relation)) {
|
||||
this.selectedRelationshipValue = relation;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.fetchInventories();
|
||||
});
|
||||
},
|
||||
changeSelectedInventory(value) {
|
||||
this.selectedInventoryValue = value;
|
||||
this.itemOptions = [];
|
||||
this.nextItemsPage = 1;
|
||||
if (value) {
|
||||
this.loadingItems = true;
|
||||
this.itemParams = [`repository_id=${value}`];
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.fetchInventoryItems(value);
|
||||
});
|
||||
},
|
||||
changeSelectedRelationship(value) {
|
||||
this.selectedRelationshipValue = value;
|
||||
},
|
||||
cancel() {
|
||||
this.selectedInventoryValue = null;
|
||||
this.selectedItemValues = [];
|
||||
this.selectedRelationshipValue = null;
|
||||
close() {
|
||||
Object.assign(this.$data, {
|
||||
selectedInventoryValue: null,
|
||||
selectedItemValues: [],
|
||||
selectedRelationshipValue: null,
|
||||
nextInventoriesPage: 1,
|
||||
nextItemsPage: 1,
|
||||
inventoriesUrl: '',
|
||||
inventoryItemsUrl: '',
|
||||
createConnectionUrl: '',
|
||||
itemOptions: [],
|
||||
inventoryOptions: [],
|
||||
itemParams: [],
|
||||
});
|
||||
$(this.$refs.repositoryItemRelationshipsModal).modal('hide');
|
||||
},
|
||||
add() {
|
||||
$(this.$refs.repositoryItemRelationshipsModal).modal('hide');
|
||||
addRelation(relation) {
|
||||
const $this = this;
|
||||
$.ajax({
|
||||
url: this.createConnectionUrl,
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
repository_row_connection: {
|
||||
relation: this.selectedRelationshipValue,
|
||||
relation_ids: this.selectedItemValues,
|
||||
connection_repository_id: this.selectedInventoryValue,
|
||||
},
|
||||
},
|
||||
success: (result) => {
|
||||
$this.addRelationCallback(result, relation);
|
||||
if ($('.dataTable')[0]) $('.dataTable').DataTable().ajax.reload(null, false);
|
||||
},
|
||||
});
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -413,10 +413,22 @@ export default {
|
|||
document.removeEventListener('mousedown', this.handleOutsideClick);
|
||||
},
|
||||
methods: {
|
||||
handleOpenAddRelationshipsModal(event, parentOrChild) {
|
||||
handleOpenAddRelationshipsModal(event, relation) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
window.repositoryItemRelationshipsModal.show(parentOrChild);
|
||||
const addRelationCallback = (data, connectionRelation) => {
|
||||
if (connectionRelation === 'parent') {
|
||||
this.parentsCount = data.parents.length;
|
||||
this.parents = data.parents;
|
||||
}
|
||||
if (connectionRelation === 'child') {
|
||||
this.childrenCount = data.children.length;
|
||||
this.children = data.children;
|
||||
}
|
||||
};
|
||||
window.repositoryItemRelationshipsModal.show(
|
||||
{ relation, addRelationCallback, optionUrls: { ...this.actions.row_connections } },
|
||||
);
|
||||
},
|
||||
handleOutsideClick(event) {
|
||||
if (!this.isShowing) return;
|
||||
|
|
|
|||
145
app/javascript/vue/shared/checklist_search.vue
Normal file
145
app/javascript/vue/shared/checklist_search.vue
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<checklistSelect
|
||||
:class="{
|
||||
['sn-select sn-select--search hover:border-sn-sleepy-grey']: true,
|
||||
'sn-select--open': shouldOpen,
|
||||
'sn-select--blank': !valueLabel,
|
||||
'disabled': disabled
|
||||
}"
|
||||
:className="className"
|
||||
:optionsClassName="optionsClassName"
|
||||
:withButtons="withButtons"
|
||||
:withEditCursor="withEditCursor"
|
||||
:initialSelectedValues="initialSelectedValues"
|
||||
:options="currentOptions"
|
||||
:placeholder="placeholder"
|
||||
:noOptionsPlaceholder="isLoading || isLazyLoading ? i18n.t('general.loading') : noOptionsPlaceholder"
|
||||
:disabled="disabled"
|
||||
:shouldOpen="shouldOpen"
|
||||
:shouldUpdateWithoutValues="shouldUpdateWithoutValues"
|
||||
:shouldUpdateOnToggleClose="shouldUpdateOnToggleClose"
|
||||
@reached-end="handleReachedEnd"
|
||||
@close="shouldOpen = false"
|
||||
@update-selected-values="selectedValues = $event"
|
||||
>
|
||||
<div class="flex w-full" @click="shouldOpen = !shouldOpen">
|
||||
<input
|
||||
ref="focusElement"
|
||||
v-model="query"
|
||||
type="text"
|
||||
class="sn-select__search-input"
|
||||
:placeholder="placeholder"
|
||||
@blur="query = ''" />
|
||||
<span class="sn-select__value">{{ valueLabel || (placeholder || i18n.t('general.select')) }}</span>
|
||||
<i class="sn-icon" :class="{ 'sn-icon-down': !shouldOpen, 'sn-icon-up': shouldOpen}"></i>
|
||||
</div>
|
||||
</checklistSelect>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import checklistSelect from './checklist_select.vue';
|
||||
import { debounce } from './debounce';
|
||||
|
||||
export default {
|
||||
name: 'ChecklistSearch',
|
||||
props: {
|
||||
withButtons: { type: Boolean, default: false },
|
||||
withEditCursor: { type: Boolean, default: false },
|
||||
initialSelectedValues: { type: Array, default: () => [] },
|
||||
options: { type: Array, default: () => [] },
|
||||
placeholder: { type: String },
|
||||
noOptionsPlaceholder: { type: String },
|
||||
disabled: { type: Boolean, default: false },
|
||||
className: { type: String, default: '' },
|
||||
optionsClassName: { type: String, default: '' },
|
||||
shouldUpdateWithoutValues: { type: Boolean, default: false },
|
||||
shouldUpdateOnToggleClose: { type: Boolean, default: false },
|
||||
isLoading: { type: Boolean, default: false },
|
||||
params: { type: Array, default: () => [] },
|
||||
optionsUrl: { type: String, required: true },
|
||||
lazyLoadEnabled: { type: Boolean, default: false },
|
||||
},
|
||||
components: { checklistSelect },
|
||||
emits: ['reached-end', 'update-options'],
|
||||
data() {
|
||||
return {
|
||||
query: null,
|
||||
currentOptions: null,
|
||||
shouldOpen: false,
|
||||
nextPage: 1,
|
||||
isLazyLoading: false,
|
||||
selectedValues: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.currentOptions = this.options;
|
||||
},
|
||||
watch: {
|
||||
query() {
|
||||
if (!this.query) {
|
||||
this.currentOptions = this.options;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.optionsUrlValue) {
|
||||
// reset current options and next page when lazy loading is enabled
|
||||
if (this.lazyLoadEnabled) {
|
||||
this.currentOptions = [];
|
||||
this.isLazyLoading = true;
|
||||
this.nextPage = 1;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
debounce(this.fetchOptions(), 10);
|
||||
});
|
||||
} else {
|
||||
this.currentOptions = this.options.filter((o) => o[1].toLowerCase().includes(this.query.toLowerCase()));
|
||||
}
|
||||
},
|
||||
options() {
|
||||
this.currentOptions = this.options;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
valueLabel() {
|
||||
if (!this.selectedValues.length) return '';
|
||||
if (this.selectedValues.length === 1) {
|
||||
return this.currentOptions.find(({ id }) => id === this.selectedValues[0])?.label;
|
||||
}
|
||||
return `${this.selectedValues.length} ${this.i18n.t('general.selected')}`;
|
||||
},
|
||||
optionsUrlValue() {
|
||||
if (!this.optionsUrl) return '';
|
||||
|
||||
let url = `${this.optionsUrl}?query=${this.query || ''}`;
|
||||
if (this.lazyLoadEnabled && this.nextPage) url = `${url}&page=${this.nextPage}`;
|
||||
if (this.params.length) url = `${url}&${this.params.join('&')}`;
|
||||
return url;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleReachedEnd() {
|
||||
if (this.query) {
|
||||
this.fetchOptions();
|
||||
} else {
|
||||
this.$emit('reached-end');
|
||||
}
|
||||
},
|
||||
fetchOptions() {
|
||||
if (!this.nextPage || !this.optionsUrl) return;
|
||||
|
||||
$.ajax({
|
||||
url: this.optionsUrlValue,
|
||||
success: (result) => {
|
||||
if (this.lazyLoadEnabled) {
|
||||
this.nextPage = result.next_page;
|
||||
this.$emit('update-options', this.currentOptions, result);
|
||||
this.isLazyLoading = false;
|
||||
return;
|
||||
}
|
||||
this.currentOptions = result;
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,20 +1,22 @@
|
|||
<template>
|
||||
<div class="w-full relative" ref="container" v-click-outside="closeDropdown">
|
||||
<button ref="focusElement"
|
||||
class="btn flex justify-between items-center w-full outline-none border-[1px] bg-white rounded p-2
|
||||
font-inter text-sm font-normal leading-5"
|
||||
:class="{
|
||||
'sci-cursor-edit': !isOpen && withEditCursor,
|
||||
'border-sn-light-grey hover:border-sn-sleepy-grey': !isOpen,
|
||||
'border-sn-science-blue': isOpen,
|
||||
'text-sn-grey': !valueLabel,
|
||||
[className]: true
|
||||
}"
|
||||
:disabled="disabled"
|
||||
@click="toggle">
|
||||
<span class="overflow-hidden text-ellipsis">{{ valueLabel || this.placeholder || this.i18n.t('general.select') }}</span>
|
||||
<i class="sn-icon" :class="{ 'sn-icon-down': !isOpen, 'sn-icon-up': isOpen}"></i>
|
||||
</button>
|
||||
<slot>
|
||||
<button ref="focusElement"
|
||||
class="btn flex justify-between items-center w-full outline-none border-[1px] bg-white rounded p-2
|
||||
font-inter text-sm font-normal leading-5"
|
||||
:class="{
|
||||
'sci-cursor-edit': !isOpen && withEditCursor,
|
||||
'border-sn-light-grey hover:border-sn-sleepy-grey': !isOpen,
|
||||
'border-sn-science-blue': isOpen,
|
||||
'text-sn-grey': !valueLabel,
|
||||
[className]: true
|
||||
}"
|
||||
:disabled="disabled"
|
||||
@click="isOpen = !isOpen">
|
||||
<span class="overflow-hidden text-ellipsis">{{ valueLabel || this.placeholder || this.i18n.t('general.select') }}</span>
|
||||
<i class="sn-icon" :class="{ 'sn-icon-down': !isOpen, 'sn-icon-up': isOpen}"></i>
|
||||
</button>
|
||||
</slot>
|
||||
<div :style="optionPositionStyle" class="py-2.5 z-10 bg-white rounded border-[1px] border-sn-light-grey shadow-sn-menu-sm" :class="{ 'hidden': !isOpen }">
|
||||
<div v-if="withButtons" class="px-2.5 pb-[1px]">
|
||||
<div class="flex gap-2 pl-2 justify-start items-center w-[calc(100%-10px)]">
|
||||
|
|
@ -39,7 +41,8 @@
|
|||
:class="{
|
||||
'block': isOpen,
|
||||
[optionsClassName]: true
|
||||
}">
|
||||
}"
|
||||
@ps-scroll-y="onScroll">
|
||||
<div v-if="options.length" class="flex flex-col gap-[1px]">
|
||||
<div v-for="option in options"
|
||||
:key="option.id"
|
||||
|
|
@ -68,6 +71,7 @@
|
|||
|
||||
export default {
|
||||
name: 'ChecklistSelect',
|
||||
emits: ['close', 'update', 'reached-end', 'update-selected-values'],
|
||||
props: {
|
||||
withButtons: { type: Boolean, default: false },
|
||||
withEditCursor: { type: Boolean, default: false },
|
||||
|
|
@ -77,6 +81,7 @@
|
|||
noOptionsPlaceholder: { type: String },
|
||||
disabled: { type: Boolean, default: false },
|
||||
className: { type: String, default: '' },
|
||||
shouldOpen: { type: Boolean },
|
||||
optionsClassName: { type: String, default: '' },
|
||||
shouldUpdateWithoutValues: { type: Boolean, default: false },
|
||||
shouldUpdateOnToggleClose: { type: Boolean, default: false }
|
||||
|
|
@ -99,7 +104,10 @@
|
|||
if (!this.selectedValues.length) return
|
||||
if (this.selectedValues.length === 1) return this.options.find(({id}) => id === this.selectedValues[0])?.label
|
||||
return `${this.selectedValues.length} ${this.i18n.t('general.selected')}`;
|
||||
}
|
||||
},
|
||||
focusElement() {
|
||||
return this.$refs.focusElement || this.$parent.$refs.focusElement;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
initialSelectedValues: {
|
||||
|
|
@ -108,19 +116,28 @@
|
|||
},
|
||||
deep: true
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
if (this.isOpen) {
|
||||
shouldOpen(value) {
|
||||
this.isOpen = value;
|
||||
},
|
||||
isOpen(value) {
|
||||
if (value) {
|
||||
this.updateOptionPosition();
|
||||
this.$refs.optionsContainer.scrollTop = 0;
|
||||
this.$nextTick(() => {
|
||||
this.focusElement.focus();
|
||||
});
|
||||
} else {
|
||||
if (this.shouldUpdateOnToggleClose && this.shouldUpdateWithoutValues) {
|
||||
this.$emit('update', this.selectedValues);
|
||||
}
|
||||
this.close();
|
||||
this.closeDropdown();
|
||||
}
|
||||
},
|
||||
selectedValues(values) {
|
||||
this.$emit('update-selected-values', values);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateOptionPosition() {
|
||||
const container = $(this.$refs.container);
|
||||
const rect = container.get(0).getBoundingClientRect();
|
||||
|
|
@ -141,11 +158,23 @@
|
|||
this.isOpen = false;
|
||||
if (this.shouldUpdateWithoutValues) {
|
||||
this.$emit('update', this.selectedValues)
|
||||
|
||||
}
|
||||
if (this.selectedValues.length && !this.shouldUpdateWithoutValues) {
|
||||
this.$emit('update', this.selectedValues);
|
||||
}
|
||||
this.$emit('close');
|
||||
this.$refs.optionsContainer.$el.scrollTop = 0;
|
||||
},
|
||||
onScroll() {
|
||||
const scrollObj = this.$refs.optionsContainer.ps;
|
||||
|
||||
if (scrollObj) {
|
||||
const reachedEnd = scrollObj.reach.y === 'end';
|
||||
if (reachedEnd && this.contentHeight !== scrollObj.contentHeight) {
|
||||
this.$emit('reached-end');
|
||||
this.contentHeight = scrollObj.contentHeight;
|
||||
}
|
||||
}
|
||||
},
|
||||
isSelected(id) {
|
||||
return this.selectedValues.includes(id);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
<perfect-scrollbar ref="optionsContainer"
|
||||
class="sn-select__options !relative !top-0 !left-[-1px] !shadow-none scroll-container px-2.5 pt-0 block"
|
||||
:class="{ [optionsClassName]: true }"
|
||||
@ps-scroll-y="onScroll"
|
||||
>
|
||||
<div v-if="options.length" class="flex flex-col gap-[1px]">
|
||||
<div
|
||||
|
|
@ -48,10 +49,11 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { vOnClickOutside } from '@vueuse/components'
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
|
||||
export default {
|
||||
name: 'Select',
|
||||
emits: ['close', 'reached-end', 'open', 'change'],
|
||||
props: {
|
||||
withClearButton: { type: Boolean, default: false },
|
||||
withEditCursor: { type: Boolean, default: false },
|
||||
|
|
@ -70,7 +72,8 @@
|
|||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
optionPositionStyle: ''
|
||||
optionPositionStyle: '',
|
||||
currentContentHeight: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -103,9 +106,21 @@
|
|||
this.close()
|
||||
}
|
||||
},
|
||||
onScroll() {
|
||||
const scrollObj = this.$refs.optionsContainer.ps;
|
||||
|
||||
if (scrollObj) {
|
||||
const reachedEnd = scrollObj.reach.y === 'end';
|
||||
if (reachedEnd && this.contentHeight !== scrollObj.contentHeight) {
|
||||
this.$emit('reached-end');
|
||||
this.contentHeight = scrollObj.contentHeight;
|
||||
}
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.optionPositionStyle = '';
|
||||
this.$refs.optionsContainer.$el.scrollTop = 0;
|
||||
this.$emit('close');
|
||||
},
|
||||
setValue(value) {
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@
|
|||
:value="value"
|
||||
:options="currentOptions"
|
||||
:placeholder="placeholder"
|
||||
:noOptionsPlaceholder="isLoading ? i18n.t('general.loading') : noOptionsPlaceholder"
|
||||
:noOptionsPlaceholder="isLoading || isLazyLoading ? i18n.t('general.loading') : noOptionsPlaceholder"
|
||||
v-bind:disabled="disabled"
|
||||
@change="change"
|
||||
@reached-end="handleReachedEnd"
|
||||
@blur="blur"
|
||||
@open="open"
|
||||
@close="close"
|
||||
|
|
@ -23,10 +24,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Select from './select.vue'
|
||||
import Select from './select.vue';
|
||||
import { debounce } from './debounce';
|
||||
|
||||
export default {
|
||||
name: 'SelectSearch',
|
||||
emits: ['change', 'close', 'open', 'blur', 'update-options', 'reached-end'],
|
||||
props: {
|
||||
withClearButton: { type: Boolean, default: false },
|
||||
withEditCursor: { type: Boolean, default: false },
|
||||
|
|
@ -40,41 +43,61 @@
|
|||
isLoading: { type: Boolean, default: false },
|
||||
className: { type: String, default: '' },
|
||||
optionsClassName: { type: String, default: '' },
|
||||
customClass: { type: String, default: '' }
|
||||
customClass: { type: String, default: '' },
|
||||
lazyLoadEnabled: { type: Boolean },
|
||||
params: { type: Array, default: () => [] },
|
||||
},
|
||||
components: { Select },
|
||||
data() {
|
||||
return {
|
||||
query: null,
|
||||
currentOptions: null,
|
||||
isOpen: false
|
||||
}
|
||||
isOpen: false,
|
||||
nextPage: 1,
|
||||
isLazyLoading: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.currentOptions = this.options;
|
||||
},
|
||||
watch: {
|
||||
query() {
|
||||
if(!this.query) {
|
||||
if (!this.query) {
|
||||
this.currentOptions = this.options;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.optionsUrl) {
|
||||
this.fetchOptions();
|
||||
if (this.optionsUrlValue) {
|
||||
// reset current options and fetch when lazy loding is enabled
|
||||
if (this.lazyLoadEnabled) {
|
||||
this.currentOptions = [];
|
||||
this.isLazyLoading = true;
|
||||
this.nextPage = 1;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
debounce(this.fetchOptions(), 10);
|
||||
});
|
||||
} else {
|
||||
this.currentOptions = this.options.filter((o) => o[1].toLowerCase().includes(this.query.toLowerCase()));
|
||||
}
|
||||
},
|
||||
options() {
|
||||
this.currentOptions = this.options;
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
valueLabel() {
|
||||
let option = this.currentOptions.find((o) => o[0] === this.value);
|
||||
return option && option[1];
|
||||
}
|
||||
},
|
||||
optionsUrlValue() {
|
||||
if (!this.optionsUrl) return '';
|
||||
|
||||
let url = `${this.optionsUrl}?query=${this.query || ''}`;
|
||||
if (this.lazyLoadEnabled && this.nextPage) url = `${url}&page=${this.nextPage}`;
|
||||
if (this.params.length) url = `${url}&${this.params.join('&')}`;
|
||||
return url;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
blur() {
|
||||
|
|
@ -94,13 +117,29 @@
|
|||
this.isOpen = false;
|
||||
this.$emit('close');
|
||||
},
|
||||
handleReachedEnd() {
|
||||
if (this.query) {
|
||||
this.fetchOptions();
|
||||
} else {
|
||||
this.$emit('reached-end');
|
||||
}
|
||||
},
|
||||
fetchOptions() {
|
||||
$.get(`${this.optionsUrl}?query=${this.query || ''}`,
|
||||
(data) => {
|
||||
this.currentOptions = data;
|
||||
}
|
||||
);
|
||||
if (!this.nextPage || !this.optionsUrl) return;
|
||||
|
||||
$.ajax({
|
||||
url: this.optionsUrlValue,
|
||||
success: (result) => {
|
||||
if (this.lazyLoadEnabled) {
|
||||
this.nextPage = result.next_page;
|
||||
this.$emit('update-options', this.currentOptions, result);
|
||||
this.isLazyLoading = false;
|
||||
return;
|
||||
}
|
||||
this.currentOptions = result;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ json.actions do
|
|||
elsif @repository.has_stock_management?
|
||||
json.stock_value_url new_repository_stock_repository_repository_row_url(@repository, @repository_row)
|
||||
end
|
||||
json.row_connections do
|
||||
json.inventories_url repository_row_connections_repositories_url
|
||||
json.inventory_items_url repository_row_connections_repository_rows_url
|
||||
json.create_url repository_repository_row_repository_row_connections_url(@repository, @repository_row)
|
||||
end
|
||||
end
|
||||
|
||||
json.default_columns do
|
||||
|
|
@ -44,7 +49,7 @@ json.relationships do
|
|||
json.parents_count @repository_row.parent_connections_count
|
||||
json.children_count @repository_row.child_connections_count
|
||||
json.parents do
|
||||
json.array! @repository_row.parent_repository_rows.each do |parent|
|
||||
json.array! @repository_row.parent_repository_rows.preload(:repository).each do |parent|
|
||||
json.code parent.code
|
||||
json.name parent.name
|
||||
json.path repository_repository_row_path(parent.repository, parent)
|
||||
|
|
@ -53,7 +58,7 @@ json.relationships do
|
|||
end
|
||||
end
|
||||
json.children do
|
||||
json.array! @repository_row.child_repository_rows.each do |child|
|
||||
json.array! @repository_row.child_repository_rows.preload(:repository).each do |child|
|
||||
json.code child.code
|
||||
json.name child.name
|
||||
json.path repository_repository_row_path(child.repository, child)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue