Replace vue-multiselect with a custom component (#366)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Co-authored-by: kolaente <k@knt.li> Reviewed-on: #366 Co-authored-by: konrad <konrad@kola-entertainments.de> Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
parent
6178fe034b
commit
fe6d975134
17
cypress/factories/label_task.js
Normal file
17
cypress/factories/label_task.js
Normal file
@ -0,0 +1,17 @@
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
export class LabelTaskFactory extends Factory {
|
||||
static table = 'label_task'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
task_id: 1,
|
||||
label_id: 1,
|
||||
created: formatISO(now),
|
||||
}
|
||||
}
|
||||
}
|
22
cypress/factories/labels.js
Normal file
22
cypress/factories/labels.js
Normal file
@ -0,0 +1,22 @@
|
||||
import faker from 'faker'
|
||||
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
export class LabelFactory extends Factory {
|
||||
static table = 'labels'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
title: faker.lorem.words(2),
|
||||
description: faker.lorem.text(10),
|
||||
hex_color: (Math.random()*0xFFFFFF<<0).toString(16), // random 6-digit hex number
|
||||
created_by_id: 1,
|
||||
created: formatISO(now),
|
||||
updated: formatISO(now),
|
||||
}
|
||||
}
|
||||
}
|
17
cypress/factories/task_assignee.js
Normal file
17
cypress/factories/task_assignee.js
Normal file
@ -0,0 +1,17 @@
|
||||
import {Factory} from '../support/factory'
|
||||
import {formatISO} from 'date-fns'
|
||||
|
||||
export class TaskAssigneeFactory extends Factory {
|
||||
static table = 'task_assignees'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
task_id: 1,
|
||||
user_id: 1,
|
||||
created: formatISO(now),
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@ export class TaskCommentFactory extends Factory {
|
||||
static table = 'task_comments'
|
||||
|
||||
static factory() {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: '{increment}',
|
||||
comment: faker.lorem.text(3),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {TeamFactory} from '../../factories/team'
|
||||
import {TeamMemberFactory} from '../../factories/team_member'
|
||||
import {UserFactory} from '../../factories/user'
|
||||
import '../../support/authenticateUser'
|
||||
|
||||
describe('Team', () => {
|
||||
@ -88,4 +89,41 @@ describe('Team', () => {
|
||||
.contains('Member')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Allows an admin to add members to the team', () => {
|
||||
TeamMemberFactory.create(1, {
|
||||
team_id: 1,
|
||||
admin: true,
|
||||
})
|
||||
TeamFactory.create(1, {
|
||||
id: 1,
|
||||
})
|
||||
const users = UserFactory.create(5)
|
||||
|
||||
cy.visit('/teams/1/edit')
|
||||
cy.get('.card')
|
||||
.contains('Team Members')
|
||||
.get('.card-content .multiselect .input-wrapper input')
|
||||
.type(users[1].username)
|
||||
cy.get('.card')
|
||||
.contains('Team Members')
|
||||
.get('.card-content .multiselect .search-results')
|
||||
.children()
|
||||
.first()
|
||||
.click()
|
||||
cy.get('.card')
|
||||
.contains('Team Members')
|
||||
.get('.card-content button.button')
|
||||
.contains('Add To Team')
|
||||
.click()
|
||||
|
||||
cy.get('table.table td')
|
||||
.contains('Admin')
|
||||
.should('exist')
|
||||
cy.get('table.table tr')
|
||||
.should('contain', users[1].username)
|
||||
.should('contain', 'Member')
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
})
|
||||
})
|
||||
|
@ -8,6 +8,9 @@ import {NamespaceFactory} from '../../factories/namespace'
|
||||
import {UserListFactory} from '../../factories/users_list'
|
||||
|
||||
import '../../support/authenticateUser'
|
||||
import {TaskAssigneeFactory} from '../../factories/task_assignee'
|
||||
import {LabelFactory} from '../../factories/labels'
|
||||
import {LabelTaskFactory} from '../../factories/label_task'
|
||||
|
||||
describe('Task', () => {
|
||||
let namespaces
|
||||
@ -202,8 +205,14 @@ describe('Task', () => {
|
||||
cy.get('.task-view .action-buttons .button')
|
||||
.contains('Move task')
|
||||
.click()
|
||||
cy.get('.task-view .content.details .field .multiselect.control .multiselect__tags .multiselect__input')
|
||||
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper .input-loader-wrapper input')
|
||||
.type(`${lists[1].title}{enter}`)
|
||||
// The requests happen with a 200ms timeout. Because of that, the results are not yet there when cypress
|
||||
// presses enter and we can't simulate pressing on enter to select the item.
|
||||
cy.get('.task-view .content.details .field .multiselect.control .search-results')
|
||||
.children()
|
||||
.first()
|
||||
.click()
|
||||
|
||||
cy.get('.task-view h6.subtitle')
|
||||
.should('contain', namespaces[0].title)
|
||||
@ -233,5 +242,141 @@ describe('Task', () => {
|
||||
cy.url()
|
||||
.should('contain', `/lists/${tasks[0].list_id}/`)
|
||||
})
|
||||
|
||||
it('Can add an assignee to a task', () => {
|
||||
const users = UserFactory.create(5)
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: 1,
|
||||
})
|
||||
UserListFactory.create(5, {
|
||||
list_id: 1,
|
||||
user_id: '{increment}',
|
||||
})
|
||||
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .action-buttons .button')
|
||||
.contains('Assign this task to a user')
|
||||
.click()
|
||||
cy.get('.task-view .column.assignees .multiselect input')
|
||||
.type(users[1].username)
|
||||
cy.get('.task-view .column.assignees .multiselect .search-results')
|
||||
.children()
|
||||
.first()
|
||||
.click()
|
||||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.task-view .column.assignees .multiselect .input-wrapper span.assignee')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Can remove an assignee from a task', () => {
|
||||
const users = UserFactory.create(2)
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: 1,
|
||||
})
|
||||
UserListFactory.create(5, {
|
||||
list_id: 1,
|
||||
user_id: '{increment}',
|
||||
})
|
||||
TaskAssigneeFactory.create(1, {
|
||||
task_id: tasks[0].id,
|
||||
user_id: users[1].id,
|
||||
})
|
||||
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .column.assignees .multiselect .input-wrapper span.assignee')
|
||||
.get('a.remove-assignee')
|
||||
.click()
|
||||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.task-view .column.assignees .multiselect .input-wrapper span.assignee')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('Can add a new label to a task', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: 1,
|
||||
})
|
||||
LabelFactory.truncate()
|
||||
const newLabelText = 'some new label'
|
||||
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .action-buttons .button')
|
||||
.contains('Add labels')
|
||||
.click()
|
||||
cy.get('.task-view .details.labels-list .multiselect input')
|
||||
.type(newLabelText)
|
||||
cy.get('.task-view .details.labels-list .multiselect .search-results')
|
||||
.children()
|
||||
.first()
|
||||
.click()
|
||||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.task-view .details.labels-list .multiselect .input-wrapper span.tag')
|
||||
.should('exist')
|
||||
.should('contain', newLabelText)
|
||||
})
|
||||
|
||||
it('Can add an existing label to a task', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: 1,
|
||||
})
|
||||
const labels = LabelFactory.create(1)
|
||||
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .action-buttons .button')
|
||||
.contains('Add labels')
|
||||
.click()
|
||||
cy.get('.task-view .details.labels-list .multiselect input')
|
||||
.type(labels[0].title)
|
||||
cy.get('.task-view .details.labels-list .multiselect .search-results')
|
||||
.children()
|
||||
.first()
|
||||
.click()
|
||||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.task-view .details.labels-list .multiselect .input-wrapper span.tag')
|
||||
.should('exist')
|
||||
.should('contain', labels[0].title)
|
||||
})
|
||||
|
||||
it('Can remove a label from a task', () => {
|
||||
const tasks = TaskFactory.create(1, {
|
||||
id: 1,
|
||||
list_id: 1,
|
||||
})
|
||||
const labels = LabelFactory.create(1)
|
||||
LabelTaskFactory.create(1, {
|
||||
task_id: tasks[0].id,
|
||||
label_id: labels[0].id,
|
||||
})
|
||||
|
||||
cy.visit(`/tasks/${tasks[0].id}`)
|
||||
|
||||
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
|
||||
.should('contain', labels[0].title)
|
||||
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
|
||||
.children()
|
||||
.first()
|
||||
.get('a.delete')
|
||||
.click()
|
||||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
|
||||
.should('not.contain', labels[0].title)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -6,6 +6,7 @@
|
||||
"serve": "vue-cli-service serve",
|
||||
"serve:dist": "node scripts/serve-dist.js",
|
||||
"build": "vue-cli-service build --modern",
|
||||
"build:report": "vue-cli-service build --report",
|
||||
"lint": "vue-cli-service lint --ignore-pattern '*.test.*'",
|
||||
"cypress:open": "cypress open",
|
||||
"test:unit": "jest",
|
||||
@ -51,7 +52,6 @@
|
||||
"node-sass": "5.0.0",
|
||||
"sass-loader": "10.1.0",
|
||||
"vue-flatpickr-component": "8.1.6",
|
||||
"vue-multiselect": "2.1.6",
|
||||
"vue-notification": "1.3.20",
|
||||
"vue-router": "3.4.9",
|
||||
"vue-template-compiler": "2.6.12",
|
||||
@ -83,10 +83,12 @@
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
"not ie < 11"
|
||||
],
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"jest": {
|
||||
"testPathIgnorePatterns": ["cypress"]
|
||||
"testPathIgnorePatterns": [
|
||||
"cypress"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -115,6 +115,7 @@ import 'flatpickr/dist/flatpickr.css'
|
||||
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
||||
import {format} from 'date-fns'
|
||||
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
|
||||
export default {
|
||||
name: 'datepicker',
|
||||
@ -188,25 +189,7 @@ export default {
|
||||
},
|
||||
hideDatePopup(e) {
|
||||
if (this.show) {
|
||||
|
||||
// We walk up the tree to see if any parent of the clicked element is the datepicker element.
|
||||
// If it is not, we hide the popup. We're doing all this hassle to prevent the popup from closing when
|
||||
// clicking an element of flatpickr.
|
||||
let parent = e.target.parentElement
|
||||
while (parent !== this.$refs.datepickerPopup) {
|
||||
if (parent.parentElement === null) {
|
||||
parent = null
|
||||
break
|
||||
}
|
||||
|
||||
parent = parent.parentElement
|
||||
}
|
||||
|
||||
if (parent === this.$refs.datepickerPopup) {
|
||||
return
|
||||
}
|
||||
|
||||
this.close()
|
||||
closeWhenClickedOutside(e, this.$refs.datepickerPopup, this.close)
|
||||
}
|
||||
},
|
||||
close() {
|
||||
|
325
src/components/input/multiselect.vue
Normal file
325
src/components/input/multiselect.vue
Normal file
@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<div
|
||||
class="multiselect"
|
||||
:class="{'has-search-results': searchResultsVisible}"
|
||||
ref="multiselectRoot"
|
||||
>
|
||||
<div class="input-wrapper input">
|
||||
<template v-if="Array.isArray(internalValue)">
|
||||
<template v-for="(item, key) in internalValue">
|
||||
<slot name="tag" :item="item">
|
||||
<span :key="`item${key}`" class="tag ml-2 mt-2">
|
||||
{{ label !== '' ? item[label] : item }}
|
||||
<a @click="() => remove(item)" class="delete is-small"></a>
|
||||
</span>
|
||||
</slot>
|
||||
</template>
|
||||
</template>
|
||||
<div class="input-loader-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
v-model="query"
|
||||
@keyup="search"
|
||||
@keyup.enter.exact.prevent="() => createOrSelectOnEnter()"
|
||||
:placeholder="placeholder"
|
||||
@keydown.down.exact.prevent="() => preSelect(0, true)"
|
||||
ref="searchInput"
|
||||
@focus="() => showSearchResults = true"
|
||||
/>
|
||||
<span class="loader is-loading" v-if="loading || localLoading"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div class="search-results" v-if="searchResultsVisible">
|
||||
<button
|
||||
v-if="creatableAvailable"
|
||||
class="button is-ghost is-fullwidth"
|
||||
ref="result--1"
|
||||
@keydown.up.prevent="() => preSelect(-2)"
|
||||
@keydown.down.prevent="() => preSelect(0)"
|
||||
@keyup.enter.prevent="create"
|
||||
@click="create"
|
||||
>
|
||||
<span>
|
||||
<slot name="searchResult" :option="query">
|
||||
{{ query }}
|
||||
</slot>
|
||||
</span>
|
||||
<span class="hint-text">
|
||||
{{ createPlaceholder }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="button is-ghost is-fullwidth"
|
||||
v-for="(data, key) in filteredSearchResults"
|
||||
:key="key"
|
||||
:ref="`result-${key}`"
|
||||
@keydown.up.prevent="() => preSelect(key - 1)"
|
||||
@keydown.down.prevent="() => preSelect(key + 1)"
|
||||
@keyup.enter.prevent="() => select(data)"
|
||||
@click="() => select(data)"
|
||||
>
|
||||
<span>
|
||||
<slot name="searchResult" :option="data">
|
||||
{{ label !== '' ? data[label] : data }}
|
||||
</slot>
|
||||
</span>
|
||||
<span class="hint-text">
|
||||
{{ selectPlaceholder }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
|
||||
/**
|
||||
* Available events:
|
||||
* @search: Triggered every time the search query input changes
|
||||
* @select: Triggered every time an option from the search results is selected. Also triggers a change in v-model.
|
||||
* @create: If nothing or no exact match was found and `creatable` is true, this event is triggered with the current value of the search query.
|
||||
* @remove: If `multiple` is enabled, this will be fired every time an item is removed from the array of selected items.
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'multiselect',
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
searchTimeout: null,
|
||||
localLoading: false,
|
||||
showSearchResults: false,
|
||||
internalValue: null,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
// When true, shows a loading spinner
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
}
|
||||
},
|
||||
// The placeholder of the search input
|
||||
placeholder: {
|
||||
type: String,
|
||||
default() {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
// The search results where the @search listener needs to put the results into
|
||||
searchResults: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
// The name of the property of the searched object to show the user.
|
||||
// If empty the component will show all raw data of an entry.
|
||||
label: {
|
||||
type: String,
|
||||
default() {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
// The object with the value, updated every time an entry is selected.
|
||||
value: {
|
||||
default() {
|
||||
return null
|
||||
}
|
||||
},
|
||||
// If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it.
|
||||
creatable: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
// The text shown next to the new value option.
|
||||
createPlaceholder: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'Create new'
|
||||
},
|
||||
},
|
||||
// The text shown next to an option.
|
||||
selectPlaceholder: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'Click or press enter to select'
|
||||
},
|
||||
},
|
||||
// If true, allows for selecting multiple items. v-model will be an array with all selected values in that case.
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('click', this.hideSearchResultsHandler)
|
||||
this.setSelectedObject(this.value)
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.hideSearchResultsHandler)
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.setSelectedObject(newVal)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
searchResultsVisible() {
|
||||
return this.showSearchResults && (
|
||||
(this.filteredSearchResults.length > 0) ||
|
||||
(this.creatable && this.query !== '')
|
||||
)
|
||||
},
|
||||
creatableAvailable() {
|
||||
return this.creatable && this.query !== '' && !this.filteredSearchResults.some(elem => {
|
||||
// Don't make create available if we have an exact match in our search results.
|
||||
if (this.label !== '') {
|
||||
return elem[this.label] === this.query
|
||||
}
|
||||
|
||||
return elem === this.query
|
||||
})
|
||||
},
|
||||
filteredSearchResults() {
|
||||
if (this.multiple && this.internalValue !== null) {
|
||||
return this.searchResults.filter(item => !this.internalValue.some(e => e === item))
|
||||
}
|
||||
|
||||
return this.searchResults
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// Searching will be triggered with a 200ms delay to avoid searching on every keyup event.
|
||||
search() {
|
||||
if (this.searchTimeout !== null) {
|
||||
clearTimeout(this.searchTimeout)
|
||||
this.searchTimeout = null
|
||||
}
|
||||
|
||||
this.localLoading = true
|
||||
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.$emit('search', this.query)
|
||||
setTimeout(() => {
|
||||
this.localLoading = false
|
||||
}, 100) // The duration of the loading timeout of the services
|
||||
this.showSearchResults = true
|
||||
}, 200)
|
||||
},
|
||||
hideSearchResultsHandler(e) {
|
||||
closeWhenClickedOutside(e, this.$refs.multiselectRoot, this.closeSearchResults)
|
||||
},
|
||||
closeSearchResults() {
|
||||
this.showSearchResults = false
|
||||
},
|
||||
select(object) {
|
||||
if (this.multiple) {
|
||||
if (this.internalValue === null) {
|
||||
this.internalValue = []
|
||||
}
|
||||
|
||||
this.internalValue.push(object)
|
||||
} else {
|
||||
this.internalValue = object
|
||||
}
|
||||
|
||||
this.$emit('input', this.internalValue)
|
||||
this.$emit('select', object)
|
||||
this.setSelectedObject(object)
|
||||
this.closeSearchResults()
|
||||
},
|
||||
setSelectedObject(object, resetOnly = false) {
|
||||
this.$set(this, 'internalValue', object)
|
||||
|
||||
// We assume we're getting an array when multiple is enabled and can therefore leave the query
|
||||
// value etc as it is
|
||||
if (this.multiple) {
|
||||
this.query = ''
|
||||
return
|
||||
}
|
||||
|
||||
if (object === null) {
|
||||
this.query = ''
|
||||
return
|
||||
}
|
||||
|
||||
if (resetOnly) {
|
||||
return
|
||||
}
|
||||
|
||||
this.query = this.label !== '' ? object[this.label] : object
|
||||
},
|
||||
preSelect(index, lookForCreatable = false) {
|
||||
|
||||
if (index === 0 && this.creatable && lookForCreatable) {
|
||||
index = -1
|
||||
}
|
||||
|
||||
if (index < -1) {
|
||||
this.$refs.searchInput.focus()
|
||||
return
|
||||
}
|
||||
|
||||
const elems = this.$refs[`result-${index}`]
|
||||
if (typeof elems === 'undefined' || elems.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(elems)) {
|
||||
elems[0].focus()
|
||||
return
|
||||
}
|
||||
|
||||
elems.focus()
|
||||
},
|
||||
create() {
|
||||
if (this.query === '') {
|
||||
return
|
||||
}
|
||||
|
||||
this.$emit('create', this.query)
|
||||
this.setSelectedObject(this.query, true)
|
||||
this.closeSearchResults()
|
||||
},
|
||||
createOrSelectOnEnter() {
|
||||
|
||||
console.log('enter', this.creatableAvailable, this.searchResults.length)
|
||||
|
||||
if (!this.creatableAvailable && this.searchResults.length === 1) {
|
||||
this.select(this.searchResults[0])
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.creatableAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
this.create()
|
||||
},
|
||||
remove(item) {
|
||||
for (const ind in this.internalValue) {
|
||||
if (this.internalValue[ind] === item) {
|
||||
this.internalValue.splice(ind, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('input', this.internalValue)
|
||||
this.$emit('remove', item)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="card filters">
|
||||
<div class="card filters has-overflow">
|
||||
<div class="card-content">
|
||||
<fancycheckbox v-model="params.filter_include_nulls">
|
||||
Include Tasks which don't have a value set
|
||||
@ -103,32 +103,16 @@
|
||||
<label class="label">Assignees</label>
|
||||
<div class="control">
|
||||
<multiselect
|
||||
:clear-on-select="true"
|
||||
:close-on-select="true"
|
||||
:hide-selected="true"
|
||||
:internal-search="true"
|
||||
:loading="usersService.loading"
|
||||
:multiple="true"
|
||||
:options="foundusers"
|
||||
:options-limit="300"
|
||||
:searchable="true"
|
||||
:showNoOptions="false"
|
||||
:taggable="false"
|
||||
@search-change="query => find('users', query)"
|
||||
@select="() => add('users', 'assignees')"
|
||||
@remove="() => remove('users', 'assignees')"
|
||||
label="username"
|
||||
placeholder="Type to search for a user..."
|
||||
track-by="id"
|
||||
@search="query => find('users', query)"
|
||||
:search-results="foundusers"
|
||||
@select="() => add('users', 'assignees')"
|
||||
label="username"
|
||||
:multiple="true"
|
||||
@remove="() => remove('users', 'assignees')"
|
||||
v-model="users"
|
||||
>
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div
|
||||
@mousedown.prevent.stop="clear('users', props.search)"
|
||||
class="multiselect__clear"
|
||||
v-if="users.length"></div>
|
||||
</template>
|
||||
</multiselect>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -136,39 +120,23 @@
|
||||
<label class="label">Labels</label>
|
||||
<div class="control">
|
||||
<multiselect
|
||||
:clear-on-select="true"
|
||||
:close-on-select="false"
|
||||
:hide-selected="true"
|
||||
:internal-search="true"
|
||||
:loading="labelService.loading"
|
||||
:multiple="true"
|
||||
:options="foundLabels"
|
||||
:options-limit="300"
|
||||
:searchable="true"
|
||||
:showNoOptions="false"
|
||||
@search-change="findLabels"
|
||||
placeholder="Type to search for a label..."
|
||||
@search="findLabels"
|
||||
:search-results="foundLabels"
|
||||
@select="label => addLabel(label)"
|
||||
label="title"
|
||||
placeholder="Type to search for a label..."
|
||||
track-by="id"
|
||||
:multiple="true"
|
||||
v-model="labels"
|
||||
>
|
||||
<template
|
||||
slot="tag"
|
||||
slot-scope="{ option }">
|
||||
<template v-slot:tag="props">
|
||||
<span
|
||||
:style="{'background': option.hexColor, 'color': option.textColor}"
|
||||
class="tag mr-2 mb-2">
|
||||
<span>{{ option.title }}</span>
|
||||
<a @click="removeLabel(option)" class="delete is-small"></a>
|
||||
:style="{'background': props.item.hexColor, 'color': props.item.textColor}"
|
||||
class="tag ml-2 mt-2">
|
||||
<span>{{ props.item.title }}</span>
|
||||
<a @click="removeLabel(props.item)" class="delete is-small"></a>
|
||||
</span>
|
||||
</template>
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div
|
||||
@mousedown.prevent.stop="clearLabels(props.search)"
|
||||
class="multiselect__clear"
|
||||
v-if="labels.length"></div>
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
</div>
|
||||
@ -178,64 +146,32 @@
|
||||
<label class="label">Lists</label>
|
||||
<div class="control">
|
||||
<multiselect
|
||||
:clear-on-select="true"
|
||||
:close-on-select="true"
|
||||
:hide-selected="true"
|
||||
:internal-search="true"
|
||||
:loading="listsService.loading"
|
||||
:multiple="true"
|
||||
:options="foundlists"
|
||||
:options-limit="300"
|
||||
:searchable="true"
|
||||
:showNoOptions="false"
|
||||
:taggable="false"
|
||||
@search-change="query => find('lists', query)"
|
||||
@select="() => add('lists', 'list_id')"
|
||||
@remove="() => remove('lists', 'list_id')"
|
||||
label="title"
|
||||
placeholder="Type to search for a list..."
|
||||
track-by="id"
|
||||
@search="query => find('lists', query)"
|
||||
:search-results="foundlists"
|
||||
@select="() => add('lists', 'list_id')"
|
||||
label="title"
|
||||
@remove="() => remove('lists', 'list_id')"
|
||||
:multiple="true"
|
||||
v-model="lists"
|
||||
>
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div
|
||||
@mousedown.prevent.stop="clear('lists', props.search)"
|
||||
class="multiselect__clear"
|
||||
v-if="lists.length"></div>
|
||||
</template>
|
||||
</multiselect>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Namespaces</label>
|
||||
<div class="control">
|
||||
<multiselect
|
||||
:clear-on-select="true"
|
||||
:close-on-select="true"
|
||||
:hide-selected="true"
|
||||
:internal-search="true"
|
||||
:loading="namespaceService.loading"
|
||||
:multiple="true"
|
||||
:options="foundnamespace"
|
||||
:options-limit="300"
|
||||
:searchable="true"
|
||||
:showNoOptions="false"
|
||||
:taggable="false"
|
||||
@search-change="query => find('namespace', query)"
|
||||
@select="() => add('namespace', 'namespace')"
|
||||
@remove="() => remove('namespace', 'namespace')"
|
||||
label="title"
|
||||
placeholder="Type to search for a namespace..."
|
||||
track-by="id"
|
||||
@search="query => find('namespace', query)"
|
||||
:search-results="foundnamespace"
|
||||
@select="() => add('namespace', 'namespace')"
|
||||
label="title"
|
||||
@remove="() => remove('namespace', 'namespace')"
|
||||
:multiple="true"
|
||||
v-model="namespace"
|
||||
>
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div
|
||||
@mousedown.prevent.stop="clear('namespace', props.search)"
|
||||
class="multiselect__clear"
|
||||
v-if="namespace.length"></div>
|
||||
</template>
|
||||
</multiselect>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -247,13 +183,13 @@
|
||||
import Fancycheckbox from '../../input/fancycheckbox'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
import Multiselect from 'vue-multiselect'
|
||||
|
||||
import {formatISO} from 'date-fns'
|
||||
import differenceWith from 'lodash/differenceWith'
|
||||
|
||||
import PrioritySelect from '@/components/tasks/partials/prioritySelect'
|
||||
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect'
|
||||
import Multiselect from '@/components/input/multiselect'
|
||||
|
||||
import UserService from '@/services/user'
|
||||
import LabelService from '@/services/label'
|
||||
|
@ -1,31 +1,20 @@
|
||||
<template>
|
||||
<multiselect
|
||||
:internal-search="true"
|
||||
:loading="namespaceService.loading"
|
||||
:multiple="false"
|
||||
:options="namespaces"
|
||||
:searchable="true"
|
||||
:showNoOptions="false"
|
||||
@search-change="findNamespaces"
|
||||
placeholder="Search for a namespace..."
|
||||
@search="findNamespaces"
|
||||
:search-results="namespaces"
|
||||
@select="select"
|
||||
label="title"
|
||||
placeholder="Search for a namespace..."
|
||||
track-by="id"
|
||||
v-model="namespace">
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div
|
||||
@mousedown.prevent.stop="clearAll(props.search)" class="multiselect__clear"
|
||||
v-if="namespace.id !== 0"></div>
|
||||
</template>
|
||||
<span slot="noResult">No namespace found. Consider changing the search query.</span>
|
||||
</multiselect>
|
||||
v-model="namespace"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NamespaceService from '../../services/namespace'
|
||||
import NamespaceModel from '../../models/namespace'
|
||||
import LoadingComponent from '../misc/loading'
|
||||
import ErrorComponent from '../misc/error'
|
||||
|
||||
import Multiselect from '@/components/input/multiselect'
|
||||
|
||||
export default {
|
||||
name: 'namespace-search',
|
||||
@ -37,12 +26,7 @@ export default {
|
||||
}
|
||||
},
|
||||
components: {
|
||||
multiselect: () => ({
|
||||
component: import(/* webpackChunkName: "multiselect" */ 'vue-multiselect'),
|
||||
loading: LoadingComponent,
|
||||
error: ErrorComponent,
|
||||
timeout: 60000,
|
||||
}),
|
||||
Multiselect,
|
||||
},
|
||||
created() {
|
||||
this.namespaceService = new NamespaceService()
|
||||
|
@ -1,106 +1,87 @@
|
||||
<template>
|
||||
<div class="card is-fullwidth">
|
||||
<div class="card is-fullwidth has-overflow">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
Shared with these {{ shareType }}s
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content content sharables-list">
|
||||
<form @submit.prevent="add()" class="add-form" v-if="userIsAdmin">
|
||||
<div class="field is-grouped">
|
||||
<p class="control is-expanded" v-bind:class="{ 'is-loading': searchService.loading}">
|
||||
<multiselect
|
||||
:internal-search="true"
|
||||
:label="searchLabel"
|
||||
:loading="searchService.loading"
|
||||
:multiple="false"
|
||||
:options="found"
|
||||
:searchable="true"
|
||||
:showNoOptions="false"
|
||||
@search-change="find"
|
||||
placeholder="Type to search..."
|
||||
track-by="id"
|
||||
v-model="sharable">
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div
|
||||
@mousedown.prevent.stop="clearAll(props.search)"
|
||||
class="multiselect__clear"
|
||||
v-if="sharable.id !== 0"></div>
|
||||
</template>
|
||||
<span slot="noResult">
|
||||
Oops! No {{ shareType }} found. Consider changing the search query.
|
||||
</span>
|
||||
</multiselect>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-success" type="submit">
|
||||
<span class="icon is-small">
|
||||
<icon icon="plus"/>
|
||||
</span>
|
||||
Add
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
<table class="table is-striped is-hoverable is-fullwidth">
|
||||
<tbody>
|
||||
<tr :key="s.id" v-for="s in sharables">
|
||||
<template v-if="shareType === 'user'">
|
||||
<td>{{ s.getDisplayName() }}</td>
|
||||
<td>
|
||||
<template v-if="s.id === userInfo.id">
|
||||
<b class="is-success">You</b>
|
||||
</template>
|
||||
</td>
|
||||
</template>
|
||||
<template v-if="shareType === 'team'">
|
||||
<td>
|
||||
<router-link :to="{name: 'teams.edit', params: {id: s.id}}">
|
||||
{{ s.name }}
|
||||
</router-link>
|
||||
</td>
|
||||
</template>
|
||||
<td class="type">
|
||||
<template v-if="s.right === rights.ADMIN">
|
||||
<div class="card-content" v-if="userIsAdmin">
|
||||
<div class="field has-addons">
|
||||
<p class="control is-expanded" v-bind:class="{ 'is-loading': searchService.loading}">
|
||||
<multiselect
|
||||
:loading="searchService.loading"
|
||||
placeholder="Type to search..."
|
||||
@search="find"
|
||||
:search-results="found"
|
||||
:label="searchLabel"
|
||||
v-model="sharable"
|
||||
/>
|
||||
</p>
|
||||
<p class="control">
|
||||
<button class="button is-primary" @click="add()">
|
||||
Share
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table is-striped is-hoverable is-fullwidth">
|
||||
<tbody>
|
||||
<tr :key="s.id" v-for="s in sharables">
|
||||
<template v-if="shareType === 'user'">
|
||||
<td>{{ s.getDisplayName() }}</td>
|
||||
<td>
|
||||
<template v-if="s.id === userInfo.id">
|
||||
<b class="is-success">You</b>
|
||||
</template>
|
||||
</td>
|
||||
</template>
|
||||
<template v-if="shareType === 'team'">
|
||||
<td>
|
||||
<router-link :to="{name: 'teams.edit', params: {id: s.id}}">
|
||||
{{ s.name }}
|
||||
</router-link>
|
||||
</td>
|
||||
</template>
|
||||
<td class="type">
|
||||
<template v-if="s.right === rights.ADMIN">
|
||||
<span class="icon is-small">
|
||||
<icon icon="lock"/>
|
||||
</span>
|
||||
Admin
|
||||
</template>
|
||||
<template v-else-if="s.right === rights.READ_WRITE">
|
||||
Admin
|
||||
</template>
|
||||
<template v-else-if="s.right === rights.READ_WRITE">
|
||||
<span class="icon is-small">
|
||||
<icon icon="pen"/>
|
||||
</span>
|
||||
Write
|
||||
</template>
|
||||
<template v-else>
|
||||
Write
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="icon is-small">
|
||||
<icon icon="users"/>
|
||||
</span>
|
||||
Read-only
|
||||
</template>
|
||||
</td>
|
||||
<td class="actions" v-if="userIsAdmin">
|
||||
<div class="select">
|
||||
<select @change="toggleType(s)" class="button buttonright" v-model="selectedRight[s.id]">
|
||||
<option :selected="s.right === rights.READ" :value="rights.READ">Read only</option>
|
||||
<option :selected="s.right === rights.READ_WRITE" :value="rights.READ_WRITE">Read &
|
||||
write
|
||||
</option>
|
||||
<option :selected="s.right === rights.ADMIN" :value="rights.ADMIN">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<button @click="() => {sharable = s; showDeleteModal = true}"
|
||||
class="button is-danger icon-only">
|
||||
Read-only
|
||||
</template>
|
||||
</td>
|
||||
<td class="actions" v-if="userIsAdmin">
|
||||
<div class="select">
|
||||
<select @change="toggleType(s)" class="button buttonright" v-model="selectedRight[s.id]">
|
||||
<option :selected="s.right === rights.READ" :value="rights.READ">Read only</option>
|
||||
<option :selected="s.right === rights.READ_WRITE" :value="rights.READ_WRITE">Read &
|
||||
write
|
||||
</option>
|
||||
<option :selected="s.right === rights.ADMIN" :value="rights.ADMIN">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<button @click="() => {sharable = s; showDeleteModal = true}"
|
||||
class="button is-danger icon-only">
|
||||
<span class="icon is-small">
|
||||
<icon icon="trash-alt"/>
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<modal
|
||||
@close="showDeleteModal = false"
|
||||
@ -131,8 +112,7 @@ import TeamService from '../../services/team'
|
||||
import TeamModel from '../../models/team'
|
||||
|
||||
import rights from '../../models/rights'
|
||||
import LoadingComponent from '../misc/loading'
|
||||
import ErrorComponent from '../misc/error'
|
||||
import Multiselect from '@/components/input/multiselect'
|
||||
|
||||
export default {
|
||||
name: 'userTeamShare',
|
||||
@ -172,12 +152,7 @@ export default {
|
||||
}
|
||||
},
|
||||
components: {
|
||||
multiselect: () => ({
|
||||
component: import(/* webpackChunkName: "multiselect" */ 'vue-multiselect'),
|
||||
loading: LoadingComponent,
|
||||
error: ErrorComponent,
|
||||
timeout: 60000,
|
||||
}),
|
||||
Multiselect,
|
||||
},
|
||||
computed: mapState({
|
||||
userInfo: state => state.auth.info,
|
||||
|
@ -1,37 +1,24 @@
|
||||
<template>
|
||||
<multiselect
|
||||
:clear-on-select="true"
|
||||
:close-on-select="false"
|
||||
:disabled="disabled"
|
||||
:hide-selected="true"
|
||||
:internal-search="true"
|
||||
:loading="listUserService.loading"
|
||||
placeholder="Type to assign a user..."
|
||||
:disabled="disabled"
|
||||
:multiple="true"
|
||||
:options="foundUsers"
|
||||
:options-limit="300"
|
||||
:searchable="true"
|
||||
:showNoOptions="false"
|
||||
@search-change="findUser"
|
||||
@search="findUser"
|
||||
:search-results="foundUsers"
|
||||
@select="addAssignee"
|
||||
label="username"
|
||||
placeholder="Type to assign a user..."
|
||||
select-label="Assign this user"
|
||||
track-by="id"
|
||||
select-placeholder="Assign this user"
|
||||
v-model="assignees"
|
||||
>
|
||||
<template slot="tag" slot-scope="{ option }">
|
||||
<user :avatar-size="30" :show-username="false" :user="option"/>
|
||||
<a @click="removeAssignee(option)" class="remove-assignee" v-if="!disabled">
|
||||
<icon icon="times"/>
|
||||
</a>
|
||||
<template v-slot:tag="props">
|
||||
<span class="assignee">
|
||||
<user :avatar-size="32" :show-username="false" :user="props.item"/>
|
||||
<a @click="removeAssignee(props.item)" class="remove-assignee" v-if="!disabled">
|
||||
<icon icon="times"/>
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div
|
||||
@mousedown.prevent.stop="clearAllFoundUsers(props.search)"
|
||||
class="multiselect__clear"
|
||||
v-if="newAssignee !== null && newAssignee.id !== 0"></div>
|
||||
</template>
|
||||
<span slot="noResult">No user found. Consider changing the search query.</span>
|
||||
</multiselect>
|
||||
</template>
|
||||
|
||||
@ -42,19 +29,13 @@ import UserModel from '../../../models/user'
|
||||
import ListUserService from '../../../services/listUsers'
|
||||
import TaskAssigneeService from '../../../services/taskAssignee'
|
||||
import User from '../../misc/user'
|
||||
import LoadingComponent from '../../misc/loading'
|
||||
import ErrorComponent from '../../misc/error'
|
||||
import Multiselect from '@/components/input/multiselect'
|
||||
|
||||
export default {
|
||||
name: 'editAssignees',
|
||||
components: {
|
||||
User,
|
||||
multiselect: () => ({
|
||||
component: import(/* webpackChunkName: "multiselect" */ 'vue-multiselect'),
|
||||
loading: LoadingComponent,
|
||||
error: ErrorComponent,
|
||||
timeout: 60000,
|
||||
}),
|
||||
Multiselect,
|
||||
},
|
||||
props: {
|
||||
taskId: {
|
||||
|
@ -1,42 +1,25 @@
|
||||
<template>
|
||||
<multiselect
|
||||
:clear-on-select="true"
|
||||
:close-on-select="false"
|
||||
:disabled="disabled"
|
||||
:hide-selected="true"
|
||||
:internal-search="true"
|
||||
:loading="labelService.loading || labelTaskService.loading"
|
||||
:multiple="true"
|
||||
:options="foundLabels"
|
||||
:options-limit="300"
|
||||
:searchable="true"
|
||||
:showNoOptions="false"
|
||||
:taggable="true"
|
||||
@search-change="findLabel"
|
||||
@select="label => addLabel(label)"
|
||||
@tag="createAndAddLabel"
|
||||
label="title"
|
||||
placeholder="Type to add a new label..."
|
||||
tag-placeholder="Add this as new label"
|
||||
track-by="id"
|
||||
:multiple="true"
|
||||
@search="findLabel"
|
||||
:search-results="foundLabels"
|
||||
@select="addLabel"
|
||||
label="title"
|
||||
:creatable="true"
|
||||
@create="createAndAddLabel"
|
||||
create-placeholder="Add this as new label"
|
||||
v-model="labels"
|
||||
>
|
||||
<template
|
||||
slot="tag"
|
||||
slot-scope="{ option }">
|
||||
<template v-slot:tag="props">
|
||||
<span
|
||||
:style="{'background': option.hexColor, 'color': option.textColor}"
|
||||
class="tag">
|
||||
<span>{{ option.title }}</span>
|
||||
<a @click="removeLabel(option)" class="delete is-small"></a>
|
||||
:style="{'background': props.item.hexColor, 'color': props.item.textColor}"
|
||||
class="tag ml-2 mt-2">
|
||||
<span>{{ props.item.title }}</span>
|
||||
<a @click="removeLabel(props.item)" class="delete is-small"></a>
|
||||
</span>
|
||||
</template>
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div
|
||||
@mousedown.prevent.stop="clearAllLabels(props.search)"
|
||||
class="multiselect__clear"
|
||||
v-if="labels.length"></div>
|
||||
</template>
|
||||
</multiselect>
|
||||
</template>
|
||||
|
||||
@ -46,8 +29,8 @@ import differenceWith from 'lodash/differenceWith'
|
||||
import LabelService from '../../../services/label'
|
||||
import LabelModel from '../../../models/label'
|
||||
import LabelTaskService from '../../../services/labelTask'
|
||||
import LoadingComponent from '../../misc/loading'
|
||||
import ErrorComponent from '../../misc/error'
|
||||
|
||||
import Multiselect from '@/components/input/multiselect'
|
||||
|
||||
export default {
|
||||
name: 'edit-labels',
|
||||
@ -75,12 +58,7 @@ export default {
|
||||
}
|
||||
},
|
||||
components: {
|
||||
multiselect: () => ({
|
||||
component: import(/* webpackChunkName: "multiselect" */ 'vue-multiselect'),
|
||||
loading: LoadingComponent,
|
||||
error: ErrorComponent,
|
||||
timeout: 60000,
|
||||
}),
|
||||
Multiselect,
|
||||
},
|
||||
watch: {
|
||||
value(newLabels) {
|
||||
|
@ -1,39 +1,27 @@
|
||||
<template>
|
||||
<multiselect
|
||||
:internal-search="true"
|
||||
:loading="listSerivce.loading"
|
||||
:multiple="false"
|
||||
:options="foundLists"
|
||||
:searchable="true"
|
||||
:showNoOptions="false"
|
||||
@search-change="findLists"
|
||||
@select="select"
|
||||
class="control is-expanded"
|
||||
label="title"
|
||||
placeholder="Type to search for a list..."
|
||||
track-by="id"
|
||||
v-focus
|
||||
:loading="listSerivce.loading"
|
||||
placeholder="Type to search for a list..."
|
||||
@search="findLists"
|
||||
:search-results="foundLists"
|
||||
@select="select"
|
||||
label="title"
|
||||
v-model="list"
|
||||
select-placeholder="Click or press enter to select this list"
|
||||
>
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div
|
||||
@mousedown.prevent.stop="clearAll(props.search)"
|
||||
class="multiselect__clear"
|
||||
v-if="list !== null && list.id !== 0"></div>
|
||||
</template>
|
||||
<template slot="option" slot-scope="props">
|
||||
<template v-slot:searchResult="props">
|
||||
<span class="list-namespace-title">{{ namespace(props.option.namespaceId) }} ></span>
|
||||
{{ props.option.title }}
|
||||
</template>
|
||||
<span slot="noResult">No list found. Consider changing the search query.</span>
|
||||
</multiselect>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListService from '../../../services/list'
|
||||
import ListModel from '../../../models/list'
|
||||
import LoadingComponent from '../../misc/loading'
|
||||
import ErrorComponent from '../../misc/error'
|
||||
import Multiselect from '@/components/input/multiselect'
|
||||
|
||||
export default {
|
||||
name: 'listSearch',
|
||||
@ -45,12 +33,7 @@ export default {
|
||||
}
|
||||
},
|
||||
components: {
|
||||
multiselect: () => ({
|
||||
component: import(/* webpackChunkName: "multiselect" */ 'vue-multiselect'),
|
||||
loading: LoadingComponent,
|
||||
error: ErrorComponent,
|
||||
timeout: 60000,
|
||||
}),
|
||||
Multiselect,
|
||||
},
|
||||
beforeMount() {
|
||||
this.listSerivce = new ListService()
|
||||
|