forked from vikunja/frontend
Compare commits
59 Commits
8db3c6c09f
...
7b17ccbf1f
Author | SHA1 | Date |
---|---|---|
renovate | 7b17ccbf1f | |
renovate | ce7563ea4c | |
renovate | d9f3555d8d | |
renovate | b2dd63630c | |
renovate | db1a41f845 | |
renovate | 08ae0046de | |
renovate | c173542b23 | |
kolaente | 518417c0de | |
renovate | c2e58a2320 | |
renovate | d94a25c83f | |
renovate | ad9ca61969 | |
renovate | d28d9218bd | |
renovate | af08713bf0 | |
renovate | 0b01f2aace | |
renovate | 579de70a7a | |
renovate | 660ab928a2 | |
renovate | 9394f57fc9 | |
konrad | 873169c371 | |
renovate | 098fd0a875 | |
drone | c0cd69dd82 | |
kolaente | 4666087aa9 | |
kolaente | 56147dc9fb | |
kolaente | ff48178051 | |
kolaente | cb3f269937 | |
kolaente | 4c560f1a03 | |
renovate | 0fe2a16a7c | |
kolaente | 9cebf5305a | |
kolaente | 71c8540c74 | |
kolaente | 8183fce829 | |
kolaente | 3becf8738b | |
kolaente | 9ddb55a5ef | |
kolaente | cdb63b578d | |
renovate | d6a10b01dd | |
renovate | b74c961723 | |
kolaente | 8b0e88b574 | |
kolaente | 175fb02629 | |
kolaente | dac9d918b5 | |
kolaente | e7de930129 | |
kolaente | a0d0c2cb1f | |
kolaente | a4d3cafdf1 | |
kolaente | f5bb697032 | |
kolaente | 62bbffb17e | |
kolaente | c2d5370e4a | |
kolaente | 6dc02c45dd | |
Dominik Pschenitschni | 0456f4a041 | |
renovate | c1dd20a30b | |
renovate | acc7bf4305 | |
renovate | 86688d7e95 | |
renovate | aaee49b70e | |
renovate | 6b23b954a8 | |
kolaente | 9fd2f4ea5c | |
konrad | dca05f852c | |
kolaente | 5aa6cce185 | |
Dominik Pschenitschni | d96ea384dc | |
renovate | 5d33144b8e | |
renovate | d462d56202 | |
renovate | 2648592ac0 | |
renovate | 1ac78729a4 | |
renovate | 9d1195a2ed |
|
@ -0,0 +1,44 @@
|
|||
<!--
|
||||
|
||||
Please fill out this issue template to report a bug.
|
||||
If you want to propose a new feature, please open a discussion thread in the forum: https://community.vikunja.io
|
||||
|
||||
-->
|
||||
|
||||
**Version information:**
|
||||
|
||||
Frontend Version:
|
||||
API Version:
|
||||
Browser and OS Version:
|
||||
|
||||
**Steps to reproduce:**
|
||||
|
||||
<!--
|
||||
Add clear steps to reproduce the bug. Provide screenshots where applicable.
|
||||
-->
|
||||
|
||||
1.
|
||||
2.
|
||||
...
|
||||
|
||||
**Expected behavior:**
|
||||
|
||||
<!--
|
||||
Describe what happened.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
**Actual behavior:**
|
||||
|
||||
<!--
|
||||
Describe what happened instead.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
**Checklist:**
|
||||
|
||||
* [ ] I have provided all required information
|
||||
* [ ] I am using the latest release or the latest unstable build
|
||||
* [ ] I was able to reproduce the bug on [try](https://try.vikunja.io)
|
66
package.json
66
package.json
|
@ -18,19 +18,19 @@
|
|||
"browserslist:update": "npx browserslist@latest --update-db"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/hotkey": "2.0.0",
|
||||
"@kyvg/vue3-notification": "2.3.5",
|
||||
"@sentry/tracing": "7.7.0",
|
||||
"@sentry/vue": "7.7.0",
|
||||
"@github/hotkey": "2.0.1",
|
||||
"@kyvg/vue3-notification": "2.3.6",
|
||||
"@sentry/tracing": "7.8.0",
|
||||
"@sentry/vue": "7.8.0",
|
||||
"@types/is-touch-device": "1.0.0",
|
||||
"@types/sortablejs": "1.13.0",
|
||||
"@vueuse/core": "8.9.3",
|
||||
"@vueuse/router": "8.9.3",
|
||||
"@vueuse/core": "9.0.2",
|
||||
"@vueuse/router": "9.0.2",
|
||||
"blurhash": "1.1.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"camel-case": "4.1.2",
|
||||
"date-fns": "2.28.0",
|
||||
"dompurify": "2.3.9",
|
||||
"date-fns": "2.29.1",
|
||||
"dompurify": "2.3.10",
|
||||
"easymde": "2.16.1",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.21",
|
||||
|
@ -48,51 +48,51 @@
|
|||
"vue-advanced-cropper": "2.8.3",
|
||||
"vue-drag-resize": "2.0.3",
|
||||
"vue-flatpickr-component": "9.0.6",
|
||||
"vue-i18n": "9.2.0-beta.38",
|
||||
"vue-router": "4.1.2",
|
||||
"vue-i18n": "9.2.0-beta.40",
|
||||
"vue-router": "4.1.3",
|
||||
"vuex": "4.0.2",
|
||||
"workbox-precaching": "6.5.3",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.2.1",
|
||||
"@cypress/vite-dev-server": "2.2.3",
|
||||
"@cypress/vue": "3.1.2",
|
||||
"@cypress/vite-dev-server": "3.0.0",
|
||||
"@cypress/vue": "4.0.0",
|
||||
"@faker-js/faker": "7.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.1.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.1.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.1.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.1.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.1.2",
|
||||
"@fortawesome/vue-fontawesome": "3.0.1",
|
||||
"@types/flexsearch": "0.7.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.30.6",
|
||||
"@typescript-eslint/parser": "5.30.6",
|
||||
"@vitejs/plugin-legacy": "1.8.2",
|
||||
"@vitejs/plugin-vue": "2.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.31.0",
|
||||
"@typescript-eslint/parser": "5.31.0",
|
||||
"@vitejs/plugin-legacy": "2.0.0",
|
||||
"@vitejs/plugin-vue": "3.0.1",
|
||||
"@vue/eslint-config-typescript": "11.0.0",
|
||||
"@vue/test-utils": "2.0.2",
|
||||
"@vue/tsconfig": "0.1.3",
|
||||
"autoprefixer": "10.4.7",
|
||||
"autoprefixer": "10.4.8",
|
||||
"axios": "0.27.2",
|
||||
"browserslist": "4.21.1",
|
||||
"caniuse-lite": "1.0.30001363",
|
||||
"cypress": "10.3.0",
|
||||
"esbuild": "0.14.49",
|
||||
"eslint": "8.19.0",
|
||||
"eslint-plugin-vue": "9.2.0",
|
||||
"browserslist": "4.21.3",
|
||||
"caniuse-lite": "1.0.30001373",
|
||||
"cypress": "10.3.1",
|
||||
"esbuild": "0.14.51",
|
||||
"eslint": "8.20.0",
|
||||
"eslint-plugin-vue": "9.3.0",
|
||||
"express": "4.18.1",
|
||||
"happy-dom": "6.0.3",
|
||||
"netlify-cli": "10.9.0",
|
||||
"happy-dom": "6.0.4",
|
||||
"netlify-cli": "10.13.0",
|
||||
"postcss": "8.4.14",
|
||||
"postcss-preset-env": "7.7.2",
|
||||
"rollup": "2.77.0",
|
||||
"rollup": "2.77.2",
|
||||
"rollup-plugin-visualizer": "5.7.1",
|
||||
"sass": "1.53.0",
|
||||
"sass": "1.54.0",
|
||||
"typescript": "4.7.4",
|
||||
"vite": "2.9.14",
|
||||
"vite": "3.0.4",
|
||||
"vite-plugin-pwa": "0.12.3",
|
||||
"vite-svg-loader": "3.4.0",
|
||||
"vitest": "0.18.1",
|
||||
"vue-tsc": "0.38.7",
|
||||
"vitest": "0.20.2",
|
||||
"vue-tsc": "0.39.4",
|
||||
"wait-on": "6.0.1",
|
||||
"workbox-cli": "6.5.3"
|
||||
},
|
||||
|
|
|
@ -10,9 +10,7 @@
|
|||
</no-auth-wrapper>
|
||||
<Notification/>
|
||||
|
||||
<transition name="fade">
|
||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||
</transition>
|
||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||
</ready>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -48,44 +48,38 @@
|
|||
</x-button>
|
||||
</template>
|
||||
|
||||
<BaseButton
|
||||
<dropdown-item
|
||||
:to="{name: 'user.settings'}"
|
||||
class="dropdown-item"
|
||||
>
|
||||
{{ $t('user.settings.title') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
v-if="imprintUrl"
|
||||
:href="imprintUrl"
|
||||
class="dropdown-item"
|
||||
>
|
||||
{{ $t('navigation.imprint') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
v-if="privacyPolicyUrl"
|
||||
:href="privacyPolicyUrl"
|
||||
class="dropdown-item"
|
||||
>
|
||||
{{ $t('navigation.privacy') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
@click="$store.commit('keyboardShortcutsActive', true)"
|
||||
class="dropdown-item"
|
||||
>
|
||||
{{ $t('keyboardShortcuts.title') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{name: 'about'}"
|
||||
class="dropdown-item"
|
||||
>
|
||||
{{ $t('about.title') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
@click="logout()"
|
||||
class="dropdown-item"
|
||||
>
|
||||
{{ $t('user.auth.logout') }}
|
||||
</BaseButton>
|
||||
</dropdown-item>
|
||||
</dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -103,6 +97,7 @@ import Rights from '@/models/constants/rights.json'
|
|||
import Update from '@/components/home/update.vue'
|
||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||
import Notifications from '@/components/notifications/notifications.vue'
|
||||
import Logo from '@/components/home/Logo.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
|
|
@ -38,16 +38,14 @@
|
|||
</keep-alive>
|
||||
</router-view>
|
||||
|
||||
<transition name="modal">
|
||||
<modal
|
||||
v-if="currentModal"
|
||||
@close="closeModal()"
|
||||
variant="scrolling"
|
||||
class="task-detail-view-modal"
|
||||
>
|
||||
<component :is="currentModal"/>
|
||||
</modal>
|
||||
</transition>
|
||||
<modal
|
||||
v-if="currentModal"
|
||||
@close="closeModal()"
|
||||
variant="scrolling"
|
||||
class="task-detail-view-modal"
|
||||
>
|
||||
<component :is="currentModal"/>
|
||||
</modal>
|
||||
|
||||
<BaseButton
|
||||
class="keyboard-shortcuts-button d-print-none"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
>
|
||||
<div class="container has-text-centered link-share-view">
|
||||
<div class="column is-10 is-offset-1">
|
||||
<Logo class="logo" />
|
||||
<Logo class="logo"/>
|
||||
<h1
|
||||
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
|
||||
class="title">
|
||||
|
@ -14,7 +14,7 @@
|
|||
</h1>
|
||||
<div class="box has-text-left view">
|
||||
<router-view/>
|
||||
<PoweredByLink />
|
||||
<PoweredByLink/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -35,14 +35,15 @@ const background = computed(() => store.state.background)
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.link-share-container.has-background .view {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
max-width: 300px;
|
||||
width: 90%;
|
||||
margin: 2rem 0 1.5rem;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.column {
|
||||
|
@ -55,6 +56,6 @@ const background = computed(() => store.state.background)
|
|||
|
||||
// FIXME: this should be defined somewhere deep
|
||||
.link-share-view .card {
|
||||
background-color: var(--white);
|
||||
background-color: var(--white);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -88,13 +88,12 @@
|
|||
@end="saveListPosition"
|
||||
handle=".handle"
|
||||
:disabled="n.id < 0 || undefined"
|
||||
tag="transition-group"
|
||||
tag="ul"
|
||||
item-key="id"
|
||||
:data-namespace-id="n.id"
|
||||
:data-namespace-index="nk"
|
||||
:component-data="{
|
||||
type: 'transition',
|
||||
tag: 'ul',
|
||||
type: 'transition-group',
|
||||
name: !drag ? 'flip-list' : null,
|
||||
class: [
|
||||
'menu-list can-be-hidden',
|
||||
|
@ -555,8 +554,4 @@ $vikunja-nav-selected-width: 0.4rem;
|
|||
.namespaces-list.loader-container.is-loading {
|
||||
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem + 1.5rem});
|
||||
}
|
||||
|
||||
a.dropdown-item:hover {
|
||||
background: var(--dropdown-item-hover-background-color) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
/>
|
||||
|
||||
<x-button
|
||||
class="datepicker__close-button is-fullwidth"
|
||||
class="datepicker__close-button"
|
||||
:shadow="false"
|
||||
@click="close"
|
||||
v-cy="'closeDatepicker'"
|
||||
|
|
|
@ -56,12 +56,13 @@
|
|||
{{ $t('menu.archive') }}
|
||||
</dropdown-item>
|
||||
<task-subscription
|
||||
class="dropdown-item has-no-shadow"
|
||||
class="has-no-shadow"
|
||||
:is-button="false"
|
||||
entity="list"
|
||||
:entity-id="list.id"
|
||||
:subscription="list.subscription"
|
||||
@change="sub => subscription = sub"
|
||||
type="dropdown"
|
||||
/>
|
||||
<dropdown-item
|
||||
:to="{ name: 'list.settings.delete', params: { listId: list.id } }"
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
v-model="value"
|
||||
ref="filters"
|
||||
class="filter-popup"
|
||||
:class="{'is-open': isOpen}"
|
||||
/>
|
||||
</modal>
|
||||
</template>
|
||||
|
|
|
@ -1,25 +1,95 @@
|
|||
<template>
|
||||
<router-link
|
||||
<component
|
||||
:is="componentNodeName"
|
||||
v-bind="elementBindings"
|
||||
:to="to"
|
||||
class="dropdown-item">
|
||||
<span class="icon" v-if="icon !== ''">
|
||||
<span class="icon" v-if="icon">
|
||||
<icon :icon="icon"/>
|
||||
</span>
|
||||
<span>
|
||||
<slot></slot>
|
||||
</span>
|
||||
</router-link>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
to: {
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
import {ref, useAttrs, watchEffect} from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
to?: object,
|
||||
icon?: string | string[],
|
||||
}>()
|
||||
|
||||
const componentNodeName = ref<Node['nodeName']>('a')
|
||||
const elementBindings = ref({})
|
||||
|
||||
const attrs = useAttrs()
|
||||
watchEffect(() => {
|
||||
let nodeName = 'a'
|
||||
|
||||
if (props.to) {
|
||||
nodeName = 'router-link'
|
||||
}
|
||||
|
||||
if ('href' in attrs) {
|
||||
nodeName = 'BaseButton'
|
||||
}
|
||||
|
||||
componentNodeName.value = nodeName
|
||||
elementBindings.value = {
|
||||
...attrs,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dropdown-item {
|
||||
color: var(--text);
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
padding: $item-padding;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
a.dropdown-item,
|
||||
button.dropdown-item {
|
||||
text-align: inherit;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--grey-100) !important;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: var(--link);
|
||||
color: var(--link-invert);
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding-right: .5rem;
|
||||
}
|
||||
|
||||
.icon:not(.has-text-success) {
|
||||
color: var(--grey-300) !important;
|
||||
}
|
||||
|
||||
&.has-text-danger .icon {
|
||||
color: var(--danger) !important;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="dropdown is-right is-active" ref="dropdown">
|
||||
<div class="dropdown" ref="dropdown">
|
||||
<slot name="trigger" :close="close" :toggleOpen="toggleOpen">
|
||||
<BaseButton class="dropdown-trigger is-flex" @click="toggleOpen">
|
||||
<icon :icon="triggerIcon" class="icon"/>
|
||||
|
@ -51,7 +51,36 @@ onClickOutside(dropdown, (e: Event) => {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-menu .dropdown-content {
|
||||
.dropdown {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
min-width: 12rem;
|
||||
padding-top: 4px;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
z-index: 20;
|
||||
display: block;
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
background-color: var(--scheme-main);
|
||||
border-radius: $radius;
|
||||
padding-bottom: .5rem;
|
||||
padding-top: .5rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
</style>
|
||||
|
||||
.dropdown-divider {
|
||||
background-color: var(--border-light);
|
||||
border: none;
|
||||
display: block;
|
||||
height: 1px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<Teleport to="body">
|
||||
<!-- FIXME: transition should not be included in the modal -->
|
||||
<transition name="modal">
|
||||
<transition :name="transitionName">
|
||||
<section
|
||||
v-if="enabled"
|
||||
class="modal-mask"
|
||||
|
|
|
@ -69,9 +69,9 @@ function createPagination(totalPages: number, currentPage: number) {
|
|||
return pages
|
||||
}
|
||||
|
||||
function getRouteForPagination(page = 1, type = 'list') {
|
||||
function getRouteForPagination(page = 1, type = null) {
|
||||
return {
|
||||
name: 'list.' + type,
|
||||
name: type,
|
||||
params: {
|
||||
type: type,
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<x-button
|
||||
v-if="isButton"
|
||||
v-if="type === 'button'"
|
||||
variant="secondary"
|
||||
:icon="iconName"
|
||||
v-tooltip="tooltipText"
|
||||
|
@ -9,6 +9,15 @@
|
|||
>
|
||||
{{ buttonText }}
|
||||
</x-button>
|
||||
<DropdownItem
|
||||
v-else-if="type === 'dropdown'"
|
||||
v-tooltip="tooltipText"
|
||||
@click="changeSubscription"
|
||||
:class="{'is-disabled': disabled}"
|
||||
:icon="iconName"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</DropdownItem>
|
||||
<BaseButton
|
||||
v-else
|
||||
v-tooltip="tooltipText"
|
||||
|
@ -27,6 +36,7 @@ import {computed, shallowRef} from 'vue'
|
|||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||
|
||||
import SubscriptionService from '@/services/subscription'
|
||||
import SubscriptionModel from '@/models/subscription'
|
||||
|
@ -34,15 +44,15 @@ import SubscriptionModel from '@/models/subscription'
|
|||
import {success} from '@/message'
|
||||
|
||||
interface Props {
|
||||
entity: string
|
||||
entityId: number
|
||||
subscription: SubscriptionModel | null
|
||||
isButton?: boolean
|
||||
entity: string
|
||||
entityId: number
|
||||
subscription: SubscriptionModel | null
|
||||
type?: 'button' | 'dropdown' | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isButton: true,
|
||||
subscription: null,
|
||||
type: 'button',
|
||||
})
|
||||
|
||||
const subscriptionEntity = computed<string | null>(() => props.subscription?.entity ?? null)
|
||||
|
|
|
@ -34,12 +34,13 @@
|
|||
{{ $t('menu.archive') }}
|
||||
</dropdown-item>
|
||||
<task-subscription
|
||||
class="dropdown-item has-no-shadow"
|
||||
class="has-no-shadow"
|
||||
:is-button="false"
|
||||
entity="namespace"
|
||||
:entity-id="namespace.id"
|
||||
:subscription="subscription"
|
||||
@change="sub => subscription = sub"
|
||||
type="dropdown"
|
||||
/>
|
||||
<dropdown-item
|
||||
:to="{ name: 'namespace.settings.delete', params: { id: namespace.id } }"
|
||||
|
|
|
@ -526,7 +526,7 @@ export default defineComponent({
|
|||
|
||||
.result {
|
||||
&-title {
|
||||
background: var(--grey-50);
|
||||
background: var(--grey-100);
|
||||
padding: .5rem;
|
||||
display: block;
|
||||
font-size: .75rem;
|
||||
|
@ -549,7 +549,7 @@ export default defineComponent({
|
|||
cursor: pointer;
|
||||
|
||||
&:focus, &:hover {
|
||||
background: var(--grey-50);
|
||||
background: var(--grey-100);
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,6 @@ const props = defineProps({
|
|||
|
||||
for (const e of prop) {
|
||||
if (!isDate(e) && !isString(e)) {
|
||||
console.log('validation failed', e, e instanceof Date)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
<template>
|
||||
<div class="control repeat-after-input">
|
||||
<div class="buttons has-addons is-centered mt-2">
|
||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'days')">{{ $t('task.repeat.everyDay') }}</x-button>
|
||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">{{ $t('task.repeat.everyWeek') }}</x-button>
|
||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">{{ $t('task.repeat.everyMonth') }}</x-button>
|
||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'days')">
|
||||
{{ $t('task.repeat.everyDay') }}
|
||||
</x-button>
|
||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">
|
||||
{{ $t('task.repeat.everyWeek') }}
|
||||
</x-button>
|
||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">
|
||||
{{ $t('task.repeat.everyMonth') }}
|
||||
</x-button>
|
||||
</div>
|
||||
<div class="is-flex is-align-items-center mb-2">
|
||||
<label for="repeatMode" class="is-fullwidth">
|
||||
|
@ -14,7 +20,10 @@
|
|||
<select @change="updateData" v-model="task.repeatMode" id="repeatMode">
|
||||
<option :value="repeatModes.REPEAT_MODE_DEFAULT">{{ $t('misc.default') }}</option>
|
||||
<option :value="repeatModes.REPEAT_MODE_MONTH">{{ $t('task.repeat.monthly') }}</option>
|
||||
<option :value="repeatModes.REPEAT_MODE_FROM_CURRENT_DATE">{{ $t('task.repeat.fromCurrentDate') }}</option>
|
||||
<option :value="repeatModes.REPEAT_MODE_FROM_CURRENT_DATE">{{
|
||||
$t('task.repeat.fromCurrentDate')
|
||||
}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -32,6 +41,7 @@
|
|||
:placeholder="$t('task.repeat.specifyAmount')"
|
||||
v-model="repeatAfter.amount"
|
||||
type="number"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
|
@ -56,8 +66,10 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive, watch} from 'vue'
|
||||
import repeatModes from '@/models/constants/taskRepeatModes'
|
||||
import repeatModes from '@/models/constants/taskRepeatModes.json'
|
||||
import TaskModel from '@/models/task'
|
||||
import {error} from '@/message'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
@ -70,6 +82,8 @@ const props = defineProps({
|
|||
},
|
||||
})
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const task = ref<TaskModel>()
|
||||
|
@ -93,14 +107,19 @@ function updateData() {
|
|||
if (task.value.repeatMode !== repeatModes.REPEAT_MODE_DEFAULT && repeatAfter.amount === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (repeatAfter.amount < 0) {
|
||||
error({message: t('task.repeat.invalidAmount')})
|
||||
return
|
||||
}
|
||||
|
||||
Object.assign(task.value.repeatAfter, repeatAfter)
|
||||
emit('update:modelValue', task.value)
|
||||
emit('change')
|
||||
}
|
||||
|
||||
function setRepeatAfter(amount: number, type) {
|
||||
Object.assign(repeatAfter, { amount, type})
|
||||
Object.assign(repeatAfter, {amount, type})
|
||||
updateData()
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
// https://stackoverflow.com/a/32108184/10924593
|
||||
export function objectIsEmpty(obj: any): boolean {
|
||||
return obj
|
||||
&& Object.keys(obj).length === 0
|
||||
&& Object.getPrototypeOf(obj) === Object.prototype
|
||||
}
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Days",
|
||||
"weeks": "Weeks",
|
||||
"months": "Months",
|
||||
"years": "Years"
|
||||
"years": "Years",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "You can use Quick Add Magic",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Dnů",
|
||||
"weeks": "Týdny",
|
||||
"months": "Měsíce",
|
||||
"years": "Roky"
|
||||
"years": "Roky",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "Můžeš použít Kouzelné rychlé přidání",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Tage",
|
||||
"weeks": "Wochen",
|
||||
"months": "Monate",
|
||||
"years": "Jahre"
|
||||
"years": "Jahre",
|
||||
"invalidAmount": "Bitte mehr als 0 eingeben."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "Du kannst Quick Add Magic verwenden",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Tääg",
|
||||
"weeks": "Wuchä",
|
||||
"months": "Monet",
|
||||
"years": "Jahr"
|
||||
"years": "Jahr",
|
||||
"invalidAmount": "Bitte mehr als 0 eingeben."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "Du chasch Quick Add Magic verwendä",
|
||||
|
|
|
@ -783,7 +783,8 @@
|
|||
"days": "Days",
|
||||
"weeks": "Weeks",
|
||||
"months": "Months",
|
||||
"years": "Years"
|
||||
"years": "Years",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "You can use Quick Add Magic",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Days",
|
||||
"weeks": "Weeks",
|
||||
"months": "Months",
|
||||
"years": "Years"
|
||||
"years": "Years",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "You can use Quick Add Magic",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Jours",
|
||||
"weeks": "Semaines",
|
||||
"months": "Mois",
|
||||
"years": "Années"
|
||||
"years": "Années",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "Tu peux utiliser Quick Add Magic",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Giorni",
|
||||
"weeks": "Settimane",
|
||||
"months": "Mesi",
|
||||
"years": "Anni"
|
||||
"years": "Anni",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "Puoi usare l'Aggiunta Rapida Magica",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Dagen",
|
||||
"weeks": "Weken",
|
||||
"months": "Maanden",
|
||||
"years": "Jaren"
|
||||
"years": "Jaren",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "You can use Quick Add Magic",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Dni",
|
||||
"weeks": "Tygodnie",
|
||||
"months": "Miesiące",
|
||||
"years": "Lata"
|
||||
"years": "Lata",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "Możesz użyć Quick Add Magic",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Days",
|
||||
"weeks": "Weeks",
|
||||
"months": "Months",
|
||||
"years": "Years"
|
||||
"years": "Years",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "You can use Quick Add Magic",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Dias",
|
||||
"weeks": "Semanas",
|
||||
"months": "Meses",
|
||||
"years": "Anos"
|
||||
"years": "Anos",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "Podes utilizar a Introdução Mágica Rápida",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Days",
|
||||
"weeks": "Weeks",
|
||||
"months": "Months",
|
||||
"years": "Years"
|
||||
"years": "Years",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "You can use Quick Add Magic",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Дней",
|
||||
"weeks": "Недель",
|
||||
"months": "Месяцев",
|
||||
"years": "Лет"
|
||||
"years": "Лет",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "Вы можете использовать Волшебное Быстрое Добавление",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Days",
|
||||
"weeks": "Weeks",
|
||||
"months": "Months",
|
||||
"years": "Years"
|
||||
"years": "Years",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "You can use Quick Add Magic",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Days",
|
||||
"weeks": "Weeks",
|
||||
"months": "Months",
|
||||
"years": "Years"
|
||||
"years": "Years",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "You can use Quick Add Magic",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Days",
|
||||
"weeks": "Weeks",
|
||||
"months": "Months",
|
||||
"years": "Years"
|
||||
"years": "Years",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "You can use Quick Add Magic",
|
||||
|
|
|
@ -780,7 +780,8 @@
|
|||
"days": "Ngày",
|
||||
"weeks": "Tuần",
|
||||
"months": "Tháng",
|
||||
"years": "Năm"
|
||||
"years": "Năm",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "Bạn có thể sử dụng Quick Add Magic",
|
||||
|
|
|
@ -314,6 +314,42 @@ describe('Parse Task Text', () => {
|
|||
expect(result.text).toBe('Lorem Ipsum github')
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
describe('Should not recognize weekdays in words', () => {
|
||||
const cases = [
|
||||
'renewed',
|
||||
'github',
|
||||
'fix monitor stand',
|
||||
'order wedding cake',
|
||||
'investigate thumping noise',
|
||||
'iron frilly napkins',
|
||||
'take photo of saturn',
|
||||
'fix sunglasses',
|
||||
'monitor blood pressure',
|
||||
'Monitor blood pressure',
|
||||
'buy almonds',
|
||||
]
|
||||
|
||||
cases.forEach(c => {
|
||||
it(`should not recognize text with ${c} at the beginning as weekday`, () => {
|
||||
const result = parseTaskText(`${c} dolor sit amet`)
|
||||
|
||||
expect(result.text).toBe(`${c} dolor sit amet`)
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
it(`should not recognize text with ${c} at the end as weekday`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum ${c}`)
|
||||
|
||||
expect(result.text).toBe(`Lorem Ipsum ${c}`)
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
it(`should not recognize text with ${c} as weekday`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum ${c} dolor`)
|
||||
|
||||
expect(result.text).toBe(`Lorem Ipsum ${c} dolor`)
|
||||
expect(result.date).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
it('should not recognize date number with no spacing around them', () => {
|
||||
const result = parseTaskText('Lorem Ispum v1.1.1')
|
||||
|
||||
|
|
|
@ -318,7 +318,7 @@ export default {
|
|||
const oldBucket = cloneDeep(ctx.state.buckets[bucketIndex])
|
||||
|
||||
const updatedBucket = {
|
||||
...ctx.state.buckets[bucketIndex],
|
||||
...oldBucket,
|
||||
...updatedBucketData,
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ import LabelTask from '@/models/labelTask'
|
|||
import LabelModel from '@/models/label'
|
||||
import UserService from '@/services/user'
|
||||
|
||||
|
||||
// IDEA: maybe use a small fuzzy search here to prevent errors
|
||||
function findPropertyByValue(object, key, value) {
|
||||
return Object.values(object).find(
|
||||
|
@ -286,12 +285,13 @@ export default {
|
|||
return foundListId
|
||||
},
|
||||
|
||||
async createNewTask({dispatch}, {
|
||||
async createNewTask({dispatch, commit}, {
|
||||
title,
|
||||
bucketId,
|
||||
listId,
|
||||
position,
|
||||
}) {
|
||||
const cancel = setLoading({commit}, 'tasks')
|
||||
const parsedTask = parseTaskText(title, getQuickAddMagicMode())
|
||||
|
||||
const foundListId = await dispatch('findListId', {
|
||||
|
@ -321,10 +321,12 @@ export default {
|
|||
|
||||
const taskService = new TaskService()
|
||||
const createdTask = await taskService.create(task)
|
||||
return dispatch('addLabelsToTask', {
|
||||
const result = await dispatch('addLabelsToTask', {
|
||||
task: createdTask,
|
||||
parsedLabels: parsedTask.labels,
|
||||
})
|
||||
cancel()
|
||||
return result
|
||||
},
|
||||
},
|
||||
}
|
|
@ -24,9 +24,6 @@ $vikunja-font: 'Quicksand', sans-serif;
|
|||
$pagination-current-border: var(--primary);
|
||||
$navbar-item-active-color: var(--primary);
|
||||
|
||||
$dropdown-content-shadow: none;
|
||||
$dropdown-item-hover-background-color: var(--grey-100);
|
||||
|
||||
$site-background: var(--grey-100);
|
||||
|
||||
$transition-duration: 150ms;
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
// imports from "bulma-css-variables/sass/components/_all";
|
||||
// @import "bulma-css-variables/sass/components/breadcrumb"; // not used
|
||||
@import "bulma-css-variables/sass/components/card";
|
||||
@import "bulma-css-variables/sass/components/dropdown";
|
||||
// @import "bulma-css-variables/sass/components/dropdown"; // moved to component
|
||||
// @import "bulma-css-variables/sass/components/level"; // not used
|
||||
@import "bulma-css-variables/sass/components/media";
|
||||
@import "bulma-css-variables/sass/components/menu";
|
||||
|
|
|
@ -91,34 +91,6 @@ button.table {
|
|||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
// FIXME: merge with dropdown-item.vue
|
||||
// for this to happen the component has to be used everywhere
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left !important;
|
||||
|
||||
.icon {
|
||||
padding-right: .5rem;
|
||||
}
|
||||
|
||||
.icon:not(.has-text-success) {
|
||||
color: var(--grey-300) !important;
|
||||
}
|
||||
|
||||
&.has-text-danger .icon {
|
||||
color: var(--danger) !important;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-max-width-desktop {
|
||||
width: 100%;
|
||||
max-width: $desktop;
|
||||
|
|
|
@ -76,12 +76,16 @@ const welcome = useDateTimeSalutation()
|
|||
|
||||
const store = useStore()
|
||||
const listHistory = computed(() => {
|
||||
// If we don't check this, it tries to load the list background right after logging out
|
||||
if(!store.state.auth.authenticated) {
|
||||
return []
|
||||
}
|
||||
|
||||
return getHistory()
|
||||
.map(l => store.getters['lists/getListById'](l.id))
|
||||
.filter(l => l !== null)
|
||||
})
|
||||
|
||||
|
||||
const migratorsEnabled = computed(() => store.state.config.availableMigrators?.length > 0)
|
||||
const userInfo = computed(() => store.state.auth.info)
|
||||
const hasTasks = computed(() => store.state.hasTasks)
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
@start="() => dragBucket = true"
|
||||
group="buckets"
|
||||
:disabled="!canWrite"
|
||||
tag="transition-group"
|
||||
tag="ul"
|
||||
:item-key="({id}) => `bucket${id}`"
|
||||
:component-data="bucketDraggableComponentData"
|
||||
>
|
||||
|
@ -62,9 +62,8 @@
|
|||
trigger-icon="ellipsis-v"
|
||||
@close="() => showSetLimitInput = false"
|
||||
>
|
||||
<a
|
||||
<dropdown-item
|
||||
@click.stop="showSetLimitInput = true"
|
||||
class="dropdown-item"
|
||||
>
|
||||
<div class="field has-addons" v-if="showSetLimitInput">
|
||||
<div class="control">
|
||||
|
@ -93,34 +92,32 @@
|
|||
$t('list.kanban.limit', {limit: bucket.limit > 0 ? bucket.limit : $t('list.kanban.noLimit')})
|
||||
}}
|
||||
</template>
|
||||
</a>
|
||||
<a
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
@click.stop="toggleDoneBucket(bucket)"
|
||||
class="dropdown-item"
|
||||
v-tooltip="$t('list.kanban.doneBucketHintExtended')"
|
||||
>
|
||||
<span class="icon is-small" :class="{'has-text-success': bucket.isDoneBucket}">
|
||||
<icon icon="check-double"/>
|
||||
</span>
|
||||
{{ $t('list.kanban.doneBucket') }}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
@click.stop="() => collapseBucket(bucket)"
|
||||
>
|
||||
{{ $t('list.kanban.collapse') }}
|
||||
</a>
|
||||
<a
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:class="{'is-disabled': buckets.length <= 1}"
|
||||
@click.stop="() => deleteBucketModal(bucket.id)"
|
||||
class="dropdown-item has-text-danger"
|
||||
class="has-text-danger"
|
||||
v-tooltip="buckets.length <= 1 ? $t('list.kanban.deleteLast') : ''"
|
||||
>
|
||||
<span class="icon is-small">
|
||||
<icon icon="trash-alt"/>
|
||||
</span>
|
||||
{{ $t('misc.delete') }}
|
||||
</a>
|
||||
</dropdown-item>
|
||||
</dropdown>
|
||||
</div>
|
||||
|
||||
|
@ -133,17 +130,17 @@
|
|||
:group="{name: 'tasks', put: shouldAcceptDrop(bucket) && !dragBucket}"
|
||||
:disabled="!canWrite"
|
||||
:data-bucket-index="bucketIndex"
|
||||
tag="transition-group"
|
||||
tag="ul"
|
||||
:item-key="(task) => `bucket${bucket.id}-task${task.id}`"
|
||||
:component-data="getTaskDraggableTaskComponentData(bucket)"
|
||||
>
|
||||
<template #footer>
|
||||
<div class="bucket-footer" v-if="canWrite">
|
||||
<div class="field" v-if="showNewTaskInput[bucket.id]">
|
||||
<div class="control" :class="{'is-loading': loading}">
|
||||
<div class="control" :class="{'is-loading': loading || taskLoading}">
|
||||
<input
|
||||
class="input"
|
||||
:disabled="loading || undefined"
|
||||
:disabled="loading || taskLoading || undefined"
|
||||
@focusout="toggleShowNewTaskInput(bucket.id)"
|
||||
@keyup.enter="addTaskToBucket(bucket.id)"
|
||||
@keyup.esc="toggleShowNewTaskInput(bucket.id)"
|
||||
|
@ -172,7 +169,7 @@
|
|||
|
||||
<template #item="{element: task}">
|
||||
<div class="task-item">
|
||||
<kanban-card class="kanban-card" :task="task"/>
|
||||
<kanban-card class="kanban-card" :task="task" :loading="taskUpdating[task.id] ?? false"/>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
@ -241,6 +238,7 @@ import Dropdown from '@/components/misc/dropdown.vue'
|
|||
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'
|
||||
import {calculateItemPosition} from '../../helpers/calculateItemPosition'
|
||||
import KanbanCard from '@/components/tasks/partials/kanban-card.vue'
|
||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||
|
||||
const DRAG_OPTIONS = {
|
||||
// sortable options
|
||||
|
@ -256,6 +254,7 @@ const MIN_SCROLL_HEIGHT_PERCENT = 0.25
|
|||
export default defineComponent({
|
||||
name: 'Kanban',
|
||||
components: {
|
||||
DropdownItem,
|
||||
ListWrapper,
|
||||
KanbanCard,
|
||||
Dropdown,
|
||||
|
@ -316,8 +315,7 @@ export default defineComponent({
|
|||
return (bucket) => ({
|
||||
ref: (el) => this.setTaskContainerRef(bucket.id, el),
|
||||
onScroll: (event) => this.handleTaskContainerScroll(bucket.id, bucket.listId, event.target),
|
||||
type: 'transition',
|
||||
tag: 'div',
|
||||
type: 'transition-group',
|
||||
name: !this.drag ? 'move-card' : null,
|
||||
class: [
|
||||
'tasks',
|
||||
|
@ -337,8 +335,7 @@ export default defineComponent({
|
|||
},
|
||||
bucketDraggableComponentData() {
|
||||
return {
|
||||
type: 'transition',
|
||||
tag: 'div',
|
||||
type: 'transition-group',
|
||||
name: !this.dragBucket ? 'move-bucket' : null,
|
||||
class: [
|
||||
'kanban-bucket-container',
|
||||
|
@ -426,6 +423,7 @@ export default defineComponent({
|
|||
const task = newBucket.tasks[newTaskIndex]
|
||||
const taskBefore = newBucket.tasks[newTaskIndex - 1] ?? null
|
||||
const taskAfter = newBucket.tasks[newTaskIndex + 1] ?? null
|
||||
this.taskUpdating[task.id] = true
|
||||
|
||||
const newTask = cloneDeep(task) // cloning the task to avoid vuex store mutations
|
||||
newTask.bucketId = newBucket.id
|
||||
|
@ -438,8 +436,7 @@ export default defineComponent({
|
|||
await this.$store.dispatch('tasks/update', newTask)
|
||||
|
||||
// Make sure the first and second task don't both get position 0 assigned
|
||||
if(newTaskIndex === 0 && taskAfter.kanbanPosition === 0) {
|
||||
console.log('first', taskAfter.id, taskAfter.kanbanPosition)
|
||||
if(newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) {
|
||||
const taskAfterAfter = newBucket.tasks[newTaskIndex + 2] ?? null
|
||||
const newTaskAfter = cloneDeep(taskAfter) // cloning the task to avoid vuex store mutations
|
||||
newTaskAfter.bucketId = newBucket.id
|
||||
|
@ -466,7 +463,7 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
this.newTaskError[bucketId] = false
|
||||
|
||||
|
||||
const task = await this.$store.dispatch('tasks/createNewTask', {
|
||||
title: this.newTaskText,
|
||||
bucketId,
|
||||
|
@ -730,10 +727,6 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
|||
}
|
||||
}
|
||||
|
||||
a.dropdown-item {
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
&.is-collapsed {
|
||||
align-self: flex-start;
|
||||
transform: rotate(90deg) translateY(-100%);
|
||||
|
|
|
@ -84,14 +84,17 @@
|
|||
handle=".handle"
|
||||
:disabled="!canWrite"
|
||||
item-key="id"
|
||||
tag="ul"
|
||||
:component-data="{
|
||||
class: { 'dragging-disabled': !canWrite || isAlphabeticalSorting },
|
||||
type: 'transition-group'
|
||||
}"
|
||||
>
|
||||
<template #item="{element: t}">
|
||||
<single-task-in-list
|
||||
:show-list-color="false"
|
||||
:disabled="!canWrite"
|
||||
:can-mark-as-done="canWrite || (list.isSavedFilter && list.isSavedFilter())"
|
||||
:the-task="t"
|
||||
@taskUpdated="updateTasks"
|
||||
>
|
||||
|
|
|
@ -21,9 +21,9 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import {ref, shallowReactive} from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {useStore} from 'vuex'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import ListDuplicateService from '@/services/listDuplicateService'
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
|
@ -32,29 +32,30 @@ import Multiselect from '@/components/input/multiselect.vue'
|
|||
import ListDuplicateModel from '@/models/listDuplicateModel'
|
||||
import NamespaceModel from '@/models/namespace'
|
||||
|
||||
import { success } from '@/message'
|
||||
import { useTitle } from '@/composables/useTitle'
|
||||
import { useNameSpaceSearch } from '@/composables/useNamespaceSearch'
|
||||
import {success} from '@/message'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useNameSpaceSearch} from '@/composables/useNamespaceSearch'
|
||||
|
||||
|
||||
const { t } = useI18n({useScope: 'global'})
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
useTitle(() => t('list.duplicate.title'))
|
||||
|
||||
|
||||
const {
|
||||
namespaces,
|
||||
findNamespaces,
|
||||
} = useNameSpaceSearch()
|
||||
|
||||
const selectedNamespace = ref<NamespaceModel>()
|
||||
|
||||
function selectNamespace(namespace: NamespaceModel) {
|
||||
selectedNamespace.value = namespace
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router= useRouter()
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
const listDuplicateService = shallowReactive(new ListDuplicateService())
|
||||
|
||||
async function duplicateList() {
|
||||
const listDuplicate = new ListDuplicateModel({
|
||||
// FIXME: should be parameter
|
||||
|
@ -64,7 +65,6 @@ async function duplicateList() {
|
|||
|
||||
const duplicate = await listDuplicateService.create(listDuplicate)
|
||||
|
||||
const store = useStore()
|
||||
store.commit('namespaces/addListToNamespace', duplicate.list)
|
||||
store.commit('lists/setList', duplicate.list)
|
||||
success({message: t('list.duplicate.success')})
|
||||
|
|
|
@ -154,9 +154,19 @@
|
|||
<transition name="flash-background" appear>
|
||||
<div class="column" v-if="activeFields.repeatAfter">
|
||||
<!-- Repeat after -->
|
||||
<div class="detail-title">
|
||||
<icon icon="history"/>
|
||||
{{ $t('task.attributes.repeat') }}
|
||||
<div class="is-flex is-justify-content-space-between">
|
||||
<div class="detail-title">
|
||||
<icon icon="history"/>
|
||||
{{ $t('task.attributes.repeat') }}
|
||||
</div>
|
||||
<BaseButton
|
||||
@click="() => {task.repeatAfter.amount = 0;saveTask()}"
|
||||
v-if="canWrite"
|
||||
class="remove">
|
||||
<span class="icon is-small">
|
||||
<icon icon="times"></icon>
|
||||
</span>
|
||||
</BaseButton>
|
||||
</div>
|
||||
<repeat-after
|
||||
:disabled="!canWrite"
|
||||
|
@ -396,20 +406,18 @@
|
|||
<created-updated :task="task" v-if="!canWrite && !shouldShowClosePopup"/>
|
||||
</div>
|
||||
|
||||
<transition name="modal">
|
||||
<modal
|
||||
@close="showDeleteModal = false"
|
||||
@submit="deleteTask()"
|
||||
v-if="showDeleteModal"
|
||||
>
|
||||
<template #header><span>{{ $t('task.detail.delete.header') }}</span></template>
|
||||
<modal
|
||||
@close="showDeleteModal = false"
|
||||
@submit="deleteTask()"
|
||||
v-if="showDeleteModal"
|
||||
>
|
||||
<template #header><span>{{ $t('task.detail.delete.header') }}</span></template>
|
||||
|
||||
<template #text>
|
||||
<p>{{ $t('task.detail.delete.text1') }}<br/>
|
||||
{{ $t('task.detail.delete.text2') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</transition>
|
||||
<template #text>
|
||||
<p>{{ $t('task.detail.delete.text1') }}<br/>
|
||||
{{ $t('task.detail.delete.text2') }}</p>
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -770,13 +778,13 @@ $flash-background-duration: 750ms;
|
|||
.date-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.remove {
|
||||
color: var(--danger);
|
||||
vertical-align: middle;
|
||||
padding-left: .5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.remove {
|
||||
color: var(--danger);
|
||||
vertical-align: middle;
|
||||
padding-left: .5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
:deep(.datepicker) {
|
||||
|
|
|
@ -66,7 +66,7 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, shallowReactive} from 'vue'
|
||||
import {computed, ref, shallowReactive} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useStore} from 'vuex'
|
||||
import {Cropper} from 'vue-advanced-cropper'
|
||||
|
@ -80,13 +80,13 @@ import { success } from '@/message'
|
|||
const {t} = useI18n({useScope: 'global'})
|
||||
const store = useStore()
|
||||
|
||||
const AVATAR_PROVIDERS = {
|
||||
const AVATAR_PROVIDERS = computed(() => ({
|
||||
default: t('misc.default'),
|
||||
initials: t('user.settings.avatar.initials'),
|
||||
gravatar: t('user.settings.avatar.gravatar'),
|
||||
marble: t('user.settings.avatar.marble'),
|
||||
upload: t('user.settings.avatar.upload'),
|
||||
}
|
||||
}))
|
||||
|
||||
useTitle(() => `${t('user.settings.avatar.title')} - ${t('user.settings.title')}`)
|
||||
|
||||
|
|
|
@ -85,7 +85,8 @@
|
|||
v-for="lang in availableLanguageOptions"
|
||||
:key="lang.code"
|
||||
:value="lang.code"
|
||||
>{{ lang.title }}</option>
|
||||
>{{ lang.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
|
@ -154,7 +155,7 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, watch, ref, reactive} from 'vue'
|
||||
import {computed, watch, ref} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useStore} from 'vuex'
|
||||
|
||||
|
@ -170,7 +171,8 @@ import {success} from '@/message'
|
|||
import {AuthenticatedHTTPFactory} from '@/http-common'
|
||||
|
||||
import {useColorScheme} from '@/composables/useColorScheme'
|
||||
import { useTitle } from '@/composables/useTitle'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {objectIsEmpty} from '@/helpers/objectIsEmpty'
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
useTitle(() => `${t('user.settings.general.title')} - ${t('user.settings.title')}`)
|
||||
|
@ -213,6 +215,7 @@ function useAvailableTimezones() {
|
|||
|
||||
return availableTimezones
|
||||
}
|
||||
|
||||
const availableTimezones = useAvailableTimezones()
|
||||
|
||||
function getPlaySoundWhenDoneSetting() {
|
||||
|
@ -223,7 +226,7 @@ const playSoundWhenDone = ref(getPlaySoundWhenDoneSetting())
|
|||
const quickAddMagicMode = ref(getQuickAddMagicMode())
|
||||
|
||||
const store = useStore()
|
||||
const settings = reactive({...store.state.auth.settings})
|
||||
const settings = ref({...store.state.auth.settings})
|
||||
const id = ref(createRandomID())
|
||||
const availableLanguageOptions = ref(
|
||||
Object.entries(availableLanguages)
|
||||
|
@ -231,10 +234,22 @@ const availableLanguageOptions = ref(
|
|||
.sort((a, b) => a.title.localeCompare(b.title)),
|
||||
)
|
||||
|
||||
watch(
|
||||
() => store.state.auth.settings,
|
||||
() => {
|
||||
// Only setting if we don't have values set yet to avoid overriding edited values
|
||||
if (!objectIsEmpty(settings.value)) {
|
||||
return
|
||||
}
|
||||
settings.value = {...store.state.auth.settings}
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const defaultList = computed({
|
||||
get: () => store.getters['lists/getListById'](settings.defaultListId),
|
||||
get: () => store.getters['lists/getListById'](settings.value.defaultListId),
|
||||
set(l) {
|
||||
settings.defaultListId = l ? l.id : DEFAULT_LIST_ID
|
||||
settings.value.defaultListId = l ? l.id : DEFAULT_LIST_ID
|
||||
},
|
||||
})
|
||||
const loading = computed(() => store.state.loading && store.state.loadingModule === 'general-settings')
|
||||
|
@ -244,13 +259,12 @@ watch(
|
|||
(play) => play && playPopSound(),
|
||||
)
|
||||
|
||||
|
||||
async function updateSettings() {
|
||||
localStorage.setItem(playSoundWhenDoneKey, playSoundWhenDone.value ? 'true' : 'false')
|
||||
setQuickAddMagicMode(quickAddMagicMode.value)
|
||||
|
||||
await store.dispatch('auth/saveUserSettings', {
|
||||
settings: {...settings},
|
||||
settings: {...settings.value},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue