Files
upstream-corteza/lib/vue/src/components/itemPicker/CItemPicker.vue
upstream-mirror 7e385fc126
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:22:23 +05:00

492 lines
13 KiB
Vue

<template>
<div
class="overflow-hidden d-flex flex-column w-100 vh-100"
>
<b-card
no-body
class="h-100"
>
<b-card-header
class="p-0"
>
<c-input-search
v-if="!hideFilter"
v-model.trim="query"
:disabled="_disabledFilter"
:placeholder="labels.searchPlaceholder"
/>
</b-card-header>
<b-card-body
class="d-flex p-0"
>
<b-card
no-body
class="col-sm-6 col-12 h-100 p-0"
>
<b-card-header
class="py-2 pl-0 pr-2"
>
<div
class="d-flex align-items-center"
>
<label
class="text-primary mb-0 py-1"
>
{{ labels.availableItems }}
</label>
<b-button
v-show="filteredAvailable.length && !disabled"
data-test-id="link-select-all"
variant="outline-light"
class="ml-auto border-0 text-muted"
@click="selectAll()"
>
{{ labels.selectAllItems }}
</b-button>
</div>
</b-card-header>
<b-card-body
class="overflow-auto py-0 pl-0 pr-2"
>
<b-list-group
vertical
class="h-100"
>
<draggable
v-model="filteredAvailable"
:sort="!_disabledSorting"
:move="_disableDragging"
:disabled="query"
draggable=".item"
group="items"
class="overflow-auto h-100"
>
<b-list-group-item
v-for="item in filteredAvailable"
:key="item.value"
class="item mb-3 border rounded"
:class="{
'handle': !isDraggable
}"
@dblclick="select(item)"
>
<c-item-picker-item
:item="item"
:disabled="disabled"
:disabled-dragging="isDraggable"
:disabled-sorting="disabledSorting"
:hide-icons="hideIcons"
@select="select(item)"
>
<template
v-for="(_, slot) of $scopedSlots"
#[slot]="scope"
>
<slot
:name="slot"
:text-field="textField"
:disabled="disabled"
:disabled-dragging="isDraggable"
:disabled-sorting="disabledSorting"
:hide-icons="hideIcons"
v-bind="scope"
/>
</template>
</c-item-picker-item>
</b-list-group-item>
<template #footer>
<h6
v-if="!filteredAvailable.length && query"
class="text-center my-4"
>
{{ labels.noItemsFound }}
</h6>
</template>
</draggable>
</b-list-group>
</b-card-body>
</b-card>
<b-card
no-body
class="h-100 pl-sm-0 col-sm-6 col-12 p-0"
>
<b-card-header
class="py-2 pl-2 pr-0"
>
<div
class="d-flex align-items-center"
>
<label
class="text-primary mb-0 py-1"
>
{{ labels.selectedItems }}
</label>
<b-button
v-show="filteredSelected.length && !disabled"
data-test-id="link-unselect-all"
variant="outline-light"
class="ml-auto border-0 text-muted"
@click="unselectAll()"
>
{{ labels.unselectAllItems }}
</b-button>
</div>
</b-card-header>
<b-card-body
class="overflow-auto py-0 pl-2 pr-0"
>
<b-list-group
vertical
class="h-100"
>
<draggable
v-model="filteredSelected"
:sort="!_disabledSorting"
:move="_disableDragging"
:disabled="query"
draggable=".item"
group="items"
class="overflow-auto h-100"
>
<b-list-group-item
v-for="item in filteredSelected"
:key="item.value"
class="item mb-3 border rounded"
:class="{
'handle': !isDraggable
}"
@dblclick="unselect(item)"
>
<c-item-picker-item
:item="item"
:disabled="disabled"
:disabled-dragging="isDraggable"
:disabled-sorting="disabledSorting"
:hide-icons="hideIcons"
selected
@unselect="unselect(item)"
>
<template
v-for="(_, slot) of $scopedSlots"
#[slot]="scope"
>
<slot
:name="slot"
:text-field="textField"
:disabled="disabled"
:disabled-dragging="isDraggable"
:disabled-sorting="disabledSorting"
:hide-icons="hideIcons"
v-bind="scope"
/>
</template>
</c-item-picker-item>
</b-list-group-item>
<template #footer>
<h6
v-if="!filteredSelected.length && query"
class="text-center my-4"
>
{{ labels.noItemsFound }}
</h6>
</template>
</draggable>
</b-list-group>
</b-card-body>
</b-card>
</b-card-body>
</b-card>
</div>
</template>
<script>
import draggable from 'vuedraggable'
import CItemPickerItem from './CItemPickerItem.vue'
import CInputSearch from '../input/CInputSearch.vue'
import { throttle } from 'lodash'
export default {
name: 'CItemPicker',
components: {
draggable,
CItemPickerItem,
CInputSearch,
},
props: {
/**
* List of all items, available and selected/picked
*
* Internally, we'll build 2 arrays for each group (available + selected)
* and work with them.
* On the outside, we'll always deal input array of (full) items and
* array of selected/picked values of those items.
*
* This component mimics the behaviour of <b-form-select> component from
* Vue Bootstrap and could serve as a drop-in replacement!
*/
options: {
type: Array,
required: true,
},
/**
* List of values that can be found in the items
*/
value: {
type: Array,
required: true,
},
valueField: {
type: String,
default: 'value',
},
textField: {
type: String,
default: 'text',
},
labels: {
type: Object,
default: () => ({
searchPlaceholder: 'Filter items',
availableItems: 'Available',
selectAllItems: 'Select all',
selectedItems: 'Selected',
unselectAllItems: 'Unselect all',
noItemsFound: 'No items found',
}),
},
disabled: {
type: Boolean,
},
disabledFilter: {
type: Boolean,
},
disabledSorting: {
type: Boolean,
},
disabledDragging: {
type: Boolean,
},
hideIcons: {
type: Boolean,
},
hideFilter: {
type: Boolean,
},
},
data () {
return {
query: '',
available: [],
selected: [],
}
},
computed: {
_disabledFilter () {
return this.disabled || this.disabledFilter
},
_disabledSorting () {
return this.disabled || this.disabledSorting
},
/**
* Provides list of all available items, filtered by query
* and a setter for draggable component to update available items on drag
*/
filteredAvailable: {
get () {
const q = this.query.toLowerCase()
return this.available.filter(i => i.text.toLowerCase().indexOf(q) > -1)
},
set (items) {
this.available = items
},
},
/**
* Provides list of all selected items, filtered by query
* and a setter for draggable component to update selected items on drag
*/
filteredSelected: {
get () {
const q = this.query.toLowerCase()
return this.selected.filter(i => i.text.toLowerCase().indexOf(q) > -1)
},
set (items) {
this.selected = items
},
},
isDraggable () {
return this.disabledDragging || this.query.length > 0
},
},
watch: {
/**
* Update parent component (if needed)
* @param v
*/
selected: {
immediate: false,
handler (items) {
const value = items.map(i => i[this.valueField])
// satisfy value.sync
this.$emit('update:value', value)
// satisfy v-model
this.$emit('input', value)
},
},
options: {
deep: true,
immediate: true,
handler () {
this.sync()
},
},
value: {
immediate: false,
handler (value = [], oldValue = []) {
/**
* Make sure we do not fall into an infinite loop
*
* If we update the value then sync will trigger recomputation of selected
* Which then emits the update event and the loop will begin
*/
if (value.length === oldValue.length) {
if (value.filter(v => !oldValue.includes(v)).length === 0) {
return
}
}
this.sync()
},
},
},
created () {
this.options.forEach(o => {
if (typeof o !== 'object') {
throw new Error('expecting array of objects for options prop')
}
})
},
beforeUnmount () {
this.setDefaultValues()
},
methods: {
_disableDragging (e) {
if (this.disabledDragging && e.to !== e.from) {
return false
}
},
selectAll () {
this.selected.push(...this.filteredAvailable)
this.available = this.frozen().filter(i => !this.selected.includes(i))
},
select: throttle(function (item) {
// remove available
this.available = this.available.filter(i => i !== item)
// put the selected item in the place where it was (if sorting is disabled)
// or at the end
if (this.disabledSorting) {
this.selected = this.frozen().filter(i => !this.available.includes(i))
} else {
if (!this.selected.some(({ value = '' }) => value === item.value)) {
this.selected.push(item)
}
}
}, 300),
unselectAll () {
this.filteredSelected.forEach(this.unselect)
},
unselect (item) {
// filtering out the unselected item
this.selected = this.selected.filter(i => i !== item)
// sync available from the list of frozen items without selected
this.available = this.frozen().filter(i => !this.selected.includes(i))
},
/**
* Returns true given item is selected or not
*
* Item is considered selected if value of item's value-field is inside the selected array.
*
* @param item object
* @returns boolean
*/
isPicked (item) {
return item[this.valueField] && this.value.includes(item[this.valueField])
},
sync () {
/**
* filter all unpicked options, freeze each item in the array and
* build list of available items
*/
this.available = this.frozen().filter(opt => !this.isPicked(opt))
/**
* filter all unpicked options, freeze each item in the array and
* build list of selected items, traverse value to keep order
*/
this.selected = this.value.map(v => {
return this.frozen().find(item => item[this.valueField] === v)
}).filter(f => f)
},
frozen () {
return this.options.map(Object.freeze)
},
setDefaultValues () {
this.query = ''
this.available = []
this.selected = []
},
},
}
</script>
<style lang="scss" scoped>
.handle {
cursor: grab;
}
.handle:active {
cursor: grabbing;
}
</style>