Merge branch 'main' into feature/ganttastic
# Conflicts: # pnpm-lock.yaml # src/components/tasks/gantt-component.vue
This commit is contained in:
commit
1bbdd3b117
60 changed files with 1421 additions and 1435 deletions
58
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
58
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal 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
17
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
30
Dockerfile
30
Dockerfile
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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'}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
19
src/helpers/scrollIntoView.ts
Normal file
19
src/helpers/scrollIntoView.ts
Normal 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',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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':
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -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'
|
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -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: [],
|
||||||
|
|
|
@ -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
143
src/stores/base.ts
Normal 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))
|
||||||
|
}
|
|
@ -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
25
src/stores/helper.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
6
src/types/IProvider.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export interface IProvider {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
authUrl: string;
|
||||||
|
clientId: string;
|
||||||
|
}
|
30
src/types/shims-vue.d.ts
vendored
30
src/types/shims-vue.d.ts
vendored
|
@ -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,
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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')})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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']) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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')})
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue