Replace vue-multiselect with a custom component (#366)
Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/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
26 changed files with 986 additions and 1022 deletions
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 table = 'task_comments'
|
||||||
|
|
||||||
static factory() {
|
static factory() {
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: '{increment}',
|
id: '{increment}',
|
||||||
comment: faker.lorem.text(3),
|
comment: faker.lorem.text(3),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {TeamFactory} from '../../factories/team'
|
import {TeamFactory} from '../../factories/team'
|
||||||
import {TeamMemberFactory} from '../../factories/team_member'
|
import {TeamMemberFactory} from '../../factories/team_member'
|
||||||
|
import {UserFactory} from '../../factories/user'
|
||||||
import '../../support/authenticateUser'
|
import '../../support/authenticateUser'
|
||||||
|
|
||||||
describe('Team', () => {
|
describe('Team', () => {
|
||||||
|
@ -88,4 +89,41 @@ describe('Team', () => {
|
||||||
.contains('Member')
|
.contains('Member')
|
||||||
.should('exist')
|
.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 {UserListFactory} from '../../factories/users_list'
|
||||||
|
|
||||||
import '../../support/authenticateUser'
|
import '../../support/authenticateUser'
|
||||||
|
import {TaskAssigneeFactory} from '../../factories/task_assignee'
|
||||||
|
import {LabelFactory} from '../../factories/labels'
|
||||||
|
import {LabelTaskFactory} from '../../factories/label_task'
|
||||||
|
|
||||||
describe('Task', () => {
|
describe('Task', () => {
|
||||||
let namespaces
|
let namespaces
|
||||||
|
@ -202,8 +205,14 @@ describe('Task', () => {
|
||||||
cy.get('.task-view .action-buttons .button')
|
cy.get('.task-view .action-buttons .button')
|
||||||
.contains('Move task')
|
.contains('Move task')
|
||||||
.click()
|
.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}`)
|
.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')
|
cy.get('.task-view h6.subtitle')
|
||||||
.should('contain', namespaces[0].title)
|
.should('contain', namespaces[0].title)
|
||||||
|
@ -233,5 +242,141 @@ describe('Task', () => {
|
||||||
cy.url()
|
cy.url()
|
||||||
.should('contain', `/lists/${tasks[0].list_id}/`)
|
.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": "vue-cli-service serve",
|
||||||
"serve:dist": "node scripts/serve-dist.js",
|
"serve:dist": "node scripts/serve-dist.js",
|
||||||
"build": "vue-cli-service build --modern",
|
"build": "vue-cli-service build --modern",
|
||||||
|
"build:report": "vue-cli-service build --report",
|
||||||
"lint": "vue-cli-service lint --ignore-pattern '*.test.*'",
|
"lint": "vue-cli-service lint --ignore-pattern '*.test.*'",
|
||||||
"cypress:open": "cypress open",
|
"cypress:open": "cypress open",
|
||||||
"test:unit": "jest",
|
"test:unit": "jest",
|
||||||
|
@ -51,7 +52,6 @@
|
||||||
"node-sass": "5.0.0",
|
"node-sass": "5.0.0",
|
||||||
"sass-loader": "10.1.0",
|
"sass-loader": "10.1.0",
|
||||||
"vue-flatpickr-component": "8.1.6",
|
"vue-flatpickr-component": "8.1.6",
|
||||||
"vue-multiselect": "2.1.6",
|
|
||||||
"vue-notification": "1.3.20",
|
"vue-notification": "1.3.20",
|
||||||
"vue-router": "3.4.9",
|
"vue-router": "3.4.9",
|
||||||
"vue-template-compiler": "2.6.12",
|
"vue-template-compiler": "2.6.12",
|
||||||
|
@ -83,10 +83,12 @@
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
"last 2 versions",
|
"last 2 versions",
|
||||||
"not ie <= 8"
|
"not ie < 11"
|
||||||
],
|
],
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"jest": {
|
"jest": {
|
||||||
"testPathIgnorePatterns": ["cypress"]
|
"testPathIgnorePatterns": [
|
||||||
|
"cypress"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,7 @@ import 'flatpickr/dist/flatpickr.css'
|
||||||
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
||||||
import {format} from 'date-fns'
|
import {format} from 'date-fns'
|
||||||
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
|
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
|
||||||
|
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'datepicker',
|
name: 'datepicker',
|
||||||
|
@ -188,25 +189,7 @@ export default {
|
||||||
},
|
},
|
||||||
hideDatePopup(e) {
|
hideDatePopup(e) {
|
||||||
if (this.show) {
|
if (this.show) {
|
||||||
|
closeWhenClickedOutside(e, this.$refs.datepickerPopup, this.close)
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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>
|
<template>
|
||||||
<div class="card filters">
|
<div class="card filters has-overflow">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<fancycheckbox v-model="params.filter_include_nulls">
|
<fancycheckbox v-model="params.filter_include_nulls">
|
||||||
Include Tasks which don't have a value set
|
Include Tasks which don't have a value set
|
||||||
|
@ -103,32 +103,16 @@
|
||||||
<label class="label">Assignees</label>
|
<label class="label">Assignees</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<multiselect
|
<multiselect
|
||||||
:clear-on-select="true"
|
|
||||||
:close-on-select="true"
|
|
||||||
:hide-selected="true"
|
|
||||||
:internal-search="true"
|
|
||||||
:loading="usersService.loading"
|
: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..."
|
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"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -136,39 +120,23 @@
|
||||||
<label class="label">Labels</label>
|
<label class="label">Labels</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<multiselect
|
<multiselect
|
||||||
:clear-on-select="true"
|
|
||||||
:close-on-select="false"
|
|
||||||
:hide-selected="true"
|
|
||||||
:internal-search="true"
|
|
||||||
:loading="labelService.loading"
|
:loading="labelService.loading"
|
||||||
:multiple="true"
|
placeholder="Type to search for a label..."
|
||||||
:options="foundLabels"
|
@search="findLabels"
|
||||||
:options-limit="300"
|
:search-results="foundLabels"
|
||||||
:searchable="true"
|
|
||||||
:showNoOptions="false"
|
|
||||||
@search-change="findLabels"
|
|
||||||
@select="label => addLabel(label)"
|
@select="label => addLabel(label)"
|
||||||
label="title"
|
label="title"
|
||||||
placeholder="Type to search for a label..."
|
:multiple="true"
|
||||||
track-by="id"
|
|
||||||
v-model="labels"
|
v-model="labels"
|
||||||
>
|
>
|
||||||
<template
|
<template v-slot:tag="props">
|
||||||
slot="tag"
|
|
||||||
slot-scope="{ option }">
|
|
||||||
<span
|
<span
|
||||||
:style="{'background': option.hexColor, 'color': option.textColor}"
|
:style="{'background': props.item.hexColor, 'color': props.item.textColor}"
|
||||||
class="tag mr-2 mb-2">
|
class="tag ml-2 mt-2">
|
||||||
<span>{{ option.title }}</span>
|
<span>{{ props.item.title }}</span>
|
||||||
<a @click="removeLabel(option)" class="delete is-small"></a>
|
<a @click="removeLabel(props.item)" class="delete is-small"></a>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template slot="clear" slot-scope="props">
|
|
||||||
<div
|
|
||||||
@mousedown.prevent.stop="clearLabels(props.search)"
|
|
||||||
class="multiselect__clear"
|
|
||||||
v-if="labels.length"></div>
|
|
||||||
</template>
|
|
||||||
</multiselect>
|
</multiselect>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -178,64 +146,32 @@
|
||||||
<label class="label">Lists</label>
|
<label class="label">Lists</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<multiselect
|
<multiselect
|
||||||
:clear-on-select="true"
|
|
||||||
:close-on-select="true"
|
|
||||||
:hide-selected="true"
|
|
||||||
:internal-search="true"
|
|
||||||
:loading="listsService.loading"
|
: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..."
|
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"
|
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>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Namespaces</label>
|
<label class="label">Namespaces</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<multiselect
|
<multiselect
|
||||||
:clear-on-select="true"
|
|
||||||
:close-on-select="true"
|
|
||||||
:hide-selected="true"
|
|
||||||
:internal-search="true"
|
|
||||||
:loading="namespaceService.loading"
|
: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..."
|
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"
|
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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -247,13 +183,13 @@
|
||||||
import Fancycheckbox from '../../input/fancycheckbox'
|
import Fancycheckbox from '../../input/fancycheckbox'
|
||||||
import flatPickr from 'vue-flatpickr-component'
|
import flatPickr from 'vue-flatpickr-component'
|
||||||
import 'flatpickr/dist/flatpickr.css'
|
import 'flatpickr/dist/flatpickr.css'
|
||||||
import Multiselect from 'vue-multiselect'
|
|
||||||
|
|
||||||
import {formatISO} from 'date-fns'
|
import {formatISO} from 'date-fns'
|
||||||
import differenceWith from 'lodash/differenceWith'
|
import differenceWith from 'lodash/differenceWith'
|
||||||
|
|
||||||
import PrioritySelect from '@/components/tasks/partials/prioritySelect'
|
import PrioritySelect from '@/components/tasks/partials/prioritySelect'
|
||||||
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect'
|
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect'
|
||||||
|
import Multiselect from '@/components/input/multiselect'
|
||||||
|
|
||||||
import UserService from '@/services/user'
|
import UserService from '@/services/user'
|
||||||
import LabelService from '@/services/label'
|
import LabelService from '@/services/label'
|
||||||
|
|
|
@ -1,31 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<multiselect
|
<multiselect
|
||||||
:internal-search="true"
|
|
||||||
:loading="namespaceService.loading"
|
:loading="namespaceService.loading"
|
||||||
:multiple="false"
|
placeholder="Search for a namespace..."
|
||||||
:options="namespaces"
|
@search="findNamespaces"
|
||||||
:searchable="true"
|
:search-results="namespaces"
|
||||||
:showNoOptions="false"
|
|
||||||
@search-change="findNamespaces"
|
|
||||||
@select="select"
|
@select="select"
|
||||||
label="title"
|
label="title"
|
||||||
placeholder="Search for a namespace..."
|
v-model="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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import NamespaceService from '../../services/namespace'
|
import NamespaceService from '../../services/namespace'
|
||||||
import NamespaceModel from '../../models/namespace'
|
import NamespaceModel from '../../models/namespace'
|
||||||
import LoadingComponent from '../misc/loading'
|
|
||||||
import ErrorComponent from '../misc/error'
|
import Multiselect from '@/components/input/multiselect'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'namespace-search',
|
name: 'namespace-search',
|
||||||
|
@ -37,12 +26,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
multiselect: () => ({
|
Multiselect,
|
||||||
component: import(/* webpackChunkName: "multiselect" */ 'vue-multiselect'),
|
|
||||||
loading: LoadingComponent,
|
|
||||||
error: ErrorComponent,
|
|
||||||
timeout: 60000,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.namespaceService = new NamespaceService()
|
this.namespaceService = new NamespaceService()
|
||||||
|
|
|
@ -1,106 +1,87 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card is-fullwidth">
|
<div class="card is-fullwidth has-overflow">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title">
|
<p class="card-header-title">
|
||||||
Shared with these {{ shareType }}s
|
Shared with these {{ shareType }}s
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content content sharables-list">
|
<div class="card-content" v-if="userIsAdmin">
|
||||||
<form @submit.prevent="add()" class="add-form" v-if="userIsAdmin">
|
<div class="field has-addons">
|
||||||
<div class="field is-grouped">
|
<p class="control is-expanded" v-bind:class="{ 'is-loading': searchService.loading}">
|
||||||
<p class="control is-expanded" v-bind:class="{ 'is-loading': searchService.loading}">
|
<multiselect
|
||||||
<multiselect
|
:loading="searchService.loading"
|
||||||
:internal-search="true"
|
placeholder="Type to search..."
|
||||||
:label="searchLabel"
|
@search="find"
|
||||||
:loading="searchService.loading"
|
:search-results="found"
|
||||||
:multiple="false"
|
:label="searchLabel"
|
||||||
:options="found"
|
v-model="sharable"
|
||||||
:searchable="true"
|
/>
|
||||||
:showNoOptions="false"
|
</p>
|
||||||
@search-change="find"
|
<p class="control">
|
||||||
placeholder="Type to search..."
|
<button class="button is-primary" @click="add()">
|
||||||
track-by="id"
|
Share
|
||||||
v-model="sharable">
|
</button>
|
||||||
<template slot="clear" slot-scope="props">
|
</p>
|
||||||
<div
|
</div>
|
||||||
@mousedown.prevent.stop="clearAll(props.search)"
|
</div>
|
||||||
class="multiselect__clear"
|
<table class="table is-striped is-hoverable is-fullwidth">
|
||||||
v-if="sharable.id !== 0"></div>
|
<tbody>
|
||||||
</template>
|
<tr :key="s.id" v-for="s in sharables">
|
||||||
<span slot="noResult">
|
<template v-if="shareType === 'user'">
|
||||||
Oops! No {{ shareType }} found. Consider changing the search query.
|
<td>{{ s.getDisplayName() }}</td>
|
||||||
</span>
|
<td>
|
||||||
</multiselect>
|
<template v-if="s.id === userInfo.id">
|
||||||
</p>
|
<b class="is-success">You</b>
|
||||||
<p class="control">
|
</template>
|
||||||
<button class="button is-success" type="submit">
|
</td>
|
||||||
<span class="icon is-small">
|
</template>
|
||||||
<icon icon="plus"/>
|
<template v-if="shareType === 'team'">
|
||||||
</span>
|
<td>
|
||||||
Add
|
<router-link :to="{name: 'teams.edit', params: {id: s.id}}">
|
||||||
</button>
|
{{ s.name }}
|
||||||
</p>
|
</router-link>
|
||||||
</div>
|
</td>
|
||||||
</form>
|
</template>
|
||||||
<table class="table is-striped is-hoverable is-fullwidth">
|
<td class="type">
|
||||||
<tbody>
|
<template v-if="s.right === rights.ADMIN">
|
||||||
<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">
|
<span class="icon is-small">
|
||||||
<icon icon="lock"/>
|
<icon icon="lock"/>
|
||||||
</span>
|
</span>
|
||||||
Admin
|
Admin
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="s.right === rights.READ_WRITE">
|
<template v-else-if="s.right === rights.READ_WRITE">
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
<icon icon="pen"/>
|
<icon icon="pen"/>
|
||||||
</span>
|
</span>
|
||||||
Write
|
Write
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
<icon icon="users"/>
|
<icon icon="users"/>
|
||||||
</span>
|
</span>
|
||||||
Read-only
|
Read-only
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td class="actions" v-if="userIsAdmin">
|
<td class="actions" v-if="userIsAdmin">
|
||||||
<div class="select">
|
<div class="select">
|
||||||
<select @change="toggleType(s)" class="button buttonright" v-model="selectedRight[s.id]">
|
<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" :value="rights.READ">Read only</option>
|
||||||
<option :selected="s.right === rights.READ_WRITE" :value="rights.READ_WRITE">Read &
|
<option :selected="s.right === rights.READ_WRITE" :value="rights.READ_WRITE">Read &
|
||||||
write
|
write
|
||||||
</option>
|
</option>
|
||||||
<option :selected="s.right === rights.ADMIN" :value="rights.ADMIN">Admin</option>
|
<option :selected="s.right === rights.ADMIN" :value="rights.ADMIN">Admin</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button @click="() => {sharable = s; showDeleteModal = true}"
|
<button @click="() => {sharable = s; showDeleteModal = true}"
|
||||||
class="button is-danger icon-only">
|
class="button is-danger icon-only">
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
<icon icon="trash-alt"/>
|
<icon icon="trash-alt"/>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
|
|
||||||
<modal
|
<modal
|
||||||
@close="showDeleteModal = false"
|
@close="showDeleteModal = false"
|
||||||
|
@ -131,8 +112,7 @@ import TeamService from '../../services/team'
|
||||||
import TeamModel from '../../models/team'
|
import TeamModel from '../../models/team'
|
||||||
|
|
||||||
import rights from '../../models/rights'
|
import rights from '../../models/rights'
|
||||||
import LoadingComponent from '../misc/loading'
|
import Multiselect from '@/components/input/multiselect'
|
||||||
import ErrorComponent from '../misc/error'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'userTeamShare',
|
name: 'userTeamShare',
|
||||||
|
@ -172,12 +152,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
multiselect: () => ({
|
Multiselect,
|
||||||
component: import(/* webpackChunkName: "multiselect" */ 'vue-multiselect'),
|
|
||||||
loading: LoadingComponent,
|
|
||||||
error: ErrorComponent,
|
|
||||||
timeout: 60000,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
computed: mapState({
|
computed: mapState({
|
||||||
userInfo: state => state.auth.info,
|
userInfo: state => state.auth.info,
|
||||||
|
|
|
@ -1,37 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<multiselect
|
<multiselect
|
||||||
:clear-on-select="true"
|
|
||||||
:close-on-select="false"
|
|
||||||
:disabled="disabled"
|
|
||||||
:hide-selected="true"
|
|
||||||
:internal-search="true"
|
|
||||||
:loading="listUserService.loading"
|
:loading="listUserService.loading"
|
||||||
|
placeholder="Type to assign a user..."
|
||||||
|
:disabled="disabled"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:options="foundUsers"
|
@search="findUser"
|
||||||
:options-limit="300"
|
:search-results="foundUsers"
|
||||||
:searchable="true"
|
|
||||||
:showNoOptions="false"
|
|
||||||
@search-change="findUser"
|
|
||||||
@select="addAssignee"
|
@select="addAssignee"
|
||||||
label="username"
|
label="username"
|
||||||
placeholder="Type to assign a user..."
|
select-placeholder="Assign this user"
|
||||||
select-label="Assign this user"
|
|
||||||
track-by="id"
|
|
||||||
v-model="assignees"
|
v-model="assignees"
|
||||||
>
|
>
|
||||||
<template slot="tag" slot-scope="{ option }">
|
<template v-slot:tag="props">
|
||||||
<user :avatar-size="30" :show-username="false" :user="option"/>
|
<span class="assignee">
|
||||||
<a @click="removeAssignee(option)" class="remove-assignee" v-if="!disabled">
|
<user :avatar-size="32" :show-username="false" :user="props.item"/>
|
||||||
<icon icon="times"/>
|
<a @click="removeAssignee(props.item)" class="remove-assignee" v-if="!disabled">
|
||||||
</a>
|
<icon icon="times"/>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
</template>
|
</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>
|
</multiselect>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -42,19 +29,13 @@ import UserModel from '../../../models/user'
|
||||||
import ListUserService from '../../../services/listUsers'
|
import ListUserService from '../../../services/listUsers'
|
||||||
import TaskAssigneeService from '../../../services/taskAssignee'
|
import TaskAssigneeService from '../../../services/taskAssignee'
|
||||||
import User from '../../misc/user'
|
import User from '../../misc/user'
|
||||||
import LoadingComponent from '../../misc/loading'
|
import Multiselect from '@/components/input/multiselect'
|
||||||
import ErrorComponent from '../../misc/error'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'editAssignees',
|
name: 'editAssignees',
|
||||||
components: {
|
components: {
|
||||||
User,
|
User,
|
||||||
multiselect: () => ({
|
Multiselect,
|
||||||
component: import(/* webpackChunkName: "multiselect" */ 'vue-multiselect'),
|
|
||||||
loading: LoadingComponent,
|
|
||||||
error: ErrorComponent,
|
|
||||||
timeout: 60000,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
taskId: {
|
taskId: {
|
||||||
|
|
|
@ -1,42 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<multiselect
|
<multiselect
|
||||||
:clear-on-select="true"
|
|
||||||
:close-on-select="false"
|
|
||||||
:disabled="disabled"
|
|
||||||
:hide-selected="true"
|
|
||||||
:internal-search="true"
|
|
||||||
:loading="labelService.loading || labelTaskService.loading"
|
: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..."
|
placeholder="Type to add a new label..."
|
||||||
tag-placeholder="Add this as new label"
|
:multiple="true"
|
||||||
track-by="id"
|
@search="findLabel"
|
||||||
|
:search-results="foundLabels"
|
||||||
|
@select="addLabel"
|
||||||
|
label="title"
|
||||||
|
:creatable="true"
|
||||||
|
@create="createAndAddLabel"
|
||||||
|
create-placeholder="Add this as new label"
|
||||||
v-model="labels"
|
v-model="labels"
|
||||||
>
|
>
|
||||||
<template
|
<template v-slot:tag="props">
|
||||||
slot="tag"
|
|
||||||
slot-scope="{ option }">
|
|
||||||
<span
|
<span
|
||||||
:style="{'background': option.hexColor, 'color': option.textColor}"
|
:style="{'background': props.item.hexColor, 'color': props.item.textColor}"
|
||||||
class="tag">
|
class="tag ml-2 mt-2">
|
||||||
<span>{{ option.title }}</span>
|
<span>{{ props.item.title }}</span>
|
||||||
<a @click="removeLabel(option)" class="delete is-small"></a>
|
<a @click="removeLabel(props.item)" class="delete is-small"></a>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template slot="clear" slot-scope="props">
|
|
||||||
<div
|
|
||||||
@mousedown.prevent.stop="clearAllLabels(props.search)"
|
|
||||||
class="multiselect__clear"
|
|
||||||
v-if="labels.length"></div>
|
|
||||||
</template>
|
|
||||||
</multiselect>
|
</multiselect>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -46,8 +29,8 @@ import differenceWith from 'lodash/differenceWith'
|
||||||
import LabelService from '../../../services/label'
|
import LabelService from '../../../services/label'
|
||||||
import LabelModel from '../../../models/label'
|
import LabelModel from '../../../models/label'
|
||||||
import LabelTaskService from '../../../services/labelTask'
|
import LabelTaskService from '../../../services/labelTask'
|
||||||
import LoadingComponent from '../../misc/loading'
|
|
||||||
import ErrorComponent from '../../misc/error'
|
import Multiselect from '@/components/input/multiselect'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'edit-labels',
|
name: 'edit-labels',
|
||||||
|
@ -75,12 +58,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
multiselect: () => ({
|
Multiselect,
|
||||||
component: import(/* webpackChunkName: "multiselect" */ 'vue-multiselect'),
|
|
||||||
loading: LoadingComponent,
|
|
||||||
error: ErrorComponent,
|
|
||||||
timeout: 60000,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
value(newLabels) {
|
value(newLabels) {
|
||||||
|
|
|
@ -1,39 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<multiselect
|
<multiselect
|
||||||
:internal-search="true"
|
|
||||||
:loading="listSerivce.loading"
|
|
||||||
:multiple="false"
|
|
||||||
:options="foundLists"
|
|
||||||
:searchable="true"
|
|
||||||
:showNoOptions="false"
|
|
||||||
@search-change="findLists"
|
|
||||||
@select="select"
|
|
||||||
class="control is-expanded"
|
class="control is-expanded"
|
||||||
label="title"
|
|
||||||
placeholder="Type to search for a list..."
|
|
||||||
track-by="id"
|
|
||||||
v-focus
|
v-focus
|
||||||
|
:loading="listSerivce.loading"
|
||||||
|
placeholder="Type to search for a list..."
|
||||||
|
@search="findLists"
|
||||||
|
:search-results="foundLists"
|
||||||
|
@select="select"
|
||||||
|
label="title"
|
||||||
v-model="list"
|
v-model="list"
|
||||||
|
select-placeholder="Click or press enter to select this list"
|
||||||
>
|
>
|
||||||
<template slot="clear" slot-scope="props">
|
<template v-slot:searchResult="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">
|
|
||||||
<span class="list-namespace-title">{{ namespace(props.option.namespaceId) }} ></span>
|
<span class="list-namespace-title">{{ namespace(props.option.namespaceId) }} ></span>
|
||||||
{{ props.option.title }}
|
{{ props.option.title }}
|
||||||
</template>
|
</template>
|
||||||
<span slot="noResult">No list found. Consider changing the search query.</span>
|
|
||||||
</multiselect>
|
</multiselect>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ListService from '../../../services/list'
|
import ListService from '../../../services/list'
|
||||||
import ListModel from '../../../models/list'
|
import ListModel from '../../../models/list'
|
||||||
import LoadingComponent from '../../misc/loading'
|
import Multiselect from '@/components/input/multiselect'
|
||||||
import ErrorComponent from '../../misc/error'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'listSearch',
|
name: 'listSearch',
|
||||||
|
@ -45,12 +33,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
multiselect: () => ({
|
Multiselect,
|
||||||
component: import(/* webpackChunkName: "multiselect" */ 'vue-multiselect'),
|
|
||||||
loading: LoadingComponent,
|
|
||||||
error: ErrorComponent,
|
|
||||||
timeout: 60000,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
this.listSerivce = new ListService()
|
this.listSerivce = new ListService()
|
||||||
|
|
|
@ -15,29 +15,16 @@
|
||||||
</label>
|
</label>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<multiselect
|
<multiselect
|
||||||
:internal-search="true"
|
|
||||||
:loading="taskService.loading"
|
|
||||||
:multiple="false"
|
|
||||||
:options="foundTasks"
|
|
||||||
:searchable="true"
|
|
||||||
:showNoOptions="false"
|
|
||||||
:taggable="true"
|
|
||||||
@search-change="findTasks"
|
|
||||||
@tag="createAndRelateTask"
|
|
||||||
label="title"
|
|
||||||
placeholder="Type search for a new task to add as related..."
|
placeholder="Type search for a new task to add as related..."
|
||||||
tag-placeholder="Add this as new related task"
|
@search="findTasks"
|
||||||
track-by="id"
|
:loading="taskService.loading"
|
||||||
|
:search-results="foundTasks"
|
||||||
|
label="title"
|
||||||
v-model="newTaskRelationTask"
|
v-model="newTaskRelationTask"
|
||||||
>
|
:creatable="true"
|
||||||
<template slot="clear" slot-scope="props">
|
create-placeholder="Add this as new related task"
|
||||||
<div
|
@create="createAndRelateTask"
|
||||||
@mousedown.prevent.stop="clearAllFoundTasks(props.search)"
|
/>
|
||||||
class="multiselect__clear"
|
|
||||||
v-if="newTaskRelationTask !== null && newTaskRelationTask.id !== 0"></div>
|
|
||||||
</template>
|
|
||||||
<span slot="noResult">No task found. Consider changing the search query.</span>
|
|
||||||
</multiselect>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control is-expanded">
|
<div class="control is-expanded">
|
||||||
|
@ -60,7 +47,7 @@
|
||||||
<template v-if="rts.length > 0">
|
<template v-if="rts.length > 0">
|
||||||
<span class="title">{{ relationKindTitle(kind, rts.length) }}</span>
|
<span class="title">{{ relationKindTitle(kind, rts.length) }}</span>
|
||||||
<div class="tasks noborder">
|
<div class="tasks noborder">
|
||||||
<div :key="t.id" class="task" v-for="t in rts">
|
<div :key="t.id" class="task" v-for="t in rts.filter(t => t)">
|
||||||
<router-link :to="{ name: $route.name, params: { id: t.id } }">
|
<router-link :to="{ name: $route.name, params: { id: t.id } }">
|
||||||
<span :class="{ 'done': t.done}" class="tasktext">
|
<span :class="{ 'done': t.done}" class="tasktext">
|
||||||
<span
|
<span
|
||||||
|
@ -107,8 +94,7 @@ import TaskRelationService from '../../../services/taskRelation'
|
||||||
import relationKinds from '../../../models/relationKinds'
|
import relationKinds from '../../../models/relationKinds'
|
||||||
import TaskRelationModel from '../../../models/taskRelation'
|
import TaskRelationModel from '../../../models/taskRelation'
|
||||||
|
|
||||||
import LoadingComponent from '../../misc/loading'
|
import Multiselect from '@/components/input/multiselect'
|
||||||
import ErrorComponent from '../../misc/error'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'relatedTasks',
|
name: 'relatedTasks',
|
||||||
|
@ -127,12 +113,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
multiselect: () => ({
|
Multiselect,
|
||||||
component: import(/* webpackChunkName: "multiselect" */ 'vue-multiselect'),
|
|
||||||
loading: LoadingComponent,
|
|
||||||
error: ErrorComponent,
|
|
||||||
timeout: 60000,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
taskId: {
|
taskId: {
|
||||||
|
@ -171,11 +152,6 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
findTasks(query) {
|
findTasks(query) {
|
||||||
if (query === '') {
|
|
||||||
this.clearAllFoundTasks()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.taskService.getAll({}, {s: query})
|
this.taskService.getAll({}, {s: query})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.$set(this, 'foundTasks', response)
|
this.$set(this, 'foundTasks', response)
|
||||||
|
@ -184,9 +160,6 @@ export default {
|
||||||
this.error(e, this)
|
this.error(e, this)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
clearAllFoundTasks() {
|
|
||||||
this.$set(this, 'foundTasks', [])
|
|
||||||
},
|
|
||||||
addTaskRelation() {
|
addTaskRelation() {
|
||||||
let rel = new TaskRelationModel({
|
let rel = new TaskRelationModel({
|
||||||
taskId: this.taskId,
|
taskId: this.taskId,
|
||||||
|
@ -199,7 +172,7 @@ export default {
|
||||||
this.$set(this.relatedTasks, this.newTaskRelationKind, [])
|
this.$set(this.relatedTasks, this.newTaskRelationKind, [])
|
||||||
}
|
}
|
||||||
this.relatedTasks[this.newTaskRelationKind].push(this.newTaskRelationTask)
|
this.relatedTasks[this.newTaskRelationKind].push(this.newTaskRelationTask)
|
||||||
this.newTaskRelationTask = new TaskModel()
|
this.newTaskRelationTask = null
|
||||||
this.saved = true
|
this.saved = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.saved = false
|
this.saved = false
|
||||||
|
|
|
@ -92,15 +92,6 @@ p {
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field.has-addons {
|
|
||||||
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
|
|
||||||
.control .select select {
|
|
||||||
height: 2.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.columns {
|
.columns {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
27
src/helpers/closeWhenClickedOutside.js
Normal file
27
src/helpers/closeWhenClickedOutside.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* Calls the close callback when a click happened outside of the rootElement.
|
||||||
|
*
|
||||||
|
* @param event The "click" event object.
|
||||||
|
* @param rootElement
|
||||||
|
* @param closeCallback A closure function to call when the click event happened outside of the rootElement.
|
||||||
|
*/
|
||||||
|
export const closeWhenClickedOutside = (event, rootElement, closeCallback) => {
|
||||||
|
// We walk up the tree to see if any parent of the clicked element is the root element.
|
||||||
|
// If it is not, we call the close callback. We're doing all this hassle to only call the
|
||||||
|
// closing callback when a click happens outside of the rootElement.
|
||||||
|
let parent = event.target.parentElement
|
||||||
|
while (parent !== rootElement) {
|
||||||
|
if (parent.parentElement === null) {
|
||||||
|
parent = null
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
parent = parent.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent === rootElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCallback()
|
||||||
|
}
|
|
@ -1,482 +1,135 @@
|
||||||
fieldset[disabled] .multiselect {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__spinner {
|
|
||||||
position: absolute;
|
|
||||||
right: 1px;
|
|
||||||
top: 1px;
|
|
||||||
width: 48px;
|
|
||||||
height: 35px;
|
|
||||||
background: $white;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
&:before, &:after {
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
margin: -8px 0 0 -8px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border-radius: 100%;
|
|
||||||
border-color: $multiselect_primary transparent transparent;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 2px;
|
|
||||||
box-shadow: 0 0 0 1px transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
animation: spinning 2.4s cubic-bezier(0.41, 0.26, 0.2, 0.62);
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
animation: spinning 2.4s cubic-bezier(0.51, 0.09, 0.21, 0.8);
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__loading-enter-active, .multiselect__loading-leave-active {
|
|
||||||
transition: opacity 0.4s ease-in-out;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__loading-enter, .multiselect__loading-leave-active {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect, .multiselect__input, .multiselect__single {
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 16px;
|
|
||||||
touch-action: manipulation;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect {
|
.multiselect {
|
||||||
box-sizing: content-box;
|
width: 100%;
|
||||||
display: block;
|
position: relative;
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 40px;
|
|
||||||
text-align: left;
|
|
||||||
color: $text;
|
|
||||||
|
|
||||||
* {
|
&.has-search-results .input-wrapper {
|
||||||
box-sizing: border-box;
|
border-radius: $radius $radius 0 0;
|
||||||
}
|
border-color: $primary !important;
|
||||||
|
background: $white !important;
|
||||||
|
|
||||||
&:focus {
|
&, &:focus-within {
|
||||||
outline: none;
|
border-bottom-color: $grey-lighter !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect--disabled {
|
.input-wrapper {
|
||||||
pointer-events: none;
|
padding: 0;
|
||||||
opacity: 0.6;
|
background: $white !important;
|
||||||
}
|
border-color: $grey-lighter !important;
|
||||||
|
flex-wrap: wrap;
|
||||||
.multiselect--active {
|
height: auto;
|
||||||
z-index: 50;
|
|
||||||
|
&:hover {
|
||||||
&:not(.multiselect--above) {
|
border-color: $grey-light !important;
|
||||||
.multiselect__current, .multiselect__input, .multiselect__tags {
|
}
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
.input-loader-wrapper {
|
||||||
}
|
display: flex;
|
||||||
}
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
.multiselect__select {
|
align-items: center;
|
||||||
transform: rotateZ(180deg);
|
|
||||||
}
|
.input {
|
||||||
}
|
border: none !important;
|
||||||
|
background: transparent;
|
||||||
.multiselect--above.multiselect--active {
|
height: auto;
|
||||||
.multiselect__current, .multiselect__input, .multiselect__tags {
|
|
||||||
border-top-left-radius: 0;
|
&::placeholder {
|
||||||
border-top-right-radius: 0;
|
font-style: normal !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.multiselect__input, .multiselect__single {
|
|
||||||
position: relative;
|
&:focus-within {
|
||||||
display: inline-block;
|
border-color: $primary !important;
|
||||||
min-height: 20px;
|
background: $white !important;
|
||||||
line-height: 20px;
|
}
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
.loader {
|
||||||
background: $white;
|
margin: 0 .5rem;
|
||||||
padding: 0 0 0 5px;
|
}
|
||||||
width: calc(100%);
|
}
|
||||||
transition: border 0.1s ease;
|
|
||||||
box-sizing: border-box;
|
.search-results {
|
||||||
margin-bottom: 8px;
|
background: $white;
|
||||||
vertical-align: top;
|
border-radius: 0 0 $radius $radius;
|
||||||
}
|
border: 1px solid $primary;
|
||||||
|
border-top: none;
|
||||||
.multiselect__input::placeholder {
|
|
||||||
color: $multiselect-dark;
|
max-height: 50vh;
|
||||||
}
|
overflow-x: auto;
|
||||||
|
position: absolute;
|
||||||
.multiselect__tag ~ {
|
z-index: 100;
|
||||||
.multiselect__input, .multiselect__single {
|
max-width: 100%;
|
||||||
width: auto;
|
min-width: 100%;
|
||||||
}
|
|
||||||
}
|
button {
|
||||||
|
background: transparent;
|
||||||
.multiselect__input:hover, .multiselect__single:hover {
|
display: block;
|
||||||
border-color: darken($white, 10);
|
text-align: left;
|
||||||
}
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
.multiselect__input:focus {
|
text-transform: none;
|
||||||
border-color: $primary;
|
font-family: $family-sans-serif;
|
||||||
outline: none;
|
font-weight: normal;
|
||||||
}
|
|
||||||
|
display: flex;
|
||||||
.multiselect__single {
|
justify-content: space-between;
|
||||||
&:focus {
|
align-items: center;
|
||||||
border-color: $primary;
|
overflow: hidden;
|
||||||
outline: none;
|
|
||||||
}
|
span:first-child {
|
||||||
|
white-space: nowrap;
|
||||||
padding-left: 5px;
|
text-overflow: ellipsis;
|
||||||
margin-bottom: 8px;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect__tags-wrap {
|
.hint-text {
|
||||||
display: inline;
|
font-size: .75rem;
|
||||||
|
color: transparent;
|
||||||
.user {
|
transition: color $transition;
|
||||||
display: inline-block;
|
padding-left: .5rem;
|
||||||
min-height: 30px;
|
}
|
||||||
margin: 0 0 .5em;
|
|
||||||
}
|
&:focus, &:hover {
|
||||||
}
|
background: $grey-lightest;
|
||||||
|
box-shadow: none !important;
|
||||||
.multiselect__tags {
|
|
||||||
display: block;
|
.hint-text {
|
||||||
padding: 8px 40px 0 8px;
|
color: $dark;
|
||||||
border-radius: 5px;
|
}
|
||||||
border: 1px solid $multiselect-border;
|
}
|
||||||
background: $white;
|
|
||||||
font-size: 14px;
|
&:active {
|
||||||
}
|
background: $grey-lighter;
|
||||||
|
}
|
||||||
.multiselect__tag {
|
}
|
||||||
position: relative;
|
}
|
||||||
display: inline-block;
|
|
||||||
padding: 4px 26px 4px 10px;
|
.assignee {
|
||||||
border-radius: 5px;
|
position: relative;
|
||||||
margin-right: 10px;
|
|
||||||
color: $white;
|
&:not(:first-child) {
|
||||||
line-height: 1;
|
margin-left: -1.75rem;
|
||||||
background: $multiselect-highlight;
|
}
|
||||||
margin-bottom: 5px;
|
|
||||||
white-space: nowrap;
|
.user img {
|
||||||
overflow: hidden;
|
border: 2px solid $white;
|
||||||
max-width: 100%;
|
}
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
.remove-assignee {
|
||||||
|
position: absolute;
|
||||||
.multiselect__tag-icon {
|
top: 4px;
|
||||||
cursor: pointer;
|
left: 2px;
|
||||||
margin-left: 7px;
|
color: $red;
|
||||||
position: absolute;
|
background: $white;
|
||||||
right: 0;
|
padding: 0 4px;
|
||||||
top: 0;
|
display: block;
|
||||||
bottom: 0;
|
border-radius: 100%;
|
||||||
font-weight: 700;
|
font-size: .75rem;
|
||||||
font-style: initial;
|
width: 18px;
|
||||||
width: 22px;
|
height: 18px;
|
||||||
text-align: center;
|
z-index: 100;
|
||||||
line-height: 22px;
|
}
|
||||||
transition: all 0.2s ease;
|
}
|
||||||
border-radius: 5px;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: "×";
|
|
||||||
color: darken($multiselect-highlight, 20);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus, &:hover {
|
|
||||||
background: lighten($multiselect-highlight, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus:after, &:hover:after {
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__current {
|
|
||||||
line-height: 16px;
|
|
||||||
min-height: 40px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 8px 30px 0 12px;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin: 0;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
border: 1px solid $multiselect-border;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__select {
|
|
||||||
line-height: 16px;
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 40px;
|
|
||||||
height: 38px;
|
|
||||||
right: 1px;
|
|
||||||
top: 1px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
margin: 0;
|
|
||||||
text-decoration: none;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
position: relative;
|
|
||||||
right: 0;
|
|
||||||
top: 65%;
|
|
||||||
color: darken($multiselect-border, 30);
|
|
||||||
margin-top: 4px;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 5px 5px 0 5px;
|
|
||||||
border-color: darken($multiselect-border, 30) transparent transparent transparent;
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__placeholder {
|
|
||||||
color: darken($white, 15);
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect--active .multiselect__placeholder {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__content-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
background: $white;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 240px;
|
|
||||||
overflow: auto;
|
|
||||||
border: 1px solid $multiselect-border;
|
|
||||||
border-top: none;
|
|
||||||
border-bottom-left-radius: 5px;
|
|
||||||
border-bottom-right-radius: 5px;
|
|
||||||
z-index: 50;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__content, .content ul.multiselect__content {
|
|
||||||
list-style: none;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
min-width: 100%;
|
|
||||||
vertical-align: top;
|
|
||||||
|
|
||||||
li + li {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect--above .multiselect__content-wrapper {
|
|
||||||
bottom: 100%;
|
|
||||||
border-radius: 5px 5px 0 0;
|
|
||||||
border-bottom: none;
|
|
||||||
border-top: 1px solid $multiselect-border;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__content::webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__element {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__option {
|
|
||||||
display: block;
|
|
||||||
padding: 12px;
|
|
||||||
min-height: 40px;
|
|
||||||
line-height: 16px;
|
|
||||||
text-decoration: none;
|
|
||||||
text-transform: none;
|
|
||||||
vertical-align: middle;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
position: absolute;
|
|
||||||
line-height: 40px;
|
|
||||||
padding-right: 12px;
|
|
||||||
padding-left: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__option--highlight {
|
|
||||||
background: $multiselect-highlight;
|
|
||||||
outline: none;
|
|
||||||
color: $white;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: attr(data-select);
|
|
||||||
background: $multiselect-highlight;
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__option--selected {
|
|
||||||
background: darken($white, 10);
|
|
||||||
color: $multiselect-dark;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: attr(data-selected);
|
|
||||||
color: silver;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.multiselect__option--highlight {
|
|
||||||
background: $multiselect-highlight-negative;
|
|
||||||
color: $white;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
background: $multiselect-highlight-negative;
|
|
||||||
content: attr(data-deselect);
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect--disabled {
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
.multiselect__current, .multiselect__select {
|
|
||||||
background: $multiselect-disabled;
|
|
||||||
color: darken($multiselect-disabled, 40);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__option--disabled {
|
|
||||||
background: $multiselect-disabled !important;
|
|
||||||
color: darken($multiselect-disabled, 40) !important;
|
|
||||||
cursor: text;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__option--group {
|
|
||||||
background: $multiselect-disabled;
|
|
||||||
color: $multiselect-dark;
|
|
||||||
|
|
||||||
&.multiselect__option--highlight {
|
|
||||||
background: $multiselect-dark;
|
|
||||||
color: $white;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
background: $multiselect-dark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__option--disabled.multiselect__option--highlight {
|
|
||||||
background: $multiselect-disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__option--group-selected.multiselect__option--highlight {
|
|
||||||
background: $multiselect-highlight-negative;
|
|
||||||
color: $white;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
background: $multiselect-highlight-negative;
|
|
||||||
content: attr(data-deselect);
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect-enter-active, .multiselect-leave-active {
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect-enter, .multiselect-leave-active {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__strong {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
line-height: 20px;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
*[dir="rtl"] {
|
|
||||||
.multiselect {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__select {
|
|
||||||
right: auto;
|
|
||||||
left: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__tags {
|
|
||||||
padding: 8px 8px 0px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__content {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__option:after {
|
|
||||||
right: auto;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__clear {
|
|
||||||
right: auto;
|
|
||||||
left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__spinner {
|
|
||||||
right: auto;
|
|
||||||
left: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spinning {
|
|
||||||
from {
|
|
||||||
transform: rotate(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: rotate(2turn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__tags {
|
|
||||||
.remove-assignee {
|
|
||||||
vertical-align: bottom;
|
|
||||||
color: $red;
|
|
||||||
margin-left: -1.8em;
|
|
||||||
background: $white;
|
|
||||||
padding: 0 4px;
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 100%;
|
|
||||||
font-size: .8em;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,10 +28,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sharables-list, .sharables-namespace {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-add .button {
|
.task-add .button {
|
||||||
padding: 10px 1em;
|
padding: 10px 1em;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
|
@ -117,32 +117,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.labels-list, .assignees {
|
&.labels-list, .assignees {
|
||||||
.multiselect__tags {
|
.multiselect {
|
||||||
padding: 3px 0 0 3px;
|
.input-wrapper {
|
||||||
border: none;
|
&:not(:focus-within):not(:hover) {
|
||||||
background: transparent;
|
background: transparent !important;
|
||||||
}
|
border-color: transparent !important;
|
||||||
|
}
|
||||||
.multiselect__input, .multiselect__single {
|
}
|
||||||
width: auto !important;
|
|
||||||
margin: 0;
|
|
||||||
padding: .35em !important;
|
|
||||||
position: relative !important;
|
|
||||||
background: transparent;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__placeholder {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__select {
|
|
||||||
// We may need to enable this since it may also be responsable for showing the loading spinner
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__content-wrapper {
|
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -128,10 +128,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.field.has-addons {
|
.field.has-addons .control .select select {
|
||||||
.control .select select {
|
height: 100%;
|
||||||
height: 2.25em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bigbuttons {
|
.bigbuttons {
|
||||||
|
|
|
@ -58,6 +58,10 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.has-overflow {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.image.is-avatar {
|
.image.is-avatar {
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card has-overflow">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title">
|
<p class="card-header-title">
|
||||||
Duplicate this list
|
Duplicate this list
|
||||||
|
@ -108,22 +108,20 @@
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>Select a namespace which should hold the duplicated list:</p>
|
<p>Select a namespace which should hold the duplicated list:</p>
|
||||||
<div class="field is-grouped">
|
|
||||||
<p class="control is-expanded">
|
<div class="field has-addons">
|
||||||
|
<div class="control is-expanded">
|
||||||
<namespace-search @selected="selectNamespace"/>
|
<namespace-search @selected="selectNamespace"/>
|
||||||
</p>
|
</div>
|
||||||
<p class="control">
|
<div class="control">
|
||||||
<button
|
<button
|
||||||
:class="{'is-loading': listDuplicateService.loading}"
|
:class="{'is-loading': listDuplicateService.loading}"
|
||||||
@click="duplicateList"
|
@click="duplicateList"
|
||||||
class="button is-success"
|
class="button is-primary"
|
||||||
type="submit">
|
type="submit">
|
||||||
<span class="icon is-small">
|
Duplicate
|
||||||
<icon icon="plus"/>
|
|
||||||
</span>
|
|
||||||
Add
|
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="loader-container is-max-width-desktop" v-bind:class="{ 'is-loading': teamService.loading}">
|
<div class="loader-container is-max-width-desktop" :class="{ 'is-loading': teamService.loading}">
|
||||||
<div class="card is-fullwidth" v-if="userIsAdmin">
|
<div class="card is-fullwidth" v-if="userIsAdmin">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title">
|
<p class="card-header-title">
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<form @submit.prevent="submit()">
|
<form @submit.prevent="save()">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="teamtext">Team Name</label>
|
<label class="label" for="teamtext">Team Name</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
|
|
||||||
<div class="columns bigbuttons">
|
<div class="columns bigbuttons">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button :class="{ 'is-loading': teamService.loading}" @click="submit()"
|
<button :class="{ 'is-loading': teamService.loading}" @click="save()"
|
||||||
class="button is-primary is-fullwidth">
|
class="button is-primary is-fullwidth">
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
@ -60,96 +60,75 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card is-fullwidth">
|
<div class="card is-fullwidth has-overflow">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title">
|
<p class="card-header-title">
|
||||||
Team Members
|
Team Members
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content content team-members">
|
<div class="card-content" v-if="userIsAdmin">
|
||||||
<form @submit.prevent="addUser()" class="add-member-form" v-if="userIsAdmin">
|
<div class="field has-addons">
|
||||||
<div class="field is-grouped">
|
<div class="control is-expanded">
|
||||||
<p
|
<multiselect
|
||||||
:class="{ 'is-loading': teamMemberService.loading}"
|
:loading="userService.loading"
|
||||||
class="control has-icons-left is-expanded">
|
placeholder="Type to search..."
|
||||||
<multiselect
|
@search="findUser"
|
||||||
:internal-search="true"
|
:search-results="foundUsers"
|
||||||
:loading="userService.loading"
|
label="username"
|
||||||
:multiple="false"
|
v-model="newMember"
|
||||||
:options="foundUsers"
|
/>
|
||||||
:searchable="true"
|
|
||||||
:showNoOptions="false"
|
|
||||||
@search-change="findUser"
|
|
||||||
label="username"
|
|
||||||
placeholder="Type to search..."
|
|
||||||
track-by="id"
|
|
||||||
v-model="newMember">
|
|
||||||
<template slot="clear" slot-scope="props">
|
|
||||||
<div
|
|
||||||
@mousedown.prevent.stop="clearAll(props.search)" class="multiselect__clear"
|
|
||||||
v-if="newMember !== null && newMember.id !== 0">
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<span slot="noResult">Oops! No user 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>
|
</div>
|
||||||
</form>
|
<div class="control">
|
||||||
<table class="table is-striped is-hoverable is-fullwidth">
|
<button class="button is-primary" @click="addUser">
|
||||||
<tbody>
|
<span class="icon is-small">
|
||||||
<tr :key="m.id" v-for="m in team.members">
|
<icon icon="plus"/>
|
||||||
<td>{{ m.getDisplayName() }}</td>
|
</span>
|
||||||
<td>
|
Add To Team
|
||||||
<template v-if="m.id === userInfo.id">
|
</button>
|
||||||
<b class="is-success">You</b>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</td>
|
|
||||||
<td class="type">
|
|
||||||
<template v-if="m.admin">
|
|
||||||
<span class="icon is-small">
|
|
||||||
<icon icon="lock"/>
|
|
||||||
</span>
|
|
||||||
Admin
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<span class="icon is-small">
|
|
||||||
<icon icon="user"/>
|
|
||||||
</span>
|
|
||||||
Member
|
|
||||||
</template>
|
|
||||||
</td>
|
|
||||||
<td class="actions" v-if="userIsAdmin">
|
|
||||||
<button :class="{'is-loading': teamMemberService.loading}" @click="toggleUserType(m)"
|
|
||||||
class="button buttonright is-primary"
|
|
||||||
v-if="m.id !== userInfo.id">
|
|
||||||
Make
|
|
||||||
<template v-if="!m.admin">
|
|
||||||
Admin
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
Member
|
|
||||||
</template>
|
|
||||||
</button>
|
|
||||||
<button :class="{'is-loading': teamMemberService.loading}" @click="() => {member = m; showUserDeleteModal = true}"
|
|
||||||
class="button is-danger"
|
|
||||||
v-if="m.id !== userInfo.id">
|
|
||||||
<span class="icon is-small">
|
|
||||||
<icon icon="trash-alt"/>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
<table class="table is-striped is-hoverable is-fullwidth">
|
||||||
|
<tbody>
|
||||||
|
<tr :key="m.id" v-for="m in team.members">
|
||||||
|
<td>{{ m.getDisplayName() }}</td>
|
||||||
|
<td>
|
||||||
|
<template v-if="m.id === userInfo.id">
|
||||||
|
<b class="is-success">You</b>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<td class="type">
|
||||||
|
<template v-if="m.admin">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<icon icon="lock"/>
|
||||||
|
</span>
|
||||||
|
Admin
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="icon is-small">
|
||||||
|
<icon icon="user"/>
|
||||||
|
</span>
|
||||||
|
Member
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<td class="actions" v-if="userIsAdmin">
|
||||||
|
<button :class="{'is-loading': teamMemberService.loading}" @click="() => toggleUserType(m)"
|
||||||
|
class="button buttonright is-primary"
|
||||||
|
v-if="m.id !== userInfo.id">
|
||||||
|
Make {{ m.admin ? 'Member' : 'Admin' }}
|
||||||
|
</button>
|
||||||
|
<button :class="{'is-loading': teamMemberService.loading}"
|
||||||
|
@click="() => {member = m; showUserDeleteModal = true}"
|
||||||
|
class="button is-danger"
|
||||||
|
v-if="m.id !== userInfo.id">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<icon icon="trash-alt"/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Team delete modal -->
|
<!-- Team delete modal -->
|
||||||
|
@ -185,9 +164,12 @@ import TeamMemberService from '../../services/teamMember'
|
||||||
import TeamMemberModel from '../../models/teamMember'
|
import TeamMemberModel from '../../models/teamMember'
|
||||||
import UserModel from '../../models/user'
|
import UserModel from '../../models/user'
|
||||||
import UserService from '../../services/user'
|
import UserService from '../../services/user'
|
||||||
|
import Rights from '../../models/rights.json'
|
||||||
|
|
||||||
import LoadingComponent from '../../components/misc/loading'
|
import LoadingComponent from '../../components/misc/loading'
|
||||||
import ErrorComponent from '../../components/misc/error'
|
import ErrorComponent from '../../components/misc/error'
|
||||||
import Rights from '../../models/rights.json'
|
|
||||||
|
import Multiselect from '@/components/input/multiselect'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'EditTeam',
|
name: 'EditTeam',
|
||||||
|
@ -210,12 +192,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
multiselect: () => ({
|
Multiselect,
|
||||||
component: import(/* webpackChunkName: "multiselect" */ 'vue-multiselect'),
|
|
||||||
loading: LoadingComponent,
|
|
||||||
error: ErrorComponent,
|
|
||||||
timeout: 60000,
|
|
||||||
}),
|
|
||||||
editor: () => ({
|
editor: () => ({
|
||||||
component: import(/* webpackChunkName: "editor" */ '../../components/input/editor'),
|
component: import(/* webpackChunkName: "editor" */ '../../components/input/editor'),
|
||||||
loading: LoadingComponent,
|
loading: LoadingComponent,
|
||||||
|
@ -253,7 +230,7 @@ export default {
|
||||||
this.error(e, this)
|
this.error(e, this)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
submit() {
|
save() {
|
||||||
if (this.team.name === '') {
|
if (this.team.name === '') {
|
||||||
this.showError = true
|
this.showError = true
|
||||||
return
|
return
|
||||||
|
@ -308,6 +285,7 @@ export default {
|
||||||
},
|
},
|
||||||
toggleUserType(member) {
|
toggleUserType(member) {
|
||||||
member.admin = !member.admin
|
member.admin = !member.admin
|
||||||
|
member.teamId = this.teamId
|
||||||
this.teamMemberService.update(member)
|
this.teamMemberService.update(member)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
for (const tm in this.team.members) {
|
for (const tm in this.team.members) {
|
||||||
|
@ -342,21 +320,3 @@ export default {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.card {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
.add-member-form {
|
|
||||||
margin: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.team-members {
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
.table {
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -15173,11 +15173,6 @@ vue-loader@^15.9.2:
|
||||||
vue-hot-reload-api "^2.3.0"
|
vue-hot-reload-api "^2.3.0"
|
||||||
vue-style-loader "^4.1.0"
|
vue-style-loader "^4.1.0"
|
||||||
|
|
||||||
vue-multiselect@2.1.6:
|
|
||||||
version "2.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/vue-multiselect/-/vue-multiselect-2.1.6.tgz#5be5d811a224804a15c43a4edbb7485028a89c7f"
|
|
||||||
integrity sha512-s7jmZPlm9FeueJg1RwJtnE9KNPtME/7C8uRWSfp9/yEN4M8XcS/d+bddoyVwVnvFyRh9msFo0HWeW0vTL8Qv+w==
|
|
||||||
|
|
||||||
vue-notification@1.3.20:
|
vue-notification@1.3.20:
|
||||||
version "1.3.20"
|
version "1.3.20"
|
||||||
resolved "https://registry.yarnpkg.com/vue-notification/-/vue-notification-1.3.20.tgz#d85618127763b46f3e25b8962b857947d5a97cbe"
|
resolved "https://registry.yarnpkg.com/vue-notification/-/vue-notification-1.3.20.tgz#d85618127763b46f3e25b8962b857947d5a97cbe"
|
||||||
|
|
Loading…
Reference in a new issue