Merge branch 'main' into feature/ganttastic
continuous-integration/drone/pr Build is failing Details

# Conflicts:
#	pnpm-lock.yaml
#	src/components/tasks/gantt-component.vue
This commit is contained in:
kolaente 2022-10-02 00:24:10 +02:00
commit 1bbdd3b117
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
60 changed files with 1421 additions and 1435 deletions

58
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@ -0,0 +1,58 @@
name: Bug Report
description: Found something you weren't expecting? Report it here!
labels: kind/bug
body:
- type: markdown
attributes:
value: |
NOTE: If your issue is a security concern, please send an email to security@vikunja.io instead of opening a public issue.
- type: markdown
attributes:
value: |
Please fill out this issue template to report a bug.
1. If you want to propose a new feature, please open a discussion thread in the forum: https://community.vikunja.io
2. Please ask questions or configuration/deploy problems on our [Matrix Room](https://matrix.to/#/#vikunja:matrix.org) or forum (https://community.vikunja.io).
3. Make sure you are using the latest release and
take a moment to check that your issue hasn't been reported before.
4. Please give all relevant information below for bug reports, because
incomplete details will be handled as an invalid report and closed.
- type: textarea
id: description
attributes:
label: Description
description: |
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below).
- type: input
id: frontend-version
attributes:
label: Vikunja Frontend Version
description: Vikunja frontend version (or commit reference) of your instance
validations:
required: true
- type: input
id: api-version
attributes:
label: Vikunja API Version
description: Vikunja API version (or commit reference) of your instance
validations:
required: true
- type: input
id: browser-version
attributes:
label: Browser and version
description: If your issue is related to a frontend problem, please provide the browser and version you used to reproduce it.
- type: dropdown
id: can-reproduce
attributes:
label: Can you reproduce the bug on the Vikunja demo site?
options:
- "Yes"
- "No"
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If this issue involves the Web Interface, please provide one or more screenshots

17
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,17 @@
blank_issues_enabled: false
contact_links:
- name: API issues
url: https://code.vikunja.io/api/issues
about: This is the frontend repo. Please open api-related bug reports and discussions in the api 0repo. Not sure if your issue is frontend or api? Ask in Matrix or the forum first.
- name: Forum
url: https://community.vikunja.io/
about: Feature Requests, Questions, configuration or deployment problems should be discussed in the forum.
- name: Security-related issues
url: https://vikunja.io/contact/#security
about: For security concerns, please send a mail to security@vikunja.io instead of opening a public issue.
- name: Chat on Matrix
url: https://matrix.to/#/#vikunja:matrix.org
about: Please ask any quick questions here.
- name: Translations
url: https://crowdin.com/project/vikunja
about: Any problems or requests for new languages about translations should be handled in crowdin.

View File

@ -6,34 +6,24 @@ WORKDIR /build
ARG USE_RELEASE=false ARG USE_RELEASE=false
ARG RELEASE_VERSION=main ARG RELEASE_VERSION=main
ENV PNPM_CACHE_FOLDER .cache/pnpm/
ADD . ./
RUN \ RUN \
if [ $USE_RELEASE = true ]; then \ if [ $USE_RELEASE = true ]; then \
wget https://dl.vikunja.io/frontend/vikunja-frontend-$RELEASE_VERSION.zip -O frontend-release.zip && \ wget https://dl.vikunja.io/frontend/vikunja-frontend-$RELEASE_VERSION.zip -O frontend-release.zip && \
unzip frontend-release.zip -d dist/ && \ unzip frontend-release.zip -d dist/ && \
exit 0; \ exit 0; \
fi fi && \
ENV PNPM_CACHE_FOLDER .cache/pnpm/
# pnpm fetch does require only lockfile
COPY pnpm-lock.yaml ./
RUN \
# https://pnpm.io/installation#using-corepack # https://pnpm.io/installation#using-corepack
corepack enable && \ corepack enable && \
# we don't use corepack prepare here by intend since # we don't use corepack prepare here by intend since
# we have renovate to keep our dependencies up to date # we have renovate to keep our dependencies up to date
# Build the frontend # Build the frontend
pnpm fetch --prod pnpm install && \
apk add --no-cache git && \
ADD . ./ echo '{"VERSION": "'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'"}' > src/version.json && \
pnpm run build
RUN apk add --no-cache git
RUN \
pnpm install -r --offline --prod && \
echo '{"VERSION": "'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'"}' > src/version.json && \
pnpm run build
# Stage 2: copy # Stage 2: copy
FROM nginx:alpine FROM nginx:alpine

View File

@ -6,7 +6,7 @@ import '../../support/authenticateUser'
describe('List View Gantt', () => { describe('List View Gantt', () => {
prepareLists() prepareLists()
it('Hides tasks with no dates', () => { it('Hides tasks with no dates', () => {
const tasks = TaskFactory.create(1) const tasks = TaskFactory.create(1)
cy.visit('/lists/1/gantt') cy.visit('/lists/1/gantt')
@ -16,10 +16,12 @@ describe('List View Gantt', () => {
}) })
it('Shows tasks from the current and next month', () => { it('Shows tasks from the current and next month', () => {
const now = new Date() const now = Date.UTC(2022, 8, 25)
const nextMonth = now cy.clock(now, ['Date'])
const nextMonth = new Date(now)
nextMonth.setDate(1) nextMonth.setDate(1)
nextMonth.setMonth(now.getMonth() + 1) nextMonth.setMonth(9)
cy.visit('/lists/1/gantt') cy.visit('/lists/1/gantt')
@ -32,7 +34,7 @@ describe('List View Gantt', () => {
const now = new Date() const now = new Date()
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
start_date: formatISO(now), start_date: formatISO(now),
end_date: formatISO(now.setDate(now.getDate() + 4)) end_date: formatISO(now.setDate(now.getDate() + 4)),
}) })
cy.visit('/lists/1/gantt') cy.visit('/lists/1/gantt')
@ -64,7 +66,7 @@ describe('List View Gantt', () => {
const now = new Date() const now = new Date()
TaskFactory.create(1, { TaskFactory.create(1, {
start_date: formatISO(now), start_date: formatISO(now),
end_date: formatISO(now.setDate(now.getDate() + 4)) end_date: formatISO(now.setDate(now.getDate() + 4)),
}) })
cy.visit('/lists/1/gantt') cy.visit('/lists/1/gantt')

View File

@ -46,7 +46,7 @@
"is-touch-device": "1.0.1", "is-touch-device": "1.0.1",
"lodash.clonedeep": "4.5.0", "lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8", "lodash.debounce": "4.0.8",
"marked": "4.1.0", "marked": "4.1.1",
"minimist": "1.2.6", "minimist": "1.2.6",
"pinia": "2.0.22", "pinia": "2.0.22",
"register-service-worker": "1.7.2", "register-service-worker": "1.7.2",
@ -59,7 +59,6 @@
"vue-flatpickr-component": "9.0.6", "vue-flatpickr-component": "9.0.6",
"vue-i18n": "9.2.2", "vue-i18n": "9.2.2",
"vue-router": "4.1.5", "vue-router": "4.1.5",
"vuex": "4.0.2",
"workbox-precaching": "6.5.4", "workbox-precaching": "6.5.4",
"zhyswan-vuedraggable": "4.1.3" "zhyswan-vuedraggable": "4.1.3"
}, },

View File

@ -52,7 +52,7 @@ specifiers:
is-touch-device: 1.0.1 is-touch-device: 1.0.1
lodash.clonedeep: 4.5.0 lodash.clonedeep: 4.5.0
lodash.debounce: 4.0.8 lodash.debounce: 4.0.8
marked: 4.1.0 marked: 4.1.1
minimist: 1.2.6 minimist: 1.2.6
netlify-cli: 11.8.3 netlify-cli: 11.8.3
pinia: 2.0.22 pinia: 2.0.22
@ -77,7 +77,6 @@ specifiers:
vue-i18n: 9.2.2 vue-i18n: 9.2.2
vue-router: 4.1.5 vue-router: 4.1.5
vue-tsc: 0.40.13 vue-tsc: 0.40.13
vuex: 4.0.2
wait-on: 6.0.1 wait-on: 6.0.1
workbox-cli: 6.5.4 workbox-cli: 6.5.4
workbox-precaching: 6.5.4 workbox-precaching: 6.5.4
@ -112,7 +111,7 @@ dependencies:
is-touch-device: 1.0.1 is-touch-device: 1.0.1
lodash.clonedeep: 4.5.0 lodash.clonedeep: 4.5.0
lodash.debounce: 4.0.8 lodash.debounce: 4.0.8
marked: 4.1.0 marked: 4.1.1
minimist: 1.2.6 minimist: 1.2.6
pinia: 2.0.22_bfjwoga25wxjazzogo7o372nwq pinia: 2.0.22_bfjwoga25wxjazzogo7o372nwq
register-service-worker: 1.7.2 register-service-worker: 1.7.2
@ -125,7 +124,6 @@ dependencies:
vue-flatpickr-component: 9.0.6_vue@3.2.40 vue-flatpickr-component: 9.0.6_vue@3.2.40
vue-i18n: 9.2.2_vue@3.2.40 vue-i18n: 9.2.2_vue@3.2.40
vue-router: 4.1.5_vue@3.2.40 vue-router: 4.1.5_vue@3.2.40
vuex: 4.0.2_vue@3.2.40
workbox-precaching: 6.5.4 workbox-precaching: 6.5.4
zhyswan-vuedraggable: 4.1.3_vue@3.2.40 zhyswan-vuedraggable: 4.1.3_vue@3.2.40
@ -5727,7 +5725,7 @@ packages:
'@types/marked': 4.0.7 '@types/marked': 4.0.7
codemirror: 5.65.9 codemirror: 5.65.9
codemirror-spell-checker: 1.1.2 codemirror-spell-checker: 1.1.2
marked: 4.1.0 marked: 4.1.1
dev: false dev: false
/ecc-jsbn/0.1.2: /ecc-jsbn/0.1.2:
@ -8858,8 +8856,8 @@ packages:
object-visit: 1.0.1 object-visit: 1.0.1
dev: true dev: true
/marked/4.1.0: /marked/4.1.1:
resolution: {integrity: sha512-+Z6KDjSPa6/723PQYyc1axYZpYYpDnECDaU6hkaf5gqBieBkMKYReL5hteF2QizhlMbgbo8umXl/clZ67+GlsA==} resolution: {integrity: sha512-0cNMnTcUJPxbA6uWmCmjWz4NJRe/0Xfk2NhXCUHjew9qJzFN20krFnsUe7QynwqOwa5m1fZ4UDg0ycKFVC0ccw==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
hasBin: true hasBin: true
dev: false dev: false
@ -12768,15 +12766,6 @@ packages:
'@vue/server-renderer': 3.2.40_vue@3.2.40 '@vue/server-renderer': 3.2.40_vue@3.2.40
'@vue/shared': 3.2.40 '@vue/shared': 3.2.40
/vuex/4.0.2_vue@3.2.40:
resolution: {integrity: sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==}
peerDependencies:
vue: ^3.0.2
dependencies:
'@vue/devtools-api': 6.2.1
vue: 3.2.40
dev: false
/wait-on/6.0.1: /wait-on/6.0.1:
resolution: {integrity: sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==} resolution: {integrity: sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}

View File

@ -18,7 +18,6 @@
import {computed, watch, type Ref} from 'vue' import {computed, watch, type Ref} from 'vue'
import {useRouter} from 'vue-router' import {useRouter} from 'vue-router'
import {useRouteQuery} from '@vueuse/router' import {useRouteQuery} from '@vueuse/router'
import {useStore} from '@/store'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import isTouchDevice from 'is-touch-device' import isTouchDevice from 'is-touch-device'
import {success} from '@/message' import {success} from '@/message'
@ -34,16 +33,17 @@ import Ready from '@/components/misc/ready.vue'
import {setLanguage} from './i18n' import {setLanguage} from './i18n'
import AccountDeleteService from '@/services/accountDelete' import AccountDeleteService from '@/services/accountDelete'
import {useBaseStore} from '@/stores/base'
import {useColorScheme} from '@/composables/useColorScheme' import {useColorScheme} from '@/composables/useColorScheme'
import {useBodyClass} from '@/composables/useBodyClass' import {useBodyClass} from '@/composables/useBodyClass'
import {useAuthStore} from './stores/auth' import {useAuthStore} from './stores/auth'
const store = useStore() const baseStore = useBaseStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const router = useRouter() const router = useRouter()
useBodyClass('is-touch', isTouchDevice()) useBodyClass('is-touch', isTouchDevice())
const keyboardShortcutsActive = computed(() => store.state.keyboardShortcutsActive) const keyboardShortcutsActive = computed(() => baseStore.keyboardShortcutsActive)
const authUser = computed(() => authStore.authUser) const authUser = computed(() => authStore.authUser)
const authLinkShare = computed(() => authStore.authLinkShare) const authLinkShare = computed(() => authStore.authLinkShare)

View File

@ -1,8 +1,8 @@
<template> <template>
<BaseButton <BaseButton
class="menu-show-button" class="menu-show-button"
@click="$store.commit('toggleMenu')" @click="baseStore.toggleMenu()"
@shortkey="() => $store.commit('toggleMenu')" @shortkey="() => baseStore.toggleMenu()"
v-shortcut="'Control+e'" v-shortcut="'Control+e'"
:title="$t('keyboardShortcuts.toggleMenu')" :title="$t('keyboardShortcuts.toggleMenu')"
:aria-label="menuActive ? $t('misc.hideMenu') : $t('misc.showMenu')" :aria-label="menuActive ? $t('misc.hideMenu') : $t('misc.showMenu')"
@ -11,12 +11,12 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed} from 'vue' import {computed} from 'vue'
import {useStore} from '@/store' import {useBaseStore} from '@/stores/base'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
const store = useStore() const baseStore = useBaseStore()
const menuActive = computed(() => store.state.menuActive) const menuActive = computed(() => baseStore.menuActive)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -70,7 +70,7 @@
{{ $t('navigation.privacy') }} {{ $t('navigation.privacy') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
@click="$store.commit('keyboardShortcutsActive', true)" @click="baseStore.setKeyboardShortcutsActive(true)"
> >
{{ $t('keyboardShortcuts.title') }} {{ $t('keyboardShortcuts.title') }}
</dropdown-item> </dropdown-item>
@ -92,9 +92,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, onMounted, nextTick} from 'vue' import {ref, computed, onMounted, nextTick} from 'vue'
import {useStore} from '@/store'
import {QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
import {RIGHTS as Rights} from '@/constants/rights' import {RIGHTS as Rights} from '@/constants/rights'
import Update from '@/components/home/update.vue' import Update from '@/components/home/update.vue'
@ -107,21 +105,24 @@ import BaseButton from '@/components/base/BaseButton.vue'
import MenuButton from '@/components/home/MenuButton.vue' import MenuButton from '@/components/home/MenuButton.vue'
import {getListTitle} from '@/helpers/getListTitle' import {getListTitle} from '@/helpers/getListTitle'
import {useBaseStore} from '@/stores/base'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
const store = useStore() const baseStore = useBaseStore()
const authStore = useAuthStore() const currentList = computed(() => baseStore.currentList)
const configStore = useConfigStore() const background = computed(() => baseStore.background)
const canWriteCurrentList = computed(() => baseStore.currentList.maxRight > Rights.READ)
const menuActive = computed(() => baseStore.menuActive)
const authStore = useAuthStore()
const userInfo = computed(() => authStore.info) const userInfo = computed(() => authStore.info)
const userAvatar = computed(() => authStore.avatarUrl) const userAvatar = computed(() => authStore.avatarUrl)
const currentList = computed(() => store.state.currentList)
const background = computed(() => store.state.background) const configStore = useConfigStore()
const imprintUrl = computed(() => configStore.legal.imprintUrl) const imprintUrl = computed(() => configStore.legal.imprintUrl)
const privacyPolicyUrl = computed(() => configStore.legal.privacyPolicyUrl) const privacyPolicyUrl = computed(() => configStore.legal.privacyPolicyUrl)
const canWriteCurrentList = computed(() => store.state.currentList.maxRight > Rights.READ)
const menuActive = computed(() => store.state.menuActive)
const usernameDropdown = ref() const usernameDropdown = ref()
const listTitle = ref() const listTitle = ref()
@ -140,7 +141,7 @@ function logout() {
} }
function openQuickActions() { function openQuickActions() {
store.commit(QUICK_ACTIONS_ACTIVE, true) baseStore.setQuickActionsActive(true)
} }
</script> </script>

View File

@ -2,7 +2,7 @@
<div class="content-auth"> <div class="content-auth">
<BaseButton <BaseButton
v-if="menuActive" v-if="menuActive"
@click="$store.commit('menuActive', false)" @click="baseStore.setMenuActive(false)"
class="menu-hide-button d-print-none" class="menu-hide-button d-print-none"
> >
<icon icon="times"/> <icon icon="times"/>
@ -26,7 +26,7 @@
> >
<BaseButton <BaseButton
v-if="menuActive" v-if="menuActive"
@click="$store.commit('menuActive', false)" @click="baseStore.setMenuActive(false)"
class="mobile-overlay d-print-none" class="mobile-overlay d-print-none"
/> />
@ -61,11 +61,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import {watch, computed, shallowRef, watchEffect, type VNode, h} from 'vue' import {watch, computed, shallowRef, watchEffect, type VNode, h} from 'vue'
import {useStore} from '@/store' import {useBaseStore} from '@/stores/base'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import {useEventListener} from '@vueuse/core' import {useEventListener} from '@vueuse/core'
import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
import Navigation from '@/components/home/navigation.vue' import Navigation from '@/components/home/navigation.vue'
import QuickActions from '@/components/quick-actions/quick-actions.vue' import QuickActions from '@/components/quick-actions/quick-actions.vue'
@ -123,20 +122,19 @@ function useRouteWithModal() {
const {routeWithModal, currentModal, closeModal} = useRouteWithModal() const {routeWithModal, currentModal, closeModal} = useRouteWithModal()
const store = useStore() const baseStore = useBaseStore()
const background = computed(() => baseStore.background)
const background = computed(() => store.state.background) const blurHash = computed(() => baseStore.blurHash)
const blurHash = computed(() => store.state.blurHash) const menuActive = computed(() => baseStore.menuActive)
const menuActive = computed(() => store.state.menuActive)
function showKeyboardShortcuts() { function showKeyboardShortcuts() {
store.commit(KEYBOARD_SHORTCUTS_ACTIVE, true) baseStore.setKeyboardShortcutsActive(true)
} }
const route = useRoute() const route = useRoute()
// hide menu on mobile // hide menu on mobile
watch(() => route.fullPath, () => window.innerWidth < 769 && store.commit(MENU_ACTIVE, false)) watch(() => route.fullPath, () => window.innerWidth < 769 && baseStore.setMenuActive(false))
// FIXME: this is really error prone // FIXME: this is really error prone
// Reset the current list highlight in menu if the current route is not list related. // Reset the current list highlight in menu if the current route is not list related.
@ -158,7 +156,7 @@ watch(() => route.name as string, (routeName) => {
routeName.startsWith('user.settings') routeName.startsWith('user.settings')
) )
) { ) {
store.dispatch(CURRENT_LIST, {list: null}) baseStore.handleSetCurrentList({list: null})
} }
}) })

View File

@ -24,15 +24,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import {computed} from 'vue' import {computed} from 'vue'
import {useStore} from '@/store'
import {useBaseStore} from '@/stores/base'
import Logo from '@/components/home/Logo.vue' import Logo from '@/components/home/Logo.vue'
import PoweredByLink from './PoweredByLink.vue' import PoweredByLink from './PoweredByLink.vue'
const store = useStore() const baseStore = useBaseStore()
const currentList = computed(() => store.state.currentList) const currentList = computed(() => baseStore.currentList)
const background = computed(() => store.state.background) const background = computed(() => baseStore.background)
const logoVisible = computed(() => store.state.logoVisible) const logoVisible = computed(() => baseStore.logoVisible)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -141,7 +141,6 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, onMounted, onBeforeMount} from 'vue' import {ref, computed, onMounted, onBeforeMount} from 'vue'
import {useStore} from '@/store'
import draggable from 'zhyswan-vuedraggable' import draggable from 'zhyswan-vuedraggable'
import type {SortableEvent} from 'sortablejs' import type {SortableEvent} from 'sortablejs'
@ -151,7 +150,6 @@ import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings
import PoweredByLink from '@/components/home/PoweredByLink.vue' import PoweredByLink from '@/components/home/PoweredByLink.vue'
import Logo from '@/components/home/Logo.vue' import Logo from '@/components/home/Logo.vue'
import {MENU_ACTIVE} from '@/store/mutation-types'
import {calculateItemPosition} from '@/helpers/calculateItemPosition' import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle' import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {getListTitle} from '@/helpers/getListTitle' import {getListTitle} from '@/helpers/getListTitle'
@ -159,6 +157,8 @@ import {useEventListener} from '@vueuse/core'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import ColorBubble from '@/components/misc/colorBubble.vue' import ColorBubble from '@/components/misc/colorBubble.vue'
import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
@ -168,10 +168,10 @@ const dragOptions = {
ghostClass: 'ghost', ghostClass: 'ghost',
} }
const store = useStore() const baseStore = useBaseStore()
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
const currentList = computed(() => store.state.currentList) const currentList = computed(() => baseStore.currentList)
const menuActive = computed(() => store.state.menuActive) const menuActive = computed(() => baseStore.menuActive)
const loading = computed(() => namespaceStore.isLoading) const loading = computed(() => namespaceStore.isLoading)
@ -202,7 +202,7 @@ const listStore = useListStore()
function resize() { function resize() {
// Hide the menu by default on mobile // Hide the menu by default on mobile
store.commit(MENU_ACTIVE, window.innerWidth >= 770) baseStore.setMenuActive(window.innerWidth >= 770)
} }
function toggleLists(namespaceId: INamespace['id']) { function toggleLists(namespaceId: INamespace['id']) {
@ -262,7 +262,7 @@ async function saveListPosition(e: SortableEvent) {
) )
try { try {
// create a copy of the list in order to not violate vuex mutations // create a copy of the list in order to not violate pinia manipulation
await listStore.updateList({ await listStore.updateList({
...list, ...list,
position, position,

View File

@ -88,12 +88,10 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import {ref, onMounted, onBeforeUnmount, toRef, watch, computed, type PropType} from 'vue'
import flatPickr from 'vue-flatpickr-component' import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css' import 'flatpickr/dist/flatpickr.css'
import {i18n} from '@/i18n'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
@ -102,146 +100,140 @@ import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours' import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {createDateFromString} from '@/helpers/time/createDateFromString' import {createDateFromString} from '@/helpers/time/createDateFromString'
import {mapState} from 'pinia'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {useI18n} from 'vue-i18n'
export default defineComponent({ const props = defineProps({
name: 'datepicker', modelValue: {
data() { type: [Date, null, String] as PropType<Date | null | string>,
return { validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
date: null, default: null,
show: false,
changed: false,
}
}, },
components: { chooseDateLabel: {
flatPickr, type: String,
BaseButton, default() {
}, const {t} = useI18n({useScope: 'global'})
props: { return t('input.datepicker.chooseDate')
modelValue: {
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
},
chooseDateLabel: {
type: String,
default() {
return i18n.global.t('input.datepicker.chooseDate')
},
},
disabled: {
type: Boolean,
default: false,
}, },
}, },
emits: ['update:modelValue', 'close', 'close-on-change'], disabled: {
mounted() { type: Boolean,
document.addEventListener('click', this.hideDatePopup) default: false,
},
beforeUnmount() {
document.removeEventListener('click', this.hideDatePopup)
},
watch: {
modelValue: {
handler: 'setDateValue',
immediate: true,
},
},
computed: {
...mapState(useAuthStore, {
weekStart: (state) => state.settings.weekStart,
}),
flatPickerConfig() {
return {
altFormat: this.$t('date.altFormatLong'),
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
inline: true,
locale: {
firstDayOfWeek: this.weekStart,
},
}
},
// Since flatpickr dates are strings, we need to convert them to native date objects.
// To make that work, we need a separate variable since flatpickr does not have a change event.
flatPickrDate: {
set(newValue) {
this.date = createDateFromString(newValue)
this.updateData()
},
get() {
if (!this.date) {
return ''
}
return formatDate(this.date, 'yyy-LL-dd H:mm')
},
},
},
methods: {
formatDateShort,
setDateValue(newVal) {
if (newVal === null) {
this.date = null
return
}
this.date = createDateFromString(newVal)
},
updateData() {
this.changed = true
this.$emit('update:modelValue', this.date)
},
toggleDatePopup() {
if (this.disabled) {
return
}
this.show = !this.show
},
hideDatePopup(e) {
if (this.show) {
closeWhenClickedOutside(e, this.$refs.datepickerPopup, this.close)
}
},
close() {
// Kind of dirty, but the timeout allows us to enter a time and click on "confirm" without
// having to click on another input field before it is actually used.
setTimeout(() => {
this.show = false
this.$emit('close', this.changed)
if (this.changed) {
this.changed = false
this.$emit('close-on-change', this.changed)
}
}, 200)
},
setDate(date) {
if (this.date === null) {
this.date = new Date()
}
const interval = calculateDayInterval(date)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
newDate.setHours(calculateNearestHours(newDate))
newDate.setMinutes(0)
newDate.setSeconds(0)
this.date = newDate
this.flatPickrDate = newDate
this.updateData()
},
getDayIntervalFromString(date) {
return calculateDayInterval(date)
},
getWeekdayFromStringInterval(date) {
const interval = calculateDayInterval(date)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
return formatDate(newDate, 'E')
},
}, },
}) })
const emit = defineEmits(['update:modelValue', 'close', 'close-on-change'])
const {t} = useI18n({useScope: 'global'})
const date = ref<Date | null>()
const show = ref(false)
const changed = ref(false)
onMounted(() => document.addEventListener('click', hideDatePopup))
onBeforeUnmount(() =>document.removeEventListener('click', hideDatePopup))
const modelValue = toRef(props, 'modelValue')
watch(
modelValue,
setDateValue,
{immediate: true},
)
const authStore = useAuthStore()
const weekStart = computed(() => authStore.settings.weekStart)
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
inline: true,
locale: {
firstDayOfWeek: weekStart.value,
},
}))
// Since flatpickr dates are strings, we need to convert them to native date objects.
// To make that work, we need a separate variable since flatpickr does not have a change event.
const flatPickrDate = computed({
set(newValue: string | Date) {
date.value = createDateFromString(newValue)
updateData()
},
get() {
if (!date.value) {
return ''
}
return formatDate(date.value, 'yyy-LL-dd H:mm')
},
})
function setDateValue(dateString: string | Date | null) {
if (dateString === null) {
date.value = null
return
}
date.value = createDateFromString(dateString)
}
function updateData() {
changed.value = true
emit('update:modelValue', date.value)
}
function toggleDatePopup() {
if (props.disabled) {
return
}
show.value = !show.value
}
const datepickerPopup = ref<HTMLElement | null>(null)
function hideDatePopup(e) {
if (show.value) {
closeWhenClickedOutside(e, datepickerPopup.value, close)
}
}
function close() {
// Kind of dirty, but the timeout allows us to enter a time and click on "confirm" without
// having to click on another input field before it is actually used.
setTimeout(() => {
show.value = false
emit('close', changed.value)
if (changed.value) {
changed.value = false
emit('close-on-change', changed.value)
}
}, 200)
}
function setDate(dateString: string) {
if (date.value === null) {
date.value = new Date()
}
const interval = calculateDayInterval(dateString)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
newDate.setHours(calculateNearestHours(newDate))
newDate.setMinutes(0)
newDate.setSeconds(0)
date.value = newDate
flatPickrDate.value = newDate
updateData()
}
function getWeekdayFromStringInterval(dateString: string) {
const interval = calculateDayInterval(dateString)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
return formatDate(newDate, 'E')
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,21 +1,25 @@
<template> <template>
<card class="filters has-overflow" :title="hasTitle ? $t('filters.title') : ''"> <card class="filters has-overflow" :title="hasTitle ? $t('filters.title') : ''">
<div class="field"> <div class="field is-flex is-flex-direction-column">
<fancycheckbox v-model="params.filter_include_nulls"> <fancycheckbox
v-model="params.filter_include_nulls"
@update:model-value="change()"
>
{{ $t('filters.attributes.includeNulls') }} {{ $t('filters.attributes.includeNulls') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox <fancycheckbox
v-model="filters.requireAllFilters" v-model="filters.requireAllFilters"
@change="setFilterConcat()" @update:model-value="setFilterConcat()"
> >
{{ $t('filters.attributes.requireAll') }} {{ $t('filters.attributes.requireAll') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox @change="setDoneFilter" v-model="filters.done"> <fancycheckbox v-model="filters.done" @update:model-value="setDoneFilter">
{{ $t('filters.attributes.showDoneTasks') }} {{ $t('filters.attributes.showDoneTasks') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox <fancycheckbox
v-if="!$route.name.includes('list.kanban') || !$route.name.includes('list.table')" v-if="!$route.name.includes('list.kanban') || !$route.name.includes('list.table')"
v-model="sortAlphabetically" v-model="sortAlphabetically"
@update:model-value="change()"
> >
{{ $t('filters.attributes.sortAlphabetically') }} {{ $t('filters.attributes.sortAlphabetically') }}
</fancycheckbox> </fancycheckbox>
@ -42,7 +46,7 @@
/> />
<fancycheckbox <fancycheckbox
v-model="filters.usePriority" v-model="filters.usePriority"
@change="setPriority" @update:model-value="setPriority"
> >
{{ $t('filters.attributes.enablePriority') }} {{ $t('filters.attributes.enablePriority') }}
</fancycheckbox> </fancycheckbox>
@ -58,7 +62,7 @@
/> />
<fancycheckbox <fancycheckbox
v-model="filters.usePercentDone" v-model="filters.usePercentDone"
@change="setPercentDoneFilter" @update:model-value="setPercentDoneFilter"
> >
{{ $t('filters.attributes.enablePercentDone') }} {{ $t('filters.attributes.enablePercentDone') }}
</fancycheckbox> </fancycheckbox>
@ -534,6 +538,7 @@ export default defineComponent({
} else { } else {
this.params.filter_concat = 'or' this.params.filter_concat = 'or'
} }
this.change()
}, },
setPriority() { setPriority() {
this.setSingleValueFilter('priority', 'priority', 'usePriority') this.setSingleValueFilter('priority', 'priority', 'usePriority')

View File

@ -33,18 +33,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {useStore} from '@/store' import {useBaseStore} from '@/stores/base'
import Shortcut from '@/components/misc/shortcut.vue' import Shortcut from '@/components/misc/shortcut.vue'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
import {KEYBOARD_SHORTCUTS as shortcuts} from './shortcuts' import {KEYBOARD_SHORTCUTS as shortcuts} from './shortcuts'
const store = useStore()
function close() { function close() {
store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false) useBaseStore().setKeyboardShortcutsActive(false)
} }
</script> </script>

View File

@ -42,7 +42,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import {ref, computed} from 'vue' import {ref, computed} from 'vue'
import {useStore} from '@/store' import {useRouter, useRoute} from 'vue-router'
import Logo from '@/assets/logo.svg?component' import Logo from '@/assets/logo.svg?component'
import ApiConfig from '@/components/misc/api-config.vue' import ApiConfig from '@/components/misc/api-config.vue'
@ -52,13 +52,14 @@ import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl' import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
import {useOnline} from '@/composables/useOnline' import {useOnline} from '@/composables/useOnline'
import {useRouter, useRoute} from 'vue-router'
import {getAuthForRoute} from '@/router' import {getAuthForRoute} from '@/router'
import {useBaseStore} from '@/stores/base'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const store = useStore() const baseStore = useBaseStore()
const ready = ref(false) const ready = ref(false)
const online = useOnline() const online = useOnline()
@ -68,7 +69,7 @@ const showLoading = computed(() => !ready.value && error.value === '')
async function load() { async function load() {
try { try {
await store.dispatch('loadApp') await baseStore.loadApp()
const redirectTo = getAuthForRoute(route) const redirectTo = getAuthForRoute(route)
if (typeof redirectTo !== 'undefined') { if (typeof redirectTo !== 'undefined') {
await router.push(redirectTo) await router.push(redirectTo)

View File

@ -61,7 +61,6 @@ import TeamService from '@/services/team'
import NamespaceModel from '@/models/namespace' import NamespaceModel from '@/models/namespace'
import TeamModel from '@/models/team' import TeamModel from '@/models/team'
import {CURRENT_LIST, LOADING, LOADING_MODULE, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
import ListModel from '@/models/list' import ListModel from '@/models/list'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
@ -70,6 +69,8 @@ import {getHistory} from '@/modules/listHistory'
import {parseTaskText, PrefixMode} from '@/modules/parseTaskText' import {parseTaskText, PrefixMode} from '@/modules/parseTaskText'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode' import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {PREFIXES} from '@/modules/parseTaskText' import {PREFIXES} from '@/modules/parseTaskText'
import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
@ -112,8 +113,10 @@ export default defineComponent({
}, },
computed: { computed: {
active() { active() {
const active = this.$store.state[QUICK_ACTIONS_ACTIVE] const active = useBaseStore().quickActionsActive
if (!active) { if (!active) {
// FIXME: computeds shouldn't have side effects.
// create a watcher instead
this.reset() this.reset()
} }
return active return active
@ -181,8 +184,7 @@ export default defineComponent({
}, },
loading() { loading() {
return this.taskService.loading || return this.taskService.loading ||
(this.$store.state[LOADING] && this.$store.state[LOADING_MODULE] === 'namespaces') || useNamespaceStore().isLoading || useListStore().isLoading ||
(this.$store.state[LOADING] && this.$store.state[LOADING_MODULE] === 'lists') ||
this.teamService.loading this.teamService.loading
}, },
placeholder() { placeholder() {
@ -219,7 +221,8 @@ export default defineComponent({
return this.$t('quickActions.hint', prefixes) return this.$t('quickActions.hint', prefixes)
}, },
currentList() { currentList() {
return Object.keys(this.$store.state[CURRENT_LIST]).length === 0 ? null : this.$store.state[CURRENT_LIST] const currentList = useBaseStore().currentList
return Object.keys(currentList).length === 0 ? null : currentList
}, },
availableCmds() { availableCmds() {
const cmds = [] const cmds = []
@ -360,7 +363,7 @@ export default defineComponent({
}, 150) }, 150)
}, },
closeQuickActions() { closeQuickActions() {
this.$store.commit(QUICK_ACTIONS_ACTIVE, false) useBaseStore().setQuickActionsActive(false)
}, },
doAction(type, item) { doAction(type, item) {
switch (type) { switch (type) {

View File

@ -162,7 +162,9 @@ async function addTask() {
} }
const taskTitleBackup = newTaskTitle.value const taskTitleBackup = newTaskTitle.value
const createdTasks: ITask[] = [] // This allows us to find the tasks with the title they had before being parsed
// by quick add magic.
const createdTasks: { [key: ITask['title']]: ITask } = {}
const tasksToCreate = parseSubtasksViaIndention(newTaskTitle.value) const tasksToCreate = parseSubtasksViaIndention(newTaskTitle.value)
const newTasks = tasksToCreate.map(async ({title}) => { const newTasks = tasksToCreate.map(async ({title}) => {
if (title === '') { if (title === '') {
@ -174,7 +176,7 @@ async function addTask() {
listId: authStore.settings.defaultListId, listId: authStore.settings.defaultListId,
position: props.defaultPosition, position: props.defaultPosition,
}) })
createdTasks.push(task) createdTasks[title] = task
return task return task
}) })
@ -184,7 +186,7 @@ async function addTask() {
const taskRelationService = new TaskRelationService() const taskRelationService = new TaskRelationService()
const relations = tasksToCreate.map(async t => { const relations = tasksToCreate.map(async t => {
const createdTask = createdTasks.find(ct => ct.title === t.title) const createdTask = createdTasks[t.title]
if (typeof createdTask === 'undefined') { if (typeof createdTask === 'undefined') {
return return
} }
@ -194,7 +196,7 @@ async function addTask() {
return return
} }
const createdParentTask = createdTasks.find(ct => ct.title === t.parent) const createdParentTask = createdTasks[t.parent]
if (typeof createdTask === 'undefined' || typeof createdParentTask === 'undefined') { if (typeof createdTask === 'undefined' || typeof createdParentTask === 'undefined') {
return return
} }

View File

@ -41,13 +41,13 @@ import {ref, computed, type PropType} from 'vue'
import {useRouter} from 'vue-router' import {useRouter} from 'vue-router'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import ColorBubble from '@/components/misc/colorBubble.vue'
import Done from '@/components/misc/Done.vue' import Done from '@/components/misc/Done.vue'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard' import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {useTaskStore} from '@/stores/tasks'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import ColorBubble from '@/components/misc/colorBubble.vue'
import {useTaskStore} from '@/stores/tasks'
const props = defineProps({ const props = defineProps({
task: { task: {

View File

@ -35,7 +35,7 @@
{{ task.title }} {{ task.title }}
</span> </span>
<labels class="labels ml-2 mr-1" :labels="task.labels" v-if="task.labels.length > 0"/> <labels class="labels ml-2 mr-1" :labels="task.labels" v-if="task.labels.length > 0" />
<user <user
:avatar-size="27" :avatar-size="27"
:is-inline="true" :is-inline="true"
@ -119,6 +119,7 @@ import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatD
import ColorBubble from '@/components/misc/colorBubble.vue' import ColorBubble from '@/components/misc/colorBubble.vue'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import {useBaseStore} from '@/stores/base'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
export default defineComponent({ export default defineComponent({
@ -188,10 +189,11 @@ export default defineComponent({
return list !== null ? list.hexColor : '' return list !== null ? list.hexColor : ''
}, },
currentList() { currentList() {
return typeof this.$store.state.currentList === 'undefined' ? { const baseStore = useBaseStore()
return typeof baseStore.currentList === 'undefined' ? {
id: 0, id: 0,
title: '', title: '',
} : this.$store.state.currentList } : baseStore.currentList
}, },
taskDetailRoute() { taskDetailRoute() {
return { return {
@ -238,8 +240,7 @@ export default defineComponent({
this.task.isFavorite = !this.task.isFavorite this.task.isFavorite = !this.task.isFavorite
this.task = await this.taskService.update(this.task) this.task = await this.taskService.update(this.task)
this.$emit('task-updated', this.task) this.$emit('task-updated', this.task)
const namespaceStore = useNamespaceStore() useNamespaceStore().loadNamespacesIfFavoritesDontExist()
namespaceStore.loadNamespacesIfFavoritesDontExist()
}, },
hideDeferDueDatePopup(e) { hideDeferDueDatePopup(e) {
if (!this.showDefer) { if (!this.showDefer) {

View File

@ -1,14 +1,9 @@
import {createRandomID} from '@/helpers/randomId'
import {parseURL} from 'ufo' import {parseURL} from 'ufo'
export interface Provider { import {createRandomID} from '@/helpers/randomId'
name: string import type {IProvider} from '@/types/IProvider'
key: string
authUrl: string
clientId: string
}
export const redirectToProvider = (provider: Provider, redirectUrl: string = '') => { export const redirectToProvider = (provider: IProvider, redirectUrl: string = '') => {
// We're not using the redirect url provided by the server to allow redirects when using the electron app. // We're not using the redirect url provided by the server to allow redirects when using the electron app.
// The implications are not quite clear yet hence the logic to pass in another redirect url still exists. // The implications are not quite clear yet hence the logic to pass in another redirect url still exists.

View File

@ -1,5 +1,5 @@
// Save the current list view to local storage // Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads. // We use local storage and not a store here to make it persistent across reloads.
export const saveListView = (listId, routeName) => { export const saveListView = (listId, routeName) => {
if (routeName.includes('settings.')) { if (routeName.includes('settings.')) {
return return

View File

@ -0,0 +1,19 @@
export function scrollIntoView(el: HTMLElement | null | undefined) {
if (!el) {
return
}
const boundingRect = el.getBoundingClientRect()
const scrollY = window.scrollY
if (
boundingRect.top > (scrollY + window.innerHeight) ||
boundingRect.top < scrollY
) {
el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
})
}
}

View File

@ -1,5 +1,5 @@
export function calculateDayInterval(date, currentDay = (new Date().getDay())) { export function calculateDayInterval(dateString: string, currentDay = (new Date().getDay())) {
switch (date) { switch (dateString) {
case 'today': case 'today':
return 0 return 0
case 'tomorrow': case 'tomorrow':

View File

@ -14,8 +14,6 @@ import Notifications from '@kyvg/vue3-notification'
// PWA // PWA
import './registerServiceWorker' import './registerServiceWorker'
// Vuex
import { store, key } from './store'
// i18n // i18n
import {i18n} from './i18n' import {i18n} from './i18n'
@ -106,8 +104,6 @@ if (window.SENTRY_ENABLED) {
} }
app.use(pinia) app.use(pinia)
app.use(store, key) // pass the injection key
app.use(router) app.use(router)
app.use(i18n) app.use(i18n)

View File

@ -1,7 +1,7 @@
import AbstractModel from './abstractModel' import AbstractModel from './abstractModel'
import UserSettingsModel from '@/models/userSettings' import UserSettingsModel from '@/models/userSettings'
import { AUTH_TYPES, type IUser } from '@/modelTypes/IUser' import { AUTH_TYPES, type IUser, type AuthType } from '@/modelTypes/IUser'
import type { IUserSettings } from '@/modelTypes/IUserSettings' import type { IUserSettings } from '@/modelTypes/IUserSettings'
export function getAvatarUrl(user: IUser, size = 50) { export function getAvatarUrl(user: IUser, size = 50) {
@ -22,7 +22,7 @@ export default class UserModel extends AbstractModel<IUser> implements IUser {
username = '' username = ''
name = '' name = ''
exp = 0 exp = 0
type = AUTH_TYPES.UNKNOWN type: AuthType = AUTH_TYPES.UNKNOWN
created: Date created: Date
updated: Date updated: Date

View File

@ -22,7 +22,8 @@ import DataExportDownload from '../views/user/DataExportDownload.vue'
import UpcomingTasksComponent from '../views/tasks/ShowTasks.vue' import UpcomingTasksComponent from '../views/tasks/ShowTasks.vue'
import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth.vue' import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth.vue'
import ListNamespaces from '../views/namespaces/ListNamespaces.vue' import ListNamespaces from '../views/namespaces/ListNamespaces.vue'
import TaskDetailView from '../views/tasks/TaskDetailView.vue' const TaskDetailView = () => import('../views/tasks/TaskDetailView.vue')
// Team Handling // Team Handling
import ListTeamsComponent from '../views/teams/ListTeams.vue' import ListTeamsComponent from '../views/teams/ListTeams.vue'
// Label Handling // Label Handling

View File

@ -1,53 +0,0 @@
import type { ActionContext } from 'vuex'
import type { StoreDefinition } from 'pinia'
import {LOADING, LOADING_MODULE} from './mutation-types'
import type { RootStoreState } from './types'
/**
* This helper sets the loading state with a 100ms delay to avoid flickering.
*
* @param {*} context The vuex module context.
* @param {null|String} module The module that is loading. This parameter allows components to listen for specific parts of the application loading.
* @param {null|function} loadFunc If not null, this function will be executed instead of the default setting loading.
*/
export function setLoading<State>(
context : ActionContext<State, RootStoreState>,
module : string | null = null,
loadFunc : (() => void) | null = null,
) {
const timeout = setTimeout(() => {
if (loadFunc === null) {
context.commit(LOADING, true, {root: true})
context.commit(LOADING_MODULE, module, {root: true})
} else {
loadFunc(true)
}
}, 100)
return () => {
clearTimeout(timeout)
if (loadFunc === null) {
context.commit(LOADING, false, {root: true})
context.commit(LOADING_MODULE, null, {root: true})
} else {
loadFunc(false)
}
}
}
export const setLoadingPinia = (store: StoreDefinition, loadFunc : ((isLoading: boolean) => void) | null = null) => {
const timeout = setTimeout(() => {
if (loadFunc === null) {
store.isLoading = true
} else {
loadFunc(true)
}
}, 100)
return () => {
clearTimeout(timeout)
if (loadFunc === null) {
store.isLoading = false
} else {
loadFunc(false)
}
}
}

View File

@ -1,138 +0,0 @@
import type {InjectionKey} from 'vue'
import {createStore, useStore as baseUseStore, Store} from 'vuex'
import {getBlobFromBlurHash} from '../helpers/getBlobFromBlurHash'
import {
BACKGROUND,
BLUR_HASH,
CURRENT_LIST,
HAS_TASKS,
KEYBOARD_SHORTCUTS_ACTIVE,
LOADING,
LOADING_MODULE, LOGO_VISIBLE,
MENU_ACTIVE,
QUICK_ACTIONS_ACTIVE,
} from './mutation-types'
import kanban from './modules/kanban'
import ListModel from '@/models/list'
import ListService from '../services/list'
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
import type { RootStoreState, StoreState } from './types'
import pinia from '@/pinia'
import {useAuthStore} from '@/stores/auth'
export const key: InjectionKey<Store<StoreState>> = Symbol()
// define your own `useStore` composition function
export function useStore () {
return baseUseStore(key)
}
export const store = createStore<RootStoreState>({
strict: import.meta.env.DEV,
modules: {
kanban,
},
state: () => ({
loading: false,
loadingModule: null,
// This is used to highlight the current list in menu for all list related views
currentList: new ListModel({
id: 0,
isArchived: false,
}),
background: '',
blurHash: '',
hasTasks: false,
menuActive: true,
keyboardShortcutsActive: false,
quickActionsActive: false,
logoVisible: true,
}),
mutations: {
[LOADING](state, loading) {
state.loading = loading
},
[LOADING_MODULE](state, module) {
state.loadingModule = module
},
[CURRENT_LIST](state, currentList) {
// Server updates don't return the right. Therefore, the right is reset after updating the list which is
// confusing because all the buttons will disappear in that case. To prevent this, we're keeping the right
// when updating the list in global state.
if (typeof state.currentList.maxRight !== 'undefined' && (typeof currentList.maxRight === 'undefined' || currentList.maxRight === null)) {
currentList.maxRight = state.currentList.maxRight
}
state.currentList = currentList
},
[HAS_TASKS](state, hasTasks) {
state.hasTasks = hasTasks
},
[MENU_ACTIVE](state, menuActive) {
state.menuActive = menuActive
},
toggleMenu(state) {
state.menuActive = !state.menuActive
},
[KEYBOARD_SHORTCUTS_ACTIVE](state, active) {
state.keyboardShortcutsActive = active
},
[QUICK_ACTIONS_ACTIVE](state, active) {
state.quickActionsActive = active
},
[BACKGROUND](state, background) {
state.background = background
},
[BLUR_HASH](state, blurHash) {
state.blurHash = blurHash
},
[LOGO_VISIBLE](state, visible: boolean) {
state.logoVisible = visible
},
},
actions: {
async [CURRENT_LIST]({state, commit}, {list, forceUpdate = false}) {
if (list === null) {
commit(CURRENT_LIST, {})
commit(BACKGROUND, null)
commit(BLUR_HASH, null)
return
}
// The forceUpdate parameter is used only when updating a list background directly because in that case
// the current list stays the same, but we want to show the new background right away.
if (list.id !== state.currentList.id || forceUpdate) {
if (list.backgroundInformation) {
try {
const blurHash = await getBlobFromBlurHash(list.backgroundBlurHash)
if (blurHash) {
commit(BLUR_HASH, window.URL.createObjectURL(blurHash))
}
const listService = new ListService()
const background = await listService.background(list)
commit(BACKGROUND, background)
} catch (e) {
console.error('Error getting background image for list', list.id, e)
}
}
}
if (typeof list.backgroundInformation === 'undefined' || list.backgroundInformation === null) {
commit(BACKGROUND, null)
commit(BLUR_HASH, null)
}
commit(CURRENT_LIST, list)
},
async loadApp() {
await checkAndSetApiUrl(window.API_URL)
const authStore = useAuthStore(pinia)
await authStore.checkAuth()
},
},
})

View File

@ -1,12 +0,0 @@
export const LOADING = 'loading'
export const LOADING_MODULE = 'loadingModule'
export const CURRENT_LIST = 'currentList'
export const HAS_TASKS = 'hasTasks'
export const MENU_ACTIVE = 'menuActive'
export const KEYBOARD_SHORTCUTS_ACTIVE = 'keyboardShortcutsActive'
export const QUICK_ACTIONS_ACTIVE = 'quickActionsActive'
export const BACKGROUND = 'background'
export const BLUR_HASH = 'blurHash'
export const LOGO_VISIBLE = 'logoVisible'
export const CONFIG = 'config'

View File

@ -1,111 +0,0 @@
import type { IBucket } from '@/modelTypes/IBucket'
import type { IUserSettings } from '@/modelTypes/IUserSettings'
import type { IList } from '@/modelTypes/IList'
import type { IAttachment } from '@/modelTypes/IAttachment'
import type { ILabel } from '@/modelTypes/ILabel'
import type { INamespace } from '@/modelTypes/INamespace'
import type { IUser } from '@/modelTypes/IUser'
export interface RootStoreState {
loading: boolean,
loadingModule: null,
currentList: IList,
background: string,
blurHash: string,
hasTasks: boolean,
menuActive: boolean,
keyboardShortcutsActive: boolean,
quickActionsActive: boolean,
logoVisible: boolean,
}
export interface AttachmentState {
attachments: IAttachment[],
}
export interface AuthState {
authenticated: boolean,
isLinkShareAuth: boolean,
info: IUser | null,
needsTotpPasscode: boolean,
avatarUrl: string,
lastUserInfoRefresh: Date | null,
settings: IUserSettings,
isLoading: boolean,
isLoadingGeneralSettings: boolean
}
export interface ConfigState {
version: string,
frontendUrl: string,
motd: string,
linkSharingEnabled: boolean,
maxFileSize: '20MB',
registrationEnabled: boolean,
availableMigrators: [],
taskAttachmentsEnabled: boolean,
totpEnabled: boolean,
enabledBackgroundProviders: [],
legal: {
imprintUrl: string,
privacyPolicyUrl: string,
},
caldavEnabled: boolean,
userDeletionEnabled: boolean,
taskCommentsEnabled: boolean,
auth: {
local: {
enabled: boolean,
},
openidConnect: {
enabled: boolean,
redirectUrl: string,
providers: [],
},
},
}
export interface KanbanState {
buckets: IBucket[],
listId: IList['id'],
bucketLoading: {},
taskPagesPerBucket: {
[id: IBucket['id']]: number
},
allTasksLoadedForBucket: {
[id: IBucket['id']]: boolean
},
}
export interface LabelState {
labels: {
[id: ILabel['id']]: ILabel
},
isLoading: boolean,
}
export interface ListState {
lists: { [id: IList['id']]: IList },
isLoading: boolean,
}
export interface NamespaceState {
namespaces: INamespace[]
isLoading: boolean,
}
export interface TaskState {
isLoading: boolean,
}
export type StoreState = RootStoreState & {
config: ConfigState,
auth: AuthState,
namespaces: NamespaceState,
kanban: KanbanState,
tasks: TaskState,
lists: ListState,
attachments: AttachmentState,
labels: LabelState,
}

View File

@ -1,9 +1,12 @@
import {defineStore, acceptHMRUpdate} from 'pinia' import {defineStore, acceptHMRUpdate} from 'pinia'
import {findIndexById} from '@/helpers/utils' import {findIndexById} from '@/helpers/utils'
import type {AttachmentState} from '@/store/types'
import type {IAttachment} from '@/modelTypes/IAttachment' import type {IAttachment} from '@/modelTypes/IAttachment'
export interface AttachmentState {
attachments: IAttachment[],
}
export const useAttachmentStore = defineStore('attachment', { export const useAttachmentStore = defineStore('attachment', {
state: (): AttachmentState => ({ state: (): AttachmentState => ({
attachments: [], attachments: [],

View File

@ -6,16 +6,26 @@ import {objectToSnakeCase} from '@/helpers/case'
import UserModel, { getAvatarUrl } from '@/models/user' import UserModel, { getAvatarUrl } from '@/models/user'
import UserSettingsService from '@/services/userSettings' import UserSettingsService from '@/services/userSettings'
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth' import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
import {setLoadingPinia} from '@/store/helper' import {setModuleLoading} from '@/stores/helper'
import {success} from '@/message' import {success} from '@/message'
import {redirectToProvider} from '@/helpers/redirectToProvider' import {redirectToProvider} from '@/helpers/redirectToProvider'
import {AUTH_TYPES, type IUser} from '@/modelTypes/IUser' import {AUTH_TYPES, type IUser} from '@/modelTypes/IUser'
import type {AuthState} from '@/store/types'
import type {IUserSettings} from '@/modelTypes/IUserSettings' import type {IUserSettings} from '@/modelTypes/IUserSettings'
import router from '@/router' import router from '@/router'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
import UserSettingsModel from '@/models/userSettings' import UserSettingsModel from '@/models/userSettings'
import {store} from '@/store'
export interface AuthState {
authenticated: boolean,
isLinkShareAuth: boolean,
info: IUser | null,
needsTotpPasscode: boolean,
avatarUrl: string,
lastUserInfoRefresh: Date | null,
settings: IUserSettings,
isLoading: boolean,
isLoadingGeneralSettings: boolean
}
export const useAuthStore = defineStore('auth', { export const useAuthStore = defineStore('auth', {
state: () : AuthState => ({ state: () : AuthState => ({
@ -93,7 +103,6 @@ export const useAuthStore = defineStore('auth', {
// Logs a user in with a set of credentials. // Logs a user in with a set of credentials.
async login(credentials) { async login(credentials) {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
store.commit('loading', true)
this.setIsLoading(true) this.setIsLoading(true)
// Delete an eventually preexisting old token // Delete an eventually preexisting old token
@ -117,7 +126,6 @@ export const useAuthStore = defineStore('auth', {
throw e throw e
} finally { } finally {
store.commit('loading', false)
this.setIsLoading(false) this.setIsLoading(false)
} }
}, },
@ -126,7 +134,6 @@ export const useAuthStore = defineStore('auth', {
// Not sure if this is the right place to put the logic in, maybe a seperate js component would be better suited. // Not sure if this is the right place to put the logic in, maybe a seperate js component would be better suited.
async register(credentials) { async register(credentials) {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
store.commit('loading', true)
this.setIsLoading(true) this.setIsLoading(true)
try { try {
await HTTP.post('register', credentials) await HTTP.post('register', credentials)
@ -138,14 +145,12 @@ export const useAuthStore = defineStore('auth', {
throw e throw e
} finally { } finally {
store.commit('loading', false)
this.setIsLoading(false) this.setIsLoading(false)
} }
}, },
async openIdAuth({provider, code}) { async openIdAuth({provider, code}) {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
store.commit('loading', true)
this.setIsLoading(true) this.setIsLoading(true)
const data = { const data = {
@ -162,7 +167,6 @@ export const useAuthStore = defineStore('auth', {
// Tell others the user is autheticated // Tell others the user is autheticated
this.checkAuth() this.checkAuth()
} finally { } finally {
store.commit('loading', false)
this.setIsLoading(false) this.setIsLoading(false)
} }
}, },
@ -271,6 +275,27 @@ export const useAuthStore = defineStore('auth', {
} }
}, },
/**
* Try to verify the email
* @returns {Promise<boolean>} if the email was successfully confirmed
*/
async verifyEmail() {
const emailVerifyToken = localStorage.getItem('emailConfirmToken')
if (emailVerifyToken) {
const stopLoading = setModuleLoading(this)
try {
await HTTPFactory().post('user/confirm', {token: emailVerifyToken})
localStorage.removeItem('emailConfirmToken')
return true
} catch(e) {
throw new Error(e.response.data.message)
} finally {
stopLoading()
}
}
return false
},
async saveUserSettings({ async saveUserSettings({
settings, settings,
showMessage = true, showMessage = true,
@ -280,8 +305,7 @@ export const useAuthStore = defineStore('auth', {
}) { }) {
const userSettingsService = new UserSettingsService() const userSettingsService = new UserSettingsService()
// FIXME const cancel = setModuleLoading(this, this.setIsLoadingGeneralSettings)
const cancel = setLoadingPinia(this, this.setIsLoadingGeneralSettings)
try { try {
saveLanguage(settings.language) saveLanguage(settings.language)
await userSettingsService.update(settings) await userSettingsService.update(settings)

143
src/stores/base.ts Normal file
View File

@ -0,0 +1,143 @@
import {defineStore, acceptHMRUpdate} from 'pinia'
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
import ListModel from '@/models/list'
import ListService from '../services/list'
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
import {useAuthStore} from '@/stores/auth'
import type {IList} from '@/modelTypes/IList'
export interface RootStoreState {
loading: boolean,
currentList: IList,
background: string,
blurHash: string,
hasTasks: boolean,
menuActive: boolean,
keyboardShortcutsActive: boolean,
quickActionsActive: boolean,
logoVisible: boolean,
}
export const useBaseStore = defineStore('base', {
state: () : RootStoreState => ({
loading: false,
// This is used to highlight the current list in menu for all list related views
currentList: new ListModel({
id: 0,
isArchived: false,
}),
background: '',
blurHash: '',
hasTasks: false,
menuActive: true,
keyboardShortcutsActive: false,
quickActionsActive: false,
logoVisible: true,
}),
actions: {
setLoading(loading: boolean) {
this.loading = loading
},
setCurrentList(currentList: IList) {
// Server updates don't return the right. Therefore, the right is reset after updating the list which is
// confusing because all the buttons will disappear in that case. To prevent this, we're keeping the right
// when updating the list in global state.
if (
typeof this.currentList.maxRight !== 'undefined' &&
(
typeof currentList.maxRight === 'undefined' ||
currentList.maxRight === null
)
) {
currentList.maxRight = this.currentList.maxRight
}
this.currentList = currentList
},
setHasTasks(hasTasks: boolean) {
this.hasTasks = hasTasks
},
setMenuActive(menuActive: boolean) {
this.menuActive = menuActive
},
toggleMenu() {
this.menuActive = !this.menuActive
},
setKeyboardShortcutsActive(active: boolean) {
this.keyboardShortcutsActive = active
},
setQuickActionsActive(active: boolean) {
this.quickActionsActive = active
},
setBackground(background: string) {
this.background = background
},
setBlurHash(blurHash: string) {
this.blurHash = blurHash
},
setLogoVisible(visible: boolean) {
this.logoVisible = visible
},
async handleSetCurrentList({list, forceUpdate = false} : {list: IList, forceUpdate: boolean}) {
if (list === null) {
this.setCurrentList({})
this.setBackground('')
this.setBlurHash('')
return
}
// The forceUpdate parameter is used only when updating a list background directly because in that case
// the current list stays the same, but we want to show the new background right away.
if (list.id !== this.currentList.id || forceUpdate) {
if (list.backgroundInformation) {
try {
const blurHash = await getBlobFromBlurHash(list.backgroundBlurHash)
if (blurHash) {
this.setBlurHash(window.URL.createObjectURL(blurHash))
}
const listService = new ListService()
const background = await listService.background(list)
this.setBackground(background)
} catch (e) {
console.error('Error getting background image for list', list.id, e)
}
}
}
if (typeof list.backgroundInformation === 'undefined' || list.backgroundInformation === null) {
this.setBackground('')
this.setBlurHash('')
}
this.setCurrentList(list)
},
async loadApp() {
await checkAndSetApiUrl(window.API_URL)
useAuthStore().checkAuth()
},
},
})
// support hot reloading
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useBaseStore, import.meta.hot))
}

View File

@ -1,10 +1,40 @@
import {defineStore, acceptHMRUpdate} from 'pinia' import {defineStore, acceptHMRUpdate} from 'pinia'
import {parseURL} from 'ufo' import {parseURL} from 'ufo'
import {CONFIG} from '../store/mutation-types'
import {HTTPFactory} from '@/http-common' import {HTTPFactory} from '@/http-common'
import {objectToCamelCase} from '@/helpers/case' import {objectToCamelCase} from '@/helpers/case'
import type {ConfigState} from '@/store/types'
import type {IProvider} from '@/types/IProvider'
export interface ConfigState {
version: string,
frontendUrl: string,
motd: string,
linkSharingEnabled: boolean,
maxFileSize: string,
registrationEnabled: boolean,
availableMigrators: [],
taskAttachmentsEnabled: boolean,
totpEnabled: boolean,
enabledBackgroundProviders: [],
legal: {
imprintUrl: string,
privacyPolicyUrl: string,
},
caldavEnabled: boolean,
userDeletionEnabled: boolean,
taskCommentsEnabled: boolean,
auth: {
local: {
enabled: boolean,
},
openidConnect: {
enabled: boolean,
redirectUrl: string,
providers: IProvider[],
},
},
}
export const useConfigStore = defineStore('config', { export const useConfigStore = defineStore('config', {
state: (): ConfigState => ({ state: (): ConfigState => ({
@ -45,13 +75,13 @@ export const useConfigStore = defineStore('config', {
}, },
}, },
actions: { actions: {
[CONFIG](config: ConfigState) { setConfig(config: ConfigState) {
Object.assign(this, config) Object.assign(this, config)
}, },
async update() { async update() {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
const {data: config} = await HTTP.get('info') const {data: config} = await HTTP.get('info')
this[CONFIG](objectToCamelCase(config)) this.setConfig(objectToCamelCase(config))
return config return config
}, },
}, },

25
src/stores/helper.ts Normal file
View File

@ -0,0 +1,25 @@
import type { StoreDefinition } from 'pinia'
export interface LoadingState {
isLoading: boolean
}
const LOADING_TIMEOUT = 100
export const setModuleLoading = <LoadingStore extends StoreDefinition<string, LoadingState>>(store: LoadingStore, loadFunc : ((isLoading: boolean) => void) | null = null) => {
const timeout = setTimeout(() => {
if (loadFunc === null) {
store.isLoading = true
} else {
loadFunc(true)
}
}, LOADING_TIMEOUT)
return () => {
clearTimeout(timeout)
if (loadFunc === null) {
store.isLoading = false
} else {
loadFunc(false)
}
}
}

View File

@ -1,14 +1,15 @@
import type { Module } from 'vuex' import {defineStore, acceptHMRUpdate} from 'pinia'
import cloneDeep from 'lodash.clonedeep' import cloneDeep from 'lodash.clonedeep'
import {findById, findIndexById} from '@/helpers/utils' import {findById, findIndexById} from '@/helpers/utils'
import {i18n} from '@/i18n' import {i18n} from '@/i18n'
import {success} from '@/message' import {success} from '@/message'
import BucketService from '../../services/bucket' import BucketService from '@/services/bucket'
import {setLoading} from '../helper'
import TaskCollectionService from '@/services/taskCollection' import TaskCollectionService from '@/services/taskCollection'
import type { RootStoreState, KanbanState } from '@/store/types'
import {setModuleLoading} from '@/stores/helper'
import type { ITask } from '@/modelTypes/ITask' import type { ITask } from '@/modelTypes/ITask'
import type { IList } from '@/modelTypes/IList' import type { IList } from '@/modelTypes/IList'
import type { IBucket } from '@/modelTypes/IBucket' import type { IBucket } from '@/modelTypes/IBucket'
@ -37,192 +38,35 @@ const addTaskToBucketAndSort = (state: KanbanState, task: ITask) => {
state.buckets[bucketIndex].tasks.sort((a, b) => a.kanbanPosition > b.kanbanPosition ? 1 : -1) state.buckets[bucketIndex].tasks.sort((a, b) => a.kanbanPosition > b.kanbanPosition ? 1 : -1)
} }
export interface KanbanState {
buckets: IBucket[],
listId: IList['id'],
bucketLoading: {
[id: IBucket['id']]: boolean
},
taskPagesPerBucket: {
[id: IBucket['id']]: number
},
allTasksLoadedForBucket: {
[id: IBucket['id']]: boolean
},
isLoading: boolean,
}
/** /**
* This store is intended to hold the currently active kanban view. * This store is intended to hold the currently active kanban view.
* It should hold only the current buckets. * It should hold only the current buckets.
*/ */
const kanbanStore : Module<KanbanState, RootStoreState> = { export const useKanbanStore = defineStore('kanban', {
namespaced: true, state: () : KanbanState => ({
state: () => ({
buckets: [], buckets: [],
listId: 0, listId: 0,
bucketLoading: {}, bucketLoading: {},
taskPagesPerBucket: {}, taskPagesPerBucket: {},
allTasksLoadedForBucket: {}, allTasksLoadedForBucket: {},
isLoading: false,
}), }),
mutations: {
setListId(state, listId: IList['id']) {
state.listId = parseInt(listId)
},
setBuckets(state, buckets: IBucket[]) {
state.buckets = buckets
buckets.forEach(b => {
state.taskPagesPerBucket[b.id] = 1
state.allTasksLoadedForBucket[b.id] = false
})
},
addBucket(state, bucket: IBucket) {
state.buckets.push(bucket)
},
removeBucket(state, bucket: IBucket) {
const bucketIndex = findIndexById(state.buckets, bucket.id)
state.buckets.splice(bucketIndex, 1)
},
setBucketById(state, bucket: IBucket) {
const bucketIndex = findIndexById(state.buckets, bucket.id)
state.buckets[bucketIndex] = bucket
},
setBucketByIndex(state, {
bucketIndex,
bucket,
} : {
bucketIndex: number,
bucket: IBucket
}) {
state.buckets[bucketIndex] = bucket
},
setTaskInBucketByIndex(state, {
bucketIndex,
taskIndex,
task,
} : {
bucketIndex: number,
taskIndex: number,
task: ITask
}) {
const bucket = state.buckets[bucketIndex]
bucket.tasks[taskIndex] = task
state.buckets[bucketIndex] = bucket
},
setTasksInBucketByBucketId(state, {
bucketId,
tasks,
} : {
bucketId: IBucket['id'],
tasks: ITask[],
}) {
const bucketIndex = findIndexById(state.buckets, bucketId)
state.buckets[bucketIndex] = {
...state.buckets[bucketIndex],
tasks,
}
},
setTaskInBucket(state, task: ITask) {
// If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task
if (state.buckets.length === 0) {
return
}
let found = false
const findAndUpdate = b => {
for (const t in state.buckets[b].tasks) {
if (state.buckets[b].tasks[t].id === task.id) {
const bucket = state.buckets[b]
bucket.tasks[t] = task
if (bucket.id !== task.bucketId) {
bucket.tasks.splice(t, 1)
addTaskToBucketAndSort(state, task)
}
state.buckets[b] = bucket
found = true
return
}
}
}
for (const b in state.buckets) {
if (state.buckets[b].id === task.bucketId) {
findAndUpdate(b)
if (found) {
return
}
}
}
for (const b in state.buckets) {
findAndUpdate(b)
if (found) {
return
}
}
},
addTaskToBucket(state, task: ITask) {
const bucketIndex = findIndexById(state.buckets, task.bucketId)
const oldBucket = state.buckets[bucketIndex]
const newBucket = {
...oldBucket,
tasks: [
...oldBucket.tasks,
task,
],
}
state.buckets[bucketIndex] = newBucket
},
addTasksToBucket(state, {tasks, bucketId}: {
tasks: ITask[];
bucketId: IBucket['id'];
}) {
const bucketIndex = findIndexById(state.buckets, bucketId)
const oldBucket = state.buckets[bucketIndex]
const newBucket = {
...oldBucket,
tasks: [
...oldBucket.tasks,
...tasks,
],
}
state.buckets[bucketIndex] = newBucket
},
removeTaskInBucket(state, task: ITask) {
// If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task
if (state.buckets.length === 0) {
return
}
const { bucketIndex, taskIndex } = getTaskIndicesById(state, task.id)
if (
!bucketIndex ||
state.buckets[bucketIndex]?.id !== task.bucketId ||
!taskIndex ||
(state.buckets[bucketIndex]?.tasks[taskIndex]?.id !== task.id)
) {
return
}
state.buckets[bucketIndex].tasks.splice(taskIndex, 1)
},
setBucketLoading(state, {bucketId, loading}) {
state.bucketLoading[bucketId] = loading
},
setTasksLoadedForBucketPage(state: KanbanState, {bucketId, page}) {
state.taskPagesPerBucket[bucketId] = page
},
setAllTasksLoadedForBucket(state: KanbanState, bucketId) {
state.allTasksLoadedForBucket[bucketId] = true
},
},
getters: { getters: {
getBucketById(state) { getBucketById(state) {
return (bucketId: IBucket['id']) => findById(state.buckets, bucketId) return (bucketId: IBucket['id']) => findById(state.buckets, bucketId)
@ -243,40 +87,216 @@ const kanbanStore : Module<KanbanState, RootStoreState> = {
}, },
actions: { actions: {
async loadBucketsForList(ctx, {listId, params}) { setIsLoading(isLoading: boolean) {
const cancel = setLoading(ctx, 'kanban') this.isLoading = isLoading
},
setListId(listId: IList['id']) {
this.listId = Number(listId)
},
setBuckets(buckets: IBucket[]) {
this.buckets = buckets
buckets.forEach(b => {
this.taskPagesPerBucket[b.id] = 1
this.allTasksLoadedForBucket[b.id] = false
})
},
addBucket(bucket: IBucket) {
this.buckets.push(bucket)
},
removeBucket(bucket: IBucket) {
const bucketIndex = findIndexById(this.buckets, bucket.id)
this.buckets.splice(bucketIndex, 1)
},
setBucketById(bucket: IBucket) {
const bucketIndex = findIndexById(this.buckets, bucket.id)
this.buckets[bucketIndex] = bucket
},
setBucketByIndex({
bucketIndex,
bucket,
} : {
bucketIndex: number,
bucket: IBucket
}) {
this.buckets[bucketIndex] = bucket
},
setTaskInBucketByIndex({
bucketIndex,
taskIndex,
task,
} : {
bucketIndex: number,
taskIndex: number,
task: ITask
}) {
const bucket = this.buckets[bucketIndex]
bucket.tasks[taskIndex] = task
this.buckets[bucketIndex] = bucket
},
setTasksInBucketByBucketId({
bucketId,
tasks,
} : {
bucketId: IBucket['id'],
tasks: ITask[],
}) {
const bucketIndex = findIndexById(this.buckets, bucketId)
this.buckets[bucketIndex] = {
...this.buckets[bucketIndex],
tasks,
}
},
setTaskInBucket(task: ITask) {
// If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task
if (this.buckets.length === 0) {
return
}
let found = false
const findAndUpdate = b => {
for (const t in this.buckets[b].tasks) {
if (this.buckets[b].tasks[t].id === task.id) {
const bucket = this.buckets[b]
bucket.tasks[t] = task
if (bucket.id !== task.bucketId) {
bucket.tasks.splice(t, 1)
addTaskToBucketAndSort(this, task)
}
this.buckets[b] = bucket
found = true
return
}
}
}
for (const b in this.buckets) {
if (this.buckets[b].id === task.bucketId) {
findAndUpdate(b)
if (found) {
return
}
}
}
for (const b in this.buckets) {
findAndUpdate(b)
if (found) {
return
}
}
},
addTaskToBucket(task: ITask) {
const bucketIndex = findIndexById(this.buckets, task.bucketId)
const oldBucket = this.buckets[bucketIndex]
const newBucket = {
...oldBucket,
tasks: [
...oldBucket.tasks,
task,
],
}
this.buckets[bucketIndex] = newBucket
},
addTasksToBucket({tasks, bucketId}: {
tasks: ITask[];
bucketId: IBucket['id'];
}) {
const bucketIndex = findIndexById(this.buckets, bucketId)
const oldBucket = this.buckets[bucketIndex]
const newBucket = {
...oldBucket,
tasks: [
...oldBucket.tasks,
...tasks,
],
}
this.buckets[bucketIndex] = newBucket
},
removeTaskInBucket(task: ITask) {
// If this gets invoked without any tasks actually loaded, we can save the hassle of finding the task
if (this.buckets.length === 0) {
return
}
const { bucketIndex, taskIndex } = getTaskIndicesById(this, task.id)
if (
!bucketIndex ||
this.buckets[bucketIndex]?.id !== task.bucketId ||
!taskIndex ||
(this.buckets[bucketIndex]?.tasks[taskIndex]?.id !== task.id)
) {
return
}
this.buckets[bucketIndex].tasks.splice(taskIndex, 1)
},
setBucketLoading({bucketId, loading}: {bucketId: IBucket['id'], loading: boolean}) {
this.bucketLoading[bucketId] = loading
},
setTasksLoadedForBucketPage({bucketId, page}: {bucketId: IBucket['id'], page: number}) {
this.taskPagesPerBucket[bucketId] = page
},
setAllTasksLoadedForBucket(bucketId: IBucket['id']) {
this.allTasksLoadedForBucket[bucketId] = true
},
async loadBucketsForList({listId, params}: {listId: IList['id'], params}) {
const cancel = setModuleLoading(this)
// Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments // Clear everything to prevent having old buckets in the list if loading the buckets from this list takes a few moments
ctx.commit('setBuckets', []) this.setBuckets([])
params.per_page = TASKS_PER_BUCKET params.per_page = TASKS_PER_BUCKET
const bucketService = new BucketService() const bucketService = new BucketService()
try { try {
const response = await bucketService.getAll({listId}, params) const buckets = await bucketService.getAll({listId}, params)
ctx.commit('setBuckets', response) this.setBuckets(buckets)
ctx.commit('setListId', listId) this.setListId(listId)
return response return buckets
} finally { } finally {
cancel() cancel()
} }
}, },
async loadNextTasksForBucket(ctx, {listId, ps = {}, bucketId}) { async loadNextTasksForBucket(
const isLoading = ctx.state.bucketLoading[bucketId] ?? false {listId, ps = {}, bucketId} :
{listId: IList['id'], ps, bucketId: IBucket['id']},
) {
const isLoading = this.bucketLoading[bucketId] ?? false
if (isLoading) { if (isLoading) {
return return
} }
const page = (ctx.state.taskPagesPerBucket[bucketId] ?? 1) + 1 const page = (this.taskPagesPerBucket[bucketId] ?? 1) + 1
const alreadyLoaded = ctx.state.allTasksLoadedForBucket[bucketId] ?? false const alreadyLoaded = this.allTasksLoadedForBucket[bucketId] ?? false
if (alreadyLoaded) { if (alreadyLoaded) {
return return
} }
const cancel = setLoading(ctx, 'kanban') const cancel = setModuleLoading(this)
ctx.commit('setBucketLoading', {bucketId: bucketId, loading: true}) this.setBucketLoading({bucketId: bucketId, loading: true})
const params = JSON.parse(JSON.stringify(ps)) const params = JSON.parse(JSON.stringify(ps))
@ -305,67 +325,67 @@ const kanbanStore : Module<KanbanState, RootStoreState> = {
const taskService = new TaskCollectionService() const taskService = new TaskCollectionService()
try { try {
const tasks = await taskService.getAll({listId}, params, page) const tasks = await taskService.getAll({listId}, params, page)
ctx.commit('addTasksToBucket', {tasks, bucketId: bucketId}) this.addTasksToBucket({tasks, bucketId: bucketId})
ctx.commit('setTasksLoadedForBucketPage', {bucketId, page}) this.setTasksLoadedForBucketPage({bucketId, page})
if (taskService.totalPages <= page) { if (taskService.totalPages <= page) {
ctx.commit('setAllTasksLoadedForBucket', bucketId) this.setAllTasksLoadedForBucket(bucketId)
} }
return tasks return tasks
} finally { } finally {
cancel() cancel()
ctx.commit('setBucketLoading', {bucketId, loading: false}) this.setBucketLoading({bucketId, loading: false})
} }
}, },
async createBucket(ctx, bucket: IBucket) { async createBucket(bucket: IBucket) {
const cancel = setLoading(ctx, 'kanban') const cancel = setModuleLoading(this)
const bucketService = new BucketService() const bucketService = new BucketService()
try { try {
const createdBucket = await bucketService.create(bucket) const createdBucket = await bucketService.create(bucket)
ctx.commit('addBucket', createdBucket) this.addBucket(createdBucket)
return createdBucket return createdBucket
} finally { } finally {
cancel() cancel()
} }
}, },
async deleteBucket(ctx, {bucket, params}) { async deleteBucket({bucket, params}: {bucket: IBucket, params}) {
const cancel = setLoading(ctx, 'kanban') const cancel = setModuleLoading(this)
const bucketService = new BucketService() const bucketService = new BucketService()
try { try {
const response = await bucketService.delete(bucket) const response = await bucketService.delete(bucket)
ctx.commit('removeBucket', bucket) this.removeBucket(bucket)
// We reload all buckets because tasks are being moved from the deleted bucket // We reload all buckets because tasks are being moved from the deleted bucket
ctx.dispatch('loadBucketsForList', {listId: bucket.listId, params}) this.loadBucketsForList({listId: bucket.listId, params})
return response return response
} finally { } finally {
cancel() cancel()
} }
}, },
async updateBucket(ctx, updatedBucketData) { async updateBucket(updatedBucketData: IBucket) {
const cancel = setLoading(ctx, 'kanban') const cancel = setModuleLoading(this)
const bucketIndex = findIndexById(ctx.state.buckets, updatedBucketData.id) const bucketIndex = findIndexById(this.buckets, updatedBucketData.id)
const oldBucket = cloneDeep(ctx.state.buckets[bucketIndex]) const oldBucket = cloneDeep(this.buckets[bucketIndex])
const updatedBucket = { const updatedBucket = {
...oldBucket, ...oldBucket,
...updatedBucketData, ...updatedBucketData,
} }
ctx.commit('setBucketByIndex', {bucketIndex, bucket: updatedBucket}) this.setBucketByIndex({bucketIndex, bucket: updatedBucket})
const bucketService = new BucketService() const bucketService = new BucketService()
try { try {
const returnedBucket = await bucketService.update(updatedBucket) const returnedBucket = await bucketService.update(updatedBucket)
ctx.commit('setBucketByIndex', {bucketIndex, bucket: returnedBucket}) this.setBucketByIndex({bucketIndex, bucket: returnedBucket})
return returnedBucket return returnedBucket
} catch(e) { } catch(e) {
// restore original state // restore original state
ctx.commit('setBucketByIndex', {bucketIndex, bucket: oldBucket}) this.setBucketByIndex({bucketIndex, bucket: oldBucket})
throw e throw e
} finally { } finally {
@ -373,23 +393,21 @@ const kanbanStore : Module<KanbanState, RootStoreState> = {
} }
}, },
async updateBucketTitle(ctx, { id, title }) { async updateBucketTitle({ id, title }: { id: IBucket['id'], title: IBucket['title'] }) {
const bucket = findById(ctx.state.buckets, id) const bucket = findById(this.buckets, id)
if (bucket?.title === title) { if (bucket?.title === title) {
// bucket title has not changed // bucket title has not changed
return return
} }
const updatedBucketData = { await this.updateBucket({ id, title })
id,
title,
}
await ctx.dispatch('updateBucket', updatedBucketData)
success({message: i18n.global.t('list.kanban.bucketTitleSavedSuccess')}) success({message: i18n.global.t('list.kanban.bucketTitleSavedSuccess')})
}, },
}, },
} })
export default kanbanStore // support hot reloading
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useKanbanStore, import.meta.hot))
}

View File

@ -4,7 +4,7 @@ import LabelService from '@/services/label'
import {success} from '@/message' import {success} from '@/message'
import {i18n} from '@/i18n' import {i18n} from '@/i18n'
import {createNewIndexer} from '@/indexes' import {createNewIndexer} from '@/indexes'
import {setLoadingPinia} from '@/store/helper' import {setModuleLoading} from '@/stores/helper'
import type {ILabel} from '@/modelTypes/ILabel' import type {ILabel} from '@/modelTypes/ILabel'
const {add, remove, update, search} = createNewIndexer('labels', ['title', 'description']) const {add, remove, update, search} = createNewIndexer('labels', ['title', 'description'])
@ -20,7 +20,12 @@ async function getAllLabels(page = 1): Promise<ILabel[]> {
} }
} }
import type {LabelState} from '@/store/types' export interface LabelState {
labels: {
[id: ILabel['id']]: ILabel
},
isLoading: boolean,
}
export const useLabelStore = defineStore('label', { export const useLabelStore = defineStore('label', {
state: () : LabelState => ({ state: () : LabelState => ({
@ -80,7 +85,7 @@ export const useLabelStore = defineStore('label', {
return return
} }
const cancel = setLoadingPinia(this) const cancel = setModuleLoading(this)
try { try {
const labels = await getAllLabels() const labels = await getAllLabels()
@ -92,7 +97,7 @@ export const useLabelStore = defineStore('label', {
}, },
async deleteLabel(label: ILabel) { async deleteLabel(label: ILabel) {
const cancel = setLoadingPinia(this) const cancel = setModuleLoading(this)
const labelService = new LabelService() const labelService = new LabelService()
try { try {
@ -106,7 +111,7 @@ export const useLabelStore = defineStore('label', {
}, },
async updateLabel(label: ILabel) { async updateLabel(label: ILabel) {
const cancel = setLoadingPinia(this) const cancel = setModuleLoading(this)
const labelService = new LabelService() const labelService = new LabelService()
try { try {
@ -120,7 +125,7 @@ export const useLabelStore = defineStore('label', {
}, },
async createLabel(label: ILabel) { async createLabel(label: ILabel) {
const cancel = setLoadingPinia(this) const cancel = setModuleLoading(this)
const labelService = new LabelService() const labelService = new LabelService()
try { try {

View File

@ -3,12 +3,11 @@ import {acceptHMRUpdate, defineStore} from 'pinia'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import ListService from '@/services/list' import ListService from '@/services/list'
import {setLoadingPinia} from '@/store/helper' import {setModuleLoading} from '@/stores/helper'
import {removeListFromHistory} from '@/modules/listHistory' import {removeListFromHistory} from '@/modules/listHistory'
import {createNewIndexer} from '@/indexes' import {createNewIndexer} from '@/indexes'
import {useNamespaceStore} from './namespaces' import {useNamespaceStore} from './namespaces'
import type {ListState} from '@/store/types'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import type {MaybeRef} from '@vueuse/core' import type {MaybeRef} from '@vueuse/core'
@ -20,6 +19,11 @@ const {add, remove, search, update} = createNewIndexer('lists', ['title', 'descr
const FavoriteListsNamespace = -2 const FavoriteListsNamespace = -2
export interface ListState {
lists: { [id: IList['id']]: IList },
isLoading: boolean,
}
export const useListStore = defineStore('list', { export const useListStore = defineStore('list', {
state: () : ListState => ({ state: () : ListState => ({
isLoading: false, isLoading: false,
@ -87,7 +91,7 @@ export const useListStore = defineStore('list', {
}, },
async createList(list: IList) { async createList(list: IList) {
const cancel = setLoadingPinia(this) const cancel = setModuleLoading(this)
const listService = new ListService() const listService = new ListService()
try { try {
@ -103,7 +107,7 @@ export const useListStore = defineStore('list', {
}, },
async updateList(list: IList) { async updateList(list: IList) {
const cancel = setLoadingPinia(this) const cancel = setModuleLoading(this)
const listService = new ListService() const listService = new ListService()
try { try {
@ -113,7 +117,7 @@ export const useListStore = defineStore('list', {
namespaceStore.setListInNamespaceById(list) namespaceStore.setListInNamespaceById(list)
// the returned list from listService.update is the same! // the returned list from listService.update is the same!
// in order to not validate vuex mutations we have to create a new copy // in order to not create a manipulation in pinia store we have to create a new copy
const newList = { const newList = {
...list, ...list,
namespaceId: FavoriteListsNamespace, namespaceId: FavoriteListsNamespace,
@ -139,7 +143,7 @@ export const useListStore = defineStore('list', {
}, },
async deleteList(list: IList) { async deleteList(list: IList) {
const cancel = setLoadingPinia(this) const cancel = setModuleLoading(this)
const listService = new ListService() const listService = new ListService()
try { try {

View File

@ -1,15 +1,19 @@
import {defineStore, acceptHMRUpdate} from 'pinia' import {defineStore, acceptHMRUpdate} from 'pinia'
import NamespaceService from '../services/namespace' import NamespaceService from '../services/namespace'
import {setLoadingPinia} from '@/store/helper' import {setModuleLoading} from '@/stores/helper'
import {createNewIndexer} from '@/indexes' import {createNewIndexer} from '@/indexes'
import type {NamespaceState} from '@/store/types'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description']) const {add, remove, search, update} = createNewIndexer('namespaces', ['title', 'description'])
export interface NamespaceState {
namespaces: INamespace[]
isLoading: boolean,
}
export const useNamespaceStore = defineStore('namespace', { export const useNamespaceStore = defineStore('namespace', {
state: (): NamespaceState => ({ state: (): NamespaceState => ({
isLoading: false, isLoading: false,
@ -135,7 +139,7 @@ export const useNamespaceStore = defineStore('namespace', {
}, },
async loadNamespaces() { async loadNamespaces() {
const cancel = setLoadingPinia(this) const cancel = setModuleLoading(this)
const namespaceService = new NamespaceService() const namespaceService = new NamespaceService()
try { try {
@ -170,7 +174,7 @@ export const useNamespaceStore = defineStore('namespace', {
}, },
async deleteNamespace(namespace: INamespace) { async deleteNamespace(namespace: INamespace) {
const cancel = setLoadingPinia(this) const cancel = setModuleLoading(this)
const namespaceService = new NamespaceService() const namespaceService = new NamespaceService()
try { try {
@ -183,7 +187,7 @@ export const useNamespaceStore = defineStore('namespace', {
}, },
async createNamespace(namespace: INamespace) { async createNamespace(namespace: INamespace) {
const cancel = setLoadingPinia(this) const cancel = setModuleLoading(this)
const namespaceService = new NamespaceService() const namespaceService = new NamespaceService()
try { try {

View File

@ -7,8 +7,7 @@ import TaskAssigneeService from '@/services/taskAssignee'
import LabelTaskService from '@/services/labelTask' import LabelTaskService from '@/services/labelTask'
import UserService from '@/services/user' import UserService from '@/services/user'
import {HAS_TASKS} from '../store/mutation-types' import {playPop} from '@/helpers/playPop'
import {setLoadingPinia} from '../store/helper'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode' import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {parseTaskText} from '@/modules/parseTaskText' import {parseTaskText} from '@/modules/parseTaskText'
@ -24,12 +23,11 @@ import type {IUser} from '@/modelTypes/IUser'
import type {IAttachment} from '@/modelTypes/IAttachment' import type {IAttachment} from '@/modelTypes/IAttachment'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import type {TaskState} from '@/store/types' import {setModuleLoading} from '@/stores/helper'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useAttachmentStore} from '@/stores/attachments' import {useAttachmentStore} from '@/stores/attachments'
import {playPop} from '@/helpers/playPop' import {useKanbanStore} from '@/stores/kanban'
import {store} from '@/store'
// IDEA: maybe use a small fuzzy search here to prevent errors // IDEA: maybe use a small fuzzy search here to prevent errors
function findPropertyByValue(object, key, value) { function findPropertyByValue(object, key, value) {
@ -38,9 +36,14 @@ function findPropertyByValue(object, key, value) {
) )
} }
// Check if the user exists // Check if the user exists in the search results
function validateUsername(users: IUser[], username: IUser['username']) { function validateUser(
return findPropertyByValue(users, 'username', username) users: IUser[],
query: IUser['username'] | IUser['name'] | IUser['email'],
) {
return findPropertyByValue(users, 'username', query) ||
findPropertyByValue(users, 'name', query) ||
findPropertyByValue(users, 'email', query)
} }
// Check if the label exists // Check if the label exists
@ -67,39 +70,54 @@ async function findAssignees(parsedTaskAssignees: string[]) {
const userService = new UserService() const userService = new UserService()
const assignees = parsedTaskAssignees.map(async a => { const assignees = parsedTaskAssignees.map(async a => {
const users = await userService.getAll({}, {s: a}) const users = await userService.getAll({}, {s: a})
return validateUsername(users, a) return validateUser(users, a)
}) })
const validatedUsers = await Promise.all(assignees) const validatedUsers = await Promise.all(assignees)
return validatedUsers.filter((item) => Boolean(item)) return validatedUsers.filter((item) => Boolean(item))
} }
export interface TaskState {
tasks: { [id: ITask['id']]: ITask }
isLoading: boolean,
}
export const useTaskStore = defineStore('task', { export const useTaskStore = defineStore('task', {
state: () : TaskState => ({ state: () : TaskState => ({
tasks: {},
isLoading: false, isLoading: false,
}), }),
getters: {
hasTasks(state) {
return Object.keys(state.tasks).length > 0
},
},
actions: { actions: {
setTasks(tasks: ITask[]) {
tasks.forEach(task => {
this.tasks[task.id] = task
})
},
async loadTasks(params) { async loadTasks(params) {
const taskService = new TaskService() const taskService = new TaskService()
const cancel = setLoadingPinia(this) const cancel = setModuleLoading(this)
try { try {
const tasks = await taskService.getAll({}, params) this.tasks = await taskService.getAll({}, params)
store.commit(HAS_TASKS, tasks.length > 0) return this.tasks
return tasks
} finally { } finally {
cancel() cancel()
} }
}, },
async update(task: ITask) { async update(task: ITask) {
const cancel = setLoadingPinia(this) const cancel = setModuleLoading(this)
const taskService = new TaskService() const taskService = new TaskService()
try { try {
const updatedTask = await taskService.update(task) const updatedTask = await taskService.update(task)
store.commit('kanban/setTaskInBucket', updatedTask) useKanbanStore().setTaskInBucket(updatedTask)
if (task.done) { if (task.done) {
playPop() playPop()
} }
@ -112,7 +130,7 @@ export const useTaskStore = defineStore('task', {
async delete(task: ITask) { async delete(task: ITask) {
const taskService = new TaskService() const taskService = new TaskService()
const response = await taskService.delete(task) const response = await taskService.delete(task)
store.commit('kanban/removeTaskInBucket', task) useKanbanStore().removeTaskInBucket(task)
return response return response
}, },
@ -125,7 +143,8 @@ export const useTaskStore = defineStore('task', {
taskId: ITask['id'] taskId: ITask['id']
attachment: IAttachment attachment: IAttachment
}) { }) {
const t = store.getters['kanban/getTaskById'](taskId) const kanbanStore = useKanbanStore()
const t = kanbanStore.getTaskById(taskId)
if (t.task !== null) { if (t.task !== null) {
const attachments = [ const attachments = [
...t.task.attachments, ...t.task.attachments,
@ -139,7 +158,7 @@ export const useTaskStore = defineStore('task', {
attachments, attachments,
}, },
} }
store.commit('kanban/setTaskInBucketByIndex', newTask) kanbanStore.setTaskInBucketByIndex(newTask)
} }
const attachmentStore = useAttachmentStore() const attachmentStore = useAttachmentStore()
attachmentStore.add(attachment) attachmentStore.add(attachment)
@ -152,12 +171,13 @@ export const useTaskStore = defineStore('task', {
user: IUser, user: IUser,
taskId: ITask['id'] taskId: ITask['id']
}) { }) {
const kanbanStore = useKanbanStore()
const taskAssigneeService = new TaskAssigneeService() const taskAssigneeService = new TaskAssigneeService()
const r = await taskAssigneeService.create(new TaskAssigneeModel({ const r = await taskAssigneeService.create(new TaskAssigneeModel({
userId: user.id, userId: user.id,
taskId: taskId, taskId: taskId,
})) }))
const t = store.getters['kanban/getTaskById'](taskId) const t = kanbanStore.getTaskById(taskId)
if (t.task === null) { if (t.task === null) {
// Don't try further adding a label if the task is not in kanban // Don't try further adding a label if the task is not in kanban
// Usually this means the kanban board hasn't been accessed until now. // Usually this means the kanban board hasn't been accessed until now.
@ -166,7 +186,7 @@ export const useTaskStore = defineStore('task', {
return r return r
} }
store.commit('kanban/setTaskInBucketByIndex', { kanbanStore.setTaskInBucketByIndex({
...t, ...t,
task: { task: {
...t.task, ...t.task,
@ -186,12 +206,13 @@ export const useTaskStore = defineStore('task', {
user: IUser, user: IUser,
taskId: ITask['id'] taskId: ITask['id']
}) { }) {
const kanbanStore = useKanbanStore()
const taskAssigneeService = new TaskAssigneeService() const taskAssigneeService = new TaskAssigneeService()
const response = await taskAssigneeService.delete(new TaskAssigneeModel({ const response = await taskAssigneeService.delete(new TaskAssigneeModel({
userId: user.id, userId: user.id,
taskId: taskId, taskId: taskId,
})) }))
const t = store.getters['kanban/getTaskById'](taskId) const t = kanbanStore.getTaskById(taskId)
if (t.task === null) { if (t.task === null) {
// Don't try further adding a label if the task is not in kanban // Don't try further adding a label if the task is not in kanban
// Usually this means the kanban board hasn't been accessed until now. // Usually this means the kanban board hasn't been accessed until now.
@ -202,7 +223,7 @@ export const useTaskStore = defineStore('task', {
const assignees = t.task.assignees.filter(({ id }) => id !== user.id) const assignees = t.task.assignees.filter(({ id }) => id !== user.id)
store.commit('kanban/setTaskInBucketByIndex', { kanbanStore.setTaskInBucketByIndex({
...t, ...t,
task: { task: {
...t.task, ...t.task,
@ -220,12 +241,13 @@ export const useTaskStore = defineStore('task', {
label: ILabel, label: ILabel,
taskId: ITask['id'] taskId: ITask['id']
}) { }) {
const kanbanStore = useKanbanStore()
const labelTaskService = new LabelTaskService() const labelTaskService = new LabelTaskService()
const r = await labelTaskService.create(new LabelTaskModel({ const r = await labelTaskService.create(new LabelTaskModel({
taskId, taskId,
labelId: label.id, labelId: label.id,
})) }))
const t = store.getters['kanban/getTaskById'](taskId) const t = kanbanStore.getTaskById(taskId)
if (t.task === null) { if (t.task === null) {
// Don't try further adding a label if the task is not in kanban // Don't try further adding a label if the task is not in kanban
// Usually this means the kanban board hasn't been accessed until now. // Usually this means the kanban board hasn't been accessed until now.
@ -234,7 +256,7 @@ export const useTaskStore = defineStore('task', {
return r return r
} }
store.commit('kanban/setTaskInBucketByIndex', { kanbanStore.setTaskInBucketByIndex({
...t, ...t,
task: { task: {
...t.task, ...t.task,
@ -252,12 +274,13 @@ export const useTaskStore = defineStore('task', {
{label, taskId}: {label, taskId}:
{label: ILabel, taskId: ITask['id']}, {label: ILabel, taskId: ITask['id']},
) { ) {
const kanbanStore = useKanbanStore()
const labelTaskService = new LabelTaskService() const labelTaskService = new LabelTaskService()
const response = await labelTaskService.delete(new LabelTaskModel({ const response = await labelTaskService.delete(new LabelTaskModel({
taskId, labelId: taskId, labelId:
label.id, label.id,
})) }))
const t = store.getters['kanban/getTaskById'](taskId) const t = kanbanStore.getTaskById(taskId)
if (t.task === null) { if (t.task === null) {
// Don't try further adding a label if the task is not in kanban // Don't try further adding a label if the task is not in kanban
// Usually this means the kanban board hasn't been accessed until now. // Usually this means the kanban board hasn't been accessed until now.
@ -269,7 +292,7 @@ export const useTaskStore = defineStore('task', {
// Remove the label from the list // Remove the label from the list
const labels = t.task.labels.filter(({ id }) => id !== label.id) const labels = t.task.labels.filter(({ id }) => id !== label.id)
store.commit('kanban/setTaskInBucketByIndex', { kanbanStore.setTaskInBucketByIndex({
...t, ...t,
task: { task: {
...t.task, ...t.task,
@ -346,7 +369,7 @@ export const useTaskStore = defineStore('task', {
} : } :
Partial<ITask>, Partial<ITask>,
) { ) {
const cancel = setLoadingPinia(this) const cancel = setModuleLoading(this)
const parsedTask = parseTaskText(title, getQuickAddMagicMode()) const parsedTask = parseTaskText(title, getQuickAddMagicMode())
const foundListId = await this.findListId({ const foundListId = await this.findListId({
@ -375,13 +398,16 @@ export const useTaskStore = defineStore('task', {
task.repeatAfter = parsedTask.repeats task.repeatAfter = parsedTask.repeats
const taskService = new TaskService() const taskService = new TaskService()
const createdTask = await taskService.create(task) try {
const result = await this.addLabelsToTask({ const createdTask = await taskService.create(task)
task: createdTask, const result = await this.addLabelsToTask({
parsedLabels: parsedTask.labels, task: createdTask,
}) parsedLabels: parsedTask.labels,
cancel() })
return result return result
} finally {
cancel()
}
}, },
}, },
}) })

6
src/types/IProvider.ts Normal file
View File

@ -0,0 +1,6 @@
export interface IProvider {
name: string;
key: string;
authUrl: string;
clientId: string;
}

View File

@ -1,30 +0,0 @@
// https://next.vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html#typescript-support
import { Store } from 'vuex'
import type {
RootStoreState,
AttachmentState,
AuthState,
ConfigState,
KanbanState,
LabelState,
ListState,
NamespaceState,
TaskState,
} from '@/store/types'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store<RootStoreState & {
config: ConfigState,
auth: AuthState,
namespaces: NamespaceState,
kanban: KanbanState,
tasks: TaskState,
lists: ListState,
attachments: AttachmentState,
labels: LabelState,
}>
}
}

View File

@ -60,7 +60,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import {ref, computed} from 'vue' import {ref, computed} from 'vue'
import {useStore} from '@/store'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import ShowTasks from '@/views/tasks/ShowTasks.vue' import ShowTasks from '@/views/tasks/ShowTasks.vue'
@ -71,18 +70,23 @@ import {getHistory} from '@/modules/listHistory'
import {parseDateOrNull} from '@/helpers/parseDateOrNull' import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate' import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
import {useDateTimeSalutation} from '@/composables/useDateTimeSalutation' import {useDateTimeSalutation} from '@/composables/useDateTimeSalutation'
import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {useTaskStore} from '@/stores/tasks'
const welcome = useDateTimeSalutation() const welcome = useDateTimeSalutation()
const store = useStore() const baseStore = useBaseStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const configStore = useConfigStore() const configStore = useConfigStore()
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
const listStore = useListStore() const listStore = useListStore()
const taskStore = useTaskStore()
const listHistory = computed(() => { const listHistory = computed(() => {
// If we don't check this, it tries to load the list background right after logging out // If we don't check this, it tries to load the list background right after logging out
if(!authStore.authenticated) { if(!authStore.authenticated) {
@ -96,15 +100,15 @@ const listHistory = computed(() => {
const migratorsEnabled = computed(() => configStore.availableMigrators?.length > 0) const migratorsEnabled = computed(() => configStore.availableMigrators?.length > 0)
const userInfo = computed(() => authStore.info) const userInfo = computed(() => authStore.info)
const hasTasks = computed(() => store.state.hasTasks) const hasTasks = computed(() => baseStore.hasTasks)
const defaultListId = computed(() => authStore.settings.defaultListId) const defaultListId = computed(() => authStore.settings.defaultListId)
const defaultNamespaceId = computed(() => namespaceStore.namespaces?.[0]?.id || 0) const defaultNamespaceId = computed(() => namespaceStore.namespaces?.[0]?.id || 0)
const hasLists = computed(() => namespaceStore.namespaces?.[0]?.lists.length > 0) const hasLists = computed(() => namespaceStore.namespaces?.[0]?.lists.length > 0)
const loading = computed(() => store.state.loading && store.state.loadingModule === 'tasks') const loading = computed(() => taskStore.isLoading)
const deletionScheduledAt = computed(() => parseDateOrNull(authStore.info?.deletionScheduledAt)) const deletionScheduledAt = computed(() => parseDateOrNull(authStore.info?.deletionScheduledAt))
// This is to reload the tasks list after adding a new task through the global task add. // This is to reload the tasks list after adding a new task through the global task add.
// FIXME: Should use vuex (somehow?) // FIXME: Should use pinia (somehow?)
const showTasksKey = ref(0) const showTasksKey = ref(0)
function updateTaskList() { function updateTaskList() {

View File

@ -68,9 +68,12 @@ import SavedFilterService from '@/services/savedFilter'
import {objectToSnakeCase} from '@/helpers/case' import {objectToSnakeCase} from '@/helpers/case'
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter' import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import {useBaseStore} from '@/stores/base'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const baseStore = useBaseStore()
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
function useSavedFilter(listId: MaybeRef<IList['id']>) { function useSavedFilter(listId: MaybeRef<IList['id']>) {
@ -126,6 +129,7 @@ const router = useRouter()
async function saveSavedFilter() { async function saveSavedFilter() {
await save() await save()
await baseStore.setCurrentList({list: filter})
router.back() router.back()
} }
</script> </script>

View File

@ -227,11 +227,10 @@
import {defineComponent} from 'vue' import {defineComponent} from 'vue'
import draggable from 'zhyswan-vuedraggable' import draggable from 'zhyswan-vuedraggable'
import cloneDeep from 'lodash.clonedeep' import cloneDeep from 'lodash.clonedeep'
import {mapState} from 'pinia'
import BucketModel from '../../models/bucket' import BucketModel from '../../models/bucket'
import {mapState} from 'vuex'
import {RIGHTS as Rights} from '@/constants/rights' import {RIGHTS as Rights} from '@/constants/rights'
import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
import ListWrapper from './ListWrapper.vue' import ListWrapper from './ListWrapper.vue'
import FilterPopup from '@/components/list/partials/filter-popup.vue' import FilterPopup from '@/components/list/partials/filter-popup.vue'
import Dropdown from '@/components/misc/dropdown.vue' import Dropdown from '@/components/misc/dropdown.vue'
@ -240,7 +239,10 @@ import {calculateItemPosition} from '../../helpers/calculateItemPosition'
import KanbanCard from '@/components/tasks/partials/kanban-card.vue' import KanbanCard from '@/components/tasks/partials/kanban-card.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import {isSavedFilter} from '@/helpers/savedFilter' import {isSavedFilter} from '@/helpers/savedFilter'
import {useBaseStore} from '@/stores/base'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import {useKanbanStore} from '@/stores/kanban'
const DRAG_OPTIONS = { const DRAG_OPTIONS = {
// sortable options // sortable options
@ -342,16 +344,18 @@ export default defineComponent({
], ],
} }
}, },
buckets() { ...mapState(useBaseStore, {
return this.$store.state.kanban.buckets
},
...mapState({
loadedListId: state => state.kanban.listId,
loading: state => state[LOADING] && state[LOADING_MODULE] === 'kanban',
taskLoading: state => state[LOADING] && state[LOADING_MODULE] === 'tasks',
canWrite: state => state.currentList.maxRight > Rights.READ, canWrite: state => state.currentList.maxRight > Rights.READ,
list: state => state.currentList, list: state => state.currentList,
}), }),
...mapState(useKanbanStore, {
buckets: state => state.buckets,
loadedListId: state => state.listId,
loading: state => state.isLoading,
}),
...mapState(useTaskStore, {
taskLoading: state => state.isLoading,
}),
}, },
methods: { methods: {
@ -364,7 +368,7 @@ export default defineComponent({
console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $attrs = ${this.$attrs} $route.params =`, this.$route.params) console.debug(`Loading buckets, loadedListId = ${this.loadedListId}, $attrs = ${this.$attrs} $route.params =`, this.$route.params)
this.$store.dispatch('kanban/loadBucketsForList', {listId, params}) useKanbanStore().loadBucketsForList({listId, params})
}, },
setTaskContainerRef(id, el) { setTaskContainerRef(id, el) {
@ -382,7 +386,7 @@ export default defineComponent({
return return
} }
this.$store.dispatch('kanban/loadNextTasksForBucket', { useKanbanStore().loadNextTasksForBucket({
listId: listId, listId: listId,
params: this.params, params: this.params,
bucketId: id, bucketId: id,
@ -390,12 +394,13 @@ export default defineComponent({
}, },
updateTasks(bucketId, tasks) { updateTasks(bucketId, tasks) {
const kanbanStore = useKanbanStore()
const newBucket = { const newBucket = {
...this.$store.getters['kanban/getBucketById'](bucketId), ...kanbanStore.getBucketById(bucketId),
tasks, tasks,
} }
this.$store.commit('kanban/setBucketById', newBucket) kanbanStore.setBucketById(newBucket)
}, },
async updateTaskPosition(e) { async updateTaskPosition(e) {
@ -426,7 +431,7 @@ export default defineComponent({
const taskAfter = newBucket.tasks[newTaskIndex + 1] ?? null const taskAfter = newBucket.tasks[newTaskIndex + 1] ?? null
this.taskUpdating[task.id] = true this.taskUpdating[task.id] = true
const newTask = cloneDeep(task) // cloning the task to avoid vuex store mutations const newTask = cloneDeep(task) // cloning the task to avoid pinia store manipulation
newTask.bucketId = newBucket.id newTask.bucketId = newBucket.id
newTask.kanbanPosition = calculateItemPosition( newTask.kanbanPosition = calculateItemPosition(
taskBefore !== null ? taskBefore.kanbanPosition : null, taskBefore !== null ? taskBefore.kanbanPosition : null,
@ -440,7 +445,7 @@ export default defineComponent({
// Make sure the first and second task don't both get position 0 assigned // Make sure the first and second task don't both get position 0 assigned
if(newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) { if(newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) {
const taskAfterAfter = newBucket.tasks[newTaskIndex + 2] ?? null const taskAfterAfter = newBucket.tasks[newTaskIndex + 2] ?? null
const newTaskAfter = cloneDeep(taskAfter) // cloning the task to avoid vuex store mutations const newTaskAfter = cloneDeep(taskAfter) // cloning the task to avoid pinia store manipulation
newTaskAfter.bucketId = newBucket.id newTaskAfter.bucketId = newBucket.id
newTaskAfter.kanbanPosition = calculateItemPosition( newTaskAfter.kanbanPosition = calculateItemPosition(
0, 0,
@ -472,7 +477,7 @@ export default defineComponent({
listId: this.listId, listId: this.listId,
}) })
this.newTaskText = '' this.newTaskText = ''
this.$store.commit('kanban/addTaskToBucket', task) useKanbanStore().addTaskToBucket(task)
this.scrollTaskContainerToBottom(bucketId) this.scrollTaskContainerToBottom(bucketId)
}, },
@ -494,7 +499,7 @@ export default defineComponent({
listId: this.listId, listId: this.listId,
}) })
await this.$store.dispatch('kanban/createBucket', newBucket) await useKanbanStore().createBucket(newBucket)
this.newBucketTitle = '' this.newBucketTitle = ''
this.showNewBucketInput = false this.showNewBucketInput = false
}, },
@ -515,7 +520,7 @@ export default defineComponent({
}) })
try { try {
await this.$store.dispatch('kanban/deleteBucket', { await useKanbanStore().deleteBucket({
bucket, bucket,
params: this.params, params: this.params,
}) })
@ -537,13 +542,13 @@ export default defineComponent({
title: bucketTitle, title: bucketTitle,
} }
await this.$store.dispatch('kanban/updateBucketTitle', updatedBucketData) await useKanbanStore().updateBucketTitle(updatedBucketData)
this.bucketTitleEditable = false this.bucketTitleEditable = false
}, },
updateBuckets(value) { updateBuckets(value) {
// (1) buckets get updated in store and tasks positions get invalidated // (1) buckets get updated in store and tasks positions get invalidated
this.$store.commit('kanban/setBuckets', value) useKanbanStore().setBuckets(value)
}, },
updateBucketPosition(e) { updateBucketPosition(e) {
@ -562,7 +567,7 @@ export default defineComponent({
), ),
} }
this.$store.dispatch('kanban/updateBucket', updatedData) useKanbanStore().updateBucket(updatedData)
}, },
async setBucketLimit(bucketId, limit) { async setBucketLimit(bucketId, limit) {
@ -570,12 +575,14 @@ export default defineComponent({
return return
} }
const kanbanStore = useKanbanStore()
const newBucket = { const newBucket = {
...this.$store.getters['kanban/getBucketById'](bucketId), ...kanbanStore.getBucketById(bucketId),
limit, limit,
} }
await this.$store.dispatch('kanban/updateBucket', newBucket) await kanbanStore.updateBucket(newBucket)
this.$message.success({message: this.$t('list.kanban.bucketLimitSavedSuccess')}) this.$message.success({message: this.$t('list.kanban.bucketLimitSavedSuccess')})
}, },
@ -600,7 +607,7 @@ export default defineComponent({
...bucket, ...bucket,
isDoneBucket: !bucket.isDoneBucket, isDoneBucket: !bucket.isDoneBucket,
} }
await this.$store.dispatch('kanban/updateBucket', newBucket) await useKanbanStore().updateBucket(newBucket)
this.$message.success({message: this.$t('list.kanban.doneBucketSavedSuccess')}) this.$message.success({message: this.$t('list.kanban.doneBucketSavedSuccess')})
}, },

View File

@ -118,7 +118,7 @@
v-if="isTaskEdit" v-if="isTaskEdit"
class="taskedit mt-0" class="taskedit mt-0"
:title="$t('list.list.editTask')" :title="$t('list.list.editTask')"
@close="() => isTaskEdit = false" @close="closeTaskEditPane()"
:shadow="false" :shadow="false"
:task="taskEditTask" :task="taskEditTask"
/> />
@ -139,7 +139,7 @@ export default { name: 'List' }
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, toRef, nextTick, onMounted, type PropType} from 'vue' import {ref, computed, toRef, nextTick, onMounted, type PropType, watch} from 'vue'
import draggable from 'zhyswan-vuedraggable' import draggable from 'zhyswan-vuedraggable'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
@ -154,13 +154,13 @@ import Nothing from '@/components/misc/nothing.vue'
import Pagination from '@/components/misc/pagination.vue' import Pagination from '@/components/misc/pagination.vue'
import {ALPHABETICAL_SORT} from '@/components/list/partials/filters.vue' import {ALPHABETICAL_SORT} from '@/components/list/partials/filters.vue'
import {useStore} from '@/store'
import {HAS_TASKS} from '@/store/mutation-types'
import {useTaskList} from '@/composables/taskList' import {useTaskList} from '@/composables/taskList'
import {RIGHTS as Rights} from '@/constants/rights' import {RIGHTS as Rights} from '@/constants/rights'
import {calculateItemPosition} from '@/helpers/calculateItemPosition' import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import {isSavedFilter} from '@/helpers/savedFilter' import {isSavedFilter} from '@/helpers/savedFilter'
import {useBaseStore} from '@/stores/base'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
@ -203,6 +203,16 @@ const DRAG_OPTIONS = {
const taskEditTask = ref<ITask | null>(null) const taskEditTask = ref<ITask | null>(null)
const isTaskEdit = ref(false) const isTaskEdit = ref(false)
function closeTaskEditPane() {
isTaskEdit.value = false
taskEditTask.value = null
}
watch(
() => props.listId,
closeTaskEditPane,
)
const { const {
tasks, tasks,
loading, loading,
@ -228,8 +238,8 @@ const firstNewPosition = computed(() => {
}) })
const taskStore = useTaskStore() const taskStore = useTaskStore()
const store = useStore() const baseStore = useBaseStore()
const list = computed(() => store.state.currentList) const list = computed(() => baseStore.currentList)
const canWrite = computed(() => { const canWrite = computed(() => {
return list.value.maxRight > Rights.READ && list.value.id > 0 return list.value.maxRight > Rights.READ && list.value.id > 0
@ -283,7 +293,7 @@ function updateTaskList(task: ITask) {
] ]
} }
store.commit(HAS_TASKS, true) baseStore.setHasTasks(true)
} }
function editTask(id: ITask['id']) { function editTask(id: ITask['id']) {

View File

@ -55,13 +55,13 @@ import Message from '@/components/misc/message.vue'
import ListModel from '@/models/list' import ListModel from '@/models/list'
import ListService from '@/services/list' import ListService from '@/services/list'
import {BACKGROUND, BLUR_HASH, CURRENT_LIST} from '@/store/mutation-types'
import {getListTitle} from '@/helpers/getListTitle' import {getListTitle} from '@/helpers/getListTitle'
import {saveListToHistory} from '@/modules/listHistory' import {saveListToHistory} from '@/modules/listHistory'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useStore} from '@/store'
import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useKanbanStore} from '@/stores/kanban'
const props = defineProps({ const props = defineProps({
listId: { listId: {
@ -75,19 +75,20 @@ const props = defineProps({
}) })
const route = useRoute() const route = useRoute()
const store = useStore()
const baseStore = useBaseStore()
const kanbanStore = useKanbanStore()
const listStore = useListStore() const listStore = useListStore()
const listService = ref(new ListService()) const listService = ref(new ListService())
const loadedListId = ref(0) const loadedListId = ref(0)
const currentList = computed(() => { const currentList = computed(() => {
return typeof store.state.currentList === 'undefined' ? { return typeof baseStore.currentList === 'undefined' ? {
id: 0, id: 0,
title: '', title: '',
isArchived: false, isArchived: false,
maxRight: null, maxRight: null,
} : store.state.currentList } : baseStore.currentList
}) })
// watchEffect would be called every time the prop would get a value assigned, even if that value was the same as before. // watchEffect would be called every time the prop would get a value assigned, even if that value was the same as before.
@ -116,7 +117,7 @@ async function loadList(listIdToLoad: number) {
props.viewName === 'list.list' || props.viewName === 'list.list' ||
props.viewName === 'list.gantt' props.viewName === 'list.gantt'
) { ) {
store.commit('kanban/setListId', 0) kanbanStore.setListId(0)
} }
// Don't load the list if we either already loaded it or aren't dealing with a list at all currently and // Don't load the list if we either already loaded it or aren't dealing with a list at all currently and
@ -139,16 +140,16 @@ async function loadList(listIdToLoad: number) {
loadedListId.value = 0 loadedListId.value = 0
const listFromStore = listStore.getListById(listData.id) const listFromStore = listStore.getListById(listData.id)
if (listFromStore !== null) { if (listFromStore !== null) {
store.commit(BACKGROUND, null) baseStore.setBackground(null)
store.commit(BLUR_HASH, null) baseStore.setBlurHash(null)
store.dispatch(CURRENT_LIST, {list: listFromStore}) baseStore.handleSetCurrentList({list: listFromStore})
} }
// We create an extra list object instead of creating it in list.value because that would trigger a ui update which would result in bad ux. // We create an extra list object instead of creating it in list.value because that would trigger a ui update which would result in bad ux.
const list = new ListModel(listData) const list = new ListModel(listData)
try { try {
const loadedList = await listService.value.get(list) const loadedList = await listService.value.get(list)
await store.dispatch(CURRENT_LIST, {list: loadedList}) await baseStore.handleSetCurrentList({list: loadedList})
} finally { } finally {
loadedListId.value = props.listId loadedListId.value = props.listId
} }

View File

@ -17,16 +17,16 @@ export default {name: 'list-setting-archive'}
<script setup lang="ts"> <script setup lang="ts">
import {computed} from 'vue' import {computed} from 'vue'
import {useStore} from '@/store'
import {useRouter, useRoute} from 'vue-router' import {useRouter, useRoute} from 'vue-router'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import { success } from '@/message' import {success} from '@/message'
import { useTitle } from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import { useListStore } from '@/stores/lists'
import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const store = useStore()
const listStore = useListStore() const listStore = useListStore()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@ -40,7 +40,7 @@ async function archiveList() {
...list.value, ...list.value,
isArchived: !list.value.isArchived, isArchived: !list.value.isArchived,
}) })
store.commit('currentList', newList) useBaseStore().setCurrentList(newList)
success({message: t('list.archive.success')}) success({message: t('list.archive.success')})
} finally { } finally {
router.back() router.back()

View File

@ -100,10 +100,11 @@ export default { name: 'list-setting-background' }
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, shallowReactive} from 'vue' import {ref, computed, shallowReactive} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useStore} from '@/store'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
@ -115,7 +116,6 @@ import type BackgroundImageModel from '@/models/backgroundImage'
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash' import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {CURRENT_LIST} from '@/store/mutation-types'
import CreateEdit from '@/components/misc/create-edit.vue' import CreateEdit from '@/components/misc/create-edit.vue'
import {success} from '@/message' import {success} from '@/message'
@ -123,7 +123,7 @@ import {success} from '@/message'
const SEARCH_DEBOUNCE = 300 const SEARCH_DEBOUNCE = 300
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const store = useStore() const baseStore = useBaseStore()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@ -149,8 +149,8 @@ const configStore = useConfigStore()
const unsplashBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('unsplash')) const unsplashBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('unsplash'))
const uploadBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('upload')) const uploadBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('upload'))
const currentList = computed(() => store.state.currentList) const currentList = computed(() => baseStore.currentList)
const hasBackground = computed(() => store.state.background !== null) const hasBackground = computed(() => baseStore.background !== null)
// Show the default collection of backgrounds // Show the default collection of backgrounds
newBackgroundSearch() newBackgroundSearch()
@ -188,8 +188,11 @@ async function setBackground(backgroundId: string) {
return return
} }
const list = await backgroundService.update({id: backgroundId, listId: route.params.listId}) const list = await backgroundService.update({
await store.dispatch(CURRENT_LIST, {list, forceUpdate: true}) id: backgroundId,
listId: route.params.listId,
})
await baseStore.handleSetCurrentList({list, forceUpdate: true})
namespaceStore.setListInNamespaceById(list) namespaceStore.setListInNamespaceById(list)
listStore.setList(list) listStore.setList(list)
success({message: t('list.background.success')}) success({message: t('list.background.success')})
@ -201,8 +204,11 @@ async function uploadBackground() {
return return
} }
const list = await backgroundUploadService.value.create(route.params.listId, backgroundUploadInput.value?.files[0]) const list = await backgroundUploadService.value.create(
await store.dispatch(CURRENT_LIST, {list, forceUpdate: true}) route.params.listId,
backgroundUploadInput.value?.files[0],
)
await baseStore.handleSetCurrentList({list, forceUpdate: true})
namespaceStore.setListInNamespaceById(list) namespaceStore.setListInNamespaceById(list)
listStore.setList(list) listStore.setList(list)
success({message: t('list.background.success')}) success({message: t('list.background.success')})
@ -210,7 +216,7 @@ async function uploadBackground() {
async function removeBackground() { async function removeBackground() {
const list = await listService.value.removeBackground(currentList.value) const list = await listService.value.removeBackground(currentList.value)
await store.dispatch(CURRENT_LIST, {list, forceUpdate: true}) await baseStore.handleSetCurrentList({list, forceUpdate: true})
namespaceStore.setListInNamespaceById(list) namespaceStore.setListInNamespaceById(list)
listStore.setList(list) listStore.setList(list)
success({message: t('list.background.removeSuccess')}) success({message: t('list.background.removeSuccess')})

View File

@ -72,17 +72,17 @@ export default { name: 'list-setting-edit' }
<script setup lang="ts"> <script setup lang="ts">
import type {PropType} from 'vue' import type {PropType} from 'vue'
import {useRouter} from 'vue-router' import {useRouter} from 'vue-router'
import {useStore} from '@/store'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import Editor from '@/components/input/AsyncEditor' import Editor from '@/components/input/AsyncEditor'
import ColorPicker from '@/components/input/colorPicker.vue' import ColorPicker from '@/components/input/colorPicker.vue'
import CreateEdit from '@/components/misc/create-edit.vue' import CreateEdit from '@/components/misc/create-edit.vue'
import {CURRENT_LIST} from '@/store/mutation-types'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import {useBaseStore} from '@/stores/base'
import {useList} from '@/stores/lists' import {useList} from '@/stores/lists'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
const props = defineProps({ const props = defineProps({
@ -93,7 +93,6 @@ const props = defineProps({
}) })
const router = useRouter() const router = useRouter()
const store = useStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
@ -103,7 +102,7 @@ useTitle(() => list?.title ? t('list.edit.title', {list: list.title}) : '')
async function save() { async function save() {
await saveList() await saveList()
await store.dispatch(CURRENT_LIST, {list}) await useBaseStore().handleSetCurrentList({list})
router.back() router.back()
} }
</script> </script>

View File

@ -28,18 +28,18 @@ export default {name: 'list-setting-share'}
<script lang="ts" setup> <script lang="ts" setup>
import {ref, computed, watchEffect} from 'vue' import {ref, computed, watchEffect} from 'vue'
import {useStore} from '@/store'
import {useRoute} from 'vue-router' import {useRoute} from 'vue-router'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useTitle} from '@vueuse/core' import {useTitle} from '@vueuse/core'
import ListService from '@/services/list' import ListService from '@/services/list'
import ListModel from '@/models/list' import ListModel from '@/models/list'
import {CURRENT_LIST} from '@/store/mutation-types'
import CreateEdit from '@/components/misc/create-edit.vue' import CreateEdit from '@/components/misc/create-edit.vue'
import LinkSharing from '@/components/sharing/linkSharing.vue' import LinkSharing from '@/components/sharing/linkSharing.vue'
import userTeam from '@/components/sharing/userTeam.vue' import userTeam from '@/components/sharing/userTeam.vue'
import {useBaseStore} from '@/stores/base'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
@ -52,7 +52,6 @@ const title = computed(() => list.value?.title
) )
useTitle(title) useTitle(title)
const store = useStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const configStore = useConfigStore() const configStore = useConfigStore()
@ -62,7 +61,7 @@ const userIsAdmin = computed(() => 'owner' in list.value && list.value.owner.id
async function loadList(listId: number) { async function loadList(listId: number) {
const listService = new ListService() const listService = new ListService()
const newList = await listService.get(new ListModel({id: listId})) const newList = await listService.get(new ListModel({id: listId}))
await store.dispatch(CURRENT_LIST, {list: newList}) await useBaseStore().handleSetCurrentList({list: newList})
list.value = newList list.value = newList
} }

View File

@ -71,7 +71,6 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed} from 'vue' import {computed} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useStore} from '@/store'
import Fancycheckbox from '@/components/input/fancycheckbox.vue' import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import ListCard from '@/components/list/partials/list-card.vue' import ListCard from '@/components/list/partials/list-card.vue'
@ -79,16 +78,16 @@ import ListCard from '@/components/list/partials/list-card.vue'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle' import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useStorage} from '@vueuse/core' import {useStorage} from '@vueuse/core'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
const {t} = useI18n() const {t} = useI18n()
const store = useStore()
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
useTitle(() => t('namespace.title')) useTitle(() => t('namespace.title'))
const showArchived = useStorage('showArchived', false) const showArchived = useStorage('showArchived', false)
const loading = computed(() => store.state.loading) const loading = computed(() => namespaceStore.isLoading)
const namespaces = computed(() => { const namespaces = computed(() => {
return namespaceStore.namespaces.filter(n => showArchived.value ? true : !n.isArchived) return namespaceStore.namespaces.filter(n => showArchived.value ? true : !n.isArchived)
// return namespaceStore.namespaces.filter(n => showArchived.value ? true : !n.isArchived).map(n => { // return namespaceStore.namespaces.filter(n => showArchived.value ? true : !n.isArchived).map(n => {

View File

@ -34,21 +34,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import {ref, computed} from 'vue' import {ref, computed} from 'vue'
import {useStore} from '@/store'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useTitle} from '@vueuse/core' import {useTitle} from '@vueuse/core'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import {LOGO_VISIBLE} from '@/store/mutation-types'
import {LIST_VIEWS, type ListView} from '@/types/ListView' import {LIST_VIEWS, type ListView} from '@/types/ListView'
import {useBaseStore} from '@/stores/base'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
useTitle(t('sharing.authenticating')) useTitle(t('sharing.authenticating'))
function useAuth() { function useAuth() {
const store = useStore() const baseStore = useBaseStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@ -81,7 +81,7 @@ function useAuth() {
const logoVisible = route.query.logoVisible const logoVisible = route.query.logoVisible
? route.query.logoVisible === 'true' ? route.query.logoVisible === 'true'
: true : true
store.commit(LOGO_VISIBLE, logoVisible) baseStore.setLogoVisible(logoVisible)
const view = route.query.view && Object.values(LIST_VIEWS).includes(route.query.view as ListView) const view = route.query.view && Object.values(LIST_VIEWS).includes(route.query.view as ListView)
? route.query.view ? route.query.view

View File

@ -45,7 +45,6 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, ref, watchEffect} from 'vue' import {computed, ref, watchEffect} from 'vue'
import {useStore} from '@/store'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
@ -56,13 +55,11 @@ import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import SingleTaskInList from '@/components/tasks/partials/singleTaskInList.vue' import SingleTaskInList from '@/components/tasks/partials/singleTaskInList.vue'
import DatepickerWithRange from '@/components/date/datepickerWithRange.vue' import DatepickerWithRange from '@/components/date/datepickerWithRange.vue'
import {DATE_RANGES} from '@/components/date/dateRanges' import {DATE_RANGES} from '@/components/date/dateRanges'
import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
import LlamaCool from '@/assets/llama-cool.svg?component' import LlamaCool from '@/assets/llama-cool.svg?component'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
const store = useStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
const route = useRoute() const route = useRoute()
@ -109,7 +106,7 @@ const pageTitle = computed(() => {
}) })
const hasTasks = computed(() => tasks.value && tasks.value.length > 0) const hasTasks = computed(() => tasks.value && tasks.value.length > 0)
const userAuthenticated = computed(() => authStore.authenticated) const userAuthenticated = computed(() => authStore.authenticated)
const loading = computed(() => store.state[LOADING] && store.state[LOADING_MODULE] === 'tasks') const loading = computed(() => taskStore.isLoading)
interface dateStrings { interface dateStrings {
dateFrom: string, dateFrom: string,

View File

@ -1,7 +1,7 @@
<template> <template>
<div :class="{ 'is-loading': taskService.loading, 'visible': visible}" class="loader-container task-view-container"> <div :class="{ 'is-loading': taskService.loading, 'visible': visible}" class="loader-container task-view-container">
<div class="task-view"> <div class="task-view">
<heading v-model:task="task" :can-write="canWrite" ref="heading"/> <Heading v-model:task="task" :can-write="canWrite" ref="heading"/>
<h6 class="subtitle" v-if="parent && parent.namespace && parent.list"> <h6 class="subtitle" v-if="parent && parent.namespace && parent.list">
{{ getNamespaceTitle(parent.namespace) }} > {{ getNamespaceTitle(parent.namespace) }} >
<router-link :to="{ name: 'list.index', params: { listId: parent.list.id } }"> <router-link :to="{ name: 'list.index', params: { listId: parent.list.id } }">
@ -260,7 +260,11 @@
<comments :can-write="canWrite" :task-id="taskId"/> <comments :can-write="canWrite" :task-id="taskId"/>
</div> </div>
<div class="column is-one-third action-buttons d-print-none" v-if="canWrite || shouldShowClosePopup"> <div class="column is-one-third action-buttons d-print-none" v-if="canWrite || shouldShowClosePopup">
<BaseButton @click="$router.back()" class="is-fullwidth is-block has-text-centered mb-4 has-text-primary" v-if="shouldShowClosePopup"> <BaseButton
v-if="shouldShowClosePopup"
@click="$router.back()"
class="is-fullwidth is-block has-text-centered mb-4 has-text-primary"
>
<icon icon="arrow-left"/> <icon icon="arrow-left"/>
{{ $t('task.detail.closePopup') }} {{ $t('task.detail.closePopup') }}
</BaseButton> </BaseButton>
@ -425,348 +429,336 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import {defineComponent} from 'vue' import {ref, reactive, toRef, shallowReactive, computed, watch, watchEffect, nextTick, type PropType} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {unrefElement} from '@vueuse/core'
import cloneDeep from 'lodash.clonedeep' import cloneDeep from 'lodash.clonedeep'
import TaskService from '../../services/task' import TaskService from '@/services/task'
import TaskModel, {TASK_DEFAULT_COLOR} from '@/models/task' import TaskModel, {TASK_DEFAULT_COLOR} from '@/models/task'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import type {IList} from '@/modelTypes/IList'
import { PRIORITIES as priorites } from '@/constants/priorities' import {PRIORITIES} from '@/constants/priorities'
import {RIGHTS as rights} from '@/constants/rights' import {RIGHTS} from '@/constants/rights'
import PrioritySelect from '../../components/tasks/partials/prioritySelect.vue'
import PercentDoneSelect from '../../components/tasks/partials/percentDoneSelect.vue'
import EditLabels from '../../components/tasks/partials/editLabels.vue'
import EditAssignees from '../../components/tasks/partials/editAssignees.vue'
import Attachments from '../../components/tasks/partials/attachments.vue'
import RelatedTasks from '../../components/tasks/partials/relatedTasks.vue'
import RepeatAfter from '../../components/tasks/partials/repeatAfter.vue'
import Reminders from '../../components/tasks/partials/reminders.vue'
import Comments from '../../components/tasks/partials/comments.vue'
import ListSearch from '../../components/tasks/partials/listSearch.vue'
import description from '@/components/tasks/partials/description.vue'
import ColorPicker from '../../components/input/colorPicker.vue'
import heading from '@/components/tasks/partials/heading.vue'
import Datepicker from '@/components/input/datepicker.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
// partials
import Attachments from '@/components/tasks/partials/attachments.vue'
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
import ColorPicker from '@/components/input/colorPicker.vue'
import Comments from '@/components/tasks/partials/comments.vue'
import CreatedUpdated from '@/components/tasks/partials/createdUpdated.vue'
import Datepicker from '@/components/input/datepicker.vue'
import Description from '@/components/tasks/partials/description.vue'
import EditAssignees from '@/components/tasks/partials/editAssignees.vue'
import EditLabels from '@/components/tasks/partials/editLabels.vue'
import Heading from '@/components/tasks/partials/heading.vue'
import ListSearch from '@/components/tasks/partials/listSearch.vue'
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect.vue'
import PrioritySelect from '@/components/tasks/partials/prioritySelect.vue'
import RelatedTasks from '@/components/tasks/partials/relatedTasks.vue'
import Reminders from '@/components/tasks/partials/reminders.vue'
import RepeatAfter from '@/components/tasks/partials/repeatAfter.vue'
import TaskSubscription from '@/components/misc/subscription.vue' import TaskSubscription from '@/components/misc/subscription.vue'
import {CURRENT_LIST} from '@/store/mutation-types'
import {uploadFile} from '@/helpers/attachments' import {uploadFile} from '@/helpers/attachments'
import ChecklistSummary from '../../components/tasks/partials/checklist-summary.vue'
import CreatedUpdated from '@/components/tasks/partials/createdUpdated.vue'
import { setTitle } from '@/helpers/setTitle'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle' import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {getListTitle} from '@/helpers/getListTitle' import {getListTitle} from '@/helpers/getListTitle'
import type {IList} from '@/modelTypes/IList' import {scrollIntoView} from '@/helpers/scrollIntoView'
import {colorIsDark} from '@/helpers/color/colorIsDark'
import {useBaseStore} from '@/stores/base'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import {useAttachmentStore} from '@/stores/attachments' import {useAttachmentStore} from '@/stores/attachments'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import {useKanbanStore} from '@/stores/kanban'
function scrollIntoView(el) { import {useTitle} from '@/composables/useTitle'
if (!el) {
import {success} from '@/message'
const props = defineProps({
taskId: {
type: Number as PropType<ITask['id']>,
required: true,
},
})
defineEmits(['close'])
const route = useRoute()
const router = useRouter()
const {t} = useI18n({useScope: 'global'})
const baseStore = useBaseStore()
const namespaceStore = useNamespaceStore()
const attachmentStore = useAttachmentStore()
const taskStore = useTaskStore()
const kanbanStore = useKanbanStore()
const task = reactive(new TaskModel())
useTitle(toRef(task, 'title'))
// We doubled the task color property here because verte does not have a real change property, leading
// to the color property change being triggered when the # is removed from it, leading to an update,
// which leads in turn to a change... This creates an infinite loop in which the task is updated, changed,
// updated, changed, updated and so on.
// To prevent this, we put the task color property in a seperate value which is set to the task color
// when it is saved and loaded.
const taskColor = ref<ITask['hexColor']>('')
// Used to avoid flashing of empty elements if the task content is not yet loaded.
const visible = ref(false)
const taskId = toRef(props, 'taskId')
const parent = computed(() => {
if (!task.listId) {
return {
namespace: null,
list: null,
}
}
if (!namespaceStore.getListAndNamespaceById) {
return null
}
return namespaceStore.getListAndNamespaceById(task.listId)
})
watch(
parent,
(parent) => {
const parentList = parent !== null ? parent.list : null
if (parentList !== null) {
baseStore.handleSetCurrentList({list: parentList})
}
},
{immediate: true },
)
const canWrite = computed(() => (
task.maxRight !== null &&
task.maxRight > RIGHTS.READ
))
const color = computed(() => {
const color = task.getHexColor
? task.getHexColor()
: false
return color === TASK_DEFAULT_COLOR
? ''
: color
})
const hasAttachments = computed(() => attachmentStore.attachments.length > 0)
// HACK:
const shouldShowClosePopup = computed(() => (route.name as string).includes('kanban'))
function attachmentUpload(...args: any[]) {
return uploadFile(taskId.value, ...args)
}
const heading = ref<HTMLElement | null>(null)
async function scrollToHeading() {
scrollIntoView(unrefElement(heading))
}
const taskService = shallowReactive(new TaskService())
async function loadTask(taskId: ITask['id']) {
if (taskId === undefined) {
return return
} }
const boundingRect = el.getBoundingClientRect() try {
const scrollY = window.scrollY Object.assign(task, await taskService.get({id: taskId}))
attachmentStore.set(task.attachments)
if ( taskColor.value = task.hexColor
boundingRect.top > (scrollY + window.innerHeight) || setActiveFields()
boundingRect.top < scrollY } finally {
) { await nextTick()
el.scrollIntoView({ scrollToHeading()
behavior: 'smooth', visible.value = true
block: 'center',
inline: 'nearest',
})
} }
} }
export default defineComponent({ watchEffect(() => taskId.value !== undefined && loadTask(taskId.value))
name: 'TaskDetailView',
components: {
BaseButton,
CreatedUpdated,
ChecklistSummary,
TaskSubscription,
Datepicker,
ColorPicker,
ListSearch,
Reminders,
RepeatAfter,
RelatedTasks,
Attachments,
EditAssignees,
EditLabels,
PercentDoneSelect,
PrioritySelect,
Comments,
description,
heading,
},
props: { type FieldType =
taskId: { | 'assignees'
type: Number, | 'attachments'
required: true, | 'color'
}, | 'dueDate'
}, | 'endDate'
| 'labels'
| 'moveList'
| 'percentDone'
| 'priority'
| 'relatedTasks'
| 'reminders'
| 'repeatAfter'
| 'startDate'
data() { const activeFields : {[type in FieldType]: boolean} = reactive({
return { assignees: false,
taskService: new TaskService(), attachments: false,
task: new TaskModel(), color: false,
// We doubled the task color property here because verte does not have a real change property, leading dueDate: false,
// to the color property change being triggered when the # is removed from it, leading to an update, endDate: false,
// which leads in turn to a change... This creates an infinite loop in which the task is updated, changed, labels: false,
// updated, changed, updated and so on. moveList: false,
// To prevent this, we put the task color property in a seperate value which is set to the task color percentDone: false,
// when it is saved and loaded. priority: false,
taskColor: '', relatedTasks: false,
reminders: false,
repeatAfter: false,
startDate: false,
})
showDeleteModal: false, function setActiveFields() {
// Used to avoid flashing of empty elements if the task content is not yet loaded. // FIXME: are these lines necessary?
visible: false, // task.startDate = task.startDate || null
// task.endDate = task.endDate || null
TASK_DEFAULT_COLOR,
activeFields: { // Set all active fields based on values in the model
assignees: false, activeFields.assignees = task.assignees.length > 0
priority: false, activeFields.attachments = task.attachments.length > 0
dueDate: false, activeFields.dueDate = task.dueDate !== null
percentDone: false, activeFields.endDate = task.endDate !== null
startDate: false, activeFields.labels = task.labels.length > 0
endDate: false, activeFields.percentDone = task.percentDone > 0
reminders: false, activeFields.priority = task.priority !== PRIORITIES.UNSET
repeatAfter: false, activeFields.relatedTasks = Object.keys(task.relatedTasks).length > 0
labels: false, activeFields.reminders = task.reminderDates.length > 0
attachments: false, activeFields.repeatAfter = task.repeatAfter.amount > 0
relatedTasks: false, activeFields.startDate = task.startDate !== null
moveList: false, }
color: false,
}, const activeFieldElements : {[id in FieldType]: HTMLElement | null} = reactive({
assignees: null,
attachments: null,
color: null,
dueDate: null,
endDate: null,
labels: null,
moveList: null,
percentDone: null,
priority: null,
relatedTasks: null,
reminders: null,
repeatAfter: null,
startDate: null,
})
function setFieldActive(fieldName: keyof typeof activeFields) {
activeFields[fieldName] = true
nextTick(() => {
const el = unrefElement(activeFieldElements[fieldName])
if (!el) {
return
} }
},
watch: { el.focus()
taskId: {
handler: 'loadTask',
immediate: true,
},
parent: {
handler(parent) {
const parentList = parent !== null ? parent.list : null
if (parentList !== null) {
this.$store.dispatch(CURRENT_LIST, {list: parentList})
}
},
immediate: true,
},
// Using a watcher here because the header component handles saving the task with the api but we want to decouple
// it from the page title.
'task.title': {
handler(title) {
setTitle(title)
},
},
},
computed: {
currentList() {
return this.$store.state[CURRENT_LIST]
},
parent() {
if (!this.task.listId) {
return {
namespace: null,
list: null,
}
}
const namespaceStore = useNamespaceStore() // scroll the field to the center of the screen if not in viewport already
scrollIntoView(el)
})
}
if (!namespaceStore.getListAndNamespaceById) { async function saveTask(args?: {
return null
}
return namespaceStore.getListAndNamespaceById(this.task.listId)
},
canWrite() {
return typeof this.task !== 'undefined' && typeof this.task.maxRight !== 'undefined' && this.task.maxRight > rights.READ
},
hasAttachments() {
const attachmentsStore = useAttachmentStore()
return attachmentsStore.attachments.length > 0
},
shouldShowClosePopup() {
return this.$route.name.includes('kanban')
},
color() {
const color = this.task.getHexColor
? this.task.getHexColor()
: false
return color === TASK_DEFAULT_COLOR
? ''
: color
},
},
methods: {
getNamespaceTitle,
getListTitle,
attachmentUpload(...args) {
return uploadFile(this.taskId, ...args)
},
async loadTask(taskId: ITask['id']) {
if (taskId === undefined) {
return
}
try {
this.task = await this.taskService.get({id: taskId})
const attachmentStore = useAttachmentStore()
attachmentStore.set(this.task.attachments)
this.taskColor = this.task.hexColor
this.setActiveFields()
await this.$nextTick()
setTitle(this.task.title)
} finally {
this.scrollToHeading()
await this.$nextTick()
this.visible = true
}
},
scrollToHeading() {
if(!this.$refs?.heading?.$el) {
return
}
this.$refs.heading.$el.scrollIntoView({block: 'center'})
},
setActiveFields() {
this.task.startDate = this.task.startDate ? this.task.startDate : null
this.task.endDate = this.task.endDate ? this.task.endDate : null
// Set all active fields based on values in the model
this.activeFields.assignees = this.task.assignees.length > 0
this.activeFields.priority = this.task.priority !== priorites.UNSET
this.activeFields.dueDate = this.task.dueDate !== null
this.activeFields.percentDone = this.task.percentDone > 0
this.activeFields.startDate = this.task.startDate !== null
this.activeFields.endDate = this.task.endDate !== null
this.activeFields.reminders = this.task.reminderDates.length > 0
this.activeFields.repeatAfter = this.task.repeatAfter.amount > 0
this.activeFields.labels = this.task.labels.length > 0
this.activeFields.attachments = this.task.attachments.length > 0
this.activeFields.relatedTasks = Object.keys(this.task.relatedTasks).length > 0
},
async saveTask(args?: {
task: ITask, task: ITask,
showNotification?: boolean, showNotification?: boolean,
undoCallback?: () => void, undoCallback?: () => void,
}) { }) {
const { const {
task, task: currentTask,
showNotification, showNotification,
undoCallback, undoCallback,
} = { } = {
...{ ...{
task: cloneDeep(this.task), task: cloneDeep(task),
showNotification: true, showNotification: true,
}, },
...args, ...args,
} }
if (!canWrite.value) {
return
}
if (!this.canWrite) { currentTask.hexColor = taskColor.value
return
}
// We're doing the whole update in a nextTick because sometimes race conditions can occur when // If no end date is being set, but a start date and due date,
// setting the due date on mobile which leads to no due date change being saved. // use the due date as the end date
await this.$nextTick() if (
currentTask.endDate === null &&
currentTask.startDate !== null &&
currentTask.dueDate !== null
) {
currentTask.endDate = currentTask.dueDate
}
const newTask = await taskStore.update(currentTask) // TODO: markraw ?
Object.assign(task, newTask)
setActiveFields()
task.hexColor = this.taskColor if (!showNotification) {
return
}
// If no end date is being set, but a start date and due date, let actions = []
// use the due date as the end date if (undoCallback !== null) {
if (task.endDate === null && task.startDate !== null && task.dueDate !== null) { actions = [{
task.endDate = task.dueDate title: 'Undo',
} callback: undoCallback,
}]
}
success({message: t('task.detail.updateSuccess')}, actions)
}
const showDeleteModal = ref(false)
this.task = await useTaskStore().update(task) async function deleteTask() {
await taskStore.delete(task)
success({message: t('task.detail.deleteSuccess')})
router.push({name: 'list.index', params: {listId: task.listId}})
}
if (!showNotification) { function toggleTaskDone() {
return const newTask = {
} ...task,
done: !task.done,
}
let actions = [] saveTask({
if (undoCallback !== undefined) { task: newTask,
actions = [{ undoCallback: toggleTaskDone,
title: 'Undo', })
callback: undoCallback, }
}]
} async function changeList(list: IList) {
this.$message.success({message: this.$t('task.detail.updateSuccess')}, actions) kanbanStore.removeTaskInBucket(task)
await saveTask({
task: {
...task,
listId: list.id,
}, },
})
}
setFieldActive(fieldName) { async function toggleFavorite() {
this.activeFields[fieldName] = true task.isFavorite = !task.isFavorite
this.$nextTick(() => { const newTask = await taskService.update(task)
const el = this.$refs[fieldName]?.$el Object.assign(task, newTask)
if (!el) { await namespaceStore.loadNamespacesIfFavoritesDontExist()
return }
}
el.focus()
// scroll the field to the center of the screen if not in viewport already
scrollIntoView(el)
})
},
async deleteTask() {
await useTaskStore().delete(this.task)
this.$message.success({message: this.$t('task.detail.deleteSuccess')})
this.$router.push({name: 'list.index', params: {listId: this.task.listId}})
},
toggleTaskDone() {
const newTask = {
...this.task,
done: !this.task.done,
}
this.saveTask({
task: newTask,
undoCallback: this.toggleTaskDone,
})
},
async changeList(list: IList) {
this.$store.commit('kanban/removeTaskInBucket', this.task)
await this.saveTask({
task: {
...this.task,
listId: list.id,
},
})
},
async toggleFavorite() {
this.task.isFavorite = !this.task.isFavorite
this.task = await this.taskService.update(this.task)
const namespaceStore = useNamespaceStore()
await namespaceStore.loadNamespacesIfFavoritesDontExist()
},
colorIsDark,
},
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -14,14 +14,14 @@
class="input" id="username" class="input" id="username"
name="username" name="username"
:placeholder="$t('user.auth.usernamePlaceholder')" :placeholder="$t('user.auth.usernamePlaceholder')"
ref="username" ref="usernameRef"
required required
type="text" type="text"
autocomplete="username" autocomplete="username"
v-focus v-focus
@keyup.enter="submit" @keyup.enter="submit"
tabindex="1" tabindex="1"
@focusout="validateField('username')" @focusout="validateUsernameField()"
/> />
</div> </div>
<p class="help is-danger" v-if="!usernameValid"> <p class="help is-danger" v-if="!usernameValid">
@ -39,7 +39,7 @@
{{ $t('user.auth.forgotPassword') }} {{ $t('user.auth.forgotPassword') }}
</router-link> </router-link>
</div> </div>
<password tabindex="2" @submit="submit" v-model="password" :validate-initially="validatePasswordInitially"/> <Password tabindex="2" @submit="submit" v-model="password" :validate-initially="validatePasswordInitially"/>
</div> </div>
<div class="field" v-if="needsTotpPasscode"> <div class="field" v-if="needsTotpPasscode">
<label class="label" for="totpPasscode">{{ $t('user.auth.totpTitle') }}</label> <label class="label" for="totpPasscode">{{ $t('user.auth.totpTitle') }}</label>
@ -68,7 +68,7 @@
<x-button <x-button
@click="submit" @click="submit"
:loading="loading" :loading="isLoading"
tabindex="4" tabindex="4"
> >
{{ $t('user.auth.login') }} {{ $t('user.auth.login') }}
@ -101,149 +101,112 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import {computed, onBeforeMount, ref} from 'vue'
import {useI18n} from 'vue-i18n'
import {useRouter} from 'vue-router'
import {useDebounceFn} from '@vueuse/core' import {useDebounceFn} from '@vueuse/core'
import {mapState as mapStateVuex} from 'vuex'
import {mapState} from 'pinia'
import {HTTPFactory} from '@/http-common'
import {LOADING} from '@/store/mutation-types'
import {getErrorText} from '@/message'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import {redirectToProvider} from '../../helpers/redirectToProvider'
import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited'
import Password from '@/components/input/password.vue' import Password from '@/components/input/password.vue'
import { setTitle } from '@/helpers/setTitle'
import {useConfigStore} from '@/stores/config' import {getErrorText} from '@/message'
import {redirectToProvider} from '@/helpers/redirectToProvider'
import {getLastVisited, clearLastVisited} from '@/helpers/saveLastVisited'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {useConfigStore} from '@/stores/config'
export default defineComponent({ import {useTitle} from '@/composables/useTitle'
components: {
Password, const router = useRouter()
Message, const {t} = useI18n({useScope: 'global'})
}, useTitle(() => t('user.auth.login'))
data() {
return { const authStore = useAuthStore()
confirmedEmailSuccess: false, const configStore = useConfigStore()
errorMessage: '',
usernameValid: true, const registrationEnabled = computed(() => configStore.registrationEnabled)
password: '', const localAuthEnabled = computed(() => configStore.auth.local.enabled)
validatePasswordInitially: false,
rememberMe: false, const openidConnect = computed(() => configStore.auth.openidConnect)
const hasOpenIdProviders = computed(() => openidConnect.value.enabled && openidConnect.value.providers?.length > 0)
const isLoading = computed(() => authStore.isLoading)
const confirmedEmailSuccess = ref(false)
const errorMessage = ref('')
const password = ref('')
const validatePasswordInitially = ref(false)
const rememberMe = ref(false)
const authenticated = computed(() => authStore.authenticated)
onBeforeMount(() => {
authStore.verifyEmail().then((confirmed) => {
confirmedEmailSuccess.value = confirmed
}).catch((e: Error) => {
errorMessage.value = e.message
})
// Check if the user is already logged in, if so, redirect them to the homepage
if (authenticated.value) {
const last = getLastVisited()
if (last !== null) {
router.push({
name: last.name,
params: last.params,
})
clearLastVisited()
} else {
router.push({name: 'home'})
} }
}, }
beforeMount() {
const HTTP = HTTPFactory()
// Try to verify the email
// FIXME: Why is this here? Can we find a better place for this?
let emailVerifyToken = localStorage.getItem('emailConfirmToken')
if (emailVerifyToken) {
const stopLoading = this.setLoading()
HTTP.post('user/confirm', {token: emailVerifyToken})
.then(() => {
localStorage.removeItem('emailConfirmToken')
this.confirmedEmailSuccess = true
})
.catch(e => {
this.errorMessage = e.response.data.message
})
.finally(stopLoading)
}
// Check if the user is already logged in, if so, redirect them to the homepage
if (this.authenticated) {
const last = getLastVisited()
if (last !== null) {
this.$router.push({
name: last.name,
params: last.params,
})
clearLastVisited()
} else {
this.$router.push({name: 'home'})
}
}
},
created() {
setTitle(this.$t('user.auth.login'))
},
computed: {
hasOpenIdProviders() {
return this.openidConnect.enabled && this.openidConnect.providers?.length > 0
},
...mapStateVuex({
loading: LOADING,
}),
...mapState(useAuthStore, {
needsTotpPasscode: state => state.needsTotpPasscode,
authenticated: state => state.authenticated,
}),
...mapState(useConfigStore, {
registrationEnabled: state => state.registrationEnabled,
localAuthEnabled: state => state.auth.local.enabled,
openidConnect: state => state.auth.openidConnect,
}),
validateField() {
// using computed so that debounced function definition stays
return useDebounceFn((field) => {
this[`${field}Valid`] = this.$refs[field]?.value !== ''
}, 100)
},
},
methods: {
setLoading() {
const timeout = setTimeout(() => {
this.loading = true
}, 100)
return () => {
clearTimeout(timeout)
this.loading = false
}
},
async submit() {
this.errorMessage = ''
// Some browsers prevent Vue bindings from working with autofilled values.
// To work around this, we're manually getting the values here instead of relying on vue bindings.
// For more info, see https://kolaente.dev/vikunja/frontend/issues/78
const credentials = {
username: this.$refs.username.value,
password: this.password,
longToken: this.rememberMe,
}
if (credentials.username === '' || credentials.password === '') {
// Trigger the validation error messages
this.validateField('username')
this.validatePasswordInitially = true
return
}
if (this.needsTotpPasscode) {
credentials.totpPasscode = this.$refs.totpPasscode.value
}
try {
const authStore = useAuthStore()
await authStore.login(credentials)
authStore.setNeedsTotpPasscode(false)
} catch (e) {
if (e.response?.data.code === 1017 && !this.credentials.totpPasscode) {
return
}
const err = getErrorText(e)
this.errorMessage = typeof err[1] !== 'undefined' ? err[1] : err[0]
}
},
redirectToProvider,
},
}) })
const usernameValid = ref(true)
const usernameRef = ref<HTMLInputElement | null>(null)
const validateUsernameField = useDebounceFn(() => {
usernameValid.value = usernameRef.value?.value !== ''
}, 100)
const needsTotpPasscode = computed(() => authStore.needsTotpPasscode)
const totpPasscode = ref<HTMLInputElement | null>(null)
async function submit() {
errorMessage.value = ''
// Some browsers prevent Vue bindings from working with autofilled values.
// To work around this, we're manually getting the values here instead of relying on vue bindings.
// For more info, see https://kolaente.dev/vikunja/frontend/issues/78
const credentials = {
username: usernameRef.value?.value,
password: password.value,
longToken: rememberMe.value,
}
if (credentials.username === '' || credentials.password === '') {
// Trigger the validation error messages
validateUsernameField()
validatePasswordInitially.value = true
return
}
if (needsTotpPasscode.value) {
credentials.totpPasscode = totpPasscode.value?.value
}
try {
await authStore.login(credentials)
authStore.setNeedsTotpPasscode(false)
} catch (e) {
if (e.response?.data.code === 1017 && !credentials.totpPasscode) {
return
}
const err = getErrorText(e)
errorMessage.value = typeof err[1] !== 'undefined' ? err[1] : err[0]
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -15,13 +15,13 @@ export default { name: 'Auth' }
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, onMounted} from 'vue' import {ref, computed, onMounted} from 'vue'
import {useStore} from '@/store'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {getErrorText} from '@/message' import {getErrorText} from '@/message'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import {clearLastVisited, getLastVisited} from '@/helpers/saveLastVisited' import {clearLastVisited, getLastVisited} from '@/helpers/saveLastVisited'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
@ -29,10 +29,9 @@ const {t} = useI18n({useScope: 'global'})
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const store = useStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const loading = computed(() => store.state.loading) const loading = computed(() => authStore.isLoading)
const errorMessage = ref('') const errorMessage = ref('')
async function authenticateWithCode() { async function authenticateWithCode() {

View File

@ -50,7 +50,7 @@
</div> </div>
<x-button <x-button
:loading="loading" :loading="isLoading"
id="register-submit" id="register-submit"
@click="submit" @click="submit"
class="mr-2" class="mr-2"
@ -73,10 +73,10 @@ import {useDebounceFn} from '@vueuse/core'
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue' import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue'
import router from '@/router' import router from '@/router'
import {store} from '@/store'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import {isEmail} from '@/helpers/isEmail' import {isEmail} from '@/helpers/isEmail'
import Password from '@/components/input/password.vue' import Password from '@/components/input/password.vue'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
const authStore = useAuthStore() const authStore = useAuthStore()
@ -95,7 +95,7 @@ const credentials = reactive({
password: '', password: '',
}) })
const loading = computed(() => store.state.loading) const isLoading = computed(() => authStore.isLoading)
const errorMessage = ref('') const errorMessage = ref('')
const validatePasswordInitially = ref(false) const validatePasswordInitially = ref(false)

View File

@ -147,19 +147,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' export default {name: 'user-settings-general'}
import { useListStore } from '@/stores/lists'
import { useAuthStore } from '@/stores/auth'
export default defineComponent({
name: 'user-settings-general',
})
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import {computed, watch, ref} from 'vue' import {computed, watch, ref} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useStore} from '@/store'
import {PrefixMode} from '@/modules/parseTaskText' import {PrefixMode} from '@/modules/parseTaskText'
@ -169,12 +162,15 @@ import {availableLanguages} from '@/i18n'
import {playSoundWhenDoneKey, playPopSound} from '@/helpers/playPop' import {playSoundWhenDoneKey, playPopSound} from '@/helpers/playPop'
import {getQuickAddMagicMode, setQuickAddMagicMode} from '@/helpers/quickAddMagicMode' import {getQuickAddMagicMode, setQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {createRandomID} from '@/helpers/randomId' import {createRandomID} from '@/helpers/randomId'
import {objectIsEmpty} from '@/helpers/objectIsEmpty'
import {success} from '@/message' import {success} from '@/message'
import {AuthenticatedHTTPFactory} from '@/http-common' import {AuthenticatedHTTPFactory} from '@/http-common'
import {useColorScheme} from '@/composables/useColorScheme' import {useColorScheme} from '@/composables/useColorScheme'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {objectIsEmpty} from '@/helpers/objectIsEmpty'
import {useListStore} from '@/stores/lists'
import {useAuthStore} from '@/stores/auth'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
useTitle(() => `${t('user.settings.general.title')} - ${t('user.settings.title')}`) useTitle(() => `${t('user.settings.general.title')} - ${t('user.settings.title')}`)
@ -227,7 +223,6 @@ function getPlaySoundWhenDoneSetting() {
const playSoundWhenDone = ref(getPlaySoundWhenDoneSetting()) const playSoundWhenDone = ref(getPlaySoundWhenDoneSetting())
const quickAddMagicMode = ref(getQuickAddMagicMode()) const quickAddMagicMode = ref(getQuickAddMagicMode())
const store = useStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const settings = ref({...authStore.settings}) const settings = ref({...authStore.settings})
const id = ref(createRandomID()) const id = ref(createRandomID())
@ -256,7 +251,7 @@ const defaultList = computed({
settings.value.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') const loading = computed(() => authStore.isLoadingGeneralSettings)
watch( watch(
playSoundWhenDone, playSoundWhenDone,