Merge branch 'main' into feature/vite
# Conflicts: # yarn.lock
This commit is contained in:
commit
b1afc5b1b7
|
@ -471,7 +471,7 @@ steps:
|
|||
image: plugins/manifest
|
||||
settings:
|
||||
tags: latest
|
||||
spec: docker-manifest.tmpl
|
||||
spec: docker-manifest-latest.tmpl
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
|
|
|
@ -102,6 +102,44 @@ describe('Lists', () => {
|
|||
cy.get('input.input[placeholder="Add a new task..."')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('Should only show the color of a list in the navigation and not in the list view', () => {
|
||||
const lists = ListFactory.create(1, {
|
||||
hex_color: '00db60',
|
||||
})
|
||||
TaskFactory.create(10, {
|
||||
list_id: lists[0].id,
|
||||
})
|
||||
cy.visit(`/lists/${lists[0].id}/`)
|
||||
|
||||
cy.get('.menu-list li .list-menu-link .color-bubble')
|
||||
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
|
||||
cy.get('.tasks-container .tasks .color-bubble')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('Should paginate for > 50 tasks', () => {
|
||||
const tasks = TaskFactory.create(100, {
|
||||
id: '{increment}',
|
||||
title: i => `task${i}`,
|
||||
list_id: 1,
|
||||
})
|
||||
cy.visit('/lists/1/list')
|
||||
|
||||
cy.get('.tasks-container .tasks')
|
||||
.should('contain', tasks[99].title)
|
||||
|
||||
cy.get('.card-content .pagination .pagination-link')
|
||||
.contains('2')
|
||||
.click()
|
||||
|
||||
cy.url()
|
||||
.should('contain', '?page=2')
|
||||
cy.get('.tasks-container .tasks')
|
||||
.should('contain', tasks[1].title)
|
||||
cy.get('.tasks-container .tasks')
|
||||
.should('not.contain', tasks[99].title)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Table View', () => {
|
||||
|
@ -371,5 +409,39 @@ describe('Lists', () => {
|
|||
cy.url()
|
||||
.should('contain', `/tasks/${tasks[0].id}`)
|
||||
})
|
||||
|
||||
it('Should remove a task from the kanban board when moving it to another list', () => {
|
||||
const lists = ListFactory.create(2)
|
||||
const tasks = TaskFactory.create(5, {
|
||||
id: '{increment}',
|
||||
list_id: 1,
|
||||
bucket_id: 1,
|
||||
})
|
||||
const task = tasks[0]
|
||||
cy.visit('/lists/1/kanban')
|
||||
|
||||
cy.getAttached('.kanban .bucket .tasks .task')
|
||||
.contains(task.title)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
|
||||
cy.get('.task-view .action-buttons .button')
|
||||
.contains('Move task')
|
||||
.click()
|
||||
cy.get('.task-view .content.details .field .multiselect.control .input-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('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.go('back')
|
||||
cy.get('.kanban .bucket')
|
||||
.should('not.contain', task.title)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -27,6 +27,10 @@ export class Factory {
|
|||
for (let i = 1; i <= count; i++) {
|
||||
const entry = merge(this.factory(), override)
|
||||
for (const e in entry) {
|
||||
if(typeof entry[e] === 'function') {
|
||||
entry[e] = entry[e](i)
|
||||
continue
|
||||
}
|
||||
if (entry[e] === '{increment}') {
|
||||
entry[e] = i
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
image: vikunja/frontend:latest
|
||||
manifests:
|
||||
-
|
||||
image: vikunja/frontend:latest-linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/frontend:latest-linux-arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/frontend:latest-linux-arm
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
|
@ -19,7 +19,7 @@
|
|||
"date-fns": "2.17.0",
|
||||
"dompurify": "2.2.6",
|
||||
"highlight.js": "10.6.0",
|
||||
"lodash": "4.17.20",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "2.0.0",
|
||||
"register-service-worker": "1.7.2",
|
||||
"snake-case": "3.0.4",
|
||||
|
@ -44,10 +44,10 @@
|
|||
"@vue/cli-service": "4.5.11",
|
||||
"axios": "0.21.1",
|
||||
"babel-eslint": "10.1.0",
|
||||
"cypress": "6.4.0",
|
||||
"cypress": "6.5.0",
|
||||
"cypress-file-upload": "5.0.2",
|
||||
"eslint": "7.20.0",
|
||||
"eslint-plugin-vue": "7.5.0",
|
||||
"eslint-plugin-vue": "7.6.0",
|
||||
"faker": "5.4.0",
|
||||
"jest": "26.6.3",
|
||||
"node-sass": "5.0.0",
|
||||
|
|
|
@ -51,8 +51,8 @@
|
|||
<aside class="menu namespaces-lists loader-container" :class="{'is-loading': loading}">
|
||||
<template v-for="n in namespaces">
|
||||
<div :key="n.id" class="namespace-title">
|
||||
<label
|
||||
:for="n.id + 'checker'"
|
||||
<span
|
||||
@click="toggleLists(n.id)"
|
||||
class="menu-label"
|
||||
v-tooltip="n.title + ' (' + n.lists.filter(l => !l.isArchived).length + ')'">
|
||||
<span class="name">
|
||||
|
@ -63,16 +63,10 @@
|
|||
</span>
|
||||
{{ n.title }} ({{ n.lists.filter(l => !l.isArchived).length }})
|
||||
</span>
|
||||
</label>
|
||||
</span>
|
||||
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
|
||||
</div>
|
||||
<input
|
||||
:id="n.id + 'checker'"
|
||||
:key="n.id + 'checker'"
|
||||
checked="checked"
|
||||
class="checkinput"
|
||||
type="checkbox"/>
|
||||
<div :key="n.id + 'child'" class="more-container">
|
||||
<div :key="n.id + 'child'" class="more-container" v-if="listsVisible[n.id]">
|
||||
<ul class="menu-list can-be-hidden">
|
||||
<template v-for="l in n.lists">
|
||||
<!-- This is a bit ugly but vue wouldn't want to let me filter this - probably because the lists
|
||||
|
@ -104,10 +98,14 @@
|
|||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<label :for="n.id + 'checker'" class="hidden-hint">
|
||||
Show hidden lists ({{ n.lists.filter(l => !l.isArchived).length }})...
|
||||
</label>
|
||||
</div>
|
||||
<span
|
||||
@click="toggleLists(n.id)"
|
||||
:key="`${n.id}_hidden_hint`"
|
||||
class="hidden-hint"
|
||||
v-else-if="n.lists.filter(l => !l.isArchived).length > 0">
|
||||
Show hidden lists ({{ n.lists.filter(l => !l.isArchived).length }})...
|
||||
</span>
|
||||
</template>
|
||||
</aside>
|
||||
<a class="menu-bottom-link" href="https://vikunja.io" target="_blank">Powered by Vikunja</a>
|
||||
|
@ -122,6 +120,11 @@ import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings
|
|||
|
||||
export default {
|
||||
name: 'navigation',
|
||||
data() {
|
||||
return {
|
||||
listsVisible: {},
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ListSettingsDropdown,
|
||||
NamespaceSettingsDropdown,
|
||||
|
@ -137,6 +140,11 @@ export default {
|
|||
}),
|
||||
beforeCreate() {
|
||||
this.$store.dispatch('namespaces/loadNamespaces')
|
||||
.then(namespaces => {
|
||||
namespaces.forEach(n => {
|
||||
this.$set(this.listsVisible, n.id, true)
|
||||
})
|
||||
})
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('resize', this.resize)
|
||||
|
@ -162,6 +170,9 @@ export default {
|
|||
this.$store.commit(MENU_ACTIVE, true)
|
||||
}
|
||||
},
|
||||
toggleLists(namespaceId) {
|
||||
this.$set(this.listsVisible, namespaceId, !this.listsVisible[namespaceId] ?? false)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
|
||||
<div class="navbar-end">
|
||||
<update/>
|
||||
<notifications/>
|
||||
<div class="user">
|
||||
<img :src="userAvatar" alt="" class="avatar"/>
|
||||
<dropdown class="is-right">
|
||||
|
@ -86,10 +87,12 @@ import Rights from '@/models/rights.json'
|
|||
import Update from '@/components/home/update'
|
||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown'
|
||||
import Dropdown from '@/components/misc/dropdown'
|
||||
import Notifications from '@/components/notifications/notifications'
|
||||
|
||||
export default {
|
||||
name: 'topNavigation',
|
||||
components: {
|
||||
Notifications,
|
||||
Dropdown,
|
||||
ListSettingsDropdown,
|
||||
Update,
|
||||
|
|
|
@ -54,6 +54,14 @@
|
|||
>
|
||||
Archive
|
||||
</dropdown-item>
|
||||
<task-subscription
|
||||
class="dropdown-item has-no-shadow"
|
||||
:is-button="false"
|
||||
entity="list"
|
||||
:entity-id="list.id"
|
||||
:subscription="subscription"
|
||||
@change="sub => subscription = sub"
|
||||
/>
|
||||
<dropdown-item
|
||||
:to="{ name: `${listRoutePrefix}.settings.delete`, params: { listId: list.id } }"
|
||||
icon="trash-alt"
|
||||
|
@ -69,10 +77,17 @@
|
|||
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
||||
import Dropdown from '@/components/misc/dropdown'
|
||||
import DropdownItem from '@/components/misc/dropdown-item'
|
||||
import TaskSubscription from '@/components/misc/subscription'
|
||||
|
||||
export default {
|
||||
name: 'list-settings-dropdown',
|
||||
data() {
|
||||
return {
|
||||
subscription: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
TaskSubscription,
|
||||
DropdownItem,
|
||||
Dropdown,
|
||||
},
|
||||
|
@ -81,6 +96,9 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.subscription = this.list.subscription
|
||||
},
|
||||
computed: {
|
||||
backgroundsEnabled() {
|
||||
return this.$store.state.config.enabledBackgroundProviders.length > 0
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<x-button
|
||||
type="secondary"
|
||||
:icon="icon"
|
||||
v-tooltip="tooltipText"
|
||||
@click="changeSubscription"
|
||||
:disabled="disabled"
|
||||
v-if="isButton"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</x-button>
|
||||
<a
|
||||
v-tooltip="tooltipText"
|
||||
@click="changeSubscription"
|
||||
:class="{'is-disabled': disabled}"
|
||||
v-else
|
||||
>
|
||||
<span class="icon">
|
||||
<icon :icon="icon"/>
|
||||
</span>
|
||||
{{ buttonText }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SubscriptionService from '@/services/subscription'
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
|
||||
export default {
|
||||
name: 'task-subscription',
|
||||
data() {
|
||||
return {
|
||||
subscriptionService: SubscriptionService,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
entity: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
subscription: {
|
||||
required: true,
|
||||
},
|
||||
entityId: {
|
||||
required: true,
|
||||
},
|
||||
isButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.subscriptionService = new SubscriptionService()
|
||||
},
|
||||
computed: {
|
||||
tooltipText() {
|
||||
if(this.disabled) {
|
||||
return `You can't unsubscribe here because you are subscribed to this ${this.entity} through its ${this.subscription.entity}.`
|
||||
}
|
||||
|
||||
return this.subscription !== null ?
|
||||
`You are currently subscribed to this ${this.entity} and will receive notifications for changes.` :
|
||||
`You are not subscribed to this ${this.entity} and won't receive notifications for changes.`
|
||||
},
|
||||
buttonText() {
|
||||
return this.subscription !== null ? 'Unsubscribe' : 'Subscribe'
|
||||
},
|
||||
icon() {
|
||||
return this.subscription !== null ? ['far', 'bell-slash'] : 'bell'
|
||||
},
|
||||
disabled() {
|
||||
if (this.subscription === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.subscription.entity !== this.entity
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeSubscription() {
|
||||
if(this.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.subscription === null) {
|
||||
this.subscribe()
|
||||
} else {
|
||||
this.unsubscribe()
|
||||
}
|
||||
},
|
||||
subscribe() {
|
||||
const subscription = new SubscriptionModel({
|
||||
entity: this.entity,
|
||||
entityId: this.entityId,
|
||||
})
|
||||
this.subscriptionService.create(subscription)
|
||||
.then(() => {
|
||||
this.$emit('change', subscription)
|
||||
this.success({message: `You are now subscribed to this ${this.entity}`}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
unsubscribe() {
|
||||
const subscription = new SubscriptionModel({
|
||||
entity: this.entity,
|
||||
entityId: this.entityId,
|
||||
})
|
||||
this.subscriptionService.delete(subscription)
|
||||
.then(() => {
|
||||
this.$emit('change', null)
|
||||
this.success({message: `You are now unsubscribed to this ${this.entity}`}, this)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -33,6 +33,14 @@
|
|||
>
|
||||
Archive
|
||||
</dropdown-item>
|
||||
<task-subscription
|
||||
class="dropdown-item has-no-shadow"
|
||||
:is-button="false"
|
||||
entity="namespace"
|
||||
:entity-id="namespace.id"
|
||||
:subscription="subscription"
|
||||
@change="sub => subscription = sub"
|
||||
/>
|
||||
<dropdown-item
|
||||
:to="{ name: 'namespace.settings.delete', params: { id: namespace.id } }"
|
||||
icon="trash-alt"
|
||||
|
@ -47,17 +55,27 @@
|
|||
<script>
|
||||
import Dropdown from '@/components/misc/dropdown'
|
||||
import DropdownItem from '@/components/misc/dropdown-item'
|
||||
import TaskSubscription from '@/components/misc/subscription'
|
||||
|
||||
export default {
|
||||
name: 'namespace-settings-dropdown',
|
||||
data() {
|
||||
return {
|
||||
subscription: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
DropdownItem,
|
||||
Dropdown,
|
||||
TaskSubscription,
|
||||
},
|
||||
props: {
|
||||
namespace: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.subscription = this.namespace.subscription
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
<template>
|
||||
<div class="notifications">
|
||||
<a @click.stop="showNotifications = !showNotifications" class="trigger">
|
||||
<span class="unread-indicator" v-if="unreadNotifications > 0"></span>
|
||||
<icon icon="bell"/>
|
||||
</a>
|
||||
|
||||
<transition name="fade">
|
||||
<div class="notifications-list" v-if="showNotifications" ref="popup">
|
||||
<span class="head">Notifications</span>
|
||||
<div
|
||||
v-for="(n, index) in notifications"
|
||||
:key="n.id"
|
||||
class="single-notification"
|
||||
>
|
||||
<div class="read-indicator" :class="{'read': n.readAt !== null}"></div>
|
||||
<user
|
||||
:user="n.notification.doer"
|
||||
:show-username="true"
|
||||
:avatar-size="16"
|
||||
v-if="n.notification.doer"/>
|
||||
<span class="detail">
|
||||
<a @click="() => to(n, index)()">
|
||||
{{ n.toText(userInfo) }}
|
||||
</a>
|
||||
<span class="created" v-tooltip="formatDate(n.created)">
|
||||
{{ formatDateSince(n.created) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="nothing" v-if="notifications.length === 0">
|
||||
You don't have any notifications. Have a nice day!<br/>
|
||||
<span class="explainer">
|
||||
Notifications will appear here when actions on namespaces, lists or tasks you subscribed to happen.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotificationService from '@/services/notification'
|
||||
import User from '@/components/misc/user'
|
||||
import names from '@/models/notificationNames.json'
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'notifications',
|
||||
components: {User},
|
||||
data() {
|
||||
return {
|
||||
notificationService: NotificationService,
|
||||
notifications: [],
|
||||
showNotifications: false,
|
||||
interval: null,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.notificationService = new NotificationService()
|
||||
},
|
||||
mounted() {
|
||||
this.loadNotifications()
|
||||
document.addEventListener('click', this.hidePopup)
|
||||
this.interval = setInterval(this.loadNotifications, 10000)
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.hidePopup)
|
||||
clearInterval(this.interval)
|
||||
},
|
||||
computed: {
|
||||
unreadNotifications() {
|
||||
return this.notifications.filter(n => n.readAt === null).length
|
||||
},
|
||||
...mapState({
|
||||
userInfo: state => state.auth.info,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
hidePopup(e) {
|
||||
if (this.showNotifications) {
|
||||
closeWhenClickedOutside(e, this.$refs.popup, () => this.showNotifications = false)
|
||||
}
|
||||
},
|
||||
loadNotifications() {
|
||||
this.notificationService.getAll()
|
||||
.then(r => {
|
||||
this.$set(this, 'notifications', r)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
to(n, index) {
|
||||
const to = {
|
||||
name: '',
|
||||
params: {},
|
||||
}
|
||||
|
||||
switch (n.name) {
|
||||
case names.TASK_COMMENT:
|
||||
case names.TASK_ASSIGNED:
|
||||
to.name = 'task.detail'
|
||||
to.params.id = n.notification.task.id
|
||||
break
|
||||
case names.TASK_DELETED:
|
||||
// Nothing
|
||||
break
|
||||
case names.LIST_CREATED:
|
||||
to.name = 'task.index'
|
||||
to.params.listId = n.notification.list.id
|
||||
break
|
||||
case names.TEAM_MEMBER_ADDED:
|
||||
to.name = 'teams.edit'
|
||||
to.params.id = n.notification.team.id
|
||||
break
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (to.name !== '') {
|
||||
this.$router.push(to)
|
||||
}
|
||||
|
||||
n.read = true
|
||||
this.notificationService.update(n)
|
||||
.then(r => {
|
||||
this.$set(this.notifications, index, r)
|
||||
})
|
||||
.catch(e => this.error(e, this))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,4 +1,5 @@
|
|||
import TaskCollectionService from '../../../services/taskCollection'
|
||||
import {cloneDeep} from 'lodash'
|
||||
|
||||
/**
|
||||
* This mixin provides a base set of methods and properties to get tasks on a list.
|
||||
|
@ -55,19 +56,6 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
const list = {listId: parseInt(this.$route.params.listId)}
|
||||
|
||||
const currentList = {
|
||||
id: list.listId,
|
||||
params: params,
|
||||
search: search,
|
||||
}
|
||||
if (JSON.stringify(currentList) === JSON.stringify(this.loadedList)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$set(this, 'tasks', [])
|
||||
|
||||
if (params === null) {
|
||||
params = this.params
|
||||
}
|
||||
|
@ -76,6 +64,20 @@ export default {
|
|||
params.s = search
|
||||
}
|
||||
|
||||
const list = {listId: parseInt(this.$route.params.listId)}
|
||||
|
||||
const currentList = {
|
||||
id: list.listId,
|
||||
params: params,
|
||||
search: search,
|
||||
page: page,
|
||||
}
|
||||
if (JSON.stringify(currentList) === JSON.stringify(this.loadedList)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$set(this, 'tasks', [])
|
||||
|
||||
this.taskCollectionService.getAll(list, params, page)
|
||||
.then(r => {
|
||||
this.$set(this, 'tasks', r)
|
||||
|
@ -110,7 +112,7 @@ export default {
|
|||
})
|
||||
}
|
||||
|
||||
this.loadedList = currentList
|
||||
this.loadedList = cloneDeep(currentList)
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
<template>
|
||||
<div class="heading">
|
||||
<h1 class="title task-id" v-if="task.identifier === ''">
|
||||
#{{ task.index }}
|
||||
</h1>
|
||||
<h1 class="title task-id" v-else>
|
||||
{{ task.identifier }}
|
||||
<h1 class="title task-id">
|
||||
{{ task.getTextIdentifier() }}
|
||||
</h1>
|
||||
<div class="is-done" v-if="task.done">Done</div>
|
||||
<h1
|
||||
|
|
|
@ -219,7 +219,7 @@ export default {
|
|||
})
|
||||
},
|
||||
removeTaskRelation() {
|
||||
let rel = new TaskRelationModel({
|
||||
const rel = new TaskRelationModel({
|
||||
relationKind: this.relationToDelete.relationKind,
|
||||
taskId: this.taskId,
|
||||
otherTaskId: this.relationToDelete.otherTaskId,
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
<div :class="{'is-loading': taskService.loading}" class="task loader-container">
|
||||
<fancycheckbox :disabled="isArchived || disabled" @change="markAsDone" v-model="task.done"/>
|
||||
<span
|
||||
v-if="showListColor && listColor !== ''"
|
||||
:style="{backgroundColor: listColor }"
|
||||
class="color-bubble"
|
||||
v-if="listColor !== ''">
|
||||
>
|
||||
</span>
|
||||
<router-link
|
||||
:to="{ name: taskDetailRoute, params: { id: task.id } }"
|
||||
|
@ -131,6 +132,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showListColor: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
theTask(newVal) {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export const parseDateOrNull = date => {
|
||||
if (date && !date.startsWith('0001')) {
|
||||
return new Date(date)
|
||||
}
|
||||
return null
|
||||
}
|
|
@ -61,8 +61,9 @@ import {
|
|||
faArchive,
|
||||
faShareAlt,
|
||||
faImage,
|
||||
faBell,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle, faSun} from '@fortawesome/free-regular-svg-icons'
|
||||
import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle, faSun, faBellSlash} from '@fortawesome/free-regular-svg-icons'
|
||||
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
|
||||
// PWA
|
||||
import './registerServiceWorker'
|
||||
|
@ -152,6 +153,8 @@ library.add(faEllipsisH)
|
|||
library.add(faArchive)
|
||||
library.add(faShareAlt)
|
||||
library.add(faImage)
|
||||
library.add(faBell)
|
||||
library.add(faBellSlash)
|
||||
|
||||
Vue.component('icon', FontAwesomeIcon)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import AbstractModel from './abstractModel'
|
|||
import TaskModel from './task'
|
||||
import UserModel from './user'
|
||||
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
|
||||
export default class ListModel extends AbstractModel {
|
||||
|
||||
|
@ -19,6 +20,10 @@ export default class ListModel extends AbstractModel {
|
|||
|
||||
this.owner = new UserModel(this.owner)
|
||||
|
||||
if(typeof this.subscription !== 'undefined' && this.subscription !== null) {
|
||||
this.subscription = new SubscriptionModel(this.subscription)
|
||||
}
|
||||
|
||||
this.created = new Date(this.created)
|
||||
this.updated = new Date(this.updated)
|
||||
}
|
||||
|
@ -37,6 +42,7 @@ export default class ListModel extends AbstractModel {
|
|||
identifier: '',
|
||||
backgroundInformation: null,
|
||||
isFavorite: false,
|
||||
subscription: null,
|
||||
|
||||
created: null,
|
||||
updated: null,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import AbstractModel from './abstractModel'
|
||||
import ListModel from './list'
|
||||
import UserModel from './user'
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
|
||||
export default class NamespaceModel extends AbstractModel {
|
||||
constructor(data) {
|
||||
|
@ -13,8 +14,13 @@ export default class NamespaceModel extends AbstractModel {
|
|||
this.lists = this.lists.map(l => {
|
||||
return new ListModel(l)
|
||||
})
|
||||
|
||||
this.owner = new UserModel(this.owner)
|
||||
|
||||
if(typeof this.subscription !== 'undefined' && this.subscription !== null) {
|
||||
this.subscription = new SubscriptionModel(this.subscription)
|
||||
}
|
||||
|
||||
this.created = new Date(this.created)
|
||||
this.updated = new Date(this.updated)
|
||||
}
|
||||
|
@ -29,6 +35,7 @@ export default class NamespaceModel extends AbstractModel {
|
|||
lists: [],
|
||||
isArchived: false,
|
||||
hexColor: '',
|
||||
subscription: null,
|
||||
|
||||
created: null,
|
||||
updated: null,
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import AbstractModel from '@/models/abstractModel'
|
||||
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
||||
import UserModel from '@/models/user'
|
||||
import TaskModel from '@/models/task'
|
||||
import TaskCommentModel from '@/models/taskComment'
|
||||
import ListModel from '@/models/list'
|
||||
import TeamModel from '@/models/team'
|
||||
import names from './notificationNames.json'
|
||||
|
||||
export default class NotificationModel extends AbstractModel {
|
||||
constructor(data) {
|
||||
super(data)
|
||||
|
||||
switch (this.name) {
|
||||
case names.TASK_COMMENT:
|
||||
this.notification.doer = new UserModel(this.notification.doer)
|
||||
this.notification.task = new TaskModel(this.notification.task)
|
||||
this.notification.comment = new TaskCommentModel(this.notification.comment)
|
||||
break
|
||||
case names.TASK_ASSIGNED:
|
||||
this.notification.doer = new UserModel(this.notification.doer)
|
||||
this.notification.task = new TaskModel(this.notification.task)
|
||||
this.notification.assignee = new UserModel(this.notification.assignee)
|
||||
break
|
||||
case names.TASK_DELETED:
|
||||
this.notification.doer = new UserModel(this.notification.doer)
|
||||
this.notification.task = new TaskModel(this.notification.task)
|
||||
break
|
||||
case names.LIST_CREATED:
|
||||
this.notification.doer = new UserModel(this.notification.doer)
|
||||
this.notification.list = new ListModel(this.notification.list)
|
||||
break
|
||||
case names.TEAM_MEMBER_ADDED:
|
||||
this.notification.doer = new UserModel(this.notification.doer)
|
||||
this.notification.member = new UserModel(this.notification.member)
|
||||
this.notification.team = new TeamModel(this.notification.team)
|
||||
break
|
||||
}
|
||||
|
||||
this.created = new Date(this.created)
|
||||
this.readAt = parseDateOrNull(this.readAt)
|
||||
}
|
||||
|
||||
defaults() {
|
||||
return {
|
||||
id: 0,
|
||||
name: '',
|
||||
notification: null,
|
||||
read: false,
|
||||
readAt: null,
|
||||
}
|
||||
}
|
||||
|
||||
toText(user = null) {
|
||||
let who = ''
|
||||
|
||||
switch (this.name) {
|
||||
case names.TASK_COMMENT:
|
||||
return `commented on ${this.notification.task.getTextIdentifier()}`
|
||||
case names.TASK_ASSIGNED:
|
||||
who = `${this.notification.assignee.getDisplayName()}`
|
||||
|
||||
if (user !== null && user.id === this.notification.assignee.id) {
|
||||
who = 'you'
|
||||
}
|
||||
|
||||
return `assigned ${who} to ${this.notification.task.getTextIdentifier()}`
|
||||
case names.TASK_DELETED:
|
||||
return `deleted ${this.notification.task.getTextIdentifier()}`
|
||||
case names.LIST_CREATED:
|
||||
return `created ${this.notification.list.title}`
|
||||
case names.TEAM_MEMBER_ADDED:
|
||||
who = `${this.notification.member.getDisplayName()}`
|
||||
|
||||
if (user !== null && user.id === this.notification.member.id) {
|
||||
who = 'you'
|
||||
}
|
||||
|
||||
return `added ${who} to the ${this.notification.team.name} team`
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"TASK_COMMENT": "task.comment",
|
||||
"TASK_ASSIGNED": "task.assigned",
|
||||
"TASK_DELETED": "task.deleted",
|
||||
"LIST_CREATED": "list.created",
|
||||
"TEAM_MEMBER_ADDED": "team.member.added"
|
||||
}
|
|
@ -28,8 +28,8 @@
|
|||
"Blocked By"
|
||||
],
|
||||
"precedes": [
|
||||
"Preceds",
|
||||
"Preceds"
|
||||
"Precedes",
|
||||
"Precedes"
|
||||
],
|
||||
"follows": [
|
||||
"Follows",
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import AbstractModel from '@/models/abstractModel'
|
||||
import UserModel from '@/models/user'
|
||||
|
||||
export default class SubscriptionModel extends AbstractModel {
|
||||
constructor(data) {
|
||||
super(data)
|
||||
this.user = new UserModel(this.user)
|
||||
this.created = new Date(this.created)
|
||||
}
|
||||
|
||||
defaults() {
|
||||
return {
|
||||
id: 0,
|
||||
entity: '',
|
||||
entityId: 0,
|
||||
created: null,
|
||||
user: {},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,13 +2,8 @@ import AbstractModel from './abstractModel'
|
|||
import UserModel from './user'
|
||||
import LabelModel from './label'
|
||||
import AttachmentModel from './attachment'
|
||||
|
||||
const parseDate = date => {
|
||||
if (date && !date.startsWith('0001')) {
|
||||
return new Date(date)
|
||||
}
|
||||
return null
|
||||
}
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
||||
|
||||
export default class TaskModel extends AbstractModel {
|
||||
|
||||
|
@ -21,10 +16,10 @@ export default class TaskModel extends AbstractModel {
|
|||
this.listId = Number(this.listId)
|
||||
|
||||
// Make date objects from timestamps
|
||||
this.dueDate = parseDate(this.dueDate)
|
||||
this.startDate = parseDate(this.startDate)
|
||||
this.endDate = parseDate(this.endDate)
|
||||
this.doneAt = parseDate(this.doneAt)
|
||||
this.dueDate = parseDateOrNull(this.dueDate)
|
||||
this.startDate = parseDateOrNull(this.startDate)
|
||||
this.endDate = parseDateOrNull(this.endDate)
|
||||
this.doneAt = parseDateOrNull(this.doneAt)
|
||||
|
||||
// Cancel all scheduled notifications for this task to be sure to only have available notifications
|
||||
this.cancelScheduledNotifications()
|
||||
|
@ -75,6 +70,10 @@ export default class TaskModel extends AbstractModel {
|
|||
this.identifier = ''
|
||||
}
|
||||
|
||||
if (typeof this.subscription !== 'undefined' && this.subscription !== null) {
|
||||
this.subscription = new SubscriptionModel(this.subscription)
|
||||
}
|
||||
|
||||
this.created = new Date(this.created)
|
||||
this.updated = new Date(this.updated)
|
||||
}
|
||||
|
@ -104,6 +103,7 @@ export default class TaskModel extends AbstractModel {
|
|||
identifier: '',
|
||||
index: 0,
|
||||
isFavorite: false,
|
||||
subscription: null,
|
||||
|
||||
createdBy: UserModel,
|
||||
created: null,
|
||||
|
@ -113,6 +113,14 @@ export default class TaskModel extends AbstractModel {
|
|||
}
|
||||
}
|
||||
|
||||
getTextIdentifier() {
|
||||
if(this.identifier === '') {
|
||||
return `#${this.index}`
|
||||
}
|
||||
|
||||
return this.identifier
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Helper functions
|
||||
///////////////
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import AbstractService from '@/services/abstractService'
|
||||
import {formatISO} from 'date-fns'
|
||||
import NotificationModel from '@/models/notification'
|
||||
|
||||
export default class NotificationService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
getAll: '/notifications',
|
||||
update: '/notifications/{id}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new NotificationModel(data)
|
||||
}
|
||||
|
||||
beforeUpdate(model) {
|
||||
model.created = formatISO(new Date(model.created))
|
||||
model.readAt = formatISO(new Date(model.readAt))
|
||||
return model
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import AbstractService from '@/services/abstractService'
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
|
||||
export default class SubscriptionService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/subscriptions/{entity}/{entityId}',
|
||||
delete: '/subscriptions/{entity}/{entityId}',
|
||||
})
|
||||
}
|
||||
|
||||
modelFactory(data) {
|
||||
return new SubscriptionModel(data)
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ export default class TaskRelationService extends AbstractService {
|
|||
constructor() {
|
||||
super({
|
||||
create: '/tasks/{taskId}/relations',
|
||||
delete: '/tasks/{taskId}/relations',
|
||||
delete: '/tasks/{taskId}/relations/{relationKind}/{otherTaskId}',
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@ export default {
|
|||
|
||||
ctx.commit('lists/addLists', lists, {root: true})
|
||||
|
||||
return Promise.resolve()
|
||||
return Promise.resolve(r)
|
||||
})
|
||||
.catch(e => {
|
||||
return Promise.reject(e)
|
||||
|
|
|
@ -22,3 +22,4 @@
|
|||
@import 'keyboard-shortcuts';
|
||||
@import 'api-config';
|
||||
@import 'datepicker';
|
||||
@import 'notifications';
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
.notifications {
|
||||
width: 50px;
|
||||
|
||||
.trigger {
|
||||
cursor: pointer;
|
||||
color: $grey-400;
|
||||
padding: 1rem;
|
||||
font-size: 1.25rem;
|
||||
position: relative;
|
||||
|
||||
.unread-indicator {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: .75rem;
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
|
||||
background: $primary;
|
||||
border-radius: 100%;
|
||||
border: 2px solid $white;
|
||||
}
|
||||
}
|
||||
|
||||
.notifications-list {
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
margin-top: 1rem;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
|
||||
background: $white;
|
||||
width: 350px;
|
||||
max-width: calc(100vw - 2rem);
|
||||
padding: .75rem .25rem;
|
||||
border-radius: $radius;
|
||||
box-shadow: $shadow-sm;
|
||||
font-size: .85rem;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
max-height: calc(100vh - 1rem - #{$navbar-height});
|
||||
}
|
||||
|
||||
.head {
|
||||
font-family: $vikunja-font;
|
||||
font-size: 1rem;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.single-notification {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
transition: background-color $transition;
|
||||
|
||||
&:hover {
|
||||
background: $grey-100;
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
.read-indicator {
|
||||
width: .35rem;
|
||||
height: .35rem;
|
||||
background: $primary;
|
||||
border-radius: 100%;
|
||||
margin-left: .5rem;
|
||||
|
||||
&.read {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
margin-right: .25rem;
|
||||
|
||||
span {
|
||||
font-family: $family-sans-serif;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail .created {
|
||||
color: $grey-400;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $grey-800;
|
||||
}
|
||||
}
|
||||
|
||||
.nothing {
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
color: $grey-500;
|
||||
|
||||
.explainer {
|
||||
font-size: .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@
|
|||
.navbar-end {
|
||||
margin-left: 0;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $desktop) {
|
||||
|
@ -194,40 +195,15 @@
|
|||
color: $vikunja-nav-color;
|
||||
}
|
||||
|
||||
.checkinput {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checkinput:checked + .more-container {
|
||||
.menu-list.can-be-hidden {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.checkinput:not(:checked) + .more-container .hidden-hint {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.hidden-hint {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
text-align: center;
|
||||
color: $grey-500;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
&.can-be-hidden {
|
||||
transition: all $transition;
|
||||
height: 0;
|
||||
//overflow: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
|
|
|
@ -120,6 +120,7 @@ export default {
|
|||
this.success({message: 'The filter was saved successfully.'}, this)
|
||||
this.filter = r
|
||||
this.filters = objectToSnakeCase(this.filter.filters)
|
||||
this.$router.back()
|
||||
})
|
||||
.catch(e => this.error(e, this))
|
||||
},
|
||||
|
|
|
@ -117,6 +117,7 @@ export default {
|
|||
.then(r => {
|
||||
this.$store.commit('namespaces/setListInNamespaceById', r)
|
||||
this.success({message: 'The list was successfully updated.'}, this)
|
||||
this.$router.back()
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
<div class="tasks-container">
|
||||
<div :class="{'short': isTaskEdit}" class="tasks mt-0" v-if="tasks && tasks.length > 0">
|
||||
<single-task-in-list
|
||||
:show-list-color="false"
|
||||
:disabled="!canWrite"
|
||||
:key="t.id"
|
||||
:the-task="t"
|
||||
|
|
|
@ -127,6 +127,7 @@ export default {
|
|||
// Update the namespace in the parent
|
||||
this.$store.commit('namespaces/setNamespaceById', r)
|
||||
this.success({message: 'The namespace was successfully updated.'}, this)
|
||||
this.$router.back()
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
<x-button type="secondary" @click="setDatesToNextWeek()" class="mr-2">Next Week</x-button>
|
||||
<x-button type="secondary" @click="setDatesToNextMonth()">Next Month</x-button>
|
||||
</div>
|
||||
<template v-if="!taskService.loading && (!hasUndoneTasks || !tasks || tasks.length === 0) && showNothingToDo">
|
||||
<template v-if="!taskService.loading && (!tasks || tasks.length === 0) && showNothingToDo">
|
||||
<h3 class="nothing">Nothing to do - Have a nice day!</h3>
|
||||
<img alt="" src="/images/cool.svg"/>
|
||||
</template>
|
||||
|
@ -73,7 +73,6 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
tasks: [],
|
||||
hasUndoneTasks: false,
|
||||
taskService: TaskService,
|
||||
showNulls: true,
|
||||
showOverdue: false,
|
||||
|
@ -139,7 +138,7 @@ export default {
|
|||
|
||||
const params = {
|
||||
sort_by: ['due_date', 'id'],
|
||||
order_by: ['asc', 'desc'],
|
||||
order_by: ['desc', 'desc'],
|
||||
filter_by: ['done'],
|
||||
filter_value: [false],
|
||||
filter_comparator: ['equals'],
|
||||
|
@ -170,14 +169,19 @@ export default {
|
|||
|
||||
this.taskService.getAll({}, params)
|
||||
.then(r => {
|
||||
if (r.length > 0) {
|
||||
for (const index in r) {
|
||||
if (r[index].done !== true) {
|
||||
this.hasUndoneTasks = true
|
||||
}
|
||||
}
|
||||
}
|
||||
this.$set(this, 'tasks', r.filter(t => !t.done))
|
||||
|
||||
// Sort all tasks to put those with a due date before the ones without a due date, the
|
||||
// soonest before the later ones.
|
||||
// We can't use the api sorting here because that sorts tasks with a due date after
|
||||
// ones without a due date.
|
||||
r.sort((a, b) => {
|
||||
return a.dueDate === null && b.dueDate === null ? -1 : 1
|
||||
})
|
||||
const tasks = r.
|
||||
filter(t => t.dueDate !== null).
|
||||
concat(r.filter(t => t.dueDate === null))
|
||||
|
||||
this.$set(this, 'tasks', tasks)
|
||||
this.$store.commit(HAS_TASKS, r.length > 0)
|
||||
})
|
||||
.catch(e => {
|
||||
|
|
|
@ -255,6 +255,12 @@
|
|||
>
|
||||
{{ task.done ? 'Mark as undone' : 'Done!' }}
|
||||
</x-button>
|
||||
<task-subscription
|
||||
entity="task"
|
||||
:entity-id="task.id"
|
||||
:subscription="task.subscription"
|
||||
@change="sub => task.subscription = sub"
|
||||
/>
|
||||
<x-button
|
||||
@click="setFieldActive('assignees')"
|
||||
@shortkey="setFieldActive('assignees')"
|
||||
|
@ -422,10 +428,12 @@ import attachmentUpload from '../../components/tasks/mixins/attachmentUpload'
|
|||
import heading from '@/components/tasks/partials/heading'
|
||||
import Datepicker from '@/components/input/datepicker'
|
||||
import {playPop} from '@/helpers/playPop'
|
||||
import TaskSubscription from '@/components/misc/subscription'
|
||||
|
||||
export default {
|
||||
name: 'TaskDetailView',
|
||||
components: {
|
||||
TaskSubscription,
|
||||
Datepicker,
|
||||
ColorPicker,
|
||||
ListSearch,
|
||||
|
@ -671,6 +679,7 @@ export default {
|
|||
changeList(list) {
|
||||
this.task.listId = list.id
|
||||
this.saveTask()
|
||||
this.$store.commit('kanban/removeTaskInBucket', this.task)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import router from '../../router'
|
||||
import TeamModel from '../../models/team'
|
||||
import TeamService from '../../services/team'
|
||||
import CreateEdit from '@/components/misc/create-edit'
|
||||
|
@ -64,7 +63,7 @@ export default {
|
|||
this.teamService
|
||||
.create(this.team)
|
||||
.then((response) => {
|
||||
router.push({
|
||||
this.$router.push({
|
||||
name: 'teams.edit',
|
||||
params: { id: response.id },
|
||||
})
|
||||
|
@ -77,9 +76,6 @@ export default {
|
|||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
back() {
|
||||
router.go(-1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
65
yarn.lock
65
yarn.lock
|
@ -2599,6 +2599,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7"
|
||||
integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==
|
||||
|
||||
"@types/node@12.12.50":
|
||||
version "12.12.50"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.50.tgz#e9b2e85fafc15f2a8aa8fdd41091b983da5fd6ee"
|
||||
integrity sha512-5ImO01Fb8YsEOYpV+aeyGYztcYcjGsBvN4D7G5r1ef2cuQOpymjWNQi5V0rKHE6PC2ru3HkoUr/Br2/8GUA84w==
|
||||
|
||||
"@types/node@^10.1.0":
|
||||
version "10.17.19"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.19.tgz#1d31ddd5503dba2af7a901aafef3392e4955620e"
|
||||
|
@ -6778,14 +6783,15 @@ cypress-file-upload@5.0.2:
|
|||
dependencies:
|
||||
mime "^2.5.0"
|
||||
|
||||
cypress@6.4.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-6.4.0.tgz#432c516bf4f1a0f042a6aa1f2c3a4278fa35a8b2"
|
||||
integrity sha512-SrsPsZ4IBterudkoFYBvkQmXOVxclh1/+ytbzpV8AH/D2FA+s2Qy5ISsaRzOFsbQa4KZWoi3AKwREmF1HucYkg==
|
||||
cypress@6.5.0:
|
||||
version "6.5.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-6.5.0.tgz#d853d7a8f915f894249a8788294bfba077278c17"
|
||||
integrity sha512-ol/yTAqHrQQpYBjxLlRSvZf4DOb9AhaQNVlwdOZgJcBHZOOa52/p/6/p3PPcvzjWGOMG6Yq0z4G+jrbWyk/9Dg==
|
||||
dependencies:
|
||||
"@cypress/listr-verbose-renderer" "^0.4.1"
|
||||
"@cypress/request" "^2.88.5"
|
||||
"@cypress/xvfb" "^1.2.4"
|
||||
"@types/node" "12.12.50"
|
||||
"@types/sinonjs__fake-timers" "^6.0.1"
|
||||
"@types/sizzle" "^2.3.2"
|
||||
arch "^2.1.2"
|
||||
|
@ -6798,7 +6804,7 @@ cypress@6.4.0:
|
|||
commander "^5.1.0"
|
||||
common-tags "^1.8.0"
|
||||
dayjs "^1.9.3"
|
||||
debug "^4.1.1"
|
||||
debug "4.3.2"
|
||||
eventemitter2 "^6.4.2"
|
||||
execa "^4.0.2"
|
||||
executable "^4.1.1"
|
||||
|
@ -6871,6 +6877,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
|
|||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
|
||||
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
||||
|
@ -7568,15 +7581,15 @@ eslint-loader@^2.2.1:
|
|||
object-hash "^1.1.4"
|
||||
rimraf "^2.6.1"
|
||||
|
||||
eslint-plugin-vue@7.5.0:
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-7.5.0.tgz#cc6d983eb22781fa2440a7573cf39af439bb5725"
|
||||
integrity sha512-QnMMTcyV8PLxBz7QQNAwISSEs6LYk2LJvGlxalXvpCtfKnqo7qcY0aZTIxPe8QOnHd7WCwiMZLOJzg6A03T0Gw==
|
||||
eslint-plugin-vue@7.6.0:
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-7.6.0.tgz#ea616e6dfd45d545adb16cba628c5a992cc31f0b"
|
||||
integrity sha512-qYpKwAvpcQXyUXVcG8Zd+fxHDx9iSgTQuO7dql7Ug/2BCvNNDr6s3I9p8MoUo23JJdO7ZAjW3vSwY/EBf4uBcw==
|
||||
dependencies:
|
||||
eslint-utils "^2.1.0"
|
||||
natural-compare "^1.4.0"
|
||||
semver "^7.3.2"
|
||||
vue-eslint-parser "^7.4.1"
|
||||
vue-eslint-parser "^7.5.0"
|
||||
|
||||
eslint-scope@^4.0.3:
|
||||
version "4.0.3"
|
||||
|
@ -7704,13 +7717,6 @@ esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||
|
||||
esquery@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
|
||||
integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==
|
||||
dependencies:
|
||||
estraverse "^4.0.0"
|
||||
|
||||
esquery@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
|
||||
|
@ -7732,7 +7738,7 @@ esrecurse@^4.3.0:
|
|||
dependencies:
|
||||
estraverse "^5.2.0"
|
||||
|
||||
estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
|
||||
estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
|
||||
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
|
||||
|
@ -11100,16 +11106,21 @@ lodash@4, lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, loda
|
|||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
|
||||
lodash@4.17.20, lodash@^4.17.20, lodash@^4.17.4:
|
||||
version "4.17.20"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||
lodash@4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
lodash@^4.17.19:
|
||||
version "4.17.19"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
||||
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
|
||||
|
||||
lodash@^4.17.20:
|
||||
version "4.17.20"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||
|
||||
log-symbols@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
|
||||
|
@ -16081,16 +16092,16 @@ vue-easymde@1.3.2:
|
|||
easymde "^2.13.0"
|
||||
marked "^1.2.7"
|
||||
|
||||
vue-eslint-parser@^7.4.1:
|
||||
version "7.4.1"
|
||||
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.4.1.tgz#e4adcf7876a7379758d9056a72235af18a587f92"
|
||||
integrity sha512-AFvhdxpFvliYq1xt/biNBslTHE/zbEvSnr1qfHA/KxRIpErmEDrQZlQnvEexednRHmLfDNOMuDYwZL5xkLzIXQ==
|
||||
vue-eslint-parser@^7.5.0:
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.5.0.tgz#b68221c55fee061899afcfb4441ec74c1495285e"
|
||||
integrity sha512-6EHzl00hIpy4yWZo3qSbtvtVw1A1cTKOv1w95QSuAqGgk4113XtRjvNIiEGo49r0YWOPYsrmI4Dl64axL5Agrw==
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
eslint-scope "^5.0.0"
|
||||
eslint-visitor-keys "^1.1.0"
|
||||
espree "^6.2.1"
|
||||
esquery "^1.0.1"
|
||||
esquery "^1.4.0"
|
||||
lodash "^4.17.15"
|
||||
|
||||
vue-flatpickr-component@8.1.6:
|
||||
|
|
Reference in New Issue