Connect add relationship modal to backend [SCI-9711] (#6749)

This commit is contained in:
wandji 2023-12-11 10:43:47 +01:00 committed by GitHub
parent d1e7ab5efb
commit dceb92021a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 456 additions and 108 deletions

View file

@ -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

View file

@ -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();
},
},
};

View file

@ -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;

View 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>

View file

@ -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);

View file

@ -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) {

View file

@ -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>

View file

@ -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)