Compare commits

..
Author SHA1 Message Date
Dominik Pschenitschni da695de78e wip: add useTask composable 2024-12-19 21:58:17 +01:00
Dominik Pschenitschni 2783df1918 feat: rename visible to isReady
By changing the naming we make it clearer _why_ we want elements to be visible (because the loaded data is ready). Also `v-if` already implies the meaning of the original term (if the expression inside v-if resolves to true the content will be visible).

The new naming is borrowed from https://vueuse.org/core/useAsyncState/.

This loading is still missing error handling, but I want to separate that in another change
2024-12-19 18:05:46 +01:00
156 changed files with 4846 additions and 6455 deletions
+12 -12
View File
@@ -363,7 +363,7 @@ steps:
- api-build
- name: frontend-dependencies
image: node:22.13.0-alpine
image: node:22.11.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@@ -377,7 +377,7 @@ steps:
# - restore-cache
- name: frontend-lint
image: node:22.13.0-alpine
image: node:22.11.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@@ -389,7 +389,7 @@ steps:
- frontend-dependencies
- name: frontend-build-prod
image: node:22.13.0-alpine
image: node:22.11.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@@ -401,7 +401,7 @@ steps:
- frontend-dependencies
- name: frontend-test-unit
image: node:22.13.0-alpine
image: node:22.11.0-alpine
pull: always
commands:
- cd frontend
@@ -412,7 +412,7 @@ steps:
- name: frontend-typecheck
failure: ignore
image: node:22.13.0-alpine
image: node:22.11.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@@ -543,7 +543,7 @@ steps:
- git fetch --tags
- name: frontend-dependencies
image: node:22.13.0-alpine
image: node:22.11.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.13.0-alpine
image: node:22.11.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.2
image: goreleaser/nfpm:v2.41.0
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.2
image: goreleaser/nfpm:v2.41.0
pull: always
commands:
- apk add git go
@@ -946,7 +946,7 @@ steps:
- git fetch --tags
- name: build
image: node:22.13.0-alpine
image: node:22.11.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@@ -1007,7 +1007,7 @@ steps:
- git fetch --tags
- name: build
image: node:22.13.0-alpine
image: node:22.11.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@@ -1398,6 +1398,6 @@ steps:
- failure
---
kind: signature
hmac: 298674d9b9d8618eef47969038fd4d7516112ecbd8a4d899ec3751d997a34ce4
hmac: 7fc1cb2b0cfdc84d7eda25ce75dd56446fc01060c03af89c91043f095e92a2b3
...
+1 -6
View File
@@ -30,10 +30,5 @@
],
"i18n-ally.sortKeys": true,
"i18n-ally.keepFulfilled": true,
"i18n-ally.keystyle": "nested",
// unplugin-vue-router
// https://uvr.esm.is/introduction.html#setup
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.autoImportFileExcludePatterns": ["vue-router/auto$"]
"i18n-ally.keystyle": "nested"
}
-6
View File
@@ -7,12 +7,6 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
All releases can be found on https://code.vikunja.io/vikunja/releases.
## [0.24.6] - 2024-12-22
### Bug Fixes
* *(export)* Update only current user export file id
## [0.24.5] - 2024-11-21
### Bug Fixes
+1 -1
View File
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM node:22.13.0-alpine AS frontendbuilder
FROM --platform=$BUILDPLATFORM node:22.11.0-alpine AS frontendbuilder
WORKDIR /build
+1 -1
View File
@@ -2,7 +2,7 @@
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/vikunjaa/status.svg)](https://drone.kolaente.de/vikunja/vikunja)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
[![Install](https://img.shields.io/badge/download-v0.24.6-brightgreen.svg)](https://vikunja.io/docs/installing)
[![Install](https://img.shields.io/badge/download-v0.24.5-brightgreen.svg)](https://vikunja.io/docs/installing)
[![Docker Pulls](https://img.shields.io/docker/pulls/vikunja/vikunja.svg)](https://hub.docker.com/r/vikunja/vikunja/)
[![Swagger Docs](https://img.shields.io/badge/swagger-docs-brightgreen.svg)](https://try.vikunja.io/api/v1/docs)
[![Go Report Card](https://goreportcard.com/badge/kolaente.dev/vikunja/vikunja)](https://goreportcard.com/report/kolaente.dev/vikunja/vikunja)
+1 -1
View File
@@ -51,7 +51,7 @@
}
},
"devDependencies": {
"electron": "34.0.0",
"electron": "33.2.1",
"electron-builder": "25.1.8"
},
"dependencies": {
+5 -5
View File
@@ -16,8 +16,8 @@ importers:
version: 4.21.2
devDependencies:
electron:
specifier: 34.0.0
version: 34.0.0
specifier: 33.2.1
version: 33.2.1
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@34.0.0:
resolution: {integrity: sha512-fpaPb0lifoUJ6UJa4Lk8/0B2Ku/xDZWdc1Gkj67jbygTCrvSon0qquju6Ltx1Kz23GRqqlIHXiy9EvrjpY7/Wg==}
electron@33.2.1:
resolution: {integrity: sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==}
engines: {node: '>= 12.20.55'}
hasBin: true
@@ -2442,7 +2442,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
electron@34.0.0:
electron@33.2.1:
dependencies:
'@electron/get': 2.0.3
'@types/node': 20.12.7
-2
View File
@@ -11,8 +11,6 @@ stats.html
node_modules
.DS_Store
dist
dist-dev
dist-test
coverage
*.zip
.vite/
+1 -1
View File
@@ -1 +1 @@
22.13.0
22.11.0
+18 -2
View File
@@ -2,14 +2,30 @@
> The todo app to organize your life.
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/vikunja/status.svg)](https://drone.kolaente.de/vikunja/vikunja)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.22.1-brightgreen.svg)](https://dl.vikunja.io)
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
This is the web frontend for Vikunja, written in Vue.js.
Take a look at [our roadmap](https://my.vikunja.cloud/share/UrdhKPqumxDXUbYpEGJLSIyNTwAnbBzVlwdDpRbv/auth) (hosted on Vikunja!) for a list of things we're currently working on!
For general information about the project, refer to the top-level readme of this repo.
## Security Reports
If you find any security-related issues you don't want to disclose publicly, please use [the contact information on our website](https://vikunja.io/contact/#security).
## Docker
There is a [docker image available](https://hub.docker.com/r/vikunja/vikunja) with support for http/2 and aggressive caching enabled.
In order to build it from sources run the command below. (Docker >= v19.03)
```shell
export DOCKER_BUILDKIT=1
docker build -t vikunja/frontend .
```
Refer to [multi-platform documentation](https://docs.docker.com/build/building/multi-platform/) in order to build for different platforms.
## Project setup
@@ -20,7 +36,7 @@ pnpm install
### Compiles and hot-reloads for development
```shell
pnpm run dev
pnpm run serve
```
### Compiles and minifies for production
-1
View File
@@ -2,7 +2,6 @@
/// <reference types="vite-svg-loader" />
/// <reference types="cypress" />
/// <reference types="@histoire/plugin-vue/components" />
/// <reference types="unplugin-vue-router/client" />
interface ImportMetaEnv {
readonly VIKUNJA_API_URL?: string
+1 -10
View File
@@ -84,14 +84,5 @@ export default [
// 'cypress/*',
// ],
},
{
// unplugin-vue-router
// https://uvr.esm.is/guide/eslint.html#eslint
settings: {
'import/core-modules': ['vue-router/auto-routes'],
},
globals: {
'definePage': 'readonly',
},
},
]
+68 -54
View File
@@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@9.15.4",
"packageManager": "pnpm@9.15.0",
"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-test/",
"build:test": "vite build --outDir dist-test && workbox copyLibraries dist-dev/",
"build:dev": "vite build --mode development --outDir dist-dev/",
"lint": "eslint 'src/**/*.{js,ts,vue}'",
"lint:fix": "pnpm run lint --fix",
@@ -55,31 +55,46 @@
"@fortawesome/vue-fontawesome": "3.0.8",
"@github/hotkey": "3.1.1",
"@infectoone/vue-ganttastic": "2.3.2",
"@intlify/unplugin-vue-i18n": "6.0.3",
"@intlify/unplugin-vue-i18n": "6.0.1",
"@kyvg/vue3-notification": "3.4.1",
"@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",
"@sentry/tracing": "7.114.0",
"@sentry/vue": "8.47.0",
"@tiptap/core": "2.10.4",
"@tiptap/extension-blockquote": "2.10.4",
"@tiptap/extension-bold": "2.10.4",
"@tiptap/extension-bullet-list": "2.10.4",
"@tiptap/extension-code": "2.10.4",
"@tiptap/extension-code-block": "2.10.4",
"@tiptap/extension-code-block-lowlight": "2.10.4",
"@tiptap/extension-document": "2.10.4",
"@tiptap/extension-dropcursor": "2.10.4",
"@tiptap/extension-gapcursor": "2.10.4",
"@tiptap/extension-hard-break": "2.10.4",
"@tiptap/extension-heading": "2.10.4",
"@tiptap/extension-history": "2.10.4",
"@tiptap/extension-horizontal-rule": "2.10.4",
"@tiptap/extension-image": "2.10.4",
"@tiptap/extension-italic": "2.10.4",
"@tiptap/extension-link": "2.10.4",
"@tiptap/extension-list-item": "2.10.4",
"@tiptap/extension-ordered-list": "2.10.4",
"@tiptap/extension-paragraph": "2.10.4",
"@tiptap/extension-placeholder": "2.10.4",
"@tiptap/extension-strike": "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-text": "2.10.4",
"@tiptap/extension-typography": "2.10.4",
"@tiptap/extension-underline": "2.10.4",
"@tiptap/pm": "2.10.4",
"@tiptap/suggestion": "2.10.4",
"@tiptap/vue-3": "2.10.4",
"@vueuse/core": "12.0.0",
"@vueuse/router": "12.0.0",
"axios": "1.7.9",
"blurhash": "2.0.5",
"bulma-css-variables": "0.9.33",
@@ -93,18 +108,17 @@
"floating-vue": "5.2.2",
"is-touch-device": "1.0.1",
"klona": "2.0.6",
"lowlight": "3.3.0",
"lowlight": "2.9.0",
"pinia": "2.3.0",
"register-service-worker": "1.7.2",
"sortablejs": "1.15.6",
"tailwindcss": "3.4.17",
"tippy.js": "6.3.7",
"ufo": "1.5.4",
"unplugin-vue-router": "0.10.9",
"vue": "3.5.13",
"vue-advanced-cropper": "2.8.9",
"vue-flatpickr-component": "11.0.5",
"vue-i18n": "11.0.1",
"vue-i18n": "10.0.5",
"vue-router": "4.5.0",
"vuemoji-picker": "0.3.2",
"workbox-precaching": "7.3.0",
@@ -112,50 +126,50 @@
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.2.5",
"@cypress/vite-dev-server": "6.0.1",
"@cypress/vue": "6.0.2",
"@faker-js/faker": "9.4.0",
"@cypress/vite-dev-server": "5.2.1",
"@cypress/vue": "6.0.1",
"@faker-js/faker": "9.3.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.7",
"@types/node": "22.10.2",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/eslint-plugin": "8.20.0",
"@typescript-eslint/parser": "8.20.0",
"@typescript-eslint/eslint-plugin": "8.18.1",
"@typescript-eslint/parser": "8.18.1",
"@vitejs/plugin-vue": "5.2.1",
"@vue/eslint-config-typescript": "14.3.0",
"@vue/eslint-config-typescript": "14.1.4",
"@vue/test-utils": "2.4.6",
"@vue/tsconfig": "0.7.0",
"autoprefixer": "10.4.20",
"browserslist": "4.24.4",
"caniuse-lite": "1.0.30001692",
"browserslist": "4.24.3",
"caniuse-lite": "1.0.30001689",
"csstype": "3.1.3",
"cypress": "14.0.0",
"esbuild": "0.24.2",
"eslint": "9.18.0",
"cypress": "13.17.0",
"esbuild": "0.24.0",
"eslint": "9.17.0",
"eslint-plugin-vue": "9.32.0",
"happy-dom": "16.6.0",
"happy-dom": "15.11.7",
"histoire": "0.17.17",
"postcss": "8.5.1",
"postcss": "8.4.49",
"postcss-easing-gradients": "3.0.1",
"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",
"postcss-preset-env": "10.1.2",
"rollup": "4.28.1",
"rollup-plugin-visualizer": "5.12.0",
"sass-embedded": "1.83.0",
"start-server-and-test": "2.0.9",
"typescript": "5.7.2",
"unplugin-inject-preload": "3.0.0",
"vite": "6.0.7",
"vite": "6.0.3",
"vite-plugin-pwa": "0.21.1",
"vite-plugin-sentry": "1.4.0",
"vite-plugin-vue-devtools": "7.7.0",
"vite-plugin-vue-devtools": "7.6.8",
"vite-svg-loader": "5.1.0",
"vitest": "3.0.1",
"vue-tsc": "2.2.0",
"wait-on": "8.0.2",
"vitest": "2.1.8",
"vue-tsc": "2.1.10",
"wait-on": "8.0.1",
"workbox-cli": "7.3.0"
},
"pnpm": {
+1704 -1705
View File
File diff suppressed because it is too large Load Diff
+6 -4
View File
@@ -1,10 +1,10 @@
<template>
<Ready>
<template v-if="authStore.authUser">
<template v-if="authUser">
<AppHeader />
<ContentAuth />
</template>
<ContentLinkShare v-else-if="authStore.authLinkShare" />
<ContentLinkShare v-else-if="authLinkShare" />
<NoAuthWrapper v-else>
<RouterView />
</NoAuthWrapper>
@@ -49,15 +49,17 @@ import DemoMode from '@/components/home/DemoMode.vue'
const importAccountDeleteService = () => import('@/services/accountDelete')
import {success} from '@/message'
const authStore = useAuthStore()
const baseStore = useBaseStore()
const authStore = useAuthStore()
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 {useFlatpickrLanguage} from '@/helpers/useFlatpickrLanguage'
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
const props = defineProps<{
modelValue: {
@@ -137,7 +137,7 @@ const flatPickerConfig = computed(() => ({
enableTime: false,
wrap: true,
mode: 'range',
locale: useFlatpickrLanguage().value,
locale: getFlatpickrLanguage(),
}))
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 {useFlatpickrLanguage} from '@/helpers/useFlatpickrLanguage'
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
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: useFlatpickrLanguage().value,
locale: getFlatpickrLanguage(),
}))
const showHowItWorks = ref(false)
@@ -25,7 +25,6 @@
</h1>
<BaseButton
v-if="!isEditorContentEmpty(currentProject.description)"
:to="{ name: 'project.info', params: { projectId: currentProject.id } }"
class="project-title-button"
>
@@ -125,7 +124,6 @@ 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'
+1 -1
View File
@@ -37,7 +37,7 @@
v-slot="{ Component }"
:route="routeWithModal"
>
<keep-alive :include="['project']">
<keep-alive :include="['project.view']">
<component :is="Component" />
</keep-alive>
</RouterView>
+32 -39
View File
@@ -76,38 +76,32 @@
v-if="projectStore.isLoading"
variant="small"
/>
<template v-else>
<nav
v-if="favoriteProjects"
class="menu"
>
<ProjectsNavigation
:model-value="favoriteProjects"
:can-edit-order="false"
:can-collapse="false"
/>
</nav>
<nav
v-if="savedFilterProjects"
class="menu"
>
<ProjectsNavigation
:model-value="savedFilterProjects"
:can-edit-order="false"
:can-collapse="false"
/>
</nav>
<nav
v-else
class="menu"
>
<ProjectsNavigation
v-if="favoriteProjects?.length"
:model-value="favoriteProjects"
:can-edit-order="false"
:can-collapse="false"
/>
<nav class="menu">
<ProjectsNavigation
:model-value="projects"
:can-edit-order="true"
:can-collapse="true"
:level="1"
/>
</nav>
</template>
<ProjectsNavigation
v-if="savedFilterProjects?.length"
:model-value="savedFilterProjects"
:can-edit-order="false"
:can-collapse="false"
/>
<ProjectsNavigation
v-if="projects?.length"
:model-value="projects"
:can-edit-order="true"
:can-collapse="true"
:level="1"
/>
</nav>
<PoweredByLink
class="mt-auto"
@@ -117,7 +111,7 @@
</template>
<script setup lang="ts">
import {computed} from 'vue'
import { storeToRefs } from 'pinia'
import PoweredByLink from '@/components/home/PoweredByLink.vue'
import Logo from '@/components/home/Logo.vue'
@@ -129,16 +123,15 @@ import ProjectsNavigation from '@/components/home/ProjectsNavigation.vue'
const baseStore = useBaseStore()
const projectStore = useProjectStore()
const projects = computed(() => projectStore.notArchivedRootProjects)
const favoriteProjects = computed(() => projectStore.favoriteProjects)
const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
const {
notArchivedRootProjects: projects,
favoriteProjects,
savedFilterProjects,
} = storeToRefs(projectStore)
</script>
<style lang="scss" scoped>
.logo {
display: block;
padding-left: 1rem;
margin-right: 1rem;
margin-bottom: 1rem;
@@ -192,7 +185,7 @@ const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
}
}
.menu + .menu {
.menu-list + .menu-list {
padding-top: math.div($navbar-padding, 2);
}
</style>
@@ -15,7 +15,7 @@
/>
</BaseButton>
<BaseButton
:to="{ name: 'project', params: { projectId: project.id} }"
:to="{ name: 'project.index', params: { projectId: project.id} }"
class="list-menu-link"
:class="{'router-link-exact-active': currentProject?.id === project.id}"
>
@@ -84,12 +84,14 @@
<script setup lang="ts">
import {computed} from 'vue'
import {useProjectStore} from '@/stores/projects'
import {useBaseStore} from '@/stores/base'
import {storeToRefs} from 'pinia'
import {useStorage} from '@vueuse/core'
import type {IProject} from '@/modelTypes/IProject'
import {useProjectStore} from '@/stores/projects'
import {useBaseStore} from '@/stores/base'
import BaseButton from '@/components/base/BaseButton.vue'
import ProjectSettingsDropdown from '@/components/project/ProjectSettingsDropdown.vue'
import {getProjectTitle} from '@/helpers/getProjectTitle'
@@ -105,18 +107,17 @@ const props = defineProps<{
const projectStore = useProjectStore()
const baseStore = useBaseStore()
const currentProject = computed(() => baseStore.currentProject)
const {currentProject} = storeToRefs(baseStore)
// Persist open state across browser reloads. Using a separate ref for the state
// allows us to use only one entry in local storage instead of one for every project id.
type OpenState = { [key: number]: boolean }
const childProjectsOpenState = useStorage<OpenState>('navigation-child-projects-open', {})
const childProjectsCollapsed = useStorage<{ [key: number]: boolean }>('navigation-child-projects-collapsed', {})
const childProjectsOpen = computed({
get() {
return childProjectsOpenState.value[props.project.id] ?? true
return childProjectsCollapsed.value[props.project.id] ?? true
},
set(open) {
childProjectsOpenState.value[props.project.id] = open
childProjectsCollapsed.value[props.project.id] = open
},
})
@@ -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 {useFlatpickrLanguage} from '@/helpers/useFlatpickrLanguage'
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
const props = withDefaults(defineProps<{
modelValue: Date | null | string
@@ -114,7 +114,7 @@ const flatPickerConfig = computed(() => ({
enableTime: true,
time_24hr: true,
inline: true,
locale: useFlatpickrLanguage().value,
locale: getFlatpickrLanguage(),
}))
// Since flatpickr dates are strings, we need to convert them to native date objects.
+21 -12
View File
@@ -123,7 +123,7 @@
</div>
</template>
<script setup lang="ts" generic="T extends { [id: string]: any }">
<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, toRefs, watch, type ComponentPublicInstance} from 'vue'
import {useI18n} from 'vue-i18n'
@@ -134,13 +134,15 @@ import CustomTransition from '@/components/misc/CustomTransition.vue'
const props = withDefaults(defineProps<{
/** The object with the value, updated every time an entry is selected */
modelValue: T | T[] | null,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
modelValue: { [id: string]: any } | 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 */
searchResults?: T[]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
searchResults?: { [id: string]: any }[]
/** 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 */
@@ -186,7 +188,8 @@ const props = withDefaults(defineProps<{
})
const emit = defineEmits<{
'update:modelValue': [value: T | T[] | null],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
'update:modelValue': [value: { [key: string]: any }[] | null],
/**
* Triggered every time the search query input changes
*/
@@ -194,7 +197,8 @@ const emit = defineEmits<{
/**
* Triggered every time an option from the search results is selected. Also triggers a change in v-model.
*/
'select': [value: T],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
'select': [value: { [key: string]: any }],
/**
* If nothing or no exact match was found and `creatable` is true, this event is triggered with the current value of the search query.
*/
@@ -202,7 +206,8 @@ const emit = defineEmits<{
/**
* If `multiple` is enabled, this will be fired every time an item is removed from the array of selected items.
*/
'remove': [value: T],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
'remove': [value: { [key: string]: any }],
}>()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -215,12 +220,13 @@ function elementInResults(elem: string | any, label: string, query: string): boo
return elem === query
}
const query = ref<string | T>('')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const query = ref<string | { [key: string]: any }>('')
const searchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
const localLoading = ref(false)
const showSearchResults = ref(false)
const internalValue = ref<string | T | T[] | null>(null)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const internalValue = ref<string | { [key: string]: any } | any[] | null>(null)
onMounted(() => document.addEventListener('click', hideSearchResultsHandler))
onBeforeUnmount(() => document.removeEventListener('click', hideSearchResultsHandler))
@@ -319,13 +325,15 @@ function handleFocus() {
}, 10)
}
function select(object: T | null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function select(object: { [key: string]: any } | null) {
if (props.multiple) {
if (internalValue.value === null) {
internalValue.value = []
}
internalValue.value.push(object)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(internalValue.value as any[]).push(object)
} else {
internalValue.value = object
}
@@ -338,7 +346,8 @@ function select(object: T | null) {
}
}
function setSelectedObject(object: string | T | null, resetOnly = false) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function setSelectedObject(object: string | { [id: string]: any } | null, resetOnly = false) {
internalValue.value = object
// We assume we're getting an array when multiple is enabled and can therefore leave the query
+49 -23
View File
@@ -17,7 +17,7 @@
v-tooltip="$t('input.editor.bold')"
class="editor-bubble__button"
:class="{ 'is-active': editor.isActive('bold') }"
@click="() => editor?.chain().focus().toggleBold().run()"
@click="editor.chain().focus().toggleBold().run()"
>
<Icon :icon="['fa', 'fa-bold']" />
</BaseButton>
@@ -25,7 +25,7 @@
v-tooltip="$t('input.editor.italic')"
class="editor-bubble__button"
:class="{ 'is-active': editor.isActive('italic') }"
@click="() => editor?.chain().focus().toggleItalic().run()"
@click="editor.chain().focus().toggleItalic().run()"
>
<Icon :icon="['fa', 'fa-italic']" />
</BaseButton>
@@ -33,7 +33,7 @@
v-tooltip="$t('input.editor.underline')"
class="editor-bubble__button"
:class="{ 'is-active': editor.isActive('underline') }"
@click="() => editor?.chain().focus().toggleUnderline().run()"
@click="editor.chain().focus().toggleUnderline().run()"
>
<Icon :icon="['fa', 'fa-underline']" />
</BaseButton>
@@ -41,7 +41,7 @@
v-tooltip="$t('input.editor.strikethrough')"
class="editor-bubble__button"
:class="{ 'is-active': editor.isActive('strike') }"
@click="() => editor?.chain().focus().toggleStrike().run()"
@click="editor.chain().focus().toggleStrike().run()"
>
<Icon :icon="['fa', 'fa-strikethrough']" />
</BaseButton>
@@ -49,7 +49,7 @@
v-tooltip="$t('input.editor.code')"
class="editor-bubble__button"
:class="{ 'is-active': editor.isActive('code') }"
@click="() => editor?.chain().focus().toggleCode().run()"
@click="editor.chain().focus().toggleCode().run()"
>
<Icon :icon="['fa', 'fa-code']" />
</BaseButton>
@@ -87,7 +87,7 @@
<li>
<BaseButton
class="done-edit"
@click="() => setEdit()"
@click="setEdit"
>
{{ $t('input.editor.edit') }}
</BaseButton>
@@ -108,7 +108,7 @@
<li v-if="!isEditing">
<BaseButton
class="done-edit"
@click="() => setEdit()"
@click="setEdit"
>
{{ $t('input.editor.edit') }}
</BaseButton>
@@ -138,16 +138,11 @@
<script setup lang="ts">
import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import {useI18n} from 'vue-i18n'
import {eventToHotkeyString} from '@github/hotkey'
import EditorToolbar from './EditorToolbar.vue'
import StarterKit from '@tiptap/starter-kit'
import {Extension, mergeAttributes} from '@tiptap/core'
import {BubbleMenu, EditorContent, type Extensions, useEditor} from '@tiptap/vue-3'
import Link from '@tiptap/extension-link'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import Table from '@tiptap/extension-table'
import TableCell from '@tiptap/extension-table-cell'
@@ -156,27 +151,46 @@ import TableRow from '@tiptap/extension-table-row'
import Typography from '@tiptap/extension-typography'
import Image from '@tiptap/extension-image'
import Underline from '@tiptap/extension-underline'
import {Placeholder} from '@tiptap/extension-placeholder'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import HardBreak from '@tiptap/extension-hard-break'
import {Blockquote} from '@tiptap/extension-blockquote'
import {Bold} from '@tiptap/extension-bold'
import {BulletList} from '@tiptap/extension-bullet-list'
import {Code} from '@tiptap/extension-code'
import {Document} from '@tiptap/extension-document'
import {Dropcursor} from '@tiptap/extension-dropcursor'
import {Gapcursor} from '@tiptap/extension-gapcursor'
import {HardBreak} from '@tiptap/extension-hard-break'
import {Heading} from '@tiptap/extension-heading'
import {History} from '@tiptap/extension-history'
import {HorizontalRule} from '@tiptap/extension-horizontal-rule'
import {Italic} from '@tiptap/extension-italic'
import {ListItem} from '@tiptap/extension-list-item'
import {OrderedList} from '@tiptap/extension-ordered-list'
import {Paragraph} from '@tiptap/extension-paragraph'
import {Strike} from '@tiptap/extension-strike'
import {Text} from '@tiptap/extension-text'
import {BubbleMenu, EditorContent, type Extensions, useEditor} from '@tiptap/vue-3'
import {Node} from '@tiptap/pm/model'
import Commands from './commands'
import suggestionSetup from './suggestion'
import {common, createLowlight} from 'lowlight'
import {lowlight} from 'lowlight'
import type {BottomAction, UploadCallback} from './types'
import type {ITask} from '@/modelTypes/ITask'
import type {IAttachment} from '@/modelTypes/IAttachment'
import AttachmentModel from '@/models/attachment'
import AttachmentService from '@/services/attachment'
import {useI18n} from 'vue-i18n'
import BaseButton from '@/components/base/BaseButton.vue'
import XButton from '@/components/input/Button.vue'
import {Placeholder} from '@tiptap/extension-placeholder'
import {eventToHotkeyString} from '@github/hotkey'
import {Extension, mergeAttributes} from '@tiptap/core'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
import inputPrompt from '@/helpers/inputPrompt'
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
@@ -337,14 +351,16 @@ const additionalLinkProtocols = [
const extensions : Extensions = [
// Starterkit:
StarterKit.configure({
codeBlock: false,
hardBreak: false,
}),
Blockquote,
Bold,
BulletList,
Code,
CodeBlockLowlight.configure({
lowlight: createLowlight(common),
lowlight,
}),
Document,
Dropcursor,
Gapcursor,
HardBreak.extend({
addKeyboardShortcuts() {
return {
@@ -358,6 +374,15 @@ const extensions : Extensions = [
}
},
}),
Heading,
History,
HorizontalRule,
Italic,
ListItem,
OrderedList,
Paragraph,
Strike,
Text,
Placeholder.configure({
placeholder: ({editor}) => {
@@ -426,6 +451,7 @@ const extensions : Extensions = [
Commands.configure({
suggestion: suggestionSetup(t),
}),
BubbleMenu,
]
// Add a custom extension for the Escape key
+51 -10
View File
@@ -15,12 +15,12 @@
<p>{{ $t('offline.text') }}</p>
</div>
</div>
<template v-else-if="baseStore.ready">
<template v-else-if="ready">
<slot />
</template>
<section v-else-if="baseStore.error !== ''">
<section v-else-if="error !== ''">
<NoAuthWrapper :show-api-config="false">
<p v-if="baseStore.error === ERROR_NO_API_URL">
<p v-if="error === ERROR_NO_API_URL">
{{ $t('ready.noApiUrlConfigured') }}
</p>
<Message
@@ -30,7 +30,7 @@
>
<p>
{{ $t('ready.errorOccured') }}<br>
{{ baseStore.error }}
{{ error }}
</p>
<p>
{{ $t('ready.checkApiUrl') }}
@@ -38,13 +38,13 @@
</Message>
<ApiConfig
:configure-open="true"
@foundApi="baseStore.loadApp()"
@foundApi="load"
/>
</NoAuthWrapper>
</section>
<CustomTransition name="fade">
<section
v-if="baseStore.loading"
v-if="showLoading"
class="vikunja-loading"
>
<Logo class="logo" />
@@ -57,19 +57,60 @@
</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} from '@/helpers/checkAndSetApiUrl'
import {ERROR_NO_API_URL, InvalidApiUrlProvidedError, NoApiUrlProvidedError} from '@/helpers/checkAndSetApiUrl'
import {useOnline} from '@/composables/useOnline'
import {useBaseStore} from '@/stores/base'
const online = 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()
</script>
<style lang="scss" scoped>
@@ -62,22 +62,29 @@ export default {inheritAttrs: false}
</script>
<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, toRefs, useAttrs, watch, watchEffect} from 'vue'
import {computed, onBeforeUnmount, onMounted, ref, toRefs, useAttrs, watch, watchEffect, type PropType} from 'vue'
const props = withDefaults(defineProps<{
modelValue: DateOption | DateOption[] | null,
/** https://flatpickr.js.org/options/ */
config: Options,
events: HookKey[],
disabled: boolean,
}>(), {
modelValue: null,
config: () => ({
defaultDate: undefined,
wrap: false,
}),
events: () => includedEvents,
disabled: false,
const props = defineProps({
modelValue: {
type: [String, Number, Date, Array] as PropType<DateOption | DateOption[] | null>,
default: null,
},
// https://flatpickr.js.org/options/
config: {
type: Object as PropType<Options>,
default: () => ({
defaultDate: null,
wrap: false,
}),
},
events: {
type: Array as PropType<HookKey[]>,
default: () => includedEvents,
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits([
@@ -62,7 +62,7 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
},
{
title: 'project.kanban.title',
available: (route) => route.name === 'project',
available: (route) => route.name === 'project.view',
shortcuts: [
{
title: 'keyboardShortcuts.task.done',
@@ -103,7 +103,7 @@
{{ $t('project.webhooks.title') }}
</DropdownItem>
<DropdownItem
:to="{ name: 'project.create', params: { parentProjectId: project.id } }"
:to="{ name: 'project.createFromParent', params: { parentProjectId: project.id } }"
icon="layer-group"
>
{{ $t('menu.createProject') }}
@@ -20,7 +20,7 @@
:key="view.id"
class="switch-view-button"
:class="{'is-active': view.id === viewId}"
:to="{ name: 'project', params: { projectId, viewId: view.id } }"
:to="{ name: 'project.view', params: { projectId, viewId: view.id } }"
>
{{ getViewTitle(view) }}
</BaseButton>
@@ -42,7 +42,7 @@
</template>
<script setup lang="ts">
import {computed, ref, shallowReactive, watch, watchEffect} from 'vue'
import {computed, ref, watch} from 'vue'
import {useRoute} from 'vue-router'
import BaseButton from '@/components/base/BaseButton.vue'
@@ -72,7 +72,7 @@ const {t} = useI18n()
const baseStore = useBaseStore()
const projectStore = useProjectStore()
const projectService = shallowReactive(new ProjectService())
const projectService = ref(new ProjectService())
const loadedProjectId = ref(0)
const currentProject = computed<IProject>(() => {
@@ -96,7 +96,8 @@ watch(
() => props.projectId,
// loadProject
async (projectIdToLoad: number) => {
saveProjectToHistory({id: projectIdToLoad})
const projectData = {id: projectIdToLoad}
saveProjectToHistory(projectData)
// Don't load the project if we either already loaded it or aren't dealing with a project at all currently and
// the currently loaded project has the right set.
@@ -116,15 +117,15 @@ watch(
// Set the current project to the one we're about to load so that the title is already shown at the top
loadedProjectId.value = 0
const projectFromStore = projectStore.projects[projectIdToLoad]
const projectFromStore = projectStore.projects[projectData.id]
if (projectFromStore) {
baseStore.handleSetCurrentProject({project: projectFromStore, currentProjectViewId: props.viewId})
}
// We create an extra project object instead of creating it in project.value because that would trigger a ui update which would result in bad ux.
const project = new ProjectModel({id: projectIdToLoad})
const project = new ProjectModel(projectData)
try {
const loadedProject = await projectService.get(project)
const loadedProject = await projectService.value.get(project)
baseStore.handleSetCurrentProject({project: loadedProject, currentProjectViewId: props.viewId})
} finally {
loadedProjectId.value = projectIdToLoad
@@ -133,7 +134,13 @@ watch(
{immediate: true},
)
watchEffect(() => baseStore.setCurrentProjectViewId(props.viewId))
watch(
() => props.viewId,
() => {
baseStore.setCurrentProjectViewId(props.viewId)
},
{immediate: true},
)
function getViewTitle(view: IProjectView) {
switch (view.title) {
@@ -37,7 +37,7 @@
:aria-label="project.title"
:title="textOnlyDescription"
:to="{
name: 'project',
name: 'project.index',
params: { projectId: project.id}
}"
/>
@@ -77,7 +77,7 @@ import {useI18n} from 'vue-i18n'
import type {RouteLocationNormalized} from 'vue-router'
import {useBaseStore} from '@/stores/base'
import {useFlatpickrLanguage} from '@/helpers/useFlatpickrLanguage'
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
import Foo from '@/components/misc/flatpickr/Flatpickr.vue'
import ProjectWrapper from '@/components/project/ProjectWrapper.vue'
@@ -85,7 +85,7 @@ import FancyCheckbox from '@/components/input/FancyCheckbox.vue'
import TaskForm from '@/components/tasks/TaskForm.vue'
import {createAsyncComponent} from '@/helpers/createAsyncComponent'
import {useGanttFilters} from '@/composables/useGanttFilters'
import {useGanttFilters} from '../../../views/project/helpers/useGanttFilters'
import {RIGHTS} from '@/constants/rights'
import type {DateISO} from '@/types/DateISO'
@@ -153,14 +153,14 @@ const flatPickerDateRange = computed<Date[]>({
const initialDateRange = [filters.value.dateFrom, filters.value.dateTo]
const {t} = useI18n({useScope: 'global'})
const flatPickerConfig = computed(() => ({
const flatPickerConfig = computed<Options>(() => ({
altFormat: t('date.altFormatShort'),
altInput: true,
defaultDate: initialDateRange,
enableTime: false,
mode: 'range',
locale: useFlatpickrLanguage().value,
} as Options))
locale: getFlatpickrLanguage(),
}))
</script>
<style lang="scss" scoped>
@@ -416,10 +416,7 @@ watch(
collapsedBuckets.value = getCollapsedBucketState(projectId)
kanbanStore.loadBucketsForProject(projectId, viewId, params)
},
{
immediate: true,
deep: true,
},
{ immediate: true },
)
function setTaskContainerRef(id: IBucket['id'], el: HTMLElement) {
@@ -449,7 +449,7 @@ async function doAction(type: ACTION_TYPE, item: DoAction) {
case ACTION_TYPE.PROJECT:
closeQuickActions()
await router.push({
name: 'project',
name: 'project.index',
params: {projectId: (item as DoAction<IProject>).id},
})
break
+1 -1
View File
@@ -54,7 +54,7 @@ import {parseKebabDate} from '@/helpers/time/parseKebabDate'
import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask'
import type {DateISO} from '@/types/DateISO'
import type {GanttFilters} from '@/composables/useGanttFilters'
import type {GanttFilters} from '@/views/project/helpers/useGanttFilters'
import {
extendDayjs,
@@ -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'
withDefaults(defineProps<{
const {
assignees,
remove,
disabled,
avatarSize = 30,
inline = false,
} = 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,
})
defineEmits<{
remove: [user: IUser],
}>()
const hasDelete = computed(() => typeof remove !== 'undefined' && !disabled)
</script>
<template>
@@ -36,13 +36,13 @@ defineEmits<{
:avatar-size="avatarSize"
:show-username="false"
:user="user"
:class="{'m-2': canRemove && !disabled}"
:class="{'m-2': hasDelete}"
/>
<BaseButton
v-if="canRemove && !disabled"
v-if="hasDelete"
:key="'delete'+user.id"
class="remove-assignee"
@click="$emit('remove', user)"
@click="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 {useFlatpickrLanguage} from '@/helpers/useFlatpickrLanguage'
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
const props = defineProps<{
modelValue: ITask,
@@ -101,7 +101,7 @@ const flatPickerConfig = computed(() => ({
enableTime: true,
time_24hr: true,
inline: true,
locale: useFlatpickrLanguage().value,
locale: getFlatpickrLanguage(),
}))
function deferDays(days: number) {
@@ -38,7 +38,7 @@
</template>
<script setup lang="ts">
import {ref, computed, watchEffect, onBeforeUnmount} from 'vue'
import {ref, computed, watch, onBeforeUnmount} from 'vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import Editor from '@/components/input/AsyncEditor'
@@ -46,25 +46,21 @@ import Editor from '@/components/input/AsyncEditor'
import type {ITask} from '@/modelTypes/ITask'
import {useTaskStore} from '@/stores/tasks'
export type AttachmentUploadFunction = (file: File, onSuccess: (attachmentUrl: string) => void) => Promise<string>
type AttachmentUploadFunction = (file: File, onSuccess: (attachmentUrl: string) => void) => Promise<string>
const props = withDefaults(defineProps<{
const {
modelValue,
attachmentUpload,
canWrite,
} = defineProps<{
modelValue: ITask,
attachmentUpload: AttachmentUploadFunction,
canWrite: boolean,
}>(), {
canWrite: true,
})
const emit = defineEmits<{
'update:modelValue': [value: ITask]
}>()
const description = ref<string>('')
watchEffect(() => {
description.value = props.modelValue.description
})
const emit = defineEmits(['update:modelValue'])
const description = ref<string>('')
const saved = ref(false)
// Since loading is global state, this variable ensures we're only showing the saving icon when saving the description.
@@ -73,6 +69,14 @@ 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() {
@@ -95,12 +99,13 @@ 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({
...props.modelValue,
...modelValue,
description: description.value,
})
emit('update:modelValue', updated)
@@ -114,12 +119,13 @@ 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) => {
props.attachmentUpload(file, (uploadedFileUrl: string) => resolve(uploadedFileUrl))
attachmentUpload(file, (uploadedFileUrl: string) => resolve(uploadedFileUrl))
})
uploadPromises.push(promise)
@@ -16,9 +16,8 @@
<template #items="{items}">
<AssigneeList
:assignees="items"
:remove="removeAssignee"
:disabled="disabled"
can-remove
@remove="removeAssignee"
/>
</template>
<template #searchResult="{option: user}">
@@ -183,7 +183,7 @@
</template>
<script setup lang="ts">
import {ref, reactive, shallowReactive, watch, computed} from 'vue'
import {ref, reactive, shallowReactive, watch, computed, type PropType} from 'vue'
import {useI18n} from 'vue-i18n'
import {useRoute} from 'vue-router'
@@ -206,17 +206,27 @@ import {useTaskStore} from '@/stores/tasks'
import {useProjectStore} from '@/stores/projects'
import {playPopSound} from '@/helpers/playPop'
const props = withDefaults(defineProps<{
taskId: number,
initialRelatedTasks?: ITask['relatedTasks'],
showNoRelationsNotice?: boolean,
projectId: number,
editEnabled: boolean,
}>(), {
initialRelatedTasks: () => ({}),
showNoRelationsNotice: false,
projectId: 0,
editEnabled: true, // this seems like a mistake
const props = defineProps({
taskId: {
type: Number,
required: true,
},
initialRelatedTasks: {
type: Object as PropType<ITask['relatedTasks']>,
default: () => ({}),
},
showNoRelationsNotice: {
type: Boolean,
default: false,
},
projectId: {
type: Number,
default: 0,
},
editEnabled: {
type: Boolean,
default: true,
},
})
const taskStore = useTaskStore()
@@ -28,7 +28,7 @@
<RouterLink
v-if="showProject && typeof project !== 'undefined'"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
:to="{ name: 'project', params: { projectId: task.projectId } }"
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
class="task-project mr-1"
:class="{'mr-2': task.hexColor !== ''}"
@click.stop
@@ -139,7 +139,7 @@
<RouterLink
v-if="showProjectSeparately"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
:to="{ name: 'project', params: { projectId: task.projectId } }"
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
class="task-project"
@click.stop
>
@@ -72,7 +72,7 @@ export function useRouteWithModal() {
}
const newRoute = {
name: 'project',
name: 'project.view',
params: {
projectId: baseStore.currentProject?.id,
viewId,
+223
View File
@@ -0,0 +1,223 @@
import { ref, reactive, computed, type MaybeRefOrGetter, toValue, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { klona } from 'klona/lite'
import type { ITask } from '@/modelTypes/ITask'
import type { IProject } from '@/modelTypes/IProject'
import type { Priority } from '@/constants/priorities'
import type {Action as MessageAction} from '@/message'
import TaskModel from '@/models/task'
import { useTaskStore } from '@/stores/tasks'
import { useProjectStore } from '@/stores/projects'
import { useKanbanStore } from '@/stores/kanban'
import { useBaseStore } from '@/stores/base'
import { RIGHTS } from '@/constants/rights'
import { success } from '@/message'
import { playPopSound } from '@/helpers/playPop'
import { TASK_REPEAT_MODES } from '@/types/IRepeatMode'
import { shallowReactive } from 'vue'
import TaskService from '@/services/task'
import { useAttachmentStore } from '@/stores/attachments'
import { storeToRefs } from 'pinia'
import { getProjectTitle } from '@/helpers/getProjectTitle'
import { uploadFile } from '@/helpers/attachments'
export function useTask(taskId: MaybeRefOrGetter<ITask['id']>) {
const {t} = useI18n()
const router = useRouter()
const taskStore = useTaskStore()
const taskTitle = ref('')
// We doubled the task color property here because verte does not have a real change property, leading
// to the color property change being triggered when the # is removed from it, leading to an update,
// which leads in turn to a change... This creates an infinite loop in which the task is updated, changed,
// updated, changed, updated and so on.
// To prevent this, we put the task color property in a seperate value which is set to the task color
// when it is saved and loaded.
const taskColor = ref<ITask['hexColor']>('')
const task = reactive({
...new TaskModel(),
title: taskTitle,
}) as ITask
const taskService = shallowReactive(new TaskService())
/** Avoid flashing of empty elements if the task content is not yet loaded. */
const isReady = ref(false)
const isLoading = computed(() => taskService.loading)
// load task
watch(
() => toValue(taskId),
async (newTaskId) => {
if (newTaskId === undefined) {
return
}
try {
const loaded = await taskService.get({ id: newTaskId })
Object.assign(task, loaded)
attachmentStore.set(task.attachments)
taskColor.value = task.hexColor
setActiveFields()
} finally {
isReady.value = true
}
},
{immediate: true},
)
const canWrite = computed(() => (
task.maxRight !== null &&
task.maxRight > RIGHTS.READ
))
const projectStore = useProjectStore()
const project = computed(() => projectStore.projects[task.projectId])
const ancestorProjects = computed(() => projectStore.getAncestors(project.value))
const ancestorProjectTitles = computed(() => ancestorProjects.value.map(project => getProjectTitle(project)))
const attachmentStore = useAttachmentStore()
const {hasAttachments} = storeToRefs(attachmentStore)
async function saveTask(
currentTask: ITask | null = null,
undoCallback?: () => void,
) {
if (currentTask === null) {
currentTask = klona(task)
}
if (!canWrite.value) {
return
}
currentTask.hexColor = taskColor.value
// If no end date is being set, but a start date and due date,
// use the due date as the end date
if (
currentTask.endDate === null &&
currentTask.startDate !== null &&
currentTask.dueDate !== null
) {
currentTask.endDate = currentTask.dueDate
}
const updatedTask = await taskStore.update(currentTask) // TODO: markraw ?
Object.assign(task, updatedTask)
setActiveFields()
let actions: MessageAction[] = []
if (undoCallback) {
actions = [{
title: t('task.undo'),
callback: undoCallback,
}]
}
success({message: t('task.detail.updateSuccess')}, actions)
}
async function deleteTask() {
await taskStore.delete(task)
success({message: t('task.detail.deleteSuccess')})
return router.push({name: 'project.index', params: {projectId: task.projectId}})
}
function uploadAttachment(file: File, onSuccess?: (url: string) => void) {
return uploadFile(toValue(taskId), file, onSuccess)
}
async function toggleTaskDone() {
const newTask = {
...klona(task),
done: !task.done,
}
if (newTask.done) {
playPopSound()
}
await saveTask(
newTask,
toggleTaskDone,
)
}
async function changeProject(project: IProject) {
const kanbanStore = useKanbanStore()
const baseStore = useBaseStore()
kanbanStore.removeTaskInBucket(task)
await saveTask({
...task,
projectId: project.id,
})
baseStore.setCurrentProject(project)
}
async function toggleFavorite() {
const newTask = await taskStore.toggleFavorite(task)
Object.assign(task, newTask)
}
async function setPriority(priority: Priority) {
const newTask: ITask = {
...task,
priority,
}
return saveTask(newTask)
}
async function setPercentDone(percentDone: number) {
const newTask: ITask = {
...task,
percentDone,
}
return saveTask(newTask)
}
async function removeRepeatAfter() {
task.repeatAfter.amount = 0
task.repeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
await saveTask()
}
return {
project,
ancestorProjects,
ancestorProjectTitles,
isReady,
isLoading,
task,
taskTitle,
taskColor,
canWrite,
hasAttachments,
saveTask,
deleteTask,
uploadAttachment,
toggleTaskDone,
changeProject,
toggleFavorite,
setPriority,
setPercentDone,
removeRepeatAfter,
}
}
+15
View File
@@ -0,0 +1,15 @@
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
}
@@ -1,22 +0,0 @@
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
})
}
-1
View File
@@ -27,7 +27,6 @@ export const SUPPORTED_LOCALES = {
'uk-UA': 'Українська',
'lt-LT': 'Lietuvių Kalba',
'bg-BG': 'Български',
'ko-KR': '한국어',
// IMPORTANT: Also add new languages to useDayjsLanguageSync
} as const
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "تم حذف الحافظة بنجاح.",
"bucketTitleSavedSuccess": "تم حفظ عنوان الحافظة بنجاح.",
"bucketLimitSavedSuccess": "تم حفظ الحد الأقصى للحافظة بنجاح.",
"collapse": "طي هذه الحافظة",
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
"collapse": "طي هذه الحافظة"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"12001": "نوع الاشتراك غير صالح.",
"12002": "أنت مشترك بالفعل في العنصر نفسه أو في العنصر الرئيس.",
"13001": "رابط المشاركة هذا يتطلب كلمة مرور للتوثيق، ولكن لم يتم توفير أي منها.",
"13002": "كلمة مرور رابط المشاركة غير صالحة.",
"14001": "The provided file is not a valid zip file."
"13002": "كلمة مرور رابط المشاركة غير صالحة."
},
"about": {
"title": "عن البرنامج",
@@ -1221,4 +1219,4 @@
"years": "سنة|سنوات"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "Колоната беше успешно изтрита.",
"bucketTitleSavedSuccess": "Заглавието на колоната беше успешно запазено.",
"bucketLimitSavedSuccess": "Лимитът на колоната беше успешно запазен.",
"collapse": "Свий тази колона",
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
"collapse": "Свий тази колона"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"12001": "Типът на абонамента е невалиден.",
"12002": "Вече сте абонирани за самия обект или за родителски обект.",
"13001": "Тази споделена връзка изисква парола за удостоверяване, но такава не е предоставена.",
"13002": "Предоставената парола за споделената връзка е невалидна.",
"14001": "The provided file is not a valid zip file."
"13002": "Предоставената парола за споделената връзка е невалидна."
},
"about": {
"title": "За приложението",
@@ -1221,4 +1219,4 @@
"years": "година|години"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"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."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "The provided link share password was invalid."
},
"about": {
"title": "About",
@@ -1221,4 +1219,4 @@
"years": "year|years"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "Sloupec byl úspěšně smazán.",
"bucketTitleSavedSuccess": "Název sloupce byl úspěšně uložen.",
"bucketLimitSavedSuccess": "Limit sloupce byl úspěšně uložen.",
"collapse": "Sbalit tento sloupec",
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
"collapse": "Sbalit tento sloupec"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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é.",
"14001": "The provided file is not a valid zip file."
"13002": "Zadané heslo pro sdílený odkaz bylo neplatné."
},
"about": {
"title": "O aplikaci",
@@ -1221,4 +1219,4 @@
"years": "rok|roky"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"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."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "Adgangskoden til delingslinket er ugyldig."
},
"about": {
"title": "Om",
@@ -1221,4 +1219,4 @@
"years": "year|years"
}
}
}
}
+4 -6
View File
@@ -324,7 +324,7 @@
"title": "Liste",
"add": "Hinzufügen",
"addPlaceholder": "Aufgabe hinzufügen…",
"empty": "Dieses Projekt ist derzeit leer.",
"empty": "Dieses Project ist derzeit leer.",
"newTaskCta": "Aufgabe erstellen.",
"editTask": "Aufgabe bearbeiten"
},
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "Die Spalte wurde erfolgreich gelöscht.",
"bucketTitleSavedSuccess": "Der Spaltenname wurde erfolgreich gespeichert.",
"bucketLimitSavedSuccess": "Das Spaltenlimit wurde erfolgreich gespeichert.",
"collapse": "Spalte einklappen",
"bucketLimitReached": "Du hast das Limit dieses Buckets erreicht. Entferne Aufgaben oder erhöhe das Limit, um neue Aufgaben hinzuzufügen."
"collapse": "Spalte einklappen"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "Die angegebene Datei ist keine gültige ZIP-Datei."
"13002": "Das Passwort für diese Linkfreigabe war ungültig."
},
"about": {
"title": "Über",
@@ -1221,4 +1219,4 @@
"years": "Jahr|Jahre"
}
}
}
}
+4 -6
View File
@@ -324,7 +324,7 @@
"title": "Liste",
"add": "Hinzufügen",
"addPlaceholder": "Aufgabe hinzufügen…",
"empty": "Dieses Projekt ist derzeit leer.",
"empty": "Dieses Project ist derzeit leer.",
"newTaskCta": "Aufgabe erstellen.",
"editTask": "Aufgabe bearbeiten"
},
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "Die Spalte wurde erfolgreich gelöscht.",
"bucketTitleSavedSuccess": "Der Spaltenname wurde erfolgreich gespeichert.",
"bucketLimitSavedSuccess": "Das Spaltenlimit wurde erfolgreich gespeichert.",
"collapse": "Spalte einklappen",
"bucketLimitReached": "Du hast das Limit dieses Buckets erreicht. Entferne Aufgaben oder erhöhe das Limit, um neue Aufgaben hinzuzufügen."
"collapse": "Spalte einklappen"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "Die angegebene Datei ist keine gültige ZIP-Datei."
"13002": "Das Passwort wo du geh hesch isch ungültig."
},
"about": {
"title": "Über",
@@ -1221,4 +1219,4 @@
"years": "Jahr|Jahre"
}
}
}
}
+1 -2
View File
@@ -1203,8 +1203,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "The provided link share password was invalid."
},
"about": {
"title": "About",
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"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."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "The provided link share password was invalid."
},
"about": {
"title": "About",
@@ -1221,4 +1219,4 @@
"years": "year|years"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "El contenedor se ha eliminado con éxito.",
"bucketTitleSavedSuccess": "El título del contenedor se ha guardado con éxito.",
"bucketLimitSavedSuccess": "El límite del contenedor se ha guardado con éxito.",
"collapse": "Contraer este contenedor",
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
"collapse": "Contraer este contenedor"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "La contraseña proporcionada del enlace compartido no es válida."
},
"about": {
"title": "Acerca de",
@@ -1221,4 +1219,4 @@
"years": "año|años"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "La colonne a bien été supprimée.",
"bucketTitleSavedSuccess": "Le titre de la colonne a bien été enregistré.",
"bucketLimitSavedSuccess": "La limite de la colonne a bien été enregistrée.",
"collapse": "Réduire cette colonne",
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
"collapse": "Réduire cette colonne"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "Mot de passe de partage de lien invalide."
},
"about": {
"title": "À propos",
@@ -1221,4 +1219,4 @@
"years": "année|années"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"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."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "The provided link share password was invalid."
},
"about": {
"title": "About",
@@ -1221,4 +1219,4 @@
"years": "year|years"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "A vödör sikeresen törölve.",
"bucketTitleSavedSuccess": "A vödör címe sikeresen elmentve.",
"bucketLimitSavedSuccess": "A vödör limit sikeresen elmentve.",
"collapse": "Zárd össze ezt a vödröt",
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
"collapse": "Zárd össze ezt a vödröt"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "A megadott linkmegosztási jelszó érvénytelen."
},
"about": {
"title": "Névjegy",
@@ -1221,4 +1219,4 @@
"years": "év|évek"
}
}
}
}
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "バケットは正常に削除されました。",
"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."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "The provided link share password was invalid."
},
"about": {
"title": "Vikunjaについて",
@@ -1221,4 +1219,4 @@
"years": "年"
}
}
}
}
File diff suppressed because it is too large Load Diff
+122 -124
View File
@@ -4,85 +4,85 @@
"welcomeMorning": "Labas rytas {username}!",
"welcomeDay": "Labas {username}!",
"welcomeEvening": "Labas vakaras {username}!",
"lastViewed": "Paskutinį kartą žiūrėta",
"addToHomeScreen": "Pridėkite aplikaciją į darbalaukį, lengvesniam pasiekiamumui ir geresnei patirčiai.",
"goToOverview": "Eiti į apžvalgą",
"lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"goToOverview": "Go to overview",
"project": {
"importText": "Importuokite savo projektus ir užduotis iš kitų tiekėjų į Vikunja:",
"import": "Importuoti duomenis į Vikunja"
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "Ši aplinka yra demontracinės būsenos. Nenaudokite realių duomenų!",
"everythingWillBeDeleted": "Viskas bus ištrinta numatomais intervalais!",
"accountWillBeDeleted": "Tavo paskyra bus ištrinta, įskaitant visus projektus, užduotis ir prisegtukus su tuo susijusius."
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Nerasta",
"text": "Norimas puslapis nerastas."
"title": "Not found",
"text": "The page you requested does not exist."
},
"ready": {
"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:"
"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:"
},
"offline": {
"title": "Esi atsijungęs(-usi).",
"text": "Prašau patikrinti interneto ryšį ir bandyti dar kartą."
"title": "You are offline.",
"text": "Please check your network connection and try again."
},
"user": {
"auth": {
"username": "Vartotojo vardas",
"usernameEmail": "Vartotojo vardas arba el. pašto adresas",
"usernamePlaceholder": "pvz.: frederick",
"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ą.",
"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.",
"usernameRequired": "Prašau nurodyti vartotojo vardą.",
"usernameMustNotContainSpace": "Vartotojo vardas negali turėti tarpų.",
"usernameMustNotLookLikeUrl": "Vartotojo vardas negali atrodyti kaip nuoroda.",
"passwordRequired": "Prašome pateikti savo slaptažodį.",
"passwordNotMin": "Slaptažodis turi būti bent 8 simbolių.",
"passwordRequired": "Please provide a password.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Slaptažodis gali būti daugiausiai 72 simbolių.",
"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."
"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."
},
"settings": {
"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.",
"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.",
"general": {
"title": "Bendriniai nustatymai",
"title": "General Settings",
"name": "Mano vardas",
"newName": "Naujas vardas",
"savedSuccess": "The settings were successfully updated.",
@@ -185,15 +185,15 @@
"title": "Delete your Vikunja Account",
"text1": "Profilio ištrynimas yra neatšaukiamas. Visi projektai, užduotys ir kita susijusi informacijas bus pašalinta.",
"text2": "Norėdama -s tęsti, įvesk savo slaptažodį. Gausi email'ą su tolimesnėmis instrukcijomis.",
"text3": "Norėdami tęsti, spauskite mygtuką žemiau. Gausite el. laišką su tolimesnėmis instrukcijomis.",
"text3": "To proceed, please press the button below. You will receive an email with further instructions.",
"confirm": "Delete my account",
"requestSuccess": "Tavo užklausa sėkminga. Gausi email'ą su tolimesnėmis instrukcijomis.",
"passwordRequired": "Prašome įvesti savo slaptažodį.",
"confirmSuccess": "Sėkmingai patvirtinai paskyros ištrynimą. Ištrinsime tavo paskyrą per tris dienas.",
"scheduled": "Ištrinsime tavo Vikunja paskyrą {date} ({dateSince}).",
"confirmSuccess": "You've successfully confirmed the deletion of your account. We will delete your account in three days.",
"scheduled": "We will delete your Vikunja account at {date} ({dateSince}).",
"scheduledCancel": "Norėdama -s nutraukti paskyros ištrynimą, spausk čia.",
"scheduledCancelText": "Norėdama -s nutraukti paskyros ištrynimą, įveskite slaptažodį žemiau:",
"scheduledCancelButton": "Norint atšaukti paskyros ištynimą, prašome spausti mygtuką žemiau:",
"scheduledCancelButton": "To cancel the deletion of your account, please press the button below:",
"scheduledCancelConfirm": "Atšaukti mano paskyros ištrynimą",
"scheduledCancelSuccess": "We will not delete your account."
},
@@ -264,7 +264,7 @@
"header": "Edit This Project",
"title": "Edit \"{project}\"",
"titlePlaceholder": "The project title goes here…",
"identifierTooltip": "Projekto Id gali būti naudojamas unikaliai identifikuoti užduotį tarp projektų. Galima palikti tuščia, norint funkciją deaktyvuoti.",
"identifierTooltip": "The project identifier can be used to uniquely identify a task across projects. You can set it to empty to disable it.",
"identifier": "Projekto identifikatorius",
"identifierPlaceholder": "Projekto identifikatorius bus čia…",
"description": "Aprašymas",
@@ -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": "Nuorodos dalinimas buvo sėkmingai ištrintas",
"deleteSuccess": "The link share was successfully deleted",
"view": "Peržiūra",
"sharedBy": "Pasidalino {0}"
},
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"bucketTitleSavedSuccess": "The bucket title has been saved successfully.",
"bucketLimitSavedSuccess": "The bucket limit been saved successfully.",
"collapse": "Collapse this bucket",
"bucketLimitReached": "Pasiekėte buketo limitą. Ištrinkite užduotis arba padidinkite limitą norėdami pridėti naujas užduotis."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -463,11 +462,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": "Laikas ir data, kai užduotis buvo sukurta",
"created": "The time and date when the task was created",
"updated": "Data ir laikas, kada užduotis buvo pakeista"
},
"operators": {
"intro": "Filtravimo operandai:",
"intro": "The available operators for filtering include:",
"notEqual": "Ne lygu",
"equal": "Lygu",
"greaterThan": "Daugiau už",
@@ -480,8 +479,8 @@
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "IR operatorius, sutampa jei visos sąlygos yra tiesa",
"or": "ARBA operatorius, sutampa jei bet kuri sąlyga yra tiesa",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Skliausteliai sąlygų grupavimui"
},
"examples": {
@@ -506,10 +505,10 @@
"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": "Aš įsitikinęs, prašau pradėti migraciją dabar!",
"confirm": "I am sure, please start migrating now!",
"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ą.",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
},
"label": {
@@ -684,34 +683,34 @@
"endOfToday": "Iki dienos pabaigos",
"beginningOflastWeek": "Praeitos savaitės pradžios",
"endOfLastWeek": "Praeitos savaitės pabaiga",
"beginningOfThisWeek": "Šios savaitės pradžia",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "Šios savaaitės pabaaiga",
"startOfNextWeek": "Kitos savaitės pradžia",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "Iki kitos savaitės pabaigos",
"in7Days": "Už 7 dienų",
"beginningOfLastMonth": "Praeito mėnesio pradžia",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "Kito mėnesio pabaaigos",
"startOfThisMonth": "Šio mėnesio pradžia",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Kito mėnesio pradžia",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "Kito mėnesio pabaiga",
"in30Days": "In 30 days",
"startOfThisYear": "Šių metų pradžia",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "Šių metų pabaiga"
}
},
"datemathHelp": {
"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.",
"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.",
"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": "Atimti vieną dieną",
"roundDay": "Suapvalinti iki artimiausios dienos",
"supportedUnits": "Palaikomi laiko formatai",
"someExamples": "Laiko išraiškų pavyzdžiai",
"minus1Day": "Subtract one day",
"roundDay": "Round down to the nearest day",
"supportedUnits": "Supported time units",
"someExamples": "Examples of time expressions",
"units": {
"seconds": "Seconds",
"minutes": "Minutes",
@@ -725,10 +724,10 @@
"now": "Dabar",
"in24h": "24-ių valandų bėgyje",
"today": "Šiandien 00:00",
"beginningOfThisWeek": "Šios savaitės pradžia 00:00 val.",
"endOfThisWeek": "Šios savaitės pabaiga",
"beginningOfThisWeek": "The beginning of this week at 00:00",
"endOfThisWeek": "The end of this week",
"in30Days": "Už 30 dienų",
"datePlusMonth": "{0} plius veianas mėnuo nuo 00:00 tos dienos"
"datePlusMonth": "{0} plus one month at 00:00 of that day"
}
}
},
@@ -736,14 +735,14 @@
"task": "Užduotis",
"new": "Sukurti užduotį",
"delete": "Ištrinti šią užduotį",
"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",
"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",
"openDetail": "Atidaryti užduoties duomenų vaizdą",
"checklistTotal": "{checked} iš {total} užduočių",
"checklistTotal": "{checked} of {total} tasks",
"checklistAllDone": "{total} užduotys",
"show": {
"titleCurrent": "Dabartinės Užduotys",
@@ -752,17 +751,17 @@
"overdue": "Rodyti neišpildytas užduotis",
"fromuntil": "Užduotys nuo {from} iki {until}",
"select": "Pasirinkti datos rėžį",
"noTasks": "Nėra darbų - Gražios dienos!"
"noTasks": "Nothing to do — Have a nice day!"
},
"detail": {
"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}",
"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}",
"doneAt": "Done {0}",
"updateSuccess": "The task was saved successfully.",
"deleteSuccess": "The task has been deleted successfully.",
@@ -900,23 +899,23 @@
"new": "New Task Relation",
"searchPlaceholder": "Type search for a task to add as related…",
"createPlaceholder": "Pridėti kaip susijusią užduotį",
"differentProject": "Ši užduotis priklauso kitam projektui.",
"differentProject": "This task belongs to a different project.",
"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": "Prašome pasirinkti užduotį arba įvesti užduoties pavadinimą.",
"taskRequired": "Please select a task or enter a new task title.",
"kinds": {
"subtask": "Sub užduotis | Sub užduotys",
"parenttask": "Tėvinė Užduotis | Tėvinė Užduotis",
"related": "Susijusi Užduotis | Susijusi Užduotis",
"parenttask": "Parent Task | Parent Tasks",
"related": "Related Task | Related Tasks",
"duplicateof": "Duplicate Of | Duplicates Of",
"duplicates": "Duplicates | Duplicates",
"blocking": "Blocking | Blocking",
"blocked": "Blocked By | Blocked By",
"precedes": "Precedes | Precedes",
"follows": "Follows | Follows",
"copiedfrom": "Nukopijuota iš | Nukopijuota iš",
"copiedfrom": "Copied From | Copied From",
"copiedto": "Copied To | Copied To"
}
},
@@ -951,13 +950,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": "Galima tai naudoti kelis kartus.",
"multiple": "You can use this multiple times.",
"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": "Kuo didesnis skaičius, tuo aukštesnis prioritetas.",
"priority2": "The higher the number, the higher the priority.",
"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 +986,7 @@
"addUser": "Add to team",
"makeMember": "Make Member",
"makeAdmin": "Make Admin",
"success": "Komanda buvo sėkmingai atnaujinta.",
"success": "The team was successfully updated.",
"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 +1006,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "Prarasite visas teises, kurias turi ši komanda. Jei persigalvotum, reikės komandos administratoriaus pagalbos, kad pridėtų atgal.",
"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.",
"success": "Jūs sėkmingai palikote komandą."
}
},
@@ -1027,10 +1026,10 @@
"title": "Keyboard Shortcuts",
"general": "General",
"allPages": "These shortcuts work on all pages.",
"currentPageOnly": "Esami trumpiniai veikia tik dabartiniame puslapyje.",
"somePagesOnly": "Šie trumpiniai veikia tik kai kuriuose puslapiuose.",
"currentPageOnly": "These shortcuts work only on the current page.",
"somePagesOnly": "These shortcuts work only on some pages.",
"toggleMenu": "Toggle The Menu",
"quickSearch": "Atidaryti paiešką/greitų veiksmų juostą",
"quickSearch": "Open the search/quick action bar",
"then": "then",
"task": {
"title": "Task Page",
@@ -1041,7 +1040,7 @@
"attachment": "Add an attachment to this task",
"related": "Modify related tasks of this task",
"color": "Change the color of this task",
"move": "Perkelti šią užduotį kitam projektui",
"move": "Move this task to another project",
"reminder": "Valdyti šios užduoties priminimus",
"description": "Praskleisti redagavimą užduoties apraše",
"delete": "Delete this task",
@@ -1087,7 +1086,7 @@
"url": "Vikunja URL",
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"use": "Naudojama Vikunja instaliacija {0}",
"use": "Using Vikunja installation at {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 +1129,7 @@
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} ir {lastUser} sureagavo {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
@@ -1185,7 +1184,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": "Negalime ištrinti paskutinio komandos nario.",
"6006": "Cannot delete the last team member.",
"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,11 +1199,10 @@
"10005": "There can be only one done bucket per project.",
"11001": "Išsaugotas filtras neegzistuoja.",
"11002": "Išsaugotu filtru negalima pasidalinti.",
"12001": "Prenumeratos objekto tipas yra nevalidus.",
"12002": "Esi prisiregistravęs prie visko ar prie tėvinio subjekto.",
"12001": "The subscription entity type is invalid.",
"12002": "You are already subscribed to the entity itself or a parent entity.",
"13001": "Šis dalinimasis nuoroda reikalauja slaptažodžio identifikavimuisi, bet jis nebuvo pateiktas.",
"13002": "Pateikta slaptažodžio dalinimosi nuoroda negalioja.",
"14001": "The provided file is not a valid zip file."
"13002": "Pateikta slaptažodžio dalinimosi nuoroda negalioja."
},
"about": {
"title": "Apie",
@@ -1221,4 +1219,4 @@
"years": "metai|metai"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"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."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "The provided link share password was invalid."
},
"about": {
"title": "Over",
@@ -1221,4 +1219,4 @@
"years": "year|years"
}
}
}
}
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "Kolumna została pomyślnie usunięta.",
"bucketTitleSavedSuccess": "Tytuł kolumny został pomyślnie zapisany.",
"bucketLimitSavedSuccess": "Limit kolumny został pomyślnie zapisany.",
"collapse": "Zwiń tę kolumnę",
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
"collapse": "Zwiń tę kolumnę"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "Podane hasło udostępnionego linku było nieprawidłowe."
},
"about": {
"title": "O aplikacji",
@@ -1221,4 +1219,4 @@
"years": "rok|lat"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "O bucket foi excluído com sucesso.",
"bucketTitleSavedSuccess": "O título do bucket foi salvo com sucesso.",
"bucketLimitSavedSuccess": "O limite do bucket foi salvo com sucesso.",
"collapse": "Recolher este bucket",
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
"collapse": "Recolher este bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "A senha de compartilhamento do link fornecido é inválida."
},
"about": {
"title": "Sobre",
@@ -1221,4 +1219,4 @@
"years": "ano|anos"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "O conjunto foi eliminado com sucesso.",
"bucketTitleSavedSuccess": "O título do conjunto foi salvo com sucesso.",
"bucketLimitSavedSuccess": "O limite do conjunto foi salvo com sucesso.",
"collapse": "Encolher este conjunto",
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
"collapse": "Encolher este conjunto"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "A palavra-passe fornecida para o link partilhado é inválida."
},
"about": {
"title": "Sobre",
@@ -1221,4 +1219,4 @@
"years": "ano|anos"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"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."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "The provided link share password was invalid."
},
"about": {
"title": "About",
@@ -1221,4 +1219,4 @@
"years": "year|years"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "Колонка удалена.",
"bucketTitleSavedSuccess": "Название колонки сохранено.",
"bucketLimitSavedSuccess": "Лимит колонки сохранён.",
"collapse": "Свернуть эту колонку",
"bucketLimitReached": "Вы достигли лимита колонки. Удалите какие-нибудь задачи или увеличьте лимит, чтобы добавить новые задачи."
"collapse": "Свернуть эту колонку"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"12001": "Некорректный тип подписки.",
"12002": "Вы уже подписаны на сам объект или родительский объект.",
"13001": "Эта ссылка требует пароля, но он не был предоставлен.",
"13002": "Неверный пароль ссылки для обмена.",
"14001": "The provided file is not a valid zip file."
"13002": "Неверный пароль ссылки для обмена."
},
"about": {
"title": "О Vikunja",
@@ -1221,4 +1219,4 @@
"years": "год|года|лет"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"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."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "The provided link share password was invalid."
},
"about": {
"title": "About",
@@ -1221,4 +1219,4 @@
"years": "year|years"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "Vedro je bilo uspešno izbrisano.",
"bucketTitleSavedSuccess": "Naslov vedra je bil uspešno shranjen.",
"bucketLimitSavedSuccess": "Omejitev vedra je bila uspešno shranjena.",
"collapse": "Zloži vedro",
"bucketLimitReached": "Dosegli ste omejitev vedra. Odstranite opravila ali povečajte omejitev, da dodate nova opravila."
"collapse": "Zloži vedro"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "Navedeno geslo za skupno rabo povezave je bilo neveljavno."
},
"about": {
"title": "O programu",
@@ -1221,4 +1219,4 @@
"years": "leto|let"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"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."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "The provided link share password was invalid."
},
"about": {
"title": "About",
@@ -1221,4 +1219,4 @@
"years": "year|years"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"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."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "The provided link share password was invalid."
},
"about": {
"title": "Om",
@@ -1221,4 +1219,4 @@
"years": "år|år"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"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."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "The provided link share password was invalid."
},
"about": {
"title": "About",
@@ -1221,4 +1219,4 @@
"years": "year|years"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "Стрічку вилучено.",
"bucketTitleSavedSuccess": "Назву стрічки збережено.",
"bucketLimitSavedSuccess": "Межу стрічки збережено.",
"collapse": "Згорнути стрічку",
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
"collapse": "Згорнути стрічку"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"12001": "The subscription entity type is invalid.",
"12002": "Ви підписані на саму суть або головну частину.",
"13001": "This link share requires a password for authentication, but none was provided.",
"13002": "Хибний пароль для поширеного посилання.",
"14001": "The provided file is not a valid zip file."
"13002": "Хибний пароль для поширеного посилання."
},
"about": {
"title": "Про",
@@ -1221,4 +1219,4 @@
"years": "рік|роки|років"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"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."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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ệ.",
"14001": "The provided file is not a valid zip file."
"13002": "Mật khẩu chia sẻ liên kết không hợp lệ."
},
"about": {
"title": "Về ứng dụng",
@@ -1221,4 +1219,4 @@
"years": "year|years"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "存储桶已删除。",
"bucketTitleSavedSuccess": "存储桶标题已保存。",
"bucketLimitSavedSuccess": "存储桶限制已保存。",
"collapse": "折叠此存储桶",
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
"collapse": "折叠此存储桶"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"12001": "订阅实体类型无效。",
"12002": "你已经订阅实体本身或上级实体。",
"13001": "此链接共享需要密码进行身份验证,但没有提供密码。",
"13002": "提供的链接共享密码无效。",
"14001": "The provided file is not a valid zip file."
"13002": "提供的链接共享密码无效。"
},
"about": {
"title": "关于",
@@ -1221,4 +1219,4 @@
"years": "年|年"
}
}
}
}
+3 -5
View File
@@ -365,8 +365,7 @@
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"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."
"collapse": "Collapse this bucket"
},
"pseudo": {
"favorites": {
@@ -1203,8 +1202,7 @@
"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.",
"14001": "The provided file is not a valid zip file."
"13002": "The provided link share password was invalid."
},
"about": {
"title": "About",
@@ -1221,4 +1219,4 @@
"years": "year|years"
}
}
}
}
+1 -3
View File
@@ -27,7 +27,6 @@ export const DAYJS_LOCALE_MAPPING = {
'uk-UA': 'uk',
'lt-LT': 'lt',
'bg-BG': 'bg',
'ko-KR': 'ko',
} as Record<SupportedLocale, ISOLanguage>
export const DAYJS_LANGUAGE_IMPORTS = {
@@ -52,8 +51,7 @@ export const DAYJS_LANGUAGE_IMPORTS = {
'pt-br': () => import('dayjs/locale/pt-br'),
'uk-ua': () => import('dayjs/locale/uk'),
'lt-lt': () => import('dayjs/locale/lt'),
'bg-bg': () => import('dayjs/locale/bg'),
'ko-kr': () => import('dayjs/locale/ko'),
'bg-bg': () => import('dayjs/locale/bg'),
} as Record<SupportedLocale, () => Promise<ILocale>>
export function useDayjsLanguageSync(dayjsGlobal: typeof dayjs) {
+17 -17
View File
@@ -73,26 +73,26 @@ setLanguage(browserLanguage).then(() => {
error(err)
}
// if (import.meta.env.DEV) {
// app.config.warnHandler = (msg) => {
// error(msg)
// throw msg
// }
if (import.meta.env.DEV) {
app.config.warnHandler = (msg) => {
error(msg)
throw msg
}
// // https://stackoverflow.com/a/52076738/15522256
// window.addEventListener('error', (err) => {
// error(err)
// throw err
// })
// https://stackoverflow.com/a/52076738/15522256
window.addEventListener('error', (err) => {
error(err)
throw err
})
// window.addEventListener('unhandledrejection', (err) => {
// // event.promise contains the promise object
// // event.reason contains the reason for the rejection
// error(err)
// throw err
// })
// }
window.addEventListener('unhandledrejection', (err) => {
// event.promise contains the promise object
// event.reason contains the reason for the rejection
error(err)
throw err
})
}
app.config.globalProperties.$message = {
error,
-27
View File
@@ -1,27 +0,0 @@
<template>
<ShowTasks v-bind="props" />
</template>
<script setup lang="ts">
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
import ShowTasks from '@/components/tasks/ShowTasks.vue';
definePage({
name: 'tasks.range',
props: route => ({
dateFrom: parseDateOrString(route.query.from as string, new Date()),
dateTo: parseDateOrString(route.query.to as string, getNextWeekDate()),
showNulls: route.query.showNulls === 'true',
showOverdue: route.query.showOverdue === 'true',
})
})
const props = defineProps<{
dateFrom?: Date | string,
dateTo?: Date | string,
showNulls?: boolean,
showOverdue?: boolean,
}>()
</script>
+368 -26
View File
@@ -1,25 +1,74 @@
import { createRouter, createWebHistory, type RouteLocation, type RouteRecordInfo, type ParamValue } from 'vue-router'
import { routes, handleHotUpdate } from 'vue-router/auto-routes'
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteLocation } from 'vue-router'
import {saveLastVisited} from '@/helpers/saveLastVisited'
import {getProjectViewId} from '@/helpers/projectView'
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash'
import {useAuthStore} from '@/stores/auth'
import {useBaseStore} from '@/stores/base'
import Register from '@/views/user/Register.vue'
import LinkSharingAuth from '@/views/sharing/LinkSharingAuth.vue'
import HomeComponent from '@/views/Home.vue'
import NotFoundComponent from '@/views/404.vue'
const About = () => import('@/views/About.vue')
// User Handling
import LoginComponent from '@/views/user/Login.vue'
import RegisterComponent from '@/views/user/Register.vue'
import OpenIdAuth from '@/views/user/OpenIdAuth.vue'
const DataExportDownload = () => import('@/views/user/DataExportDownload.vue')
// Tasks
import UpcomingTasksComponent from '@/views/tasks/ShowTasks.vue'
import LinkShareAuthComponent from '@/views/sharing/LinkSharingAuth.vue'
const TaskDetailView = () => import('@/views/tasks/TaskDetailView.vue')
declare module 'vue-router/auto-routes' {
export interface RouteNamedMap {
'/register': RouteRecordInfo<'/register', '/register', {}, {}>,
'/share/[share]/auth': RouteRecordInfo<'/share/[share]/auth', '/share/:share/auth', { share: ParamValue<true> }, { share: ParamValue<false> }>,
// Team Handling
const ListTeamsComponent = () => import('@/views/teams/ListTeams.vue')
// Label Handling
const ListLabelsComponent = () => import('@/views/labels/ListLabels.vue')
const NewLabelComponent = () => import('@/views/labels/NewLabel.vue')
// Migration
const MigrationComponent = () => import('@/views/migrate/Migration.vue')
const MigrationHandlerComponent = () => import('@/views/migrate/MigrationHandler.vue')
// Project View
import ProjectView from '@/views/project/ProjectView.vue'
const ProjectInfo = () => import('@/views/project/ProjectInfo.vue')
}
}
// Project Settings
const ListProjects = () => import('@/views/project/ListProjects.vue')
const ProjectSettingEdit = () => import('@/views/project/settings/ProjectSettingsEdit.vue')
const ProjectSettingBackground = () => import('@/views/project/settings/ProjectSettingsBackground.vue')
const ProjectSettingDuplicate = () => import('@/views/project/settings/ProjectSettingsDuplicate.vue')
const ProjectSettingShare = () => import('@/views/project/settings/ProjectSettingsShare.vue')
const ProjectSettingWebhooks = () => import('@/views/project/settings/ProjectSettingsWebhooks.vue')
const ProjectSettingDelete = () => import('@/views/project/settings/ProjectSettingsDelete.vue')
const ProjectSettingArchive = () => import('@/views/project/settings/ProjectSettingsArchive.vue')
const ProjectSettingViews = () => import('@/views/project/settings/ProjectSettingsViews.vue')
console.log(routes)
// Saved Filters
const FilterNew = () => import('@/views/filters/FilterNew.vue')
const FilterEdit = () => import('@/views/filters/FilterEdit.vue')
const FilterDelete = () => import('@/views/filters/FilterDelete.vue')
const PasswordResetComponent = () => import('@/views/user/PasswordReset.vue')
const GetPasswordResetComponent = () => import('@/views/user/RequestPasswordReset.vue')
const UserSettingsComponent = () => import('@/views/user/Settings.vue')
const UserSettingsAvatarComponent = () => import('@/views/user/settings/Avatar.vue')
const UserSettingsCaldavComponent = () => import('@/views/user/settings/Caldav.vue')
const UserSettingsDataExportComponent = () => import('@/views/user/settings/DataExport.vue')
const UserSettingsDeletionComponent = () => import('@/views/user/settings/Deletion.vue')
const UserSettingsEmailUpdateComponent = () => import('@/views/user/settings/EmailUpdate.vue')
const UserSettingsGeneralComponent = () => import('@/views/user/settings/General.vue')
const UserSettingsPasswordUpdateComponent = () => import('@/views/user/settings/PasswordUpdate.vue')
const UserSettingsTOTPComponent = () => import('@/views/user/settings/TOTP.vue')
const UserSettingsApiTokensComponent = () => import('@/views/user/settings/ApiTokens.vue')
// Project Handling
const NewProjectComponent = () => import('@/views/project/NewProject.vue')
const EditTeamComponent = () => import('@/views/teams/EditTeam.vue')
const NewTeamComponent = () => import('@/views/teams/NewTeam.vue')
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -38,23 +87,147 @@ const router = createRouter({
return {left: 0, top: 0}
},
routes: [
...routes,
{
path: '/',
name: 'home',
component: HomeComponent,
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: NotFoundComponent,
},
// if you omit the last `*`, the `/` character in params will be encoded when resolving or pushing
{
path: '/:pathMatch(.*)',
name: 'bad-not-found',
component: NotFoundComponent,
},
{
path: '/login',
name: 'user.login',
component: LoginComponent,
meta: {
title: 'user.auth.login',
},
},
{
path: '/get-password-reset',
name: 'user.password-reset.request',
component: GetPasswordResetComponent,
meta: {
title: 'user.auth.resetPassword',
},
},
{
path: '/password-reset',
name: 'user.password-reset.reset',
component: PasswordResetComponent,
meta: {
title: 'user.auth.resetPassword',
},
},
{
path: '/register',
name: 'user.register',
// FIXME: use dynamic imports
// component: () => import('@/views/user/Register.vue'),
component: Register,
component: RegisterComponent,
meta: {
title: 'user.auth.createAccount',
},
},
{
path: '/user/settings',
name: 'user.settings',
component: UserSettingsComponent,
redirect: {name: 'user.settings.general'},
children: [
{
path: '/user/settings/avatar',
name: 'user.settings.avatar',
component: UserSettingsAvatarComponent,
},
{
path: '/user/settings/caldav',
name: 'user.settings.caldav',
component: UserSettingsCaldavComponent,
},
{
path: '/user/settings/data-export',
name: 'user.settings.data-export',
component: UserSettingsDataExportComponent,
},
{
path: '/user/settings/deletion',
name: 'user.settings.deletion',
component: UserSettingsDeletionComponent,
},
{
path: '/user/settings/email-update',
name: 'user.settings.email-update',
component: UserSettingsEmailUpdateComponent,
},
{
path: '/user/settings/general',
name: 'user.settings.general',
component: UserSettingsGeneralComponent,
},
{
path: '/user/settings/password-update',
name: 'user.settings.password-update',
component: UserSettingsPasswordUpdateComponent,
},
{
path: '/user/settings/totp',
name: 'user.settings.totp',
component: UserSettingsTOTPComponent,
},
{
path: '/user/settings/api-tokens',
name: 'user.settings.apiTokens',
component: UserSettingsApiTokensComponent,
},
{
path: '/user/settings/migrate',
name: 'migrate.start',
component: MigrationComponent,
},
{
path: '/migrate/:service',
name: 'migrate.service',
component: MigrationHandlerComponent,
props: route => ({
service: route.params.service as string,
code: route.query.code as string,
}),
},
],
},
{
path: '/user/export/download',
name: 'user.export.download',
component: DataExportDownload,
},
{
path: '/share/:share/auth',
name: 'link-share.auth',
// FIXME: use dynamic imports
// component: () => import('@/views/sharing/LinkSharingAuth.vue'),
component: LinkSharingAuth,
component: LinkShareAuthComponent,
},
{
path: '/tasks/:id',
name: 'task.detail',
component: TaskDetailView,
props: route => ({ taskId: Number(route.params.id as string) }),
},
{
path: '/tasks/by/upcoming',
name: 'tasks.range',
component: UpcomingTasksComponent,
props: route => ({
dateFrom: parseDateOrString(route.query.from as string, new Date()),
dateTo: parseDateOrString(route.query.to as string, getNextWeekDate()),
showNulls: route.query.showNulls === 'true',
showOverdue: route.query.showOverdue === 'true',
}),
},
{
// Redirect old list routes to the respective project routes
@@ -69,10 +242,98 @@ const router = createRouter({
}
},
},
{
path: '/projects',
name: 'projects.index',
component: ListProjects,
},
{
path: '/projects/new',
name: 'project.create',
component: NewProjectComponent,
meta: {
showAsModal: true,
},
},
{
path: '/projects/:parentProjectId/new',
name: 'project.createFromParent',
component: NewProjectComponent,
props: route => ({ parentProjectId: Number(route.params.parentProjectId as string) }),
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/edit',
name: 'project.settings.edit',
component: ProjectSettingEdit,
props: route => ({ projectId: Number(route.params.projectId as string) }),
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/background',
name: 'project.settings.background',
component: ProjectSettingBackground,
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/duplicate',
name: 'project.settings.duplicate',
component: ProjectSettingDuplicate,
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/share',
name: 'project.settings.share',
component: ProjectSettingShare,
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/webhooks',
name: 'project.settings.webhooks',
component: ProjectSettingWebhooks,
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/delete',
name: 'project.settings.delete',
component: ProjectSettingDelete,
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/archive',
name: 'project.settings.archive',
component: ProjectSettingArchive,
meta: {
showAsModal: true,
},
},
{
path: '/projects/:projectId/settings/views',
name: 'project.settings.views',
component: ProjectSettingViews,
meta: {
showAsModal: true,
},
props: route => ({ projectId: Number(route.params.projectId as string) }),
},
{
path: '/projects/:projectId/settings/edit',
name: 'filter.settings.edit',
component: () => import('@/views/filters/FilterEdit.vue'),
component: FilterEdit,
meta: {
showAsModal: true,
},
@@ -81,20 +342,101 @@ const router = createRouter({
{
path: '/projects/:projectId/settings/delete',
name: 'filter.settings.delete',
component: () => import('@/views/filters/FilterDelete.vue'),
component: FilterDelete,
meta: {
showAsModal: true,
},
props: route => ({ projectId: Number(route.params.projectId as string) }),
},
{
path: '/projects/:projectId/info',
name: 'project.info',
component: ProjectInfo,
meta: {
showAsModal: true,
},
props: route => ({ projectId: Number(route.params.projectId as string) }),
},
{
path: '/projects/:projectId',
name: 'project.index',
redirect(to) {
const viewId = getProjectViewId(Number(to.params.projectId as string))
if (viewId) {
console.debug('Replaced list view with', viewId)
}
return {
name: 'project.view',
params: {
projectId: parseInt(to.params.projectId as string),
viewId: viewId ?? 0,
},
}
},
},
{
path: '/projects/:projectId/:viewId',
name: 'project.view',
component: ProjectView,
props: route => ({
projectId: parseInt(route.params.projectId as string),
viewId: route.params.viewId ? parseInt(route.params.viewId as string): undefined,
}),
},
{
path: '/teams',
name: 'teams.index',
component: ListTeamsComponent,
},
{
path: '/teams/new',
name: 'teams.create',
component: NewTeamComponent,
meta: {
showAsModal: true,
},
},
{
path: '/teams/:id/edit',
name: 'teams.edit',
component: EditTeamComponent,
},
{
path: '/labels',
name: 'labels.index',
component: ListLabelsComponent,
},
{
path: '/labels/new',
name: 'labels.create',
component: NewLabelComponent,
meta: {
showAsModal: true,
},
},
{
path: '/filters/new',
name: 'filters.create',
component: FilterNew,
meta: {
showAsModal: true,
},
},
{
path: '/auth/openid/:provider',
name: 'openid.auth',
component: OpenIdAuth,
},
{
path: '/about',
name: 'about',
component: About,
},
],
})
// Update routes at runtime without reloading the page
if (import.meta.hot) {
handleHotUpdate(router)
}
export async function getAuthForRoute(to: RouteLocation, authStore) {
if (authStore.authUser || authStore.authLinkShare) {
return
+1 -1
View File
@@ -87,7 +87,7 @@ export function useSavedFilter(projectId?: MaybeRefOrGetter<IProject['id']>) {
async function createFilter() {
filter.value = await filterService.create(filter.value)
await projectStore.loadAllProjects()
router.push({name: 'project', params: {projectId: getProjectId(filter.value)}})
router.push({name: 'project.index', params: {projectId: getProjectId(filter.value)}})
}
async function saveFilter() {
+6 -18
View File
@@ -36,18 +36,6 @@ 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()
@@ -198,7 +186,6 @@ 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)
@@ -213,7 +200,6 @@ 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()
@@ -420,15 +406,17 @@ 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
const fullProvider: IProvider = configStore.auth.openidConnect.providers.find((p: IProvider) => p.key === loggedInVia)
if (fullProvider) {
redirectToProviderOnLogout(fullProvider)
if (
configStore.auth.local.enabled === false &&
configStore.auth.openidConnect.enabled &&
configStore.auth.openidConnect.providers?.length === 1)
{
redirectToProviderOnLogout(configStore.auth.openidConnect.providers[0])
}
}
+19 -39
View File
@@ -1,14 +1,11 @@
import {ref, computed, readonly} from 'vue'
import {useI18n} from 'vue-i18n'
import {useRouter, useRoute} from 'vue-router'
import { readonly, ref} from 'vue'
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, ERROR_NO_API_URL, InvalidApiUrlProvidedError, NoApiUrlProvidedError} from '@/helpers/checkAndSetApiUrl'
import ProjectService from '../services/project'
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
import {useMenuActive} from '@/composables/useMenuActive'
@@ -18,16 +15,8 @@ import type {Right} from '@/constants/rights'
import type {IProjectView} from '@/modelTypes/IProjectView'
export const useBaseStore = defineStore('base', () => {
const authStore = useAuthStore()
const {t} = useI18n()
const router = useRouter()
const route = useRoute()
const loading = ref(false)
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({
@@ -44,6 +33,10 @@ 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
@@ -98,6 +91,10 @@ export const useBaseStore = defineStore('base', () => {
logoVisible.value = visible
}
function setReady(value: boolean) {
ready.value = value
}
function setUpdateAvailable(value: boolean) {
updateAvailable.value = value
}
@@ -142,36 +139,16 @@ export const useBaseStore = defineStore('base', () => {
setCurrentProject(project, currentProjectViewId)
}
const authStore = useAuthStore()
async function loadApp() {
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)
}
await checkAndSetApiUrl(window.API_URL)
await authStore.checkAuth()
ready.value = true
}
loadApp()
return {
error: readonly(error),
loading: readonly(loading),
ready: readonly(ready),
loadApp,
currentProject: readonly(currentProject),
currentProjectViewId: readonly(currentProjectViewId),
background: readonly(background),
@@ -182,6 +159,8 @@ export const useBaseStore = defineStore('base', () => {
logoVisible: readonly(logoVisible),
updateAvailable: readonly(updateAvailable),
setLoading,
setReady,
setCurrentProject,
setCurrentProjectViewId,
setHasTasks,
@@ -193,6 +172,7 @@ export const useBaseStore = defineStore('base', () => {
setUpdateAvailable,
handleSetCurrentProject,
loadApp,
...useMenuActive(),
}
+1 -1
View File
@@ -6,7 +6,7 @@ import {HTTPFactory} from '@/helpers/fetcher'
import {objectToCamelCase} from '@/helpers/case'
import type {IProvider} from '@/types/IProvider'
import type {MIGRATORS} from '@/helpers/migrators'
import type {MIGRATORS} from '@/views/migrate/migrators'
import {InvalidApiUrlProvidedError} from '@/helpers/checkAndSetApiUrl'
export interface ConfigState {
+2 -2
View File
@@ -150,7 +150,7 @@ export const useProjectStore = defineStore('project', () => {
const createdProject = await projectService.create(project)
setProject(createdProject)
router.push({
name: 'project',
name: 'project.index',
params: { projectId: createdProject.id },
})
return createdProject
@@ -315,7 +315,7 @@ export function useProject(projectId: MaybeRefOrGetter<IProject['id']>) {
projectStore.setProject(duplicate.duplicatedProject)
success({message: t('project.duplicate.success')})
router.push({name: 'project', params: {projectId: duplicate.duplicatedProject.id}})
router.push({name: 'project.index', params: {projectId: duplicate.duplicatedProject.id}})
}
return {
+5 -2
View File
@@ -7,8 +7,11 @@ import type Modal from '@/components/misc/Modal.vue'
import type Card from '@/components/misc/Card.vue'
// Here we define globally imported components
// See: https://github.com/vuejs/language-tools/wiki/Global-Component-Types
declare module 'vue' {
// See:
// https://github.com/johnsoncodehk/volar/blob/2ca8fd3434423c7bea1c8e08132df3b9ce84eea7/extensions/vscode-vue-language-features/README.md#usage
// Under the hidden collapsible "Define Global Components"
declare module '@vue/runtime-core' {
export interface GlobalComponents {
Icon: FontAwesomeIconFixedTypes
Notifications: FunctionalComponent<Notifications>
@@ -8,13 +8,5 @@
<script setup lang="ts">
import {useTitle} from '@/composables/useTitle'
// regarding the name of this component, see
// https://github.com/posva/unplugin-vue-router/issues/412#issuecomment-2150643118
definePage({
name: 'not-found',
})
defineOptions({ name: 'NotFound' })
useTitle(() => '404')
</script>
@@ -32,8 +32,6 @@ import {computed} from 'vue'
import {useConfigStore} from '@/stores/config'
definePage({ name: 'about' })
const configStore = useConfigStore()
const apiVersion = computed(() => configStore.version)
</script>
@@ -58,7 +58,7 @@
import {ref, computed} from 'vue'
import Message from '@/components/misc/Message.vue'
import ShowTasks from '@/components/tasks/ShowTasks.vue'
import ShowTasks from '@/views/tasks/ShowTasks.vue'
import ProjectCardGrid from '@/components/project/partials/ProjectCardGrid.vue'
import AddTask from '@/components/tasks/AddTask.vue'
@@ -73,8 +73,6 @@ import {useConfigStore} from '@/stores/config'
import {useAuthStore} from '@/stores/auth'
import {useTaskStore} from '@/stores/tasks'
definePage({ name: 'home' })
const salutation = useDaytimeSalutation()
const baseStore = useBaseStore()
@@ -88,11 +88,6 @@ import Filters from '@/components/project/partials/Filters.vue'
import {useSavedFilter} from '@/services/savedFilter'
definePage({
name: 'filters.create',
meta: { showAsModal: true }
})
const {
filter,
filters,
@@ -149,8 +149,6 @@ import {useLabelStore} from '@/stores/labels'
import { useTitle } from '@/composables/useTitle'
definePage({ name: 'labels.index' })
const {t} = useI18n({useScope: 'global'})
const labelEditLabel = ref<ILabel>(new LabelModel())
@@ -54,8 +54,6 @@ import {useTitle} from '@/composables/useTitle'
import {success} from '@/message'
import {getRandomColorHex} from '@/helpers/color/randomColor'
definePage({ name: 'labels.create' })
const router = useRouter()
const {t} = useI18n({useScope: 'global'})
@@ -24,12 +24,10 @@
import {computed} from 'vue'
import {useI18n} from 'vue-i18n'
import {MIGRATORS} from '@/helpers/migrators'
import {MIGRATORS} from './migrators'
import {useTitle} from '@/composables/useTitle'
import {useConfigStore} from '@/stores/config'
definePage({ name: 'migrate.start' })
const {t} = useI18n({useScope: 'global'})
useTitle(() => t('migrate.title'))
@@ -7,13 +7,6 @@
<template v-if="isMigrating === false">
<template v-if="migrator.isFileMigrator">
<p>{{ $t('migrate.importUpload', {name: migrator.name}) }}</p>
<Message
v-if="migrationError"
variant="danger"
class="mb-4"
>
{{ migrationError }}
</Message>
<input
ref="uploadInput"
class="is-hidden"
@@ -110,6 +103,16 @@
</div>
</template>
<script lang="ts">
export default {
beforeRouteEnter(to) {
if (MIGRATORS[to.params.service as string] === undefined) {
return {name: 'not-found'}
}
},
}
</script>
<script setup lang="ts">
import {computed, ref, shallowReactive} from 'vue'
import {useI18n} from 'vue-i18n'
@@ -123,23 +126,9 @@ import AbstractMigrationFileService from '@/services/migrator/abstractMigrationF
import {formatDateLong} from '@/helpers/time/formatDate'
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {MIGRATORS, type Migrator} from '@/helpers/migrators'
import {MIGRATORS, type Migrator} from './migrators'
import {useTitle} from '@/composables/useTitle'
import {useProjectStore} from '@/stores/projects'
import {getErrorText} from '@/message'
definePage({
name: 'migrate.service',
props: route => ({
service: route.params.service as string,
code: route.query.code as string,
}),
beforeEnter(to) {
if (MIGRATORS[to.params.service as string] === undefined) {
return {name: 'not-found'}
}
}
})
const props = defineProps<{
service: string,
@@ -158,7 +147,6 @@ const lastMigrationStartedAt = ref<Date | null>(null)
const message = ref('')
const migratorAuthCode = ref('')
const migrationJustStarted = ref(false)
const migrationError = ref('')
const migrator = computed<Migrator>(() => MIGRATORS[props.service])
@@ -210,7 +198,6 @@ async function migrate() {
isMigrating.value = true
lastMigrationFinishedAt.value = null
message.value = ''
migrationError.value = ''
let migrationConfig: MigrationConfig | File = {code: migratorAuthCode.value}
@@ -232,7 +219,7 @@ async function migrate() {
await migrationService.migrate(migrationConfig as MigrationConfig)
migrationJustStarted.value = true
} catch (e) {
migrationError.value = getErrorText(e)
console.log(e)
} finally {
isMigrating.value = false
}

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 471 B

After

Width:  |  Height:  |  Size: 471 B

Before

Width:  |  Height:  |  Size: 745 B

After

Width:  |  Height:  |  Size: 745 B

Before

Width:  |  Height:  |  Size: 512 B

After

Width:  |  Height:  |  Size: 512 B

Some files were not shown because too many files have changed in this diff Show More