Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ad9c69fca | ||
|
|
6fddac75f9 | ||
|
|
8a6f3b34f9 | ||
|
|
e7b261f84e | ||
|
|
94fb88ed58 | ||
|
|
e70fec3ddb | ||
|
|
bb1456e1aa | ||
|
|
1f4d097beb | ||
|
|
e00ce92716 | ||
|
|
b7fc29327a | ||
|
|
abb4126bce | ||
|
|
e9a932e0f0 | ||
|
|
9d985f7e96 | ||
|
|
811a933cd3 | ||
|
|
c118e788b8 | ||
|
|
2522cffa61 | ||
|
|
33b9917c7d | ||
|
|
845fe8ea88 | ||
|
|
5a0cee956e | ||
|
|
173473fb5a | ||
|
|
79071a1909 | ||
|
|
140765ad20 | ||
|
|
000f1adb75 | ||
|
|
0c3ef89d1a | ||
|
|
1ad4ba10a7 | ||
|
|
8ba9ded3e2 | ||
|
|
68896c69cb | ||
|
|
e1bf797709 | ||
|
|
83732ec26d | ||
|
|
23a3f78337 | ||
|
|
d0059d0292 | ||
|
|
b41cf7241a | ||
|
|
f144a0f11e | ||
|
|
8bf6251a70 | ||
|
|
54f9c5c6d4 | ||
|
|
76cb5ff9a4 | ||
|
|
1db99dba47 | ||
|
|
017fa1b5af | ||
|
|
7e66f3ec6b | ||
|
|
748fa2b798 | ||
|
|
304de78c48 | ||
|
|
3d5d17336e | ||
|
|
b181fff729 | ||
|
|
cc4fd97dd8 | ||
|
|
c9a9781bc7 | ||
|
|
ae187d7a14 | ||
|
|
f428c8a017 | ||
|
|
f777a1dcd3 | ||
|
|
9e5994105a | ||
|
|
8cec9995e3 | ||
|
|
466d0a73a9 | ||
|
|
00441ee8b1 | ||
|
|
c92f6c5ee5 | ||
|
|
eab72d4594 | ||
|
|
91e4bad544 | ||
|
|
fbc48c43f5 | ||
|
|
a49c06d408 | ||
|
|
d308583b9e | ||
|
|
2ba9d1c9fe | ||
|
|
13aaece2e0 | ||
|
|
57cb9a3d19 | ||
|
|
746c3359a5 | ||
|
|
6dc2348fbe | ||
|
|
d88295f31f | ||
|
|
42f5cdf2d9 | ||
|
|
b8fd5dde3b | ||
|
|
3d727e6856 | ||
|
|
a6644d9c89 | ||
|
|
c4c8017605 |
+12
-12
@@ -363,7 +363,7 @@ steps:
|
||||
- api-build
|
||||
|
||||
- name: frontend-dependencies
|
||||
image: node:22.12.0-alpine
|
||||
image: node:22.13.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -377,7 +377,7 @@ steps:
|
||||
# - restore-cache
|
||||
|
||||
- name: frontend-lint
|
||||
image: node:22.12.0-alpine
|
||||
image: node:22.13.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -389,7 +389,7 @@ steps:
|
||||
- frontend-dependencies
|
||||
|
||||
- name: frontend-build-prod
|
||||
image: node:22.12.0-alpine
|
||||
image: node:22.13.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -401,7 +401,7 @@ steps:
|
||||
- frontend-dependencies
|
||||
|
||||
- name: frontend-test-unit
|
||||
image: node:22.12.0-alpine
|
||||
image: node:22.13.0-alpine
|
||||
pull: always
|
||||
commands:
|
||||
- cd frontend
|
||||
@@ -412,7 +412,7 @@ steps:
|
||||
|
||||
- name: frontend-typecheck
|
||||
failure: ignore
|
||||
image: node:22.12.0-alpine
|
||||
image: node:22.13.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -543,7 +543,7 @@ steps:
|
||||
- git fetch --tags
|
||||
|
||||
- name: frontend-dependencies
|
||||
image: node:22.12.0-alpine
|
||||
image: node:22.13.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -555,7 +555,7 @@ steps:
|
||||
- pnpm install --fetch-timeout 100000
|
||||
|
||||
- name: frontend-build
|
||||
image: node:22.12.0-alpine
|
||||
image: node:22.13.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -763,7 +763,7 @@ steps:
|
||||
|
||||
# Build os packages and push it to our bucket
|
||||
- name: build-os-packages-unstable
|
||||
image: goreleaser/nfpm:v2.41.0
|
||||
image: goreleaser/nfpm:v2.41.2
|
||||
pull: always
|
||||
commands:
|
||||
- apk add git go
|
||||
@@ -779,7 +779,7 @@ steps:
|
||||
depends_on: [ after-build-compress ]
|
||||
|
||||
- name: build-os-packages-version
|
||||
image: goreleaser/nfpm:v2.41.0
|
||||
image: goreleaser/nfpm:v2.41.2
|
||||
pull: always
|
||||
commands:
|
||||
- apk add git go
|
||||
@@ -946,7 +946,7 @@ steps:
|
||||
- git fetch --tags
|
||||
|
||||
- name: build
|
||||
image: node:22.12.0-alpine
|
||||
image: node:22.13.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -1007,7 +1007,7 @@ steps:
|
||||
- git fetch --tags
|
||||
|
||||
- name: build
|
||||
image: node:22.12.0-alpine
|
||||
image: node:22.13.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -1398,6 +1398,6 @@ steps:
|
||||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: 3e5069e96f4fa86dcc8453202f51a7bef36f958fb497477f0d5b5d56b985f3b7
|
||||
hmac: 298674d9b9d8618eef47969038fd4d7516112ecbd8a4d899ec3751d997a34ce4
|
||||
|
||||
...
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM --platform=$BUILDPLATFORM node:22.12.0-alpine AS frontendbuilder
|
||||
FROM --platform=$BUILDPLATFORM node:22.13.0-alpine AS frontendbuilder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "33.2.1",
|
||||
"electron": "34.0.0",
|
||||
"electron-builder": "25.1.8"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
Generated
+5
-5
@@ -16,8 +16,8 @@ importers:
|
||||
version: 4.21.2
|
||||
devDependencies:
|
||||
electron:
|
||||
specifier: 33.2.1
|
||||
version: 33.2.1
|
||||
specifier: 34.0.0
|
||||
version: 34.0.0
|
||||
electron-builder:
|
||||
specifier: 25.1.8
|
||||
version: 25.1.8(electron-builder-squirrel-windows@24.13.3)
|
||||
@@ -586,8 +586,8 @@ packages:
|
||||
electron-publish@25.1.7:
|
||||
resolution: {integrity: sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==}
|
||||
|
||||
electron@33.2.1:
|
||||
resolution: {integrity: sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==}
|
||||
electron@34.0.0:
|
||||
resolution: {integrity: sha512-fpaPb0lifoUJ6UJa4Lk8/0B2Ku/xDZWdc1Gkj67jbygTCrvSon0qquju6Ltx1Kz23GRqqlIHXiy9EvrjpY7/Wg==}
|
||||
engines: {node: '>= 12.20.55'}
|
||||
hasBin: true
|
||||
|
||||
@@ -2442,7 +2442,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron@33.2.1:
|
||||
electron@34.0.0:
|
||||
dependencies:
|
||||
'@electron/get': 2.0.3
|
||||
'@types/node': 20.12.7
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
22.12.0
|
||||
22.13.0
|
||||
+51
-48
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@9.15.1",
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
@@ -30,7 +30,7 @@
|
||||
"preview:dev": "vite preview --outDir dist-dev --mode development --port 4173",
|
||||
"preview:test": "vite preview --port 4173 --outDir dist-test",
|
||||
"build": "vite build && workbox copyLibraries dist/",
|
||||
"build:test": "vite build --outDir dist-test && workbox copyLibraries dist-dev/",
|
||||
"build:test": "vite build --outDir dist-test && workbox copyLibraries dist-test/",
|
||||
"build:dev": "vite build --mode development --outDir dist-dev/",
|
||||
"lint": "eslint 'src/**/*.{js,ts,vue}'",
|
||||
"lint:fix": "pnpm run lint --fix",
|
||||
@@ -55,31 +55,31 @@
|
||||
"@fortawesome/vue-fontawesome": "3.0.8",
|
||||
"@github/hotkey": "3.1.1",
|
||||
"@infectoone/vue-ganttastic": "2.3.2",
|
||||
"@intlify/unplugin-vue-i18n": "6.0.2",
|
||||
"@intlify/unplugin-vue-i18n": "6.0.3",
|
||||
"@kyvg/vue3-notification": "3.4.1",
|
||||
"@sentry/tracing": "7.114.0",
|
||||
"@sentry/vue": "8.47.0",
|
||||
"@tiptap/core": "2.10.4",
|
||||
"@tiptap/extension-code-block": "2.10.4",
|
||||
"@tiptap/extension-code-block-lowlight": "2.10.4",
|
||||
"@tiptap/extension-hard-break": "2.10.4",
|
||||
"@tiptap/extension-image": "2.10.4",
|
||||
"@tiptap/extension-link": "2.10.4",
|
||||
"@tiptap/extension-placeholder": "2.10.4",
|
||||
"@tiptap/extension-table": "2.10.4",
|
||||
"@tiptap/extension-table-cell": "2.10.4",
|
||||
"@tiptap/extension-table-header": "2.10.4",
|
||||
"@tiptap/extension-table-row": "2.10.4",
|
||||
"@tiptap/extension-task-item": "2.10.4",
|
||||
"@tiptap/extension-task-list": "2.10.4",
|
||||
"@tiptap/extension-typography": "2.10.4",
|
||||
"@tiptap/extension-underline": "2.10.4",
|
||||
"@tiptap/pm": "2.10.4",
|
||||
"@tiptap/starter-kit": "^2.10.4",
|
||||
"@tiptap/suggestion": "2.10.4",
|
||||
"@tiptap/vue-3": "2.10.4",
|
||||
"@vueuse/core": "12.2.0",
|
||||
"@vueuse/router": "12.2.0",
|
||||
"@sentry/tracing": "7.120.3",
|
||||
"@sentry/vue": "8.50.0",
|
||||
"@tiptap/core": "2.11.2",
|
||||
"@tiptap/extension-code-block": "2.11.2",
|
||||
"@tiptap/extension-code-block-lowlight": "2.11.2",
|
||||
"@tiptap/extension-hard-break": "2.11.2",
|
||||
"@tiptap/extension-image": "2.11.2",
|
||||
"@tiptap/extension-link": "2.11.2",
|
||||
"@tiptap/extension-placeholder": "2.11.2",
|
||||
"@tiptap/extension-table": "2.11.2",
|
||||
"@tiptap/extension-table-cell": "2.11.2",
|
||||
"@tiptap/extension-table-header": "2.11.2",
|
||||
"@tiptap/extension-table-row": "2.11.2",
|
||||
"@tiptap/extension-task-item": "2.11.2",
|
||||
"@tiptap/extension-task-list": "2.11.2",
|
||||
"@tiptap/extension-typography": "2.11.2",
|
||||
"@tiptap/extension-underline": "2.11.2",
|
||||
"@tiptap/pm": "2.11.2",
|
||||
"@tiptap/starter-kit": "2.11.2",
|
||||
"@tiptap/suggestion": "2.11.2",
|
||||
"@tiptap/vue-3": "2.11.2",
|
||||
"@vueuse/core": "12.4.0",
|
||||
"@vueuse/router": "12.4.0",
|
||||
"axios": "1.7.9",
|
||||
"blurhash": "2.0.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
@@ -100,6 +100,7 @@
|
||||
"tailwindcss": "3.4.17",
|
||||
"tippy.js": "6.3.7",
|
||||
"ufo": "1.5.4",
|
||||
"validator": "13.12.0",
|
||||
"vue": "3.5.13",
|
||||
"vue-advanced-cropper": "2.8.9",
|
||||
"vue-flatpickr-component": "11.0.5",
|
||||
@@ -107,54 +108,56 @@
|
||||
"vue-router": "4.5.0",
|
||||
"vuemoji-picker": "0.3.2",
|
||||
"workbox-precaching": "7.3.0",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
"zhyswan-vuedraggable": "4.1.3",
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.2.5",
|
||||
"@cypress/vite-dev-server": "5.2.1",
|
||||
"@cypress/vue": "6.0.1",
|
||||
"@faker-js/faker": "9.3.0",
|
||||
"@cypress/vite-dev-server": "6.0.1",
|
||||
"@cypress/vue": "6.0.2",
|
||||
"@faker-js/faker": "9.4.0",
|
||||
"@histoire/plugin-screenshot": "0.17.17",
|
||||
"@histoire/plugin-vue": "0.17.17",
|
||||
"@tsconfig/node22": "22.0.0",
|
||||
"@types/codemirror": "5.60.15",
|
||||
"@types/flexsearch": "0.7.6",
|
||||
"@types/is-touch-device": "1.0.3",
|
||||
"@types/node": "22.10.2",
|
||||
"@types/node": "22.10.7",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "8.18.2",
|
||||
"@typescript-eslint/parser": "8.18.2",
|
||||
"@types/validator": "13.12.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.20.0",
|
||||
"@typescript-eslint/parser": "8.20.0",
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"@vue/eslint-config-typescript": "14.2.0",
|
||||
"@vue/eslint-config-typescript": "14.3.0",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"@vue/tsconfig": "0.7.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"browserslist": "4.24.3",
|
||||
"caniuse-lite": "1.0.30001690",
|
||||
"browserslist": "4.24.4",
|
||||
"caniuse-lite": "1.0.30001692",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "13.17.0",
|
||||
"esbuild": "0.24.2",
|
||||
"eslint": "9.17.0",
|
||||
"eslint": "9.18.0",
|
||||
"eslint-plugin-vue": "9.32.0",
|
||||
"happy-dom": "16.0.0",
|
||||
"happy-dom": "16.6.0",
|
||||
"histoire": "0.17.17",
|
||||
"postcss": "8.4.49",
|
||||
"postcss": "8.5.1",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-preset-env": "10.1.2",
|
||||
"rollup": "4.29.1",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"sass-embedded": "1.83.0",
|
||||
"start-server-and-test": "2.0.9",
|
||||
"typescript": "5.7.2",
|
||||
"postcss-preset-env": "10.1.3",
|
||||
"rollup": "4.30.1",
|
||||
"rollup-plugin-visualizer": "5.14.0",
|
||||
"sass-embedded": "1.83.4",
|
||||
"start-server-and-test": "2.0.10",
|
||||
"typescript": "5.7.3",
|
||||
"unplugin-inject-preload": "3.0.0",
|
||||
"vite": "6.0.6",
|
||||
"vite": "6.0.7",
|
||||
"vite-plugin-pwa": "0.21.1",
|
||||
"vite-plugin-sentry": "1.4.0",
|
||||
"vite-plugin-vue-devtools": "7.6.8",
|
||||
"vite-plugin-vue-devtools": "7.7.0",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "2.1.8",
|
||||
"vue-tsc": "2.2.0",
|
||||
"wait-on": "8.0.1",
|
||||
"wait-on": "8.0.2",
|
||||
"workbox-cli": "7.3.0"
|
||||
},
|
||||
"pnpm": {
|
||||
|
||||
Generated
+1226
-1175
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<Ready>
|
||||
<template v-if="authUser">
|
||||
<template v-if="authStore.authUser">
|
||||
<AppHeader />
|
||||
<ContentAuth />
|
||||
</template>
|
||||
<ContentLinkShare v-else-if="authLinkShare" />
|
||||
<ContentLinkShare v-else-if="authStore.authLinkShare" />
|
||||
<NoAuthWrapper v-else>
|
||||
<RouterView />
|
||||
</NoAuthWrapper>
|
||||
@@ -49,17 +49,15 @@ import DemoMode from '@/components/home/DemoMode.vue'
|
||||
const importAccountDeleteService = () => import('@/services/accountDelete')
|
||||
import {success} from '@/message'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const authStore = useAuthStore()
|
||||
const baseStore = useBaseStore()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
useBodyClass('is-touch', isTouchDevice())
|
||||
const keyboardShortcutsActive = computed(() => baseStore.keyboardShortcutsActive)
|
||||
|
||||
const authUser = computed(() => authStore.authUser)
|
||||
const authLinkShare = computed(() => authStore.authLinkShare)
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
// setup account deletion verification
|
||||
|
||||
@@ -112,7 +112,7 @@ import Popup from '@/components/misc/Popup.vue'
|
||||
import {DATE_RANGES} from '@/components/date/dateRanges'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import DatemathHelp from '@/components/date/DatemathHelp.vue'
|
||||
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
|
||||
import {useFlatpickrLanguage} from '@/helpers/useFlatpickrLanguage'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: {
|
||||
@@ -137,7 +137,7 @@ const flatPickerConfig = computed(() => ({
|
||||
enableTime: false,
|
||||
wrap: true,
|
||||
mode: 'range',
|
||||
locale: getFlatpickrLanguage(),
|
||||
locale: useFlatpickrLanguage().value,
|
||||
}))
|
||||
|
||||
const showHowItWorks = ref(false)
|
||||
|
||||
@@ -89,7 +89,7 @@ import Popup from '@/components/misc/Popup.vue'
|
||||
import {DATE_VALUES} from '@/components/date/dateRanges'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import DatemathHelp from '@/components/date/DatemathHelp.vue'
|
||||
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
|
||||
import {useFlatpickrLanguage} from '@/helpers/useFlatpickrLanguage'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: string | Date | null,
|
||||
@@ -112,7 +112,7 @@ const flatPickerConfig = computed(() => ({
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
enableTime: false,
|
||||
wrap: true,
|
||||
locale: getFlatpickrLanguage(),
|
||||
locale: useFlatpickrLanguage().value,
|
||||
}))
|
||||
|
||||
const showHowItWorks = ref(false)
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
</h1>
|
||||
|
||||
<BaseButton
|
||||
v-if="!isEditorContentEmpty(currentProject.description)"
|
||||
:to="{ name: 'project.info', params: { projectId: currentProject.id } }"
|
||||
class="project-title-button"
|
||||
>
|
||||
@@ -124,6 +125,7 @@ import MenuButton from '@/components/home/MenuButton.vue'
|
||||
import OpenQuickActions from '@/components/misc/OpenQuickActions.vue'
|
||||
|
||||
import { getProjectTitle } from '@/helpers/getProjectTitle'
|
||||
import { isEditorContentEmpty } from '@/helpers/editorContentEmpty'
|
||||
|
||||
import { useBaseStore } from '@/stores/base'
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
|
||||
@@ -82,7 +82,7 @@ import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
|
||||
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
|
||||
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
|
||||
import {useFlatpickrLanguage} from '@/helpers/useFlatpickrLanguage'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: Date | null | string
|
||||
@@ -114,7 +114,7 @@ const flatPickerConfig = computed(() => ({
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
inline: true,
|
||||
locale: getFlatpickrLanguage(),
|
||||
locale: useFlatpickrLanguage().value,
|
||||
}))
|
||||
|
||||
// Since flatpickr dates are strings, we need to convert them to native date objects.
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts" generic="T extends { [id: string]: any }">
|
||||
import {computed, onBeforeUnmount, onMounted, ref, toRefs, watch, type ComponentPublicInstance} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
@@ -134,15 +134,13 @@ import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
/** The object with the value, updated every time an entry is selected */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
modelValue: { [id: string]: any } | null
|
||||
modelValue: T | T[] | null,
|
||||
/** When true, shows a loading spinner */
|
||||
loading?: boolean
|
||||
/** The placeholder of the search input */
|
||||
placeholder?: string
|
||||
/** The search results where the @search listener needs to put the results into */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
searchResults?: { [id: string]: any }[]
|
||||
searchResults?: T[]
|
||||
/** The name of the property of the searched object to show the user. If empty the component will show all raw data of an entry */
|
||||
label?: string
|
||||
/** The id attribute of the input element */
|
||||
@@ -188,8 +186,7 @@ const props = withDefaults(defineProps<{
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
'update:modelValue': [value: { [key: string]: any }[] | null],
|
||||
'update:modelValue': [value: T | T[] | null],
|
||||
/**
|
||||
* Triggered every time the search query input changes
|
||||
*/
|
||||
@@ -197,8 +194,7 @@ const emit = defineEmits<{
|
||||
/**
|
||||
* Triggered every time an option from the search results is selected. Also triggers a change in v-model.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
'select': [value: { [key: string]: any }],
|
||||
'select': [value: T],
|
||||
/**
|
||||
* If nothing or no exact match was found and `creatable` is true, this event is triggered with the current value of the search query.
|
||||
*/
|
||||
@@ -206,8 +202,7 @@ const emit = defineEmits<{
|
||||
/**
|
||||
* If `multiple` is enabled, this will be fired every time an item is removed from the array of selected items.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
'remove': [value: { [key: string]: any }],
|
||||
'remove': [value: T],
|
||||
}>()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -220,13 +215,12 @@ function elementInResults(elem: string | any, label: string, query: string): boo
|
||||
return elem === query
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const query = ref<string | { [key: string]: any }>('')
|
||||
const query = ref<string | T>('')
|
||||
const searchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
const localLoading = ref(false)
|
||||
const showSearchResults = ref(false)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const internalValue = ref<string | { [key: string]: any } | any[] | null>(null)
|
||||
|
||||
const internalValue = ref<string | T | T[] | null>(null)
|
||||
|
||||
onMounted(() => document.addEventListener('click', hideSearchResultsHandler))
|
||||
onBeforeUnmount(() => document.removeEventListener('click', hideSearchResultsHandler))
|
||||
@@ -325,15 +319,13 @@ function handleFocus() {
|
||||
}, 10)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function select(object: { [key: string]: any } | null) {
|
||||
function select(object: T | null) {
|
||||
if (props.multiple) {
|
||||
if (internalValue.value === null) {
|
||||
internalValue.value = []
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(internalValue.value as any[]).push(object)
|
||||
internalValue.value.push(object)
|
||||
} else {
|
||||
internalValue.value = object
|
||||
}
|
||||
@@ -346,8 +338,7 @@ function select(object: { [key: string]: any } | null) {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function setSelectedObject(object: string | { [id: string]: any } | null, resetOnly = false) {
|
||||
function setSelectedObject(object: string | T | null, resetOnly = false) {
|
||||
internalValue.value = object
|
||||
|
||||
// We assume we're getting an array when multiple is enabled and can therefore leave the query
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch, watchEffect} from 'vue'
|
||||
import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {eventToHotkeyString} from '@github/hotkey'
|
||||
|
||||
@@ -162,6 +162,8 @@ import TaskItem from '@tiptap/extension-task-item'
|
||||
import TaskList from '@tiptap/extension-task-list'
|
||||
import HardBreak from '@tiptap/extension-hard-break'
|
||||
|
||||
import {Node} from '@tiptap/pm/model'
|
||||
|
||||
import Commands from './commands'
|
||||
import suggestionSetup from './suggestion'
|
||||
|
||||
@@ -179,7 +181,17 @@ import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
|
||||
import inputPrompt from '@/helpers/inputPrompt'
|
||||
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
const {
|
||||
modelValue,
|
||||
uploadCallback,
|
||||
isEditEnabled,
|
||||
bottomActions,
|
||||
showSave,
|
||||
placeholder,
|
||||
editShortcut,
|
||||
enableDiscardShortcut,
|
||||
} = withDefaults(defineProps<{
|
||||
modelValue: string,
|
||||
uploadCallback?: UploadCallback,
|
||||
isEditEnabled?: boolean,
|
||||
bottomActions?: BottomAction[],
|
||||
@@ -188,7 +200,6 @@ const props = withDefaults(defineProps<{
|
||||
editShortcut?: string,
|
||||
enableDiscardShortcut?: boolean,
|
||||
}>(), {
|
||||
uploadCallback: undefined,
|
||||
isEditEnabled: true,
|
||||
bottomActions: () => [],
|
||||
showSave: false,
|
||||
@@ -197,9 +208,7 @@ const props = withDefaults(defineProps<{
|
||||
enableDiscardShortcut: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['save'])
|
||||
|
||||
const modelValue = defineModel<string>({ default: '' })
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
||||
const tiptapInstanceRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
@@ -264,7 +273,7 @@ const CustomImage = Image.extend({
|
||||
|
||||
nextTick(async () => {
|
||||
|
||||
const img = document.getElementById(id) as HTMLImageElement | null
|
||||
const img = document.getElementById(id)
|
||||
|
||||
if (!img) return
|
||||
|
||||
@@ -295,13 +304,13 @@ const CustomImage = Image.extend({
|
||||
type Mode = 'edit' | 'preview'
|
||||
|
||||
const internalMode = ref<Mode>('preview')
|
||||
const isEditing = computed(() => internalMode.value === 'edit' && props.isEditEnabled)
|
||||
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
|
||||
const contentHasChanged = ref<boolean>(false)
|
||||
|
||||
let lastSavedState = ''
|
||||
|
||||
watch(
|
||||
modelValue,
|
||||
() => modelValue,
|
||||
(newValue) => {
|
||||
if (!contentHasChanged.value) {
|
||||
lastSavedState = newValue
|
||||
@@ -311,7 +320,7 @@ watch(
|
||||
)
|
||||
|
||||
watch(
|
||||
internalMode,
|
||||
() => internalMode.value,
|
||||
mode => {
|
||||
if (mode === 'preview') {
|
||||
contentHasChanged.value = false
|
||||
@@ -351,19 +360,25 @@ const extensions : Extensions = [
|
||||
}),
|
||||
|
||||
Placeholder.configure({
|
||||
placeholder({editor}) {
|
||||
if (!isEditing.value || editor.getText() !== '' && !editor.isFocused) {
|
||||
placeholder: ({editor}) => {
|
||||
if (!isEditing.value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return props.placeholder || t('input.editor.placeholder')
|
||||
if (editor.getText() !== '' && !editor.isFocused) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return placeholder !== ''
|
||||
? placeholder
|
||||
: t('input.editor.placeholder')
|
||||
},
|
||||
}),
|
||||
Typography,
|
||||
Underline,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
validate: (href) => (new RegExp(
|
||||
validate: (href: string) => (new RegExp(
|
||||
`^(https?|${additionalLinkProtocols.join('|')}):\\/\\/`,
|
||||
'i',
|
||||
)).test(href),
|
||||
@@ -382,8 +397,8 @@ const extensions : Extensions = [
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
nested: true,
|
||||
onReadOnlyChecked(node, checked) {
|
||||
if (!props.isEditEnabled) {
|
||||
onReadOnlyChecked: (node: Node, checked: boolean): boolean => {
|
||||
if (!isEditEnabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -414,7 +429,7 @@ const extensions : Extensions = [
|
||||
]
|
||||
|
||||
// Add a custom extension for the Escape key
|
||||
if (props.enableDiscardShortcut) {
|
||||
if (enableDiscardShortcut) {
|
||||
extensions.push(Extension.create({
|
||||
name: 'escapeKey',
|
||||
|
||||
@@ -433,13 +448,21 @@ const editor = useEditor({
|
||||
// eslint-disable-next-line vue/no-ref-object-reactivity-loss
|
||||
editable: isEditing.value,
|
||||
extensions: extensions,
|
||||
onUpdate: bubbleNow,
|
||||
onUpdate: () => {
|
||||
bubbleNow()
|
||||
},
|
||||
})
|
||||
|
||||
watchEffect(() => editor.value?.setEditable(isEditing.value))
|
||||
watch(
|
||||
() => isEditing.value,
|
||||
() => {
|
||||
editor.value?.setEditable(isEditing.value)
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
watch(
|
||||
modelValue,
|
||||
() => modelValue,
|
||||
value => {
|
||||
if (!editor?.value) return
|
||||
|
||||
@@ -453,14 +476,13 @@ watch(
|
||||
)
|
||||
|
||||
function bubbleNow() {
|
||||
const editorVal = editor.value!.getHTML()
|
||||
if (editorVal === modelValue.value ||
|
||||
(editorVal === '<p></p>') && modelValue.value === '') {
|
||||
if (editor.value?.getHTML() === modelValue ||
|
||||
(editor.value?.getHTML() === '<p></p>') && modelValue === '') {
|
||||
return
|
||||
}
|
||||
|
||||
contentHasChanged.value = true
|
||||
modelValue.value = editorVal
|
||||
emit('update:modelValue', editor.value?.getHTML())
|
||||
}
|
||||
|
||||
function bubbleSave() {
|
||||
@@ -480,7 +502,7 @@ function exitEditMode() {
|
||||
}
|
||||
|
||||
function setEditIfApplicable() {
|
||||
if (!props.isEditEnabled) return
|
||||
if (!isEditEnabled) return
|
||||
if (isEditing.value) return
|
||||
|
||||
setEdit()
|
||||
@@ -497,20 +519,21 @@ onBeforeUnmount(() => editor.value?.destroy())
|
||||
|
||||
const uploadInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
async function uploadAndInsertFiles(files: File[] | FileList) {
|
||||
const urls = await props.uploadCallback?.(files)
|
||||
urls?.forEach(url => {
|
||||
editor.value
|
||||
?.chain()
|
||||
.focus()
|
||||
.setImage({src: url})
|
||||
.run()
|
||||
function uploadAndInsertFiles(files: File[] | FileList) {
|
||||
uploadCallback(files).then(urls => {
|
||||
urls?.forEach(url => {
|
||||
editor.value
|
||||
?.chain()
|
||||
.focus()
|
||||
.setImage({src: url})
|
||||
.run()
|
||||
})
|
||||
bubbleSave()
|
||||
})
|
||||
bubbleSave()
|
||||
}
|
||||
|
||||
function triggerImageInput(event) {
|
||||
if (typeof props.uploadCallback !== 'undefined') {
|
||||
if (typeof uploadCallback !== 'undefined') {
|
||||
uploadInputRef.value?.click()
|
||||
return
|
||||
}
|
||||
@@ -520,7 +543,7 @@ function triggerImageInput(event) {
|
||||
|
||||
async function addImage(event) {
|
||||
|
||||
if (typeof props.uploadCallback !== 'undefined') {
|
||||
if (typeof uploadCallback !== 'undefined') {
|
||||
const files = uploadInputRef.value?.files
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
@@ -545,28 +568,28 @@ function setLink(event) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.editShortcut !== '') {
|
||||
if (editShortcut !== '') {
|
||||
document.addEventListener('keydown', setFocusToEditor)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
if (typeof props.uploadCallback !== 'undefined') {
|
||||
if (typeof uploadCallback !== 'undefined') {
|
||||
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
|
||||
input?.addEventListener('paste', handleImagePaste)
|
||||
}
|
||||
|
||||
setModeAndValue(modelValue.value)
|
||||
setModeAndValue(modelValue)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
nextTick(() => {
|
||||
if (typeof props.uploadCallback !== 'undefined') {
|
||||
if (typeof uploadCallback !== 'undefined') {
|
||||
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
|
||||
input?.removeEventListener('paste', handleImagePaste)
|
||||
}
|
||||
})
|
||||
if (props.editShortcut !== '') {
|
||||
if (editShortcut !== '') {
|
||||
document.removeEventListener('keydown', setFocusToEditor)
|
||||
}
|
||||
})
|
||||
@@ -597,7 +620,7 @@ function setFocusToEditor(event) {
|
||||
|
||||
const hotkeyString = eventToHotkeyString(event)
|
||||
if (!hotkeyString) return
|
||||
if (hotkeyString !== props.editShortcut ||
|
||||
if (hotkeyString !== editShortcut ||
|
||||
event.target.tagName.toLowerCase() === 'input' ||
|
||||
event.target.tagName.toLowerCase() === 'textarea' ||
|
||||
event.target.contentEditable === 'true') {
|
||||
@@ -606,7 +629,7 @@ function setFocusToEditor(event) {
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
if (!isEditing.value && props.isEditEnabled) {
|
||||
if (!isEditing.value && isEditEnabled) {
|
||||
internalMode.value = 'edit'
|
||||
}
|
||||
|
||||
@@ -630,7 +653,7 @@ function clickTasklistCheckbox(event) {
|
||||
}
|
||||
|
||||
watch(
|
||||
isEditing,
|
||||
() => isEditing.value,
|
||||
async editing => {
|
||||
await nextTick()
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
<p>{{ $t('offline.text') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else-if="ready">
|
||||
<template v-else-if="baseStore.ready">
|
||||
<slot />
|
||||
</template>
|
||||
<section v-else-if="error !== ''">
|
||||
<section v-else-if="baseStore.error !== ''">
|
||||
<NoAuthWrapper :show-api-config="false">
|
||||
<p v-if="error === ERROR_NO_API_URL">
|
||||
<p v-if="baseStore.error === ERROR_NO_API_URL">
|
||||
{{ $t('ready.noApiUrlConfigured') }}
|
||||
</p>
|
||||
<Message
|
||||
@@ -30,7 +30,7 @@
|
||||
>
|
||||
<p>
|
||||
{{ $t('ready.errorOccured') }}<br>
|
||||
{{ error }}
|
||||
{{ baseStore.error }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('ready.checkApiUrl') }}
|
||||
@@ -38,13 +38,13 @@
|
||||
</Message>
|
||||
<ApiConfig
|
||||
:configure-open="true"
|
||||
@foundApi="load"
|
||||
@foundApi="baseStore.loadApp()"
|
||||
/>
|
||||
</NoAuthWrapper>
|
||||
</section>
|
||||
<CustomTransition name="fade">
|
||||
<section
|
||||
v-if="showLoading"
|
||||
v-if="baseStore.loading"
|
||||
class="vikunja-loading"
|
||||
>
|
||||
<Logo class="logo" />
|
||||
@@ -57,60 +57,19 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, computed} from 'vue'
|
||||
import {useRouter, useRoute} from 'vue-router'
|
||||
|
||||
import Logo from '@/assets/logo.svg?component'
|
||||
import ApiConfig from '@/components/misc/ApiConfig.vue'
|
||||
import Message from '@/components/misc/Message.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import NoAuthWrapper from '@/components/misc/NoAuthWrapper.vue'
|
||||
|
||||
import {ERROR_NO_API_URL, InvalidApiUrlProvidedError, NoApiUrlProvidedError} from '@/helpers/checkAndSetApiUrl'
|
||||
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
|
||||
|
||||
import {useOnline} from '@/composables/useOnline'
|
||||
|
||||
import {getAuthForRoute} from '@/router'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const ready = computed(() => baseStore.ready)
|
||||
const online = useOnline()
|
||||
|
||||
const error = ref('')
|
||||
const showLoading = computed(() => !ready.value && error.value === '')
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
await baseStore.loadApp()
|
||||
baseStore.setReady(true)
|
||||
const redirectTo = await getAuthForRoute(route, authStore)
|
||||
if (typeof redirectTo !== 'undefined') {
|
||||
await router.push(redirectTo)
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof NoApiUrlProvidedError) {
|
||||
error.value = ERROR_NO_API_URL
|
||||
return
|
||||
}
|
||||
if (e instanceof InvalidApiUrlProvidedError) {
|
||||
error.value = t('apiConfig.error')
|
||||
return
|
||||
}
|
||||
error.value = String(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
load()
|
||||
const baseStore = useBaseStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -77,7 +77,7 @@ import {useI18n} from 'vue-i18n'
|
||||
import type {RouteLocationNormalized} from 'vue-router'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
|
||||
import {useFlatpickrLanguage} from '@/helpers/useFlatpickrLanguage'
|
||||
|
||||
import Foo from '@/components/misc/flatpickr/Flatpickr.vue'
|
||||
import ProjectWrapper from '@/components/project/ProjectWrapper.vue'
|
||||
@@ -153,14 +153,14 @@ const flatPickerDateRange = computed<Date[]>({
|
||||
const initialDateRange = [filters.value.dateFrom, filters.value.dateTo]
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const flatPickerConfig = computed<Options>(() => ({
|
||||
const flatPickerConfig = computed(() => ({
|
||||
altFormat: t('date.altFormatShort'),
|
||||
altInput: true,
|
||||
defaultDate: initialDateRange,
|
||||
enableTime: false,
|
||||
mode: 'range',
|
||||
locale: getFlatpickrLanguage(),
|
||||
}))
|
||||
locale: useFlatpickrLanguage().value,
|
||||
} as Options))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import User from '@/components/misc/User.vue'
|
||||
import {computed} from 'vue'
|
||||
|
||||
const {
|
||||
assignees,
|
||||
remove,
|
||||
disabled,
|
||||
avatarSize = 30,
|
||||
inline = false,
|
||||
} = defineProps<{
|
||||
withDefaults(defineProps<{
|
||||
assignees: IUser[],
|
||||
remove?: (user: IUser) => void,
|
||||
disabled?: boolean,
|
||||
avatarSize?: number,
|
||||
inline?: boolean,
|
||||
}>()
|
||||
/** add this boolean prop to enable removal of assignees */
|
||||
canRemove?: boolean,
|
||||
}>(), {
|
||||
avatarSize: 30,
|
||||
inline: false,
|
||||
canRemove: false,
|
||||
})
|
||||
|
||||
const hasDelete = computed(() => typeof remove !== 'undefined' && !disabled)
|
||||
defineEmits<{
|
||||
remove: [user: IUser],
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -36,13 +36,13 @@ const hasDelete = computed(() => typeof remove !== 'undefined' && !disabled)
|
||||
:avatar-size="avatarSize"
|
||||
:show-username="false"
|
||||
:user="user"
|
||||
:class="{'m-2': hasDelete}"
|
||||
:class="{'m-2': canRemove && !disabled}"
|
||||
/>
|
||||
<BaseButton
|
||||
v-if="hasDelete"
|
||||
v-if="canRemove && !disabled"
|
||||
:key="'delete'+user.id"
|
||||
class="remove-assignee"
|
||||
@click="remove(user)"
|
||||
@click="$emit('remove', user)"
|
||||
>
|
||||
<Icon icon="times" />
|
||||
</BaseButton>
|
||||
|
||||
@@ -44,7 +44,7 @@ import flatPickr from 'vue-flatpickr-component'
|
||||
|
||||
import TaskService from '@/services/task'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
|
||||
import {useFlatpickrLanguage} from '@/helpers/useFlatpickrLanguage'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: ITask,
|
||||
@@ -101,7 +101,7 @@ const flatPickerConfig = computed(() => ({
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
inline: true,
|
||||
locale: getFlatpickrLanguage(),
|
||||
locale: useFlatpickrLanguage().value,
|
||||
}))
|
||||
|
||||
function deferDays(days: number) {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, watch, onBeforeUnmount} from 'vue'
|
||||
import {ref, computed, watchEffect, onBeforeUnmount} from 'vue'
|
||||
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import Editor from '@/components/input/AsyncEditor'
|
||||
@@ -46,21 +46,25 @@ import Editor from '@/components/input/AsyncEditor'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
type AttachmentUploadFunction = (file: File, onSuccess: (attachmentUrl: string) => void) => Promise<string>
|
||||
export type AttachmentUploadFunction = (file: File, onSuccess: (attachmentUrl: string) => void) => Promise<string>
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
attachmentUpload,
|
||||
canWrite,
|
||||
} = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: ITask,
|
||||
attachmentUpload: AttachmentUploadFunction,
|
||||
canWrite: boolean,
|
||||
}>(), {
|
||||
canWrite: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ITask]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const description = ref<string>('')
|
||||
watchEffect(() => {
|
||||
description.value = props.modelValue.description
|
||||
})
|
||||
|
||||
const saved = ref(false)
|
||||
|
||||
// Since loading is global state, this variable ensures we're only showing the saving icon when saving the description.
|
||||
@@ -69,14 +73,6 @@ const saving = ref(false)
|
||||
const taskStore = useTaskStore()
|
||||
const loading = computed(() => taskStore.isLoading)
|
||||
|
||||
watch(
|
||||
() => modelValue.description,
|
||||
value => {
|
||||
description.value = value
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const changeTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
async function saveWithDelay() {
|
||||
@@ -99,13 +95,12 @@ async function save() {
|
||||
if (changeTimeout.value !== null) {
|
||||
clearTimeout(changeTimeout.value)
|
||||
}
|
||||
|
||||
saved.value = false
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
// FIXME: don't update state from internal.
|
||||
const updated = await taskStore.update({
|
||||
...modelValue,
|
||||
...props.modelValue,
|
||||
description: description.value,
|
||||
})
|
||||
emit('update:modelValue', updated)
|
||||
@@ -119,13 +114,12 @@ async function save() {
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadCallback(files: File[] | FileList): (Promise<string[]>) {
|
||||
|
||||
async function uploadCallback(files: File[] | FileList): Promise<string[]> {
|
||||
const uploadPromises: Promise<string>[] = []
|
||||
|
||||
files.forEach((file: File) => {
|
||||
const promise = new Promise<string>((resolve) => {
|
||||
attachmentUpload(file, (uploadedFileUrl: string) => resolve(uploadedFileUrl))
|
||||
props.attachmentUpload(file, (uploadedFileUrl: string) => resolve(uploadedFileUrl))
|
||||
})
|
||||
|
||||
uploadPromises.push(promise)
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
<template #items="{items}">
|
||||
<AssigneeList
|
||||
:assignees="items"
|
||||
:remove="removeAssignee"
|
||||
:disabled="disabled"
|
||||
can-remove
|
||||
@remove="removeAssignee"
|
||||
/>
|
||||
</template>
|
||||
<template #searchResult="{option: user}">
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import FlatpickrLanguages from 'flatpickr/dist/l10n'
|
||||
import type { CustomLocale, key } from 'flatpickr/dist/types/locale'
|
||||
|
||||
export function getFlatpickrLanguage(): CustomLocale {
|
||||
const authStore = useAuthStore()
|
||||
const lang = authStore.settings.language
|
||||
const langPair = lang.split('-')
|
||||
let language = FlatpickrLanguages[lang === 'vi-vn' ? 'vn' : 'en']
|
||||
if (langPair.length > 0 && FlatpickrLanguages[langPair[0] as key] !== undefined) {
|
||||
language = FlatpickrLanguages[langPair[0] as key]
|
||||
}
|
||||
language.firstDayOfWeek = authStore.settings.weekStart ?? language.firstDayOfWeek
|
||||
return language
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
// TODO: only import needed languages
|
||||
import FlatpickrLanguages from 'flatpickr/dist/l10n'
|
||||
import type { key } from 'flatpickr/dist/types/locale'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export function useFlatpickrLanguage() {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
return computed(() => {
|
||||
const userLanguage = authStore.settings.language
|
||||
if (!userLanguage) {
|
||||
return FlatpickrLanguages.en
|
||||
}
|
||||
|
||||
const langPair = userLanguage.split('-')
|
||||
const code = userLanguage === 'vi-VN' ? 'vn' : 'en'
|
||||
const language = FlatpickrLanguages?.[langPair?.[0] as key] || FlatpickrLanguages[code]
|
||||
language.firstDayOfWeek = authStore.settings.weekStart ?? language.firstDayOfWeek
|
||||
return language
|
||||
})
|
||||
}
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "نوع الاشتراك غير صالح.",
|
||||
"12002": "أنت مشترك بالفعل في العنصر نفسه أو في العنصر الرئيس.",
|
||||
"13001": "رابط المشاركة هذا يتطلب كلمة مرور للتوثيق، ولكن لم يتم توفير أي منها.",
|
||||
"13002": "كلمة مرور رابط المشاركة غير صالحة."
|
||||
"13002": "كلمة مرور رابط المشاركة غير صالحة.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "عن البرنامج",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "Типът на абонамента е невалиден.",
|
||||
"12002": "Вече сте абонирани за самия обект или за родителски обект.",
|
||||
"13001": "Тази споделена връзка изисква парола за удостоверяване, но такава не е предоставена.",
|
||||
"13002": "Предоставената парола за споделената връзка е невалидна."
|
||||
"13002": "Предоставената парола за споделената връзка е невалидна.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "За приложението",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "The provided link share password was invalid."
|
||||
"13002": "The provided link share password was invalid.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "Typ předplatného je neplatný.",
|
||||
"12002": "Již jste přihlášeni k odběru samotného subjektu nebo nadřazeného subjektu.",
|
||||
"13001": "Tento sdílený odkaz vyžaduje heslo k ověření, ale žádné nebylo poskytnuto.",
|
||||
"13002": "Zadané heslo pro sdílený odkaz bylo neplatné."
|
||||
"13002": "Zadané heslo pro sdílený odkaz bylo neplatné.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "O aplikaci",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "Enhedstypen for abonnementet er ugyldigt.",
|
||||
"12002": "Du er allerede abonneret på selve enheden eller en overordnet enhed.",
|
||||
"13001": "Dette delingslink kræver en adgangskode til godkendelse, men ingen blev angivet.",
|
||||
"13002": "Adgangskoden til delingslinket er ugyldig."
|
||||
"13002": "Adgangskoden til delingslinket er ugyldig.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "Om",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "Der Abonnement-Typ ist ungültig.",
|
||||
"12002": "Du bist bereits zu dieser oder einer übergeordneten Entität abonniert.",
|
||||
"13001": "Das Passwort für diese Linkfreigabe wurde nicht angegeben.",
|
||||
"13002": "Das Passwort für diese Linkfreigabe war ungültig."
|
||||
"13002": "Das Passwort für diese Linkfreigabe war ungültig.",
|
||||
"14001": "Die angegebene Datei ist keine gültige ZIP-Datei."
|
||||
},
|
||||
"about": {
|
||||
"title": "Über",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "De Abonnement Entitätstyp isch ungültig.",
|
||||
"12002": "Du bisch scho zu dere oder ere übergordnete Entität abonniert.",
|
||||
"13001": "De teilti Link bruucht es Passwort, aber du hesch keis aahgeh.",
|
||||
"13002": "Das Passwort wo du geh hesch isch ungültig."
|
||||
"13002": "Das Passwort wo du geh hesch isch ungültig.",
|
||||
"14001": "Die angegebene Datei ist keine gültige ZIP-Datei."
|
||||
},
|
||||
"about": {
|
||||
"title": "Über",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "The provided link share password was invalid."
|
||||
"13002": "The provided link share password was invalid.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "The provided link share password was invalid."
|
||||
"13002": "The provided link share password was invalid.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "El tipo de entidad de suscripción es inválido.",
|
||||
"12002": "Ya está suscrito a la entidad en sí misma o a una entidad padre.",
|
||||
"13001": "Este enlace compartido requiere una contraseña para la autenticación, pero no se ha proporcionado ninguna.",
|
||||
"13002": "La contraseña proporcionada del enlace compartido no es válida."
|
||||
"13002": "La contraseña proporcionada del enlace compartido no es válida.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "Acerca de",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "Le type d’entité d’abonnement est invalide.",
|
||||
"12002": "Vous êtes déjà abonné·e à l’entité elle-même ou à une entité parente.",
|
||||
"13001": "Le mot de passe requis n’a pas été fourni pour ce partage de lien.",
|
||||
"13002": "Mot de passe de partage de lien invalide."
|
||||
"13002": "Mot de passe de partage de lien invalide.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "À propos",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "The provided link share password was invalid."
|
||||
"13002": "The provided link share password was invalid.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "A feliratkozás típusa érvénytelen.",
|
||||
"12002": "Ön már feliratkozott magára az entitásra, vagy egy szülő entitásra.",
|
||||
"13001": "A linkmegosztáshoz jelszó szükséges a hitelesítéshez, de nem adták meg.",
|
||||
"13002": "A megadott linkmegosztási jelszó érvénytelen."
|
||||
"13002": "A megadott linkmegosztási jelszó érvénytelen.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "Névjegy",
|
||||
|
||||
+444
-443
File diff suppressed because it is too large
Load Diff
@@ -1203,7 +1203,8 @@
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "The provided link share password was invalid."
|
||||
"13002": "The provided link share password was invalid.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "Vikunjaについて",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "구독 엔터티 유형이 잘못되었습니다.",
|
||||
"12002": "이미 엔티티 자체 또는 상위 엔티티에 가입되어 있습니다.",
|
||||
"13001": "이 링크 공유에는 인증을 위한 비밀번호가 필요하지만 비밀번호가 제공되지 않았습니다.",
|
||||
"13002": "제공한 링크 공유 비밀번호가 잘못되었습니다."
|
||||
"13002": "제공한 링크 공유 비밀번호가 잘못되었습니다.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "소개",
|
||||
|
||||
+106
-105
@@ -18,71 +18,71 @@
|
||||
"accountWillBeDeleted": "Tavo paskyra bus ištrinta, įskaitant visus projektus, užduotis ir prisegtukus su tuo susijusius."
|
||||
},
|
||||
"404": {
|
||||
"title": "Not found",
|
||||
"text": "The page you requested does not exist."
|
||||
"title": "Nerasta",
|
||||
"text": "Norimas puslapis nerastas."
|
||||
},
|
||||
"ready": {
|
||||
"loading": "Vikunja is loading…",
|
||||
"errorOccured": "An error occurred:",
|
||||
"checkApiUrl": "Please check if the api url is correct.",
|
||||
"noApiUrlConfigured": "No API url was configured. Please set one below:"
|
||||
"loading": "Vikunja kraunasi…",
|
||||
"errorOccured": "Įvyko klaida:",
|
||||
"checkApiUrl": "Prašau patikrinti ar Api nuoroda yra teisinga.",
|
||||
"noApiUrlConfigured": "Jokia Api nuoroda nebuvo sukonfiguruota. Prašau, tai padaryti žemiau:"
|
||||
},
|
||||
"offline": {
|
||||
"title": "You are offline.",
|
||||
"text": "Please check your network connection and try again."
|
||||
"title": "Esi atsijungęs(-usi).",
|
||||
"text": "Prašau patikrinti interneto ryšį ir bandyti dar kartą."
|
||||
},
|
||||
"user": {
|
||||
"auth": {
|
||||
"username": "Vartotojo vardas",
|
||||
"usernameEmail": "Vartotojo vardas arba el. pašto adresas",
|
||||
"usernamePlaceholder": "pvz.: frederick",
|
||||
"email": "Email address",
|
||||
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "e.g. •••••••••••",
|
||||
"forgotPassword": "Forgot your password?",
|
||||
"resetPassword": "Reset your password",
|
||||
"resetPasswordAction": "Send me a password reset link",
|
||||
"resetPasswordSuccess": "Check your inbox! You should have an email with instructions on how to reset your password.",
|
||||
"passwordsDontMatch": "Passwords don't match",
|
||||
"confirmEmailSuccess": "You successfully confirmed your email! You can log in now.",
|
||||
"totpTitle": "Two Factor Authentication Code",
|
||||
"totpPlaceholder": "e.g. 123456",
|
||||
"login": "Login",
|
||||
"createAccount": "Create account",
|
||||
"loginWith": "Log in with {provider}",
|
||||
"authenticating": "Authenticating…",
|
||||
"openIdStateError": "State does not match, refusing to continue!",
|
||||
"openIdGeneralError": "An error occurred while authenticating against the third party.",
|
||||
"logout": "Logout",
|
||||
"emailInvalid": "Please enter a valid email address.",
|
||||
"email": "El. pašto adresas",
|
||||
"emailPlaceholder": "pvz.: frederic{'@'}vikunja.io",
|
||||
"password": "Slaptažodis",
|
||||
"passwordPlaceholder": "pvz.: •••••••••••",
|
||||
"forgotPassword": "Pamiršote slaptažodį?",
|
||||
"resetPassword": "Pakeisti slaptažodį",
|
||||
"resetPasswordAction": "Atsiųskite man slaptažodžio atnaujinimo nuorodą",
|
||||
"resetPasswordSuccess": "Patikrink savo pašto dėžutę! Turėjai gauti laišką, kaip atnaujinti slaptažodį.",
|
||||
"passwordsDontMatch": "Slaptažodžiai nesutampa",
|
||||
"confirmEmailSuccess": "Sėkmingai patvirtinai savo el. paštą. Dabar gali prisijungti.",
|
||||
"totpTitle": "Dviejų faktorių identifikavimo kodas",
|
||||
"totpPlaceholder": "pvz.: 123456",
|
||||
"login": "Prisijungti",
|
||||
"createAccount": "Sukurti paskyrą",
|
||||
"loginWith": "Prisijungti su {provider}",
|
||||
"authenticating": "Identifikuojama…",
|
||||
"openIdStateError": "Būsena neatitinka, atsisakome toliau tęsti!",
|
||||
"openIdGeneralError": "Įvyko klaida bandant identifikuoti su trečiąja šalimi.",
|
||||
"logout": "Atsijungti",
|
||||
"emailInvalid": "Prašome įvesti galiojantį el. pašto adresą.",
|
||||
"usernameRequired": "Prašau nurodyti vartotojo vardą.",
|
||||
"usernameMustNotContainSpace": "Vartotojo vardas negali turėti tarpų.",
|
||||
"usernameMustNotLookLikeUrl": "Vartotojo vardas negali atrodyti kaip nuoroda.",
|
||||
"passwordRequired": "Please provide a password.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordRequired": "Prašome pateikti savo slaptažodį.",
|
||||
"passwordNotMin": "Slaptažodis turi būti bent 8 simbolių.",
|
||||
"passwordNotMax": "Slaptažodis gali būti daugiausiai 72 simbolių.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
"alreadyHaveAnAccount": "Already have an account?",
|
||||
"remember": "Stay logged in",
|
||||
"registrationDisabled": "Registration is disabled."
|
||||
"showPassword": "Rodyti slaptažodį",
|
||||
"hidePassword": "Paslėpti slaptažodį",
|
||||
"noAccountYet": "Dar neturi paskyros?",
|
||||
"alreadyHaveAnAccount": "Jau turi paskyrą?",
|
||||
"remember": "Likti prisijungus",
|
||||
"registrationDisabled": "Registracija yra išjungta."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"newPasswordTitle": "Update Your Password",
|
||||
"newPassword": "New password",
|
||||
"newPasswordConfirm": "New password confirmation",
|
||||
"currentPassword": "Current password",
|
||||
"currentPasswordPlaceholder": "Your current password",
|
||||
"passwordsDontMatch": "The new password and its confirmation don't match.",
|
||||
"passwordUpdateSuccess": "The password was successfully updated.",
|
||||
"updateEmailTitle": "Update Your Email Address",
|
||||
"updateEmailNew": "New email address",
|
||||
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
|
||||
"title": "Nustatymai",
|
||||
"newPasswordTitle": "Atnaujinti savo slaptažodį",
|
||||
"newPassword": "Naujas slaptažodis",
|
||||
"newPasswordConfirm": "Naujo slaptažodžio patvirtinimas",
|
||||
"currentPassword": "Dabartinis slaptažodis",
|
||||
"currentPasswordPlaceholder": "Tavo dabartinis slaptažodis",
|
||||
"passwordsDontMatch": "Naujo slaptažodis ir patvirtinimas nesutampa.",
|
||||
"passwordUpdateSuccess": "Slaptažodis buvo sėkmingai atnaujintas.",
|
||||
"updateEmailTitle": "Atnaujink savo el. paštą",
|
||||
"updateEmailNew": "Naujas el. pašto adresas",
|
||||
"updateEmailSuccess": "El. paštas buvo sėkmingai atnaujintas. Nusiuntėme nuorodą patvirtinimui.",
|
||||
"general": {
|
||||
"title": "General Settings",
|
||||
"title": "Bendriniai nustatymai",
|
||||
"name": "Mano vardas",
|
||||
"newName": "Naujas vardas",
|
||||
"savedSuccess": "The settings were successfully updated.",
|
||||
@@ -290,7 +290,7 @@
|
||||
"remove": "Pašalinti pasidalinimo nuorodą",
|
||||
"removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!",
|
||||
"createSuccess": "Pasidalinimo nuoroda buvo sėkmingai sukurta.",
|
||||
"deleteSuccess": "The link share was successfully deleted",
|
||||
"deleteSuccess": "Nuorodos dalinimas buvo sėkmingai ištrintas",
|
||||
"view": "Peržiūra",
|
||||
"sharedBy": "Pasidalino {0}"
|
||||
},
|
||||
@@ -366,7 +366,7 @@
|
||||
"bucketTitleSavedSuccess": "The bucket title has been saved successfully.",
|
||||
"bucketLimitSavedSuccess": "The bucket limit been saved successfully.",
|
||||
"collapse": "Collapse this bucket",
|
||||
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
|
||||
"bucketLimitReached": "Pasiekėte buketo limitą. Ištrinkite užduotis arba padidinkite limitą norėdami pridėti naujas užduotis."
|
||||
},
|
||||
"pseudo": {
|
||||
"favorites": {
|
||||
@@ -463,11 +463,11 @@
|
||||
"labels": "Etiketės sietinos su užduotimi",
|
||||
"project": "The project the task belongs to (only available for saved filters, not on a project level)",
|
||||
"reminders": "The reminders of the task as a date field, will return all tasks with at least one reminder matching the query",
|
||||
"created": "The time and date when the task was created",
|
||||
"created": "Laikas ir data, kai užduotis buvo sukurta",
|
||||
"updated": "Data ir laikas, kada užduotis buvo pakeista"
|
||||
},
|
||||
"operators": {
|
||||
"intro": "The available operators for filtering include:",
|
||||
"intro": "Filtravimo operandai:",
|
||||
"notEqual": "Ne lygu",
|
||||
"equal": "Lygu",
|
||||
"greaterThan": "Daugiau už",
|
||||
@@ -506,7 +506,7 @@
|
||||
"inProgress": "Importavimas progrese…",
|
||||
"alreadyMigrated1": "Atrodo, kad jau importavai savo info is {name} {date}.",
|
||||
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
|
||||
"confirm": "I am sure, please start migrating now!",
|
||||
"confirm": "Aš įsitikinęs, prašau pradėti migraciją dabar!",
|
||||
"importUpload": "Importuoti duomenis iš {name} į Vikunja, spausk mygtuką žemiau, norėdamas pasirinkti failą.",
|
||||
"upload": "Įkelti failą",
|
||||
"migrationStartedWillReciveEmail": "Vikunja dabar importuos tavo sąrašus/projektus, užduotis, užrašus, priminimus ir dokumentus iš {service}. Kadangi tai užtruks, atsiųsime el. laišką, kai bus baigta. Gali uždaryti šį langą.",
|
||||
@@ -684,34 +684,34 @@
|
||||
"endOfToday": "Iki dienos pabaigos",
|
||||
"beginningOflastWeek": "Praeitos savaitės pradžios",
|
||||
"endOfLastWeek": "Praeitos savaitės pabaiga",
|
||||
"beginningOfThisWeek": "Beginning of this week",
|
||||
"beginningOfThisWeek": "Šios savaitės pradžia",
|
||||
"endOfThisWeek": "Šios savaaitės pabaaiga",
|
||||
"startOfNextWeek": "Start of next week",
|
||||
"startOfNextWeek": "Kitos savaitės pradžia",
|
||||
"endOfNextWeek": "Iki kitos savaitės pabaigos",
|
||||
"in7Days": "Už 7 dienų",
|
||||
"beginningOfLastMonth": "Beginning of last month",
|
||||
"beginningOfLastMonth": "Praeito mėnesio pradžia",
|
||||
"endOfLastMonth": "Kito mėnesio pabaaigos",
|
||||
"startOfThisMonth": "Start of this month",
|
||||
"startOfThisMonth": "Šio mėnesio pradžia",
|
||||
"endOfThisMonth": "End of this month",
|
||||
"startOfNextMonth": "Start of next month",
|
||||
"startOfNextMonth": "Kito mėnesio pradžia",
|
||||
"endOfNextMonth": "Kito mėnesio pabaiga",
|
||||
"in30Days": "In 30 days",
|
||||
"startOfThisYear": "Beginning of this year",
|
||||
"startOfThisYear": "Šių metų pradžia",
|
||||
"endOfThisYear": "Šių metų pabaiga"
|
||||
}
|
||||
},
|
||||
"datemathHelp": {
|
||||
"canuse": "You can use date math to filter for relative dates.",
|
||||
"learnhow": "Check out how it works",
|
||||
"title": "Date Math",
|
||||
"intro": "Specify relative dates which are resolved on the fly by Vikunja when applying the filter.",
|
||||
"canuse": "Galite naudoti datų matematiką norint filtruoti aktualias datas.",
|
||||
"learnhow": "Peržiūrėkite, kaip tai veikia",
|
||||
"title": "Datų matematika",
|
||||
"intro": "Nurodyti reliatyvias datas, kurios yra išpręstos eigoje Vikunjos kai pritaikomas filtras.",
|
||||
"expression": "Each Date Math expression starts with an anchor date, which can either be {0}, or a date string ending with {1}. This anchor date can optionally be followed by one or more maths expressions.",
|
||||
"similar": "These expressions are similar to the ones provided by {0} and {1}.",
|
||||
"add1Day": "Pridėti vieną dieną",
|
||||
"minus1Day": "Subtract one day",
|
||||
"roundDay": "Round down to the nearest day",
|
||||
"supportedUnits": "Supported time units",
|
||||
"someExamples": "Examples of time expressions",
|
||||
"minus1Day": "Atimti vieną dieną",
|
||||
"roundDay": "Suapvalinti iki artimiausios dienos",
|
||||
"supportedUnits": "Palaikomi laiko formatai",
|
||||
"someExamples": "Laiko išraiškų pavyzdžiai",
|
||||
"units": {
|
||||
"seconds": "Seconds",
|
||||
"minutes": "Minutes",
|
||||
@@ -725,10 +725,10 @@
|
||||
"now": "Dabar",
|
||||
"in24h": "24-ių valandų bėgyje",
|
||||
"today": "Šiandien 00:00",
|
||||
"beginningOfThisWeek": "The beginning of this week at 00:00",
|
||||
"endOfThisWeek": "The end of this week",
|
||||
"beginningOfThisWeek": "Šios savaitės pradžia 00:00 val.",
|
||||
"endOfThisWeek": "Šios savaitės pabaiga",
|
||||
"in30Days": "Už 30 dienų",
|
||||
"datePlusMonth": "{0} plus one month at 00:00 of that day"
|
||||
"datePlusMonth": "{0} plius veianas mėnuo nuo 00:00 tos dienos"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -736,14 +736,14 @@
|
||||
"task": "Užduotis",
|
||||
"new": "Sukurti užduotį",
|
||||
"delete": "Ištrinti šią užduotį",
|
||||
"createSuccess": "The task was successfully created.",
|
||||
"addReminder": "Add a reminder…",
|
||||
"doneSuccess": "The task was successfully marked as done.",
|
||||
"undoneSuccess": "The task was successfully un-marked as done.",
|
||||
"revertSuccess": "The task change was successfully undone.",
|
||||
"undo": "Undo",
|
||||
"createSuccess": "Užduotis buvo sėkmingai sukurta.",
|
||||
"addReminder": "Pridėti priminimą…",
|
||||
"doneSuccess": "Užduotis buvo sėkmingai pažymėta kaip baigta.",
|
||||
"undoneSuccess": "Užduotis buvo sėkmingai atžymėta kaip atlikta.",
|
||||
"revertSuccess": "Užduoties pakeitimai buvo sėkmingai pakeisti.",
|
||||
"undo": "Anuliuoti",
|
||||
"openDetail": "Atidaryti užduoties duomenų vaizdą",
|
||||
"checklistTotal": "{checked} of {total} tasks",
|
||||
"checklistTotal": "{checked} iš {total} užduočių",
|
||||
"checklistAllDone": "{total} užduotys",
|
||||
"show": {
|
||||
"titleCurrent": "Dabartinės Užduotys",
|
||||
@@ -752,17 +752,17 @@
|
||||
"overdue": "Rodyti neišpildytas užduotis",
|
||||
"fromuntil": "Užduotys nuo {from} iki {until}",
|
||||
"select": "Pasirinkti datos rėžį",
|
||||
"noTasks": "Nothing to do — Have a nice day!"
|
||||
"noTasks": "Nėra darbų - Gražios dienos!"
|
||||
},
|
||||
"detail": {
|
||||
"chooseDueDate": "Click here to set a due date",
|
||||
"chooseStartDate": "Click here to set a start date",
|
||||
"chooseEndDate": "Click here to set an end date",
|
||||
"move": "Move task to a different project",
|
||||
"done": "Mark task done!",
|
||||
"undone": "Mark as undone",
|
||||
"created": "Created {0} by {1}",
|
||||
"updated": "Updated {0}",
|
||||
"chooseDueDate": "Paspauskite čia norėdami pakeisti atlikimo datą",
|
||||
"chooseStartDate": "Paspauskite čia nustatyti pradžios datą",
|
||||
"chooseEndDate": "Spauskite čia norėdami nustatyti pabaigos datą",
|
||||
"move": "Perkelti užduotį į kitą projektą",
|
||||
"done": "Pažymėti užduotį kaip atliką!",
|
||||
"undone": "Pažymėti kaip neatliktą",
|
||||
"created": "Sukurta {0} {1}",
|
||||
"updated": "Atnaujinta {0}",
|
||||
"doneAt": "Done {0}",
|
||||
"updateSuccess": "The task was saved successfully.",
|
||||
"deleteSuccess": "The task has been deleted successfully.",
|
||||
@@ -900,23 +900,23 @@
|
||||
"new": "New Task Relation",
|
||||
"searchPlaceholder": "Type search for a task to add as related…",
|
||||
"createPlaceholder": "Pridėti kaip susijusią užduotį",
|
||||
"differentProject": "This task belongs to a different project.",
|
||||
"differentProject": "Ši užduotis priklauso kitam projektui.",
|
||||
"noneYet": "Kol kas užduoties ryšio nėra.",
|
||||
"delete": "Delete Task Relation",
|
||||
"deleteText1": "Are you sure you want to delete this task relation?",
|
||||
"select": "Pasirinkite ryšio tipą",
|
||||
"taskRequired": "Please select a task or enter a new task title.",
|
||||
"taskRequired": "Prašome pasirinkti užduotį arba įvesti užduoties pavadinimą.",
|
||||
"kinds": {
|
||||
"subtask": "Sub užduotis | Sub užduotys",
|
||||
"parenttask": "Parent Task | Parent Tasks",
|
||||
"related": "Related Task | Related Tasks",
|
||||
"parenttask": "Tėvinė Užduotis | Tėvinė Užduotis",
|
||||
"related": "Susijusi Užduotis | Susijusi Užduotis",
|
||||
"duplicateof": "Duplicate Of | Duplicates Of",
|
||||
"duplicates": "Duplicates | Duplicates",
|
||||
"blocking": "Blocking | Blocking",
|
||||
"blocked": "Blocked By | Blocked By",
|
||||
"precedes": "Precedes | Precedes",
|
||||
"follows": "Follows | Follows",
|
||||
"copiedfrom": "Copied From | Copied From",
|
||||
"copiedfrom": "Nukopijuota iš | Nukopijuota iš",
|
||||
"copiedto": "Copied To | Copied To"
|
||||
}
|
||||
},
|
||||
@@ -951,13 +951,13 @@
|
||||
"hint": "Use magic prefixes to define due dates, assignees and other task properties.",
|
||||
"title": "Quick Add Magic",
|
||||
"intro": "When creating a task, you can use special keywords to directly add attributes to the newly created task. This allows to add commonly used attributes to tasks much faster.",
|
||||
"multiple": "You can use this multiple times.",
|
||||
"multiple": "Galima tai naudoti kelis kartus.",
|
||||
"label1": "Norėdama -s pridėti etiketę, paprasčiausiai naudoti etiketės prefikso pavadinimą su {prefix}.",
|
||||
"label2": "Vikunja pirmiausiai patikrins ar etiketė egzistuoja, ir sukurs, jei neegzistuoja.",
|
||||
"label3": "Norint naudoti tarpus, paprasčiausiai pridėkite \" ar ' prieš etiketės pavadinimą.",
|
||||
"label4": "Pavyzdžiui: {prefix} \"Etiketė su tarpais\".",
|
||||
"priority1": "To set a task's priority, add a number 1-5, prefixed with a {prefix}.",
|
||||
"priority2": "The higher the number, the higher the priority.",
|
||||
"priority2": "Kuo didesnis skaičius, tuo aukštesnis prioritetas.",
|
||||
"assignees": "Norint tiesiogiai priskirti užduotį vartotojui, pridėkite vartotojo vardą kaip prefiksą su {prefix} prie užduoties.",
|
||||
"project1": "Nustatyti užduoties projektą, įveskite prefiksą su {prefix}.",
|
||||
"project2": "This will return an error if the project does not exist.",
|
||||
@@ -987,7 +987,7 @@
|
||||
"addUser": "Add to team",
|
||||
"makeMember": "Make Member",
|
||||
"makeAdmin": "Make Admin",
|
||||
"success": "The team was successfully updated.",
|
||||
"success": "Komanda buvo sėkmingai atnaujinta.",
|
||||
"userAddedSuccess": "The team member was successfully added.",
|
||||
"madeMember": "The team member was successfully made member.",
|
||||
"madeAdmin": "The team member was successfully made admin.",
|
||||
@@ -1007,7 +1007,7 @@
|
||||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will lose access to all projects this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"text2": "Prarasite visas teises, kurias turi ši komanda. Jei persigalvotum, reikės komandos administratoriaus pagalbos, kad pridėtų atgal.",
|
||||
"success": "Jūs sėkmingai palikote komandą."
|
||||
}
|
||||
},
|
||||
@@ -1027,10 +1027,10 @@
|
||||
"title": "Keyboard Shortcuts",
|
||||
"general": "General",
|
||||
"allPages": "These shortcuts work on all pages.",
|
||||
"currentPageOnly": "These shortcuts work only on the current page.",
|
||||
"somePagesOnly": "These shortcuts work only on some pages.",
|
||||
"currentPageOnly": "Esami trumpiniai veikia tik dabartiniame puslapyje.",
|
||||
"somePagesOnly": "Šie trumpiniai veikia tik kai kuriuose puslapiuose.",
|
||||
"toggleMenu": "Toggle The Menu",
|
||||
"quickSearch": "Open the search/quick action bar",
|
||||
"quickSearch": "Atidaryti paiešką/greitų veiksmų juostą",
|
||||
"then": "then",
|
||||
"task": {
|
||||
"title": "Task Page",
|
||||
@@ -1041,7 +1041,7 @@
|
||||
"attachment": "Add an attachment to this task",
|
||||
"related": "Modify related tasks of this task",
|
||||
"color": "Change the color of this task",
|
||||
"move": "Move this task to another project",
|
||||
"move": "Perkelti šią užduotį kitam projektui",
|
||||
"reminder": "Valdyti šios užduoties priminimus",
|
||||
"description": "Praskleisti redagavimą užduoties apraše",
|
||||
"delete": "Delete this task",
|
||||
@@ -1087,7 +1087,7 @@
|
||||
"url": "Vikunja URL",
|
||||
"urlPlaceholder": "eg. https://localhost:3456",
|
||||
"change": "change",
|
||||
"use": "Using Vikunja installation at {0}",
|
||||
"use": "Naudojama Vikunja instaliacija {0}",
|
||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||
"success": "Using Vikunja installation at \"{domain}\".",
|
||||
"urlRequired": "A url is required."
|
||||
@@ -1130,7 +1130,7 @@
|
||||
},
|
||||
"reaction": {
|
||||
"reactedWith": "{user} reacted with {value}",
|
||||
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
|
||||
"reactedWithAnd": "{users} ir {lastUser} sureagavo {value}",
|
||||
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
|
||||
"add": "Add your reaction"
|
||||
},
|
||||
@@ -1185,7 +1185,7 @@
|
||||
"6002": "The team does not exist.",
|
||||
"6004": "The team already has access to that project.",
|
||||
"6005": "The user is already a member of that team.",
|
||||
"6006": "Cannot delete the last team member.",
|
||||
"6006": "Negalime ištrinti paskutinio komandos nario.",
|
||||
"6007": "The team does not have access to the project to perform that action.",
|
||||
"7002": "Naudotojas jau turi prieigą prie šio projekto.",
|
||||
"7003": "You do not have access to that project.",
|
||||
@@ -1200,10 +1200,11 @@
|
||||
"10005": "There can be only one done bucket per project.",
|
||||
"11001": "Išsaugotas filtras neegzistuoja.",
|
||||
"11002": "Išsaugotu filtru negalima pasidalinti.",
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"12001": "Prenumeratos objekto tipas yra nevalidus.",
|
||||
"12002": "Esi prisiregistravęs prie visko ar prie tėvinio subjekto.",
|
||||
"13001": "Šis dalinimasis nuoroda reikalauja slaptažodžio identifikavimuisi, bet jis nebuvo pateiktas.",
|
||||
"13002": "Pateikta slaptažodžio dalinimosi nuoroda negalioja."
|
||||
"13002": "Pateikta slaptažodžio dalinimosi nuoroda negalioja.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "Apie",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "The provided link share password was invalid."
|
||||
"13002": "The provided link share password was invalid.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "Over",
|
||||
|
||||
+458
-457
File diff suppressed because it is too large
Load Diff
@@ -1203,7 +1203,8 @@
|
||||
"12001": "Typ subskrypcji jest nieprawidłowy.",
|
||||
"12002": "Subskrybujesz już tę jednostkę lub jej jednostkę nadrzędną.",
|
||||
"13001": "Ten udostępniony link wymaga hasła do uwierzytelnienia, ale nie został podany.",
|
||||
"13002": "Podane hasło udostępnionego linku było nieprawidłowe."
|
||||
"13002": "Podane hasło udostępnionego linku było nieprawidłowe.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "O aplikacji",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "A entidade de subscrição é inválida.",
|
||||
"12002": "Você já assinou a entidade em si ou uma entidade pai.",
|
||||
"13001": "Este link compartilhado requer uma senha para autenticação, mas nenhum foi fornecido.",
|
||||
"13002": "A senha de compartilhamento do link fornecido é inválida."
|
||||
"13002": "A senha de compartilhamento do link fornecido é inválida.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "Sobre",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "O tipo de entidade da subscrição é inválido.",
|
||||
"12002": "Já estás subcrito na entidade ou numa entidade parente.",
|
||||
"13001": "Este link partilhado requer uma palavra-passe para autenticação, mas nenhuma foi fornecida.",
|
||||
"13002": "A palavra-passe fornecida para o link partilhado é inválida."
|
||||
"13002": "A palavra-passe fornecida para o link partilhado é inválida.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "Sobre",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "The provided link share password was invalid."
|
||||
"13002": "The provided link share password was invalid.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "Некорректный тип подписки.",
|
||||
"12002": "Вы уже подписаны на сам объект или родительский объект.",
|
||||
"13001": "Эта ссылка требует пароля, но он не был предоставлен.",
|
||||
"13002": "Неверный пароль ссылки для обмена."
|
||||
"13002": "Неверный пароль ссылки для обмена.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "О Vikunja",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "The provided link share password was invalid."
|
||||
"13002": "The provided link share password was invalid.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
|
||||
@@ -366,7 +366,7 @@
|
||||
"bucketTitleSavedSuccess": "Naslov vedra je bil uspešno shranjen.",
|
||||
"bucketLimitSavedSuccess": "Omejitev vedra je bila uspešno shranjena.",
|
||||
"collapse": "Zloži vedro",
|
||||
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
|
||||
"bucketLimitReached": "Dosegli ste omejitev vedra. Odstranite opravila ali povečajte omejitev, da dodate nova opravila."
|
||||
},
|
||||
"pseudo": {
|
||||
"favorites": {
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "Tipa entitete naročnine je neveljaven.",
|
||||
"12002": "Ste že naročeni na samo entiteto ali nadrejeno entiteto.",
|
||||
"13001": "Ta skupna raba povezave zahteva geslo za preverjanje pristnosti, vendar ni bil podan.",
|
||||
"13002": "Navedeno geslo za skupno rabo povezave je bilo neveljavno."
|
||||
"13002": "Navedeno geslo za skupno rabo povezave je bilo neveljavno.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "O programu",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "The provided link share password was invalid."
|
||||
"13002": "The provided link share password was invalid.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "The provided link share password was invalid."
|
||||
"13002": "The provided link share password was invalid.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "Om",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "The provided link share password was invalid."
|
||||
"13002": "The provided link share password was invalid.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "Ви підписані на саму суть або головну частину.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "Хибний пароль для поширеного посилання."
|
||||
"13002": "Хибний пароль для поширеного посилання.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "Про",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "Loại đăng ký không hợp lệ.",
|
||||
"12002": "Bạn đã đăng ký với tổ chức này hoặc tổ chức chính rồi.",
|
||||
"13001": "Chia sẻ liên kết này yêu cầu mật khẩu để xác thực, nhưng không có mật khẩu nào được cung cấp.",
|
||||
"13002": "Mật khẩu chia sẻ liên kết không hợp lệ."
|
||||
"13002": "Mật khẩu chia sẻ liên kết không hợp lệ.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "Về ứng dụng",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "订阅实体类型无效。",
|
||||
"12002": "你已经订阅实体本身或上级实体。",
|
||||
"13001": "此链接共享需要密码进行身份验证,但没有提供密码。",
|
||||
"13002": "提供的链接共享密码无效。"
|
||||
"13002": "提供的链接共享密码无效。",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "关于",
|
||||
|
||||
@@ -1203,7 +1203,8 @@
|
||||
"12001": "The subscription entity type is invalid.",
|
||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
||||
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||
"13002": "The provided link share password was invalid."
|
||||
"13002": "The provided link share password was invalid.",
|
||||
"14001": "The provided file is not a valid zip file."
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
|
||||
export const LabelTaskSchema = AbstractSchema.extend({
|
||||
id: IdSchema.nullable(),
|
||||
taskId: IdSchema.nullable(),
|
||||
labelId: IdSchema.nullable(),
|
||||
})
|
||||
|
||||
export type LabelTask = TypeOf<typeof LabelTaskSchema>
|
||||
@@ -0,0 +1,10 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {object, nativeEnum} from 'zod'
|
||||
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
|
||||
export const AbstractSchema = object({
|
||||
maxRight: nativeEnum(RIGHTS).nullable(),
|
||||
})
|
||||
|
||||
export type IAbstract = TypeOf<typeof AbstractSchema>
|
||||
@@ -0,0 +1,19 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {UserSchema} from './user'
|
||||
import {FileSchema} from './file'
|
||||
|
||||
export const AttachmentSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
|
||||
taskId: IdSchema.default(0), // iTaskSchema.shape.id
|
||||
createdBy: UserSchema,
|
||||
file: FileSchema,
|
||||
created: DateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type IAttachment = TypeOf<typeof AttachmentSchema>
|
||||
@@ -0,0 +1,18 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {z, string, object} from 'zod'
|
||||
|
||||
export const AVATAR_PROVIDER = [
|
||||
'default',
|
||||
'initials',
|
||||
'gravatar',
|
||||
'marble',
|
||||
'upload',
|
||||
] as const
|
||||
export const AvatarProviderSchema = z.enum(AVATAR_PROVIDER)
|
||||
export type IAvatarProvider = TypeOf<typeof AvatarProviderSchema>
|
||||
|
||||
export const AvatarSchema = object({
|
||||
// FIXME: shouldn't the default be 'default'?
|
||||
avatarProvider: string().or(AvatarProviderSchema).default(''),
|
||||
})
|
||||
export type IAvatar = TypeOf<typeof AvatarSchema>
|
||||
@@ -0,0 +1,19 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {object, record, string, unknown} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
|
||||
export const BackgroundImageSchema = object({
|
||||
id: IdSchema.default(0),
|
||||
url: string().url().default(''),
|
||||
thumb: string().default(''),
|
||||
// FIXME: not sure if this needs to defined, since it seems provider specific
|
||||
// {
|
||||
// author: string(),
|
||||
// authorName: string(),
|
||||
// }
|
||||
info: record(unknown()).default({}),
|
||||
blurHash: string().default(''),
|
||||
})
|
||||
|
||||
export type BackgroundImage = TypeOf<typeof BackgroundImageSchema>
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {number, array, boolean} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
import {TextFieldSchema} from './common/textField'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {UserSchema} from './user'
|
||||
import {TaskSchema} from './task'
|
||||
|
||||
export const BucketSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
title: TextFieldSchema,
|
||||
listId: IdSchema.default(0),
|
||||
limit: number().default(0),
|
||||
tasks: array(TaskSchema).default([]),
|
||||
isDoneBucket: boolean().default(false),
|
||||
position: number().default(0),
|
||||
|
||||
createdBy: UserSchema.nullable(),
|
||||
created: DateSchema.nullable(),
|
||||
updated: DateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type IBucket = TypeOf<typeof BucketSchema>
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
import type {TypeOf} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
|
||||
export const CaldavTokenSchema = AbstractSchema.extend({
|
||||
id: IdSchema,
|
||||
created: DateSchema,
|
||||
})
|
||||
|
||||
export type CaldavToken = TypeOf<typeof CaldavTokenSchema>
|
||||
@@ -0,0 +1,22 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {nativeEnum} from 'zod'
|
||||
|
||||
export const RELATION_KIND = {
|
||||
'SUBTASK': 'subtask',
|
||||
'PARENTTASK': 'parenttask',
|
||||
'RELATED': 'related',
|
||||
'DUPLICATES': 'duplicates',
|
||||
'BLOCKING': 'blocking',
|
||||
'BLOCKED': 'blocked',
|
||||
'PROCEDES': 'precedes',
|
||||
'FOLLOWS': 'follows',
|
||||
'COPIEDFROM': 'copiedfrom',
|
||||
'COPIEDTO': 'copiedto',
|
||||
} as const
|
||||
|
||||
export const RELATION_KINDS = [...Object.values(RELATION_KIND)] as const
|
||||
|
||||
export const RelationKindSchema = nativeEnum(RELATION_KIND)
|
||||
|
||||
export type IRelationKind = TypeOf<typeof RelationKindSchema>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import {preprocess, date} from 'zod'
|
||||
|
||||
export const DateSchema = preprocess((arg) => {
|
||||
if (
|
||||
// FIXME: Add comment why we check for `0001`
|
||||
typeof arg == 'string' && !arg.startsWith('0001') ||
|
||||
arg instanceof Date
|
||||
) {
|
||||
return new Date(arg)
|
||||
}
|
||||
}, date())
|
||||
@@ -0,0 +1,80 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {z, nativeEnum, array, boolean, object, number} from 'zod'
|
||||
|
||||
export enum SORT_BY {
|
||||
ID = 'id',
|
||||
DONE = 'done',
|
||||
TITLE = 'title',
|
||||
PRIORITY = 'priority',
|
||||
DONE_AT = 'done_at',
|
||||
DUE_DATE = 'due_date',
|
||||
START_DATE = 'start_date',
|
||||
END_DATE = 'end_date',
|
||||
PERCENT_DONE = 'percent_done',
|
||||
CREATED = 'created',
|
||||
UPDATED = 'updated',
|
||||
POSITION = 'position',
|
||||
KANBAN_POSITION = 'kanban_position',
|
||||
}
|
||||
|
||||
export enum ORDER_BY {
|
||||
ASC = 'asc',
|
||||
DESC = 'desc',
|
||||
NONE = 'none',
|
||||
}
|
||||
|
||||
export enum FILTER_BY {
|
||||
DONE = 'done',
|
||||
DUE_DATE = 'due_date',
|
||||
START_DATE = 'start_date',
|
||||
END_DATE = 'end_date',
|
||||
NAMESPACE = 'namespace',
|
||||
ASSIGNEES = 'assignees',
|
||||
LIST_ID = 'list_id',
|
||||
BUCKET_ID = 'bucket_id',
|
||||
PRIORITY = 'priority',
|
||||
PERCENT_DONE = 'percent_done',
|
||||
LABELS = 'labels',
|
||||
UNDEFINED = 'undefined', // FIXME: Why do we have a value that is undefined as string?
|
||||
}
|
||||
|
||||
export enum FILTER_COMPARATOR {
|
||||
EQUALS = 'equals',
|
||||
LESS = 'less',
|
||||
GREATER = 'greater',
|
||||
GREATER_EQUALS = 'greater_equals',
|
||||
LESS_EQUALS = 'less_equals',
|
||||
IN = 'in',
|
||||
}
|
||||
|
||||
export enum FILTER_CONCAT {
|
||||
AND = 'and',
|
||||
OR = 'or',
|
||||
IN = 'in',
|
||||
}
|
||||
|
||||
const TASKS_PER_BUCKET = 25
|
||||
|
||||
export const FilterSchema = object({
|
||||
sortBy: array(nativeEnum(SORT_BY)).default([SORT_BY.DONE, SORT_BY.ID]), // FIXME: create from taskSchema,
|
||||
// fixme default order seem so also be `desc`
|
||||
// see line from ListTable:
|
||||
// if (typeof order === 'undefined' || order === 'none') {
|
||||
orderBy: array(nativeEnum(ORDER_BY)).default([ORDER_BY.ASC, ORDER_BY.DESC]),
|
||||
// FIXME: create from taskSchema
|
||||
filterBy: array(nativeEnum(FILTER_BY)).default([FILTER_BY.DONE]),
|
||||
// FIXME: create from taskSchema
|
||||
// FIXME: might need to preprocess values, e.g. date.
|
||||
// see line from 'filters.vue':
|
||||
// params.filter_value = params.filter_value.map(v => v instanceof Date ? v.toISOString() : v)
|
||||
filterValue: array(z.enum(['false'])).default(['false']),
|
||||
// FIXME: is `in` value correct?
|
||||
// found in `quick-actions.vue`:
|
||||
// params.filter_comparator.push('in')
|
||||
filterComparator: array(nativeEnum(FILTER_COMPARATOR)).default([FILTER_COMPARATOR.EQUALS]),
|
||||
filterConcat: z.nativeEnum(FILTER_CONCAT).default(FILTER_CONCAT.AND),
|
||||
filterIncludeNulls: boolean().default(true),
|
||||
perPage: number().default(TASKS_PER_BUCKET), // FIXME: is perPage is just available for the bucket endpoint?
|
||||
})
|
||||
|
||||
export type IFilter = TypeOf<typeof FilterSchema>
|
||||
@@ -0,0 +1,10 @@
|
||||
import {string} from 'zod'
|
||||
import isHexColor from 'validator/lib/isHexColor'
|
||||
|
||||
export const HexColorSchema = string().transform(
|
||||
(value) => {
|
||||
if (!value || value.startsWith('#')) {
|
||||
return value
|
||||
}
|
||||
return '#' + value
|
||||
}).refine(value => isHexColor(value))
|
||||
@@ -0,0 +1,6 @@
|
||||
import {number, preprocess} from 'zod'
|
||||
|
||||
export const IdSchema = preprocess(
|
||||
(value: unknown) => Number(value),
|
||||
number().positive().int(),
|
||||
)
|
||||
@@ -1,24 +1,26 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {nativeEnum, number, object, preprocess} from 'zod'
|
||||
|
||||
import {SECONDS_A_HOUR} from '@/constants/date'
|
||||
import { REPEAT_TYPES, type IRepeatAfter } from '@/types/IRepeatAfter'
|
||||
import { nativeEnum, number, object, preprocess } from 'zod'
|
||||
import {REPEAT_TYPES, type IRepeatAfter} from '@/types/IRepeatAfter'
|
||||
|
||||
/**
|
||||
* Parses `repeatAfterSeconds` into a usable js object.
|
||||
*/
|
||||
export function parseRepeatAfter(repeatAfterSeconds: number): IRepeatAfter {
|
||||
let repeatAfter: IRepeatAfter = {type: 'hours', amount: repeatAfterSeconds / SECONDS_A_HOUR}
|
||||
let repeatAfter: IRepeatAfter
|
||||
|
||||
// if its dividable by 24, its something with days, otherwise hours
|
||||
// if its dividable by SECONDS_A_DAY, its something with days, otherwise hours
|
||||
if (repeatAfterSeconds % SECONDS_A_DAY === 0) {
|
||||
if (repeatAfterSeconds % SECONDS_A_WEEK === 0) {
|
||||
repeatAfter = {type: 'weeks', amount: repeatAfterSeconds / SECONDS_A_WEEK}
|
||||
} else if (repeatAfterSeconds % SECONDS_A_MONTH === 0) {
|
||||
repeatAfter = {type:'months', amount: repeatAfterSeconds / SECONDS_A_MONTH}
|
||||
} else if (repeatAfterSeconds % SECONDS_A_YEAR === 0) {
|
||||
repeatAfter = {type: 'years', amount: repeatAfterSeconds / SECONDS_A_YEAR}
|
||||
} else {
|
||||
repeatAfter = {type: 'days', amount: repeatAfterSeconds / SECONDS_A_DAY}
|
||||
}
|
||||
repeatAfter = {type: REPEAT_TYPES.HOURS, amount: repeatAfterSeconds / SECONDS_A_HOUR}
|
||||
} else if (repeatAfterSeconds % SECONDS_A_WEEK === 0) {
|
||||
repeatAfter = {type: REPEAT_TYPES.WEEKS, amount: repeatAfterSeconds / SECONDS_A_WEEK}
|
||||
} else if (repeatAfterSeconds % SECONDS_A_MONTH === 0) {
|
||||
repeatAfter = {type: REPEAT_TYPES.MONTHS, amount: repeatAfterSeconds / SECONDS_A_MONTH}
|
||||
} else if (repeatAfterSeconds % SECONDS_A_YEAR === 0) {
|
||||
repeatAfter = {type: REPEAT_TYPES.YEARS, amount: repeatAfterSeconds / SECONDS_A_YEAR}
|
||||
} else {
|
||||
repeatAfter = {type: REPEAT_TYPES.DAYS, amount: repeatAfterSeconds / SECONDS_A_DAY}
|
||||
}
|
||||
return repeatAfter
|
||||
}
|
||||
@@ -37,4 +39,6 @@ export const RepeatsSchema = preprocess(
|
||||
type: nativeEnum(REPEAT_TYPES),
|
||||
amount: number().int(),
|
||||
}),
|
||||
)
|
||||
)
|
||||
|
||||
export type RepeatAfter = TypeOf<typeof RepeatsSchema>
|
||||
@@ -0,0 +1,3 @@
|
||||
import {string} from 'zod'
|
||||
|
||||
export const TextFieldSchema = string().transform((value) => value.trim()).default('')
|
||||
@@ -0,0 +1,11 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {string} from 'zod'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
|
||||
export const EmailUpdateSchema = AbstractSchema.extend({
|
||||
newEmail: string().email().default(''),
|
||||
password: string().default(''),
|
||||
})
|
||||
|
||||
export type EmailUpdate = TypeOf<typeof EmailUpdateSchema>
|
||||
@@ -0,0 +1,15 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {object, number, string} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
|
||||
export const FileSchema = object({
|
||||
id: IdSchema.default(0),
|
||||
mime: string().default(''),
|
||||
name: string().default(''),
|
||||
size: number().default(0),
|
||||
created: DateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type File = TypeOf<typeof FileSchema>
|
||||
@@ -0,0 +1,33 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {string} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
import {HexColorSchema} from './common/hexColor'
|
||||
|
||||
import {UserSchema} from './user'
|
||||
import {AbstractSchema} from './abstract'
|
||||
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
|
||||
const DEFAULT_LABEL_BACKGROUND_COLOR = 'e8e8e8'
|
||||
|
||||
export const LabelSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
title: string().default(''),
|
||||
hexColor: HexColorSchema.default(DEFAULT_LABEL_BACKGROUND_COLOR),
|
||||
textColor: string(), // implicit
|
||||
description: string().default(''),
|
||||
createdBy: UserSchema, // FIXME: default: current user?
|
||||
listId: IdSchema.default(0),
|
||||
|
||||
created: DateSchema.nullable(),
|
||||
updated: DateSchema.nullable(),
|
||||
}).transform((obj) => {
|
||||
// FIXME: remove textColor location => should be defined in UI
|
||||
obj.textColor = colorIsDark(obj.hexColor) ? '#4a4a4a' : '#ffffff'
|
||||
return obj
|
||||
},
|
||||
)
|
||||
|
||||
export type ILabel = TypeOf<typeof LabelSchema>
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {number, string, nativeEnum} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {UserSchema} from './user'
|
||||
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
|
||||
export const LinkShareSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
hash: string().default(''),
|
||||
right: nativeEnum(RIGHTS).default(RIGHTS.READ),
|
||||
sharedBy: UserSchema,
|
||||
sharingType: number().default(0), // FIXME: use correct numbers
|
||||
listId: IdSchema.default(0),
|
||||
name: string().default(''),
|
||||
password: string().default(''),
|
||||
|
||||
created: DateSchema.nullable(),
|
||||
updated: DateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type LinkShare = TypeOf<typeof LinkShareSchema>
|
||||
@@ -0,0 +1,32 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {boolean, number, string, array, any} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {SubscriptionSchema} from './subscription'
|
||||
import {TaskSchema} from './task'
|
||||
import {UserSchema} from './user'
|
||||
|
||||
export const ListSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
hash: string().default(''),
|
||||
description: string().default(''),
|
||||
owner: UserSchema,
|
||||
tasks: array(TaskSchema),
|
||||
namespaceId: IdSchema.default(0), // INamespace['id'],
|
||||
isArchived: boolean().default(false),
|
||||
hexColor: string().default(''),
|
||||
identifier: string().default(''),
|
||||
backgroundInformation: any().nullable().default(null), // FIXME: what is this for?
|
||||
isFavorite: boolean().default(false),
|
||||
subscription: SubscriptionSchema.nullable(),
|
||||
position: number().default(0),
|
||||
backgroundBlurHash: string().default(''),
|
||||
|
||||
created: DateSchema.nullable(),
|
||||
updated: DateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type List = TypeOf<typeof ListSchema>
|
||||
@@ -0,0 +1,14 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {ListSchema} from './list'
|
||||
|
||||
export const ListDuplicationSchema = AbstractSchema.extend({
|
||||
listId: IdSchema.default(0),
|
||||
namespaceId: IdSchema.default(0), // INamespace['id'],
|
||||
list: ListSchema,
|
||||
})
|
||||
|
||||
export type ListDuplication = TypeOf<typeof ListDuplicationSchema>
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {boolean, string, array} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {HexColorSchema} from './common/hexColor'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {ListSchema} from './list'
|
||||
import {UserSchema} from './user'
|
||||
import {SubscriptionSchema} from './subscription'
|
||||
|
||||
export const NamespaceSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
title: string().default(''),
|
||||
description: string().default(''),
|
||||
owner: UserSchema,
|
||||
lists: array(ListSchema),
|
||||
isArchived: boolean().default(false),
|
||||
hexColor: HexColorSchema.default(''),
|
||||
subscription: SubscriptionSchema.nullable(),
|
||||
|
||||
created: IdSchema.nullable(),
|
||||
updated: IdSchema.nullable(),
|
||||
})
|
||||
|
||||
export type Namespace = TypeOf<typeof NamespaceSchema>
|
||||
@@ -0,0 +1,54 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {union, boolean, object, string} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {TaskSchema} from './task'
|
||||
import {TaskCommentSchema} from './taskComment'
|
||||
import {TeamSchema} from './team'
|
||||
import {UserSchema} from './user'
|
||||
|
||||
const NotificationTypeSchema = object({
|
||||
doer: UserSchema,
|
||||
})
|
||||
|
||||
const NotificationTypeTask = NotificationTypeSchema.extend({
|
||||
task: TaskSchema,
|
||||
comment: TaskCommentSchema,
|
||||
})
|
||||
|
||||
const NotificationTypeAssigned = NotificationTypeSchema.extend({
|
||||
task: TaskSchema,
|
||||
assignee: UserSchema,
|
||||
})
|
||||
|
||||
const NotificationTypeDeleted = NotificationTypeSchema.extend({
|
||||
task: TaskSchema,
|
||||
})
|
||||
|
||||
const NotificationTypeCreated = NotificationTypeSchema.extend({
|
||||
task: TaskSchema,
|
||||
})
|
||||
|
||||
const NotificationTypeMemberAdded = NotificationTypeSchema.extend({
|
||||
member: UserSchema,
|
||||
team: TeamSchema,
|
||||
})
|
||||
|
||||
export const NotificationSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
name: string().default(''),
|
||||
notification: union([
|
||||
NotificationTypeTask,
|
||||
NotificationTypeAssigned,
|
||||
NotificationTypeDeleted,
|
||||
NotificationTypeCreated,
|
||||
NotificationTypeMemberAdded,
|
||||
]),
|
||||
read: boolean().default(false),
|
||||
readAt: DateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type Notification = TypeOf<typeof NotificationSchema>
|
||||
@@ -0,0 +1,13 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {string} from 'zod'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
|
||||
// FIXME: is it correct that this extends the Abstract Schema?
|
||||
export const PasswordResetSchema = AbstractSchema.extend({
|
||||
token: string().default(''),
|
||||
newPassword: string().default(''),
|
||||
email: string().email().default(''),
|
||||
})
|
||||
|
||||
export type PasswordReset = TypeOf<typeof PasswordResetSchema>
|
||||
@@ -0,0 +1,15 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {string} from 'zod'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
|
||||
// FIXME: is it correct that this extends the Abstract Schema?
|
||||
export const PasswordUpdateSchema = AbstractSchema.extend({
|
||||
newPassword: string().default(''),
|
||||
oldPassword: string().default(''),
|
||||
}).refine((data) => data.newPassword === data.oldPassword, {
|
||||
message: 'Passwords don\'t match',
|
||||
path: ['confirm'], // path of error
|
||||
})
|
||||
|
||||
export type PasswordUpdate = TypeOf<typeof PasswordUpdateSchema>
|
||||
@@ -0,0 +1,23 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {string} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
import {FilterSchema} from './common/filter'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {UserSchema} from './user'
|
||||
|
||||
// FIXME: is it correct that this extends the Abstract Schema?
|
||||
export const SavedFilterSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
title: string().default(''),
|
||||
description: string().default(''),
|
||||
filters: FilterSchema,
|
||||
|
||||
owner: UserSchema,
|
||||
created: DateSchema.nullable(),
|
||||
updated: DateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type SavedFilter = TypeOf<typeof SavedFilterSchema>
|
||||
@@ -0,0 +1,19 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {string} from 'zod'
|
||||
|
||||
import {DateSchema} from './common/date'
|
||||
import {IdSchema} from './common/id'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {UserSchema} from './user'
|
||||
|
||||
export const SubscriptionSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
entity: string().default(''), // FIXME: correct type?
|
||||
entityId: IdSchema.default(0), // FIXME: correct type?
|
||||
user: UserSchema,
|
||||
|
||||
created: DateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type Subscription = TypeOf<typeof SubscriptionSchema>
|
||||
@@ -0,0 +1,119 @@
|
||||
import type {ZodType, TypeOf} from 'zod'
|
||||
import {nativeEnum, boolean, number, string, array, record, unknown, lazy} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
import {HexColorSchema} from './common/hexColor'
|
||||
import {TextFieldSchema} from './common/textField'
|
||||
import {RelationKindSchema} from './common/RelationKind'
|
||||
import {RepeatsSchema} from './common/repeats'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {AttachmentSchema} from './attachment'
|
||||
import {LabelSchema} from './label'
|
||||
import {SubscriptionSchema} from './subscription'
|
||||
import {UserSchema} from './user'
|
||||
|
||||
import {PRIORITIES} from '@/constants/priorities'
|
||||
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
|
||||
|
||||
const LabelsSchema = array(LabelSchema)
|
||||
.transform((labels) => labels.sort((f, s) => f.title > s.title ? 1 : -1)) // FIXME: use
|
||||
.default([])
|
||||
|
||||
export type ILabels = TypeOf<typeof LabelsSchema>
|
||||
|
||||
const RelatedTasksSchema = record(RelationKindSchema, record(string(), unknown()))
|
||||
export type IRelatedTasksSchema = TypeOf<typeof RelatedTasksSchema>
|
||||
|
||||
const RelatedTasksLazySchema : ZodType<Task['relatedTasks']> = lazy(() =>
|
||||
record(RelationKindSchema, TaskSchema),
|
||||
)
|
||||
export type IRelatedTasksLazySchema = TypeOf<typeof RelatedTasksLazySchema>
|
||||
|
||||
// export interface ITask extends IAbstract {
|
||||
// id: number
|
||||
// title: string
|
||||
// description: string
|
||||
// done: boolean
|
||||
// doneAt: Date | null
|
||||
// priority: Priority
|
||||
// labels: ILabel[]
|
||||
// assignees: IUser[]
|
||||
|
||||
// dueDate: Date | null
|
||||
// startDate: Date | null
|
||||
// endDate: Date | null
|
||||
// repeatAfter: number | IRepeatAfter
|
||||
// repeatFromCurrentDate: boolean
|
||||
// repeatMode: IRepeatMode
|
||||
// reminderDates: Date[]
|
||||
// parentTaskId: ITask['id']
|
||||
// hexColor: string
|
||||
// percentDone: number
|
||||
// relatedTasks: Partial<Record<IRelationKind, ITask>>,
|
||||
// attachments: IAttachment[]
|
||||
// identifier: string
|
||||
// index: number
|
||||
// isFavorite: boolean
|
||||
// subscription: ISubscription
|
||||
|
||||
// position: number
|
||||
// kanbanPosition: number
|
||||
|
||||
// createdBy: IUser
|
||||
// created: Date
|
||||
// updated: Date
|
||||
|
||||
// listId: IList['id'] // Meta, only used when creating a new task
|
||||
// bucketId: IBucket['id']
|
||||
// }
|
||||
|
||||
export const TaskSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
title: TextFieldSchema,
|
||||
description: TextFieldSchema,
|
||||
done: boolean().default(false),
|
||||
doneAt: DateSchema.nullable().default(null),
|
||||
priority: nativeEnum(PRIORITIES).default(PRIORITIES.UNSET),
|
||||
labels: LabelsSchema,
|
||||
assignees: array(UserSchema).default([]),
|
||||
|
||||
dueDate: DateSchema.nullable(), // FIXME: default value is `0`. Shouldn't this be `null`?
|
||||
startDate: DateSchema.nullable(), // FIXME: default value is `0`. Shouldn't this be `null`?
|
||||
endDate: DateSchema.nullable(), // FIXME: default value is `0`. Shouldn't this be `null`?
|
||||
repeatAfter: RepeatsSchema, // FIXME: default value is `0`. Shouldn't this be `null`?
|
||||
repeatFromCurrentDate: boolean().default(false),
|
||||
repeatMode: nativeEnum(TASK_REPEAT_MODES).default(TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT),
|
||||
|
||||
// TODO: schedule notifications
|
||||
// FIXME: triggered notificaitons not supported anymore / remove feature?
|
||||
reminderDates: array(DateSchema).default([]),
|
||||
parentTaskId: IdSchema.default(0), // shouldn't this have `null` as default?
|
||||
hexColor: HexColorSchema.default(''),
|
||||
percentDone: number().default(0),
|
||||
relatedTasks: RelatedTasksSchema.default({}),
|
||||
attachments: array(AttachmentSchema).default([]),
|
||||
identifier: string().default(''),
|
||||
index: number().default(0),
|
||||
isFavorite: boolean().default(false),
|
||||
subscription: SubscriptionSchema.nullable().default(null),
|
||||
|
||||
position: number().default(0),
|
||||
kanbanPosition: number().default(0),
|
||||
|
||||
createdBy: UserSchema,
|
||||
created: DateSchema.nullable(),
|
||||
updated: DateSchema.nullable(),
|
||||
|
||||
listId: IdSchema.default(0), //IList['id'], // Meta, only used when creating a new task
|
||||
bucketId: IdSchema.default(0), // IBucket['id'],
|
||||
}).transform((obj) => {
|
||||
if (obj.identifier === `-${obj.index}`) {
|
||||
obj.identifier = ''
|
||||
}
|
||||
return obj
|
||||
})
|
||||
|
||||
|
||||
export type Task = TypeOf<typeof TaskSchema>
|
||||
@@ -0,0 +1,14 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
|
||||
export const TaskAssigneeSchema = AbstractSchema.extend({
|
||||
created: DateSchema.nullable(),
|
||||
userId: IdSchema.default(0), // IUser['id']
|
||||
taskId: IdSchema.default(0), // ITask['id']
|
||||
})
|
||||
|
||||
export type TaskAssignee = TypeOf<typeof TaskAssigneeSchema>
|
||||
@@ -0,0 +1,20 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {string} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {UserSchema} from './user'
|
||||
|
||||
export const TaskCommentSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
taskId: IdSchema.default(0),
|
||||
comment: string().default(''),
|
||||
author: UserSchema,
|
||||
|
||||
created: DateSchema.nullable(),
|
||||
updated: DateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type TaskComment = TypeOf<typeof TaskCommentSchema>
|
||||
@@ -0,0 +1,24 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {nativeEnum} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {UserSchema} from './user'
|
||||
|
||||
import {RELATION_KIND} from '@/types/IRelationKind'
|
||||
|
||||
export const TaskRelationSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
otherTaskId: IdSchema.default(0),
|
||||
taskId: IdSchema.default(0),
|
||||
relationKind: nativeEnum(RELATION_KIND).nullable().default(null), // FIXME: default value was empty string?
|
||||
|
||||
createdBy: UserSchema,
|
||||
// FIXME: shouldn't the empty value of dates be `new Date()`
|
||||
// Because e.g. : `new Date(null)` => Thu Jan 01 1970 01:00:00 GMT+0100 (Central European Standard Time)
|
||||
created: DateSchema.nullable().default(null),
|
||||
})
|
||||
|
||||
export type ITaskRelation = TypeOf<typeof TaskRelationSchema>
|
||||
@@ -0,0 +1,25 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {array, nativeEnum, string} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {UserSchema} from './user'
|
||||
import {TeamMemberSchema} from './teamMember'
|
||||
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
|
||||
export const TeamSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
name: string().default(''),
|
||||
description: string().default(''),
|
||||
members: array(TeamMemberSchema),
|
||||
right: nativeEnum(RIGHTS).default(RIGHTS.READ),
|
||||
|
||||
createdBy: UserSchema, // FIXME: default was {},
|
||||
created: DateSchema.nullable(),
|
||||
updated: DateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type Team = TypeOf<typeof TeamSchema>
|
||||
@@ -0,0 +1,11 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
|
||||
import {TeamShareBaseSchema} from './teamShareBase'
|
||||
|
||||
export const TeamListSchema = TeamShareBaseSchema.extend({
|
||||
listId: IdSchema.default(0), // IList['id']
|
||||
})
|
||||
|
||||
export type TeamList = TypeOf<typeof TeamListSchema>
|
||||
@@ -0,0 +1,13 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {boolean} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
|
||||
import {UserSchema} from './user'
|
||||
|
||||
export const TeamMemberSchema = UserSchema.extend({
|
||||
admin: boolean().default(false),
|
||||
teamId: IdSchema.default(0), // IList['id']
|
||||
})
|
||||
|
||||
export type TeamMember = TypeOf<typeof TeamMemberSchema>
|
||||
@@ -0,0 +1,11 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
|
||||
import {TeamShareBaseSchema} from './teamShareBase'
|
||||
|
||||
export const TeamNamespaceSchema = TeamShareBaseSchema.extend({
|
||||
namespaceId: IdSchema.default(0), // INamespace['id']
|
||||
})
|
||||
|
||||
export type ITeamNamespace = TypeOf<typeof TeamNamespaceSchema>
|
||||
@@ -0,0 +1,19 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {nativeEnum} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
|
||||
export const TeamShareBaseSchema = AbstractSchema.extend({
|
||||
teamId: IdSchema.default(0), // ITeam['id']
|
||||
right: nativeEnum(RIGHTS).default(RIGHTS.READ),
|
||||
|
||||
created: DateSchema.nullable(),
|
||||
updated: DateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type ITeamShareBase = TypeOf<typeof TeamShareBaseSchema>
|
||||
@@ -0,0 +1,9 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
|
||||
import {object, string} from 'zod'
|
||||
|
||||
export const TokenSchema = object({
|
||||
token: string(),
|
||||
})
|
||||
|
||||
export type IToken = TypeOf<typeof TokenSchema>
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {string, boolean} from 'zod'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
|
||||
export const TotpSchema = AbstractSchema.extend({
|
||||
secret: string().default(''),
|
||||
enabled: boolean().default(false),
|
||||
url: string().url().default(''),
|
||||
})
|
||||
|
||||
export type Totp = TypeOf<typeof TotpSchema>
|
||||
@@ -0,0 +1,21 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {string} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
import {UserSettingsSchema} from './userSettings'
|
||||
|
||||
export const UserSchema = AbstractSchema.extend({
|
||||
id: IdSchema.default(0),
|
||||
email: string().email().default(''),
|
||||
username: string().default(''),
|
||||
name: string().default(''),
|
||||
settings: UserSettingsSchema.nullable(),
|
||||
|
||||
created: DateSchema.nullable(),
|
||||
updated: DateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type User = TypeOf<typeof UserSchema>
|
||||
@@ -0,0 +1,11 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
|
||||
import {UserShareBaseSchema} from './userShareBase'
|
||||
|
||||
export const UserListSchema = UserShareBaseSchema.extend({
|
||||
listId: IdSchema.default(0), // IList['id']
|
||||
})
|
||||
|
||||
export type IUserList = TypeOf<typeof UserListSchema>
|
||||
@@ -0,0 +1,11 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
|
||||
import {UserShareBaseSchema} from './userShareBase'
|
||||
|
||||
export const UserNamespaceSchema = UserShareBaseSchema.extend({
|
||||
namespaceId: IdSchema.default(0), // INamespace['id']
|
||||
})
|
||||
|
||||
export type IUserNamespace = TypeOf<typeof UserNamespaceSchema>
|
||||
@@ -0,0 +1,29 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {boolean, string, undefined, nativeEnum} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
|
||||
const WEEKDAYS = {
|
||||
MONDAY: 0,
|
||||
TUESDAY: 1,
|
||||
WEDNESDAY: 2,
|
||||
THURSDAY: 3,
|
||||
FRIDAY: 4,
|
||||
SATURDAY: 5,
|
||||
SUNDAY: 6,
|
||||
} as const
|
||||
|
||||
export const UserSettingsSchema = AbstractSchema.extend({
|
||||
name: string().default(''),
|
||||
emailRemindersEnabled: boolean().default(true),
|
||||
discoverableByName: boolean().default(false),
|
||||
discoverableByEmail: boolean().default(false),
|
||||
overdueTasksRemindersEnabled: boolean().default(true),
|
||||
defaultListId: IdSchema.or(undefined()), // iListSchema['id'] // FIXME: shouldn't this be `null`?
|
||||
weekStart: nativeEnum(WEEKDAYS).default(WEEKDAYS.MONDAY),
|
||||
timezone: string().default(''),
|
||||
})
|
||||
|
||||
export type IUserSettings = TypeOf<typeof UserSettingsSchema>
|
||||
@@ -0,0 +1,19 @@
|
||||
import type {TypeOf} from 'zod'
|
||||
import {nativeEnum} from 'zod'
|
||||
|
||||
import {IdSchema} from './common/id'
|
||||
import {DateSchema} from './common/date'
|
||||
|
||||
import {AbstractSchema} from './abstract'
|
||||
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
|
||||
export const UserShareBaseSchema = AbstractSchema.extend({
|
||||
userId: IdSchema, // FIXME: default of model is `''`
|
||||
right: nativeEnum(RIGHTS).default(RIGHTS.READ),
|
||||
|
||||
created: DateSchema.nullable(),
|
||||
updated: DateSchema.nullable(),
|
||||
})
|
||||
|
||||
export type TeamMember = TypeOf<typeof UserShareBaseSchema>
|
||||
@@ -1,6 +1,13 @@
|
||||
import type {IAbstract} from './IAbstract'
|
||||
|
||||
export type AvatarProvider = 'default' | 'initials' | 'gravatar' | 'marble' | 'upload'
|
||||
export const AVATAR_PROVIDER = [
|
||||
'default',
|
||||
'initials',
|
||||
'gravatar',
|
||||
'marble',
|
||||
'upload',
|
||||
] as const
|
||||
export type AvatarProvider = typeof AVATAR_PROVIDER[number]
|
||||
|
||||
export interface IAvatar extends IAbstract {
|
||||
avatarProvider: AvatarProvider
|
||||
|
||||
@@ -36,6 +36,18 @@ function redirectToProviderIfNothingElseIsEnabled() {
|
||||
}
|
||||
}
|
||||
|
||||
function getLoggedInVia(): string | null {
|
||||
return localStorage.getItem('loggedInViaProvider')
|
||||
}
|
||||
|
||||
function setLoggedInVia(provider: string | null): void {
|
||||
if (provider) {
|
||||
localStorage.setItem('loggedInViaProvider', provider)
|
||||
} else {
|
||||
localStorage.removeItem('loggedInViaProvider')
|
||||
}
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const configStore = useConfigStore()
|
||||
|
||||
@@ -186,6 +198,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
async function openIdAuth({provider, code}) {
|
||||
const HTTP = HTTPFactory()
|
||||
setIsLoading(true)
|
||||
setLoggedInVia(null)
|
||||
|
||||
const fullProvider: IProvider = configStore.auth.openidConnect.providers.find((p: IProvider) => p.key === provider)
|
||||
|
||||
@@ -200,6 +213,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const response = await HTTP.post(`/auth/openid/${provider}/callback`, data)
|
||||
// Save the token to local storage for later use
|
||||
saveToken(response.data.token, true)
|
||||
setLoggedInVia(provider)
|
||||
|
||||
// Tell others the user is autheticated
|
||||
await checkAuth()
|
||||
@@ -406,17 +420,15 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
async function logout() {
|
||||
removeToken()
|
||||
const loggedInVia = getLoggedInVia()
|
||||
window.localStorage.clear() // Clear all settings and history we might have saved in local storage.
|
||||
await router.push({name: 'user.login'})
|
||||
await checkAuth()
|
||||
|
||||
// if configured, redirect to OIDC Provider on logout
|
||||
if (
|
||||
configStore.auth.local.enabled === false &&
|
||||
configStore.auth.openidConnect.enabled &&
|
||||
configStore.auth.openidConnect.providers?.length === 1)
|
||||
{
|
||||
redirectToProviderOnLogout(configStore.auth.openidConnect.providers[0])
|
||||
const fullProvider: IProvider = configStore.auth.openidConnect.providers.find((p: IProvider) => p.key === loggedInVia)
|
||||
if (fullProvider) {
|
||||
redirectToProviderOnLogout(fullProvider)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+39
-19
@@ -1,11 +1,14 @@
|
||||
import { readonly, ref} from 'vue'
|
||||
import {ref, computed, readonly} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRouter, useRoute} from 'vue-router'
|
||||
import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||
|
||||
import {getAuthForRoute} from '@/router'
|
||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
||||
|
||||
import ProjectModel from '@/models/project'
|
||||
import ProjectService from '../services/project'
|
||||
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||
import ProjectService from '@/services/project'
|
||||
import {checkAndSetApiUrl, ERROR_NO_API_URL, InvalidApiUrlProvidedError, NoApiUrlProvidedError} from '@/helpers/checkAndSetApiUrl'
|
||||
|
||||
import {useMenuActive} from '@/composables/useMenuActive'
|
||||
|
||||
@@ -15,8 +18,16 @@ import type {Right} from '@/constants/rights'
|
||||
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||
|
||||
export const useBaseStore = defineStore('base', () => {
|
||||
const loading = ref(false)
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const ready = ref(false)
|
||||
const error = ref('')
|
||||
const loading = computed(() => !ready.value && error.value === '')
|
||||
|
||||
// This is used to highlight the current project in menu for all project related views
|
||||
const currentProject = ref<IProject | null>(new ProjectModel({
|
||||
@@ -33,10 +44,6 @@ export const useBaseStore = defineStore('base', () => {
|
||||
const logoVisible = ref(true)
|
||||
const updateAvailable = ref(false)
|
||||
|
||||
function setLoading(newLoading: boolean) {
|
||||
loading.value = newLoading
|
||||
}
|
||||
|
||||
function setCurrentProject(newCurrentProject: IProject | null, currentViewId?: IProjectView['id']) {
|
||||
// Server updates don't return the right. Therefore, the right is reset after updating the project which is
|
||||
// confusing because all the buttons will disappear in that case. To prevent this, we're keeping the right
|
||||
@@ -91,10 +98,6 @@ export const useBaseStore = defineStore('base', () => {
|
||||
logoVisible.value = visible
|
||||
}
|
||||
|
||||
function setReady(value: boolean) {
|
||||
ready.value = value
|
||||
}
|
||||
|
||||
function setUpdateAvailable(value: boolean) {
|
||||
updateAvailable.value = value
|
||||
}
|
||||
@@ -139,16 +142,36 @@ export const useBaseStore = defineStore('base', () => {
|
||||
setCurrentProject(project, currentProjectViewId)
|
||||
}
|
||||
|
||||
const authStore = useAuthStore()
|
||||
async function loadApp() {
|
||||
await checkAndSetApiUrl(window.API_URL)
|
||||
await authStore.checkAuth()
|
||||
ready.value = true
|
||||
try {
|
||||
await checkAndSetApiUrl(window.API_URL)
|
||||
await authStore.checkAuth()
|
||||
ready.value = true
|
||||
const redirectTo = await getAuthForRoute(route, authStore)
|
||||
if (typeof redirectTo !== 'undefined') {
|
||||
await router.push(redirectTo)
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof NoApiUrlProvidedError) {
|
||||
error.value = ERROR_NO_API_URL
|
||||
return
|
||||
}
|
||||
if (e instanceof InvalidApiUrlProvidedError) {
|
||||
error.value = t('apiConfig.error')
|
||||
return
|
||||
}
|
||||
error.value = String(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
loadApp()
|
||||
|
||||
return {
|
||||
error: readonly(error),
|
||||
loading: readonly(loading),
|
||||
ready: readonly(ready),
|
||||
loadApp,
|
||||
|
||||
currentProject: readonly(currentProject),
|
||||
currentProjectViewId: readonly(currentProjectViewId),
|
||||
background: readonly(background),
|
||||
@@ -159,8 +182,6 @@ export const useBaseStore = defineStore('base', () => {
|
||||
logoVisible: readonly(logoVisible),
|
||||
updateAvailable: readonly(updateAvailable),
|
||||
|
||||
setLoading,
|
||||
setReady,
|
||||
setCurrentProject,
|
||||
setCurrentProjectViewId,
|
||||
setHasTasks,
|
||||
@@ -172,7 +193,6 @@ export const useBaseStore = defineStore('base', () => {
|
||||
setUpdateAvailable,
|
||||
|
||||
handleSetCurrentProject,
|
||||
loadApp,
|
||||
|
||||
...useMenuActive(),
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user