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
89 changed files with 4483 additions and 5954 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
...
-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
+68 -53
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,7 +108,7 @@
"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",
@@ -103,7 +118,7 @@
"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",
@@ -111,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": {
+1703 -1551
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'
+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>
@@ -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([
@@ -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'
@@ -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) {
@@ -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()
+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) {
+102 -51
View File
@@ -10,10 +10,65 @@ 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')
// 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')
// 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),
@@ -35,7 +90,7 @@ const router = createRouter({
{
path: '/',
name: 'home',
component: () => import('@/views/Home.vue'),
component: HomeComponent,
},
{
path: '/:pathMatch(.*)*',
@@ -51,7 +106,7 @@ const router = createRouter({
{
path: '/login',
name: 'user.login',
component: () => import('@/views/user/Login.vue'),
component: LoginComponent,
meta: {
title: 'user.auth.login',
},
@@ -59,7 +114,7 @@ const router = createRouter({
{
path: '/get-password-reset',
name: 'user.password-reset.request',
component: () => import('@/views/user/RequestPasswordReset.vue'),
component: GetPasswordResetComponent,
meta: {
title: 'user.auth.resetPassword',
},
@@ -67,7 +122,7 @@ const router = createRouter({
{
path: '/password-reset',
name: 'user.password-reset.reset',
component: () => import('@/views/user/PasswordReset.vue'),
component: PasswordResetComponent,
meta: {
title: 'user.auth.resetPassword',
},
@@ -75,9 +130,7 @@ const router = createRouter({
{
path: '/register',
name: 'user.register',
// FIXME: use dynamic imports
// component: () => import('@/views/user/Register.vue'),
component: Register,
component: RegisterComponent,
meta: {
title: 'user.auth.createAccount',
},
@@ -85,63 +138,63 @@ const router = createRouter({
{
path: '/user/settings',
name: 'user.settings',
component: () => import('@/views/user/Settings.vue'),
component: UserSettingsComponent,
redirect: {name: 'user.settings.general'},
children: [
{
path: '/user/settings/avatar',
name: 'user.settings.avatar',
component: () => import('@/views/user/settings/Avatar.vue'),
component: UserSettingsAvatarComponent,
},
{
path: '/user/settings/caldav',
name: 'user.settings.caldav',
component: () => import('@/views/user/settings/Caldav.vue'),
component: UserSettingsCaldavComponent,
},
{
path: '/user/settings/data-export',
name: 'user.settings.data-export',
component: () => import('@/views/user/settings/DataExport.vue'),
component: UserSettingsDataExportComponent,
},
{
path: '/user/settings/deletion',
name: 'user.settings.deletion',
component: () => import('@/views/user/settings/Deletion.vue'),
component: UserSettingsDeletionComponent,
},
{
path: '/user/settings/email-update',
name: 'user.settings.email-update',
component: () => import('@/views/user/settings/EmailUpdate.vue'),
component: UserSettingsEmailUpdateComponent,
},
{
path: '/user/settings/general',
name: 'user.settings.general',
component: () => import('@/views/user/settings/General.vue'),
component: UserSettingsGeneralComponent,
},
{
path: '/user/settings/password-update',
name: 'user.settings.password-update',
component: () => import('@/views/user/settings/PasswordUpdate.vue'),
component: UserSettingsPasswordUpdateComponent,
},
{
path: '/user/settings/totp',
name: 'user.settings.totp',
component: () => import('@/views/user/settings/TOTP.vue'),
component: UserSettingsTOTPComponent,
},
{
path: '/user/settings/api-tokens',
name: 'user.settings.apiTokens',
component: () => import('@/views/user/settings/ApiTokens.vue'),
component: UserSettingsApiTokensComponent,
},
{
path: '/user/settings/migrate',
name: 'migrate.start',
component: () => import('@/views/migrate/Migration.vue'),
component: MigrationComponent,
},
{
path: '/migrate/:service',
name: 'migrate.service',
component: () => import('@/views/migrate/MigrationHandler.vue'),
component: MigrationHandlerComponent,
props: route => ({
service: route.params.service as string,
code: route.query.code as string,
@@ -152,25 +205,23 @@ const router = createRouter({
{
path: '/user/export/download',
name: 'user.export.download',
component: () => import('@/views/user/DataExportDownload.vue'),
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: () => import('@/views/tasks/TaskDetailView.vue'),
component: TaskDetailView,
props: route => ({ taskId: Number(route.params.id as string) }),
},
{
path: '/tasks/by/upcoming',
name: 'tasks.range',
component: () => import('@/views/tasks/ShowTasks.vue'),
component: UpcomingTasksComponent,
props: route => ({
dateFrom: parseDateOrString(route.query.from as string, new Date()),
dateTo: parseDateOrString(route.query.to as string, getNextWeekDate()),
@@ -194,12 +245,12 @@ const router = createRouter({
{
path: '/projects',
name: 'projects.index',
component: () => import('@/views/project/ListProjects.vue'),
component: ListProjects,
},
{
path: '/projects/new',
name: 'project.create',
component: () => import('@/views/project/NewProject.vue'),
component: NewProjectComponent,
meta: {
showAsModal: true,
},
@@ -207,7 +258,7 @@ const router = createRouter({
{
path: '/projects/:parentProjectId/new',
name: 'project.createFromParent',
component: () => import('@/views/project/NewProject.vue'),
component: NewProjectComponent,
props: route => ({ parentProjectId: Number(route.params.parentProjectId as string) }),
meta: {
showAsModal: true,
@@ -216,7 +267,7 @@ const router = createRouter({
{
path: '/projects/:projectId/settings/edit',
name: 'project.settings.edit',
component: () => import('@/views/project/settings/ProjectSettingsEdit.vue'),
component: ProjectSettingEdit,
props: route => ({ projectId: Number(route.params.projectId as string) }),
meta: {
showAsModal: true,
@@ -225,7 +276,7 @@ const router = createRouter({
{
path: '/projects/:projectId/settings/background',
name: 'project.settings.background',
component: () => import('@/views/project/settings/ProjectSettingsBackground.vue'),
component: ProjectSettingBackground,
meta: {
showAsModal: true,
},
@@ -233,7 +284,7 @@ const router = createRouter({
{
path: '/projects/:projectId/settings/duplicate',
name: 'project.settings.duplicate',
component: () => import('@/views/project/settings/ProjectSettingsDuplicate.vue'),
component: ProjectSettingDuplicate,
meta: {
showAsModal: true,
},
@@ -241,7 +292,7 @@ const router = createRouter({
{
path: '/projects/:projectId/settings/share',
name: 'project.settings.share',
component: () => import('@/views/project/settings/ProjectSettingsShare.vue'),
component: ProjectSettingShare,
meta: {
showAsModal: true,
},
@@ -249,7 +300,7 @@ const router = createRouter({
{
path: '/projects/:projectId/settings/webhooks',
name: 'project.settings.webhooks',
component: () => import('@/views/project/settings/ProjectSettingsWebhooks.vue'),
component: ProjectSettingWebhooks,
meta: {
showAsModal: true,
},
@@ -257,7 +308,7 @@ const router = createRouter({
{
path: '/projects/:projectId/settings/delete',
name: 'project.settings.delete',
component: () => import('@/views/project/settings/ProjectSettingsDelete.vue'),
component: ProjectSettingDelete,
meta: {
showAsModal: true,
},
@@ -265,7 +316,7 @@ const router = createRouter({
{
path: '/projects/:projectId/settings/archive',
name: 'project.settings.archive',
component: () => import('@/views/project/settings/ProjectSettingsArchive.vue'),
component: ProjectSettingArchive,
meta: {
showAsModal: true,
},
@@ -273,7 +324,7 @@ const router = createRouter({
{
path: '/projects/:projectId/settings/views',
name: 'project.settings.views',
component: () => import('@/views/project/settings/ProjectSettingsViews.vue'),
component: ProjectSettingViews,
meta: {
showAsModal: true,
},
@@ -282,7 +333,7 @@ const router = createRouter({
{
path: '/projects/:projectId/settings/edit',
name: 'filter.settings.edit',
component: () => import('@/views/filters/FilterEdit.vue'),
component: FilterEdit,
meta: {
showAsModal: true,
},
@@ -291,7 +342,7 @@ const router = createRouter({
{
path: '/projects/:projectId/settings/delete',
name: 'filter.settings.delete',
component: () => import('@/views/filters/FilterDelete.vue'),
component: FilterDelete,
meta: {
showAsModal: true,
},
@@ -300,7 +351,7 @@ const router = createRouter({
{
path: '/projects/:projectId/info',
name: 'project.info',
component: () => import('@/views/project/ProjectInfo.vue') ,
component: ProjectInfo,
meta: {
showAsModal: true,
},
@@ -328,7 +379,7 @@ const router = createRouter({
{
path: '/projects/:projectId/:viewId',
name: 'project.view',
component: () => import('@/views/project/ProjectView.vue'),
component: ProjectView,
props: route => ({
projectId: parseInt(route.params.projectId as string),
viewId: route.params.viewId ? parseInt(route.params.viewId as string): undefined,
@@ -337,12 +388,12 @@ const router = createRouter({
{
path: '/teams',
name: 'teams.index',
component: () => import('@/views/teams/ListTeams.vue'),
component: ListTeamsComponent,
},
{
path: '/teams/new',
name: 'teams.create',
component: () => import('@/views/teams/NewTeam.vue'),
component: NewTeamComponent,
meta: {
showAsModal: true,
},
@@ -350,17 +401,17 @@ const router = createRouter({
{
path: '/teams/:id/edit',
name: 'teams.edit',
component: () => import('@/views/teams/EditTeam.vue'),
component: EditTeamComponent,
},
{
path: '/labels',
name: 'labels.index',
component: () => import('@/views/labels/ListLabels.vue'),
component: ListLabelsComponent,
},
{
path: '/labels/new',
name: 'labels.create',
component: () => import('@/views/labels/NewLabel.vue'),
component: NewLabelComponent,
meta: {
showAsModal: true,
},
@@ -368,7 +419,7 @@ const router = createRouter({
{
path: '/filters/new',
name: 'filters.create',
component: () => import('@/views/filters/FilterNew.vue'),
component: FilterNew,
meta: {
showAsModal: true,
},
@@ -376,12 +427,12 @@ const router = createRouter({
{
path: '/auth/openid/:provider',
name: 'openid.auth',
component: () => import('@/views/user/OpenIdAuth.vue'),
component: OpenIdAuth,
},
{
path: '/about',
name: 'about',
component: () => import('@/views/About.vue'),
component: About,
},
],
})
+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(),
}
+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>
@@ -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"
@@ -136,7 +129,6 @@ import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {MIGRATORS, type Migrator} from './migrators'
import {useTitle} from '@/composables/useTitle'
import {useProjectStore} from '@/stores/projects'
import {getErrorText} from '@/message'
const props = defineProps<{
service: string,
@@ -155,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])
@@ -207,7 +198,6 @@ async function migrate() {
isMigrating.value = true
lastMigrationFinishedAt.value = null
message.value = ''
migrationError.value = ''
let migrationConfig: MigrationConfig | File = {code: migratorAuthCode.value}
@@ -229,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
}
@@ -32,7 +32,7 @@
</template>
<script setup lang="ts">
import {computed, ref, watchEffect} from 'vue'
import {computed, shallowReactive, watchEffect} from 'vue'
import {useTitle} from '@/composables/useTitle'
import {useI18n} from 'vue-i18n'
import {useRoute, useRouter} from 'vue-router'
@@ -46,36 +46,45 @@ const projectStore = useProjectStore()
const route = useRoute()
const router = useRouter()
const totalTasks = ref<number | null>(null)
const projectId = computed(() => Number(route.params.projectId))
const project = computed(() => projectStore.projects[route.params.projectId])
const projectIdsToDelete = ref<number[]>([])
const project = computed(() => projectStore.projects[projectId.value])
const projectIdsToDelete = computed(() => {
if (!projectId.value) {
return []
}
return [
...projectStore
.getChildProjects(projectId.value)
.map(p => p.id),
projectId.value,
]
})
const taskService = shallowReactive(new TaskService())
watchEffect(
async () => {
if (!route.params.projectId) {
if (!projectIdsToDelete.value.length) {
return
}
projectIdsToDelete.value = projectStore
.getChildProjects(parseInt(route.params.projectId))
.map(p => p.id)
projectIdsToDelete.value.push(parseInt(route.params.projectId))
const taskService = new TaskService()
await taskService.getAll({}, {filter: `project in ${projectIdsToDelete.value.join(',')}`})
totalTasks.value = taskService.totalPages * taskService.resultCount
},
)
const totalTasks = computed(() => taskService.totalPages * taskService.resultCount)
useTitle(() => t('project.delete.title', {project: project?.value?.title}))
const deleteNotice = computed(() => {
if(totalTasks.value && totalTasks.value > 0) {
if (totalTasks.value && totalTasks.value > 0) {
if (projectIdsToDelete.value.length <= 1) {
return t('project.delete.tasksToDelete', {count: totalTasks.value})
} else if (projectIdsToDelete.value.length > 1) {
}
if (projectIdsToDelete.value.length > 1) {
return t('project.delete.tasksAndChildProjectsToDelete', {tasks: totalTasks.value, projects: projectIdsToDelete.value.length})
}
}
+59 -212
View File
@@ -2,13 +2,13 @@
<div
class="loader-container task-view-container"
:class="{
'is-loading': taskService.loading || !visible,
'is-loading': isLoading || !isReady,
'is-modal': isModal,
}"
>
<!-- Removing everything until the task is loaded to prevent empty initialization of other components -->
<div
v-if="visible"
v-if="isReady"
class="task-view"
>
<Heading
@@ -24,7 +24,7 @@
class="subtitle"
>
<template
v-for="p in projectStore.getAncestors(project)"
v-for="p in ancestorProjects"
:key="p.id"
>
<a
@@ -117,7 +117,7 @@
:ref="e => setFieldRef('dueDate', e)"
v-model="task.dueDate"
:choose-date-label="$t('task.detail.chooseDueDate')"
:disabled="taskService.loading || !canWrite"
:disabled="isLoading || !canWrite"
@closeOnChange="saveTask()"
/>
<BaseButton
@@ -171,7 +171,7 @@
:ref="e => setFieldRef('startDate', e)"
v-model="task.startDate"
:choose-date-label="$t('task.detail.chooseStartDate')"
:disabled="taskService.loading || !canWrite"
:disabled="isLoading || !canWrite"
@closeOnChange="saveTask()"
/>
<BaseButton
@@ -204,7 +204,7 @@
:ref="e => setFieldRef('endDate', e)"
v-model="task.endDate"
:choose-date-label="$t('task.detail.chooseEndDate')"
:disabled="taskService.loading || !canWrite"
:disabled="isLoading || !canWrite"
@closeOnChange="saveTask()"
/>
<BaseButton
@@ -320,7 +320,7 @@
<Description
:model-value="task"
:can-write="canWrite"
:attachment-upload="attachmentUpload"
:attachment-upload="uploadAttachment"
@update:modelValue="Object.assign(task, $event)"
/>
</div>
@@ -415,10 +415,9 @@
{{ task.done ? $t('task.detail.undone') : $t('task.detail.done') }}
</x-button>
<TaskSubscription
v-model="task.subscription"
entity="task"
:entity-id="task.id"
:model-value="task.subscription"
@update:modelValue="sub => task.subscription = sub"
/>
<x-button
v-shortcut="'s'"
@@ -585,22 +584,14 @@
</template>
<script lang="ts" setup>
import {ref, reactive, shallowReactive, computed, watch, nextTick, onMounted, onBeforeUnmount} from 'vue'
import {ref, reactive, computed, nextTick, watchPostEffect, watch} from 'vue'
import {useRouter, type RouteLocation} from 'vue-router'
import {storeToRefs} from 'pinia'
import {useI18n} from 'vue-i18n'
import {unrefElement} from '@vueuse/core'
import {klona} from 'klona/lite'
import {unrefElement, useEventListener} from '@vueuse/core'
import {eventToHotkeyString} from '@github/hotkey'
import TaskService from '@/services/task'
import TaskModel from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import type {IProject} from '@/modelTypes/IProject'
import {PRIORITIES, type Priority} from '@/constants/priorities'
import {RIGHTS} from '@/constants/rights'
import {PRIORITIES} from '@/constants/priorities'
import BaseButton from '@/components/base/BaseButton.vue'
@@ -626,23 +617,14 @@ import CustomTransition from '@/components/misc/CustomTransition.vue'
import AssigneeList from '@/components/tasks/partials/AssigneeList.vue'
import Reactions from '@/components/input/Reactions.vue'
import {uploadFile} from '@/helpers/attachments'
import {getProjectTitle} from '@/helpers/getProjectTitle'
import {scrollIntoView} from '@/helpers/scrollIntoView'
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
import {playPopSound} from '@/helpers/playPop'
import {useAttachmentStore} from '@/stores/attachments'
import {useTaskStore} from '@/stores/tasks'
import {useKanbanStore} from '@/stores/kanban'
import {useProjectStore} from '@/stores/projects'
import {useAuthStore} from '@/stores/auth'
import {useBaseStore} from '@/stores/base'
import {useTitle} from '@/composables/useTitle'
import {success} from '@/message'
import type {Action as MessageAction} from '@/message'
import {useTask} from '@/composables/useTask'
const props = defineProps<{
taskId: ITask['id'],
@@ -654,98 +636,59 @@ defineEmits<{
}>()
const router = useRouter()
const {t} = useI18n({useScope: 'global'})
const projectStore = useProjectStore()
const attachmentStore = useAttachmentStore()
const {hasAttachments} = storeToRefs(attachmentStore)
const taskStore = useTaskStore()
const kanbanStore = useKanbanStore()
const authStore = useAuthStore()
const baseStore = useBaseStore()
const task = ref<ITask>(new TaskModel())
const taskTitle = computed(() => task.value.title)
const {
project,
ancestorProjects,
isReady,
isLoading,
task,
taskTitle,
taskColor,
canWrite,
hasAttachments,
saveTask,
deleteTask,
uploadAttachment,
toggleTaskDone,
changeProject,
toggleFavorite,
setPriority,
setPercentDone,
removeRepeatAfter,
} = useTask(() => props.taskId)
useTitle(taskTitle)
// See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660
function saveTaskViaHotkey(event) {
useEventListener('keydown', (event) => {
const hotkeyString = eventToHotkeyString(event)
if (!hotkeyString) return
if (hotkeyString !== 'Control+s' && hotkeyString !== 'Meta+s') return
event.preventDefault()
saveTask()
}
onMounted(() => {
document.addEventListener('keydown', saveTaskViaHotkey)
})
onBeforeUnmount(() => {
document.removeEventListener('keydown', saveTaskViaHotkey)
})
// We doubled the task color property here because verte does not have a real change property, leading
// to the color property change being triggered when the # is removed from it, leading to an update,
// which leads in turn to a change... This creates an infinite loop in which the task is updated, changed,
// updated, changed, updated and so on.
// To prevent this, we put the task color property in a seperate value which is set to the task color
// when it is saved and loaded.
const taskColor = ref<ITask['hexColor']>('')
// Used to avoid flashing of empty elements if the task content is not yet loaded.
const visible = ref(false)
const project = computed(() => projectStore.projects[task.value.projectId])
const canWrite = computed(() => (
task.value.maxRight !== null &&
task.value.maxRight > RIGHTS.READ
))
const color = computed(() => {
const color = task.value.getHexColor
? task.value.getHexColor()
: undefined
return color
})
const color = computed(() => task?.getHexColor())
const isModal = computed(() => Boolean(props.backdropView))
function attachmentUpload(file: File, onSuccess?: (url: string) => void) {
return uploadFile(props.taskId, file, onSuccess)
}
const heading = ref<HTMLElement | null>(null)
async function scrollToHeading() {
scrollIntoView(unrefElement(heading))
}
const taskService = shallowReactive(new TaskService())
// load task
watch(
() => props.taskId,
async (id) => {
if (id === undefined) {
return
}
try {
const loaded = await taskService.get({id})
Object.assign(task.value, loaded)
attachmentStore.set(task.value.attachments)
taskColor.value = task.value.hexColor
setActiveFields()
} finally {
await nextTick()
scrollToHeading()
visible.value = true
}
}, {immediate: true})
watchPostEffect(() => {
if (isReady.value) {
scrollIntoView(unrefElement(heading))
}
})
type FieldType =
| 'assignees'
@@ -784,17 +727,19 @@ function setActiveFields() {
// task.endDate = task.endDate || null
// Set all active fields based on values in the model
activeFields.assignees = task.value.assignees.length > 0
activeFields.attachments = task.value.attachments.length > 0
activeFields.dueDate = task.value.dueDate !== null
activeFields.endDate = task.value.endDate !== null
activeFields.labels = task.value.labels.length > 0
activeFields.percentDone = task.value.percentDone > 0
activeFields.priority = task.value.priority !== PRIORITIES.UNSET
activeFields.relatedTasks = Object.keys(task.value.relatedTasks).length > 0
activeFields.reminders = task.value.reminders.length > 0
activeFields.repeatAfter = task.value.repeatAfter?.amount > 0 || task.value.repeatMode !== TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
activeFields.startDate = task.value.startDate !== null
Object.assign(activeFields, {
assignees: task.assignees.length > 0,
attachments: task.attachments.length > 0,
dueDate: task.dueDate !== null,
endDate: task.endDate !== null,
labels: task.labels.length > 0,
percentDone: task.percentDone > 0,
priority: task.priority !== PRIORITIES.UNSET,
relatedTasks: Object.keys(task.relatedTasks).length > 0,
reminders: task.reminders.length > 0,
repeatAfter: task.repeatAfter?.amount > 0 || task.repeatMode !== TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT,
startDate: task.startDate !== null,
})
}
const activeFieldElements: { [id in FieldType]: HTMLElement | null } = reactive({
@@ -833,106 +778,8 @@ function setFieldActive(fieldName: keyof typeof activeFields) {
})
}
async function saveTask(
currentTask: ITask | null = null,
undoCallback?: () => void,
) {
if (currentTask === null) {
currentTask = klona(task.value)
}
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.value, updatedTask)
setActiveFields()
let actions: MessageAction[] = []
if (undoCallback) {
actions = [{
title: t('task.undo'),
callback: undoCallback,
}]
}
success({message: t('task.detail.updateSuccess')}, actions)
}
const showDeleteModal = ref(false)
async function deleteTask() {
await taskStore.delete(task.value)
success({message: t('task.detail.deleteSuccess')})
router.push({name: 'project.index', params: {projectId: task.value.projectId}})
}
async function toggleTaskDone() {
const newTask = {
...task.value,
done: !task.value.done,
}
if (newTask.done) {
playPopSound()
}
await saveTask(
newTask,
toggleTaskDone,
)
}
async function changeProject(project: IProject) {
kanbanStore.removeTaskInBucket(task.value)
await saveTask({
...task.value,
projectId: project.id,
})
baseStore.setCurrentProject(project)
}
async function toggleFavorite() {
const newTask = await taskStore.toggleFavorite(task.value)
Object.assign(task.value, newTask)
}
async function setPriority(priority: Priority) {
const newTask: ITask = {
...task.value,
priority,
}
return saveTask(newTask)
}
async function setPercentDone(percentDone: number) {
const newTask: ITask = {
...task.value,
percentDone,
}
return saveTask(newTask)
}
async function removeRepeatAfter() {
task.value.repeatAfter.amount = 0
task.value.repeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
await saveTask()
}
function setRelatedTasksActive() {
setFieldActive('relatedTasks')
@@ -1,7 +1,6 @@
<script setup lang="ts">
import ApiTokenService from '@/services/apiToken'
import {computed, onMounted, ref} from 'vue'
import {useFlatpickrLanguage} from '@/helpers/useFlatpickrLanguage'
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
import XButton from '@/components/input/Button.vue'
import BaseButton from '@/components/base/BaseButton.vue'
@@ -13,6 +12,7 @@ import 'flatpickr/dist/flatpickr.css'
import {useI18n} from 'vue-i18n'
import Message from '@/components/misc/Message.vue'
import type {IApiToken} from '@/modelTypes/IApiToken'
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
const service = new ApiTokenService()
const tokens = ref<IApiToken[]>([])
@@ -42,7 +42,7 @@ const flatPickerConfig = computed(() => ({
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
locale: useFlatpickrLanguage().value,
locale: getFlatpickrLanguage(),
minDate: now,
}))
+22 -59
View File
@@ -18,23 +18,22 @@ module code.vikunja.io/api
require (
dario.cat/mergo v1.0.1
github.com/ThreeDotsLabs/watermill v1.4.4
github.com/ThreeDotsLabs/watermill v1.4.1
github.com/adlio/trello v1.12.0
github.com/arran4/golang-ical v0.3.1
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/bbrks/go-blurhash v1.1.1
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
github.com/coreos/go-oidc/v3 v3.12.0
github.com/coreos/go-oidc/v3 v3.11.0
github.com/cweill/gotests v1.6.0
github.com/d4l3k/messagediff v1.2.1
github.com/disintegration/imaging v1.6.2
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b
github.com/gabriel-vasile/mimetype v1.4.8
github.com/gabriel-vasile/mimetype v1.4.7
github.com/ganigeorgiev/fexpr v0.4.1
github.com/getsentry/sentry-go v0.31.1
github.com/getsentry/sentry-go/echo v0.31.1
github.com/getsentry/sentry-go v0.30.0
github.com/go-sql-driver/mysql v1.8.1
github.com/go-testfixtures/testfixtures/v3 v3.14.0
github.com/go-testfixtures/testfixtures/v3 v3.11.0
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
@@ -59,7 +58,7 @@ require (
github.com/redis/go-redis/v9 v9.7.0
github.com/robfig/cron/v3 v3.0.1
github.com/samedi/caldav-go v3.0.0+incompatible
github.com/spf13/afero v1.12.0
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.10.0
@@ -67,17 +66,17 @@ require (
github.com/tkuchiki/go-timezone v0.2.3
github.com/typesense/typesense-go/v2 v2.0.0
github.com/ulule/limiter/v3 v3.11.2
github.com/wneessen/go-mail v0.6.0
github.com/wneessen/go-mail v0.5.2
github.com/yuin/goldmark v1.7.8
golang.org/x/crypto v0.32.0
golang.org/x/crypto v0.31.0
golang.org/x/image v0.23.0
golang.org/x/oauth2 v0.25.0
golang.org/x/oauth2 v0.24.0
golang.org/x/sync v0.10.0
golang.org/x/sys v0.29.0
golang.org/x/term v0.28.0
golang.org/x/sys v0.28.0
golang.org/x/term v0.27.0
golang.org/x/text v0.21.0
gopkg.in/d4l3k/messagediff.v1 v1.2.1
mvdan.cc/xurls/v2 v2.6.0
mvdan.cc/xurls/v2 v2.5.0
src.techknowlogick.com/xgo v1.8.1-0.20241105013731-313dedef864f
src.techknowlogick.com/xormigrate v1.7.1
xorm.io/builder v0.3.13
@@ -85,56 +84,33 @@ require (
)
require (
cel.dev/expr v0.16.1 // indirect
cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/auth v0.13.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/iam v1.2.2 // indirect
cloud.google.com/go/longrunning v0.6.2 // indirect
cloud.google.com/go/monitoring v1.21.2 // indirect
cloud.google.com/go/spanner v1.73.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/ClickHouse/ch-go v0.61.5 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.30.0 // indirect
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.24.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/envoyproxy/go-control-plane v0.13.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-chi/chi/v5 v5.1.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.3 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.22.8 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/googleapis/go-sql-spanner v1.7.4 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -163,7 +139,6 @@ require (
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
@@ -185,27 +160,15 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/sdk v1.29.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.opentelemetry.io/otel v1.26.0 // indirect
go.opentelemetry.io/otel/trace v1.26.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.28.0 // indirect
google.golang.org/api v0.215.0 // indirect
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/grpc v1.67.3 // indirect
google.golang.org/protobuf v1.36.1 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
@@ -214,6 +177,6 @@ require (
replace github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6 and https://github.com/samedi/caldav-go/pull/7
go 1.22.0
go 1.22
toolchain go1.23.5
toolchain go1.23.4
-1522
View File
File diff suppressed because it is too large Load Diff
+7 -18
View File
@@ -100,12 +100,6 @@ func getRouteDetail(route echo.Route) (method string, detail *RouteDetail) {
}
}
func ensureAPITokenRoutesGroup(group string) {
if _, has := apiTokenRoutes[group]; !has {
apiTokenRoutes[group] = make(APITokenRoute)
}
}
// CollectRoutesForAPITokenUsage gets called for every added APITokenRoute and builds a list of all routes we can use for the api tokens.
func CollectRoutesForAPITokenUsage(route echo.Route, middlewares []echo.MiddlewareFunc) {
@@ -145,15 +139,10 @@ func CollectRoutesForAPITokenUsage(route echo.Route, middlewares []echo.Middlewa
// and if that's the case, add it to its parent instead.
// Otherwise, we add it to the "other" key.
if len(routeParts) == 1 {
if routeGroupName == "notifications" && route.Method == http.MethodPost {
ensureAPITokenRoutesGroup("notifications")
apiTokenRoutes["notifications"]["mark_all_as_read"] = routeDetail
return
if _, has := apiTokenRoutes["other"]; !has {
apiTokenRoutes["other"] = make(APITokenRoute)
}
ensureAPITokenRoutesGroup("other")
_, exists := apiTokenRoutes["other"][routeGroupName]
if exists {
routeGroupName += "_" + strings.ToLower(route.Method)
@@ -179,7 +168,10 @@ func CollectRoutesForAPITokenUsage(route echo.Route, middlewares []echo.Middlewa
if strings.HasSuffix(routeGroupName, "_bulk") {
parent := strings.TrimSuffix(routeGroupName, "_bulk")
ensureAPITokenRoutesGroup(parent)
_, has := apiTokenRoutes[parent]
if !has {
apiTokenRoutes[parent] = make(APITokenRoute)
}
method, routeDetail := getRouteDetail(route)
apiTokenRoutes[parent][method+"_bulk"] = routeDetail
@@ -210,7 +202,6 @@ func CollectRoutesForAPITokenUsage(route echo.Route, middlewares []echo.Middlewa
}
}
}
}
// GetAvailableAPIRoutesForToken returns a list of all API routes which are available for token usage.
@@ -238,9 +229,7 @@ func CanDoAPIRoute(c echo.Context, token *APIToken) (can bool) {
routeGroupName = strings.TrimSuffix(routeGroupName, "_bulk")
if routeGroupName == "user" ||
routeGroupName == "users" ||
routeGroupName == "routes" {
if routeGroupName == "user" {
routeGroupName = "other"
}
+1 -5
View File
@@ -36,7 +36,6 @@ import (
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils"
"code.vikunja.io/api/pkg/version"
"xorm.io/xorm"
)
@@ -107,10 +106,7 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
// Save the file id with the user
u.ExportFileID = exportFile.ID
_, err = s.
Where("id = ?", u.ID).
Cols("export_file_id").
Update(u)
_, err = s.Cols("export_file_id").Update(u)
if err != nil {
return
}
+1 -1
View File
@@ -123,7 +123,7 @@ func (b *TaskBucket) Update(s *xorm.Session, a web.Auth) (err error) {
// Check the bucket limit
// Only check the bucket limit if the task is being moved between buckets, allow reordering the task within a bucket
if b.BucketID != 0 && b.BucketID != oldTaskBucket.BucketID {
err = checkBucketLimit(s, a, &task, bucket)
err = checkBucketLimit(s, &task, bucket)
if err != nil {
return err
}
+5 -25
View File
@@ -656,39 +656,19 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth, vi
}
// Checks if adding a new task would exceed the bucket limit
func checkBucketLimit(s *xorm.Session, a web.Auth, t *Task, bucket *Bucket) (err error) {
func checkBucketLimit(s *xorm.Session, t *Task, bucket *Bucket) (err error) {
if bucket.Limit > 0 {
var taskCount int64
view, err := GetProjectViewByID(s, bucket.ProjectViewID)
taskCount, err := s.
Where("bucket_id = ?", bucket.ID).
GroupBy("task_id").
Count(&TaskBucket{})
if err != nil {
return err
}
if view.ProjectID < 0 || (view.Filter != nil && view.Filter.Filter != "") {
tc := &TaskCollection{
ProjectID: view.ProjectID,
ProjectViewID: bucket.ProjectViewID,
}
_, _, taskCount, err = tc.ReadAll(s, a, "", 1, 1)
if err != nil {
return err
}
} else {
taskCount, err = s.
Where("bucket_id = ?", bucket.ID).
GroupBy("task_id").
Count(&TaskBucket{})
if err != nil {
return err
}
}
if taskCount >= bucket.Limit {
return ErrBucketLimitExceeded{TaskID: t.ID, BucketID: bucket.ID, Limit: bucket.Limit}
}
}
return nil
}
+3 -35
View File
@@ -192,20 +192,10 @@ func Restore(filename string) error {
func restoreTableData(tables map[string]*zip.File) error {
jsonFields := map[string][]string{
"api_tokens": {"permissions"},
"notifications": {"notification"},
"project_views": {"filter", "bucket_configuration"},
"saved_filters": {"filters"},
"users": {"frontend_settings"},
}
floatFields := map[string][]string{
"buckets": {"position"},
"project_views": {"position"},
"projects": {"position"},
"task_positions": {"position"},
}
// Restore all db data
for table, d := range tables {
content, err := unmarshalFileToJSON(d)
@@ -213,7 +203,8 @@ func restoreTableData(tables map[string]*zip.File) error {
return fmt.Errorf("could not read table %s: %w", table, err)
}
processFields := func(fields []string, isFloat bool) error {
fields, hasJSONFields := jsonFields[table]
if hasJSONFields {
for i := range content {
for _, f := range fields {
@@ -231,32 +222,9 @@ func restoreTableData(tables map[string]*zip.File) error {
decoded = []byte(content[i][f].(string))
}
if isFloat {
val, err := strconv.ParseFloat(string(decoded), 64)
if err != nil {
return fmt.Errorf("could not parse double value for field '%s': %w", f, err)
}
content[i][f] = val
} else {
content[i][f] = string(decoded)
}
content[i][f] = string(decoded)
}
}
return nil
}
// Process JSON fields
if fields, hasJSONFields := jsonFields[table]; hasJSONFields {
if err := processFields(fields, false); err != nil {
return err
}
}
// Process double fields
if fields, hasDoubleFields := floatFields[table]; hasDoubleFields {
if err := processFields(fields, true); err != nil {
return err
}
}
if err := db.Restore(table, content); err != nil {
-42
View File
@@ -1,42 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"net/http"
"code.vikunja.io/api/pkg/web"
)
// ErrNotAZipFile represents a "ErrNotAZipFile" kind of error.
type ErrNotAZipFile struct{}
func (err *ErrNotAZipFile) Error() string {
return "The provided file is not a valid zip file"
}
// ErrCodeNotAZipFile holds the unique world-error code of this error
const ErrCodeNotAZipFile = 14001
// HTTPError holds the http error description
func (err *ErrNotAZipFile) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusBadRequest,
Code: ErrCodeNotAZipFile,
Message: "The provided file is not a valid zip file.",
}
}
@@ -67,9 +67,6 @@ func (v *FileMigrator) Name() string {
func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) error {
r, err := zip.NewReader(file, size)
if err != nil {
if err.Error() == "zip: not a valid zip file" {
return &migration.ErrNotAZipFile{}
}
return fmt.Errorf("could not open import file: %w", err)
}
+13 -13
View File
@@ -64,7 +64,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/code_vikunja_io_api_pkg_modules_auth.Token"
"$ref": "#/definitions/auth.Token"
}
},
"500": {
@@ -741,7 +741,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/code_vikunja_io_api_pkg_modules_auth.Token"
"$ref": "#/definitions/auth.Token"
}
},
"400": {
@@ -3923,7 +3923,7 @@ const docTemplate = `{
"200": {
"description": "The valid jwt auth token.",
"schema": {
"$ref": "#/definitions/code_vikunja_io_api_pkg_modules_auth.Token"
"$ref": "#/definitions/auth.Token"
}
},
"400": {
@@ -7360,7 +7360,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/code_vikunja_io_api_pkg_modules_auth.Token"
"$ref": "#/definitions/auth.Token"
}
},
"400": {
@@ -7696,6 +7696,15 @@ const docTemplate = `{
}
},
"definitions": {
"auth.Token": {
"type": "object",
"properties": {
"token": {
"type": "string",
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}
}
},
"background.Image": {
"type": "object",
"properties": {
@@ -7716,15 +7725,6 @@ const docTemplate = `{
}
}
},
"code_vikunja_io_api_pkg_modules_auth.Token": {
"type": "object",
"properties": {
"token": {
"type": "string",
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}
}
},
"files.File": {
"type": "object",
"properties": {
+13 -13
View File
@@ -56,7 +56,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/code_vikunja_io_api_pkg_modules_auth.Token"
"$ref": "#/definitions/auth.Token"
}
},
"500": {
@@ -733,7 +733,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/code_vikunja_io_api_pkg_modules_auth.Token"
"$ref": "#/definitions/auth.Token"
}
},
"400": {
@@ -3915,7 +3915,7 @@
"200": {
"description": "The valid jwt auth token.",
"schema": {
"$ref": "#/definitions/code_vikunja_io_api_pkg_modules_auth.Token"
"$ref": "#/definitions/auth.Token"
}
},
"400": {
@@ -7352,7 +7352,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/code_vikunja_io_api_pkg_modules_auth.Token"
"$ref": "#/definitions/auth.Token"
}
},
"400": {
@@ -7688,6 +7688,15 @@
}
},
"definitions": {
"auth.Token": {
"type": "object",
"properties": {
"token": {
"type": "string",
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}
}
},
"background.Image": {
"type": "object",
"properties": {
@@ -7708,15 +7717,6 @@
}
}
},
"code_vikunja_io_api_pkg_modules_auth.Token": {
"type": "object",
"properties": {
"token": {
"type": "string",
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
}
}
},
"files.File": {
"type": "object",
"properties": {
+10 -10
View File
@@ -1,5 +1,11 @@
basePath: /api/v1
definitions:
auth.Token:
properties:
token:
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
type: string
type: object
background.Image:
properties:
blur_hash:
@@ -14,12 +20,6 @@ definitions:
url:
type: string
type: object
code_vikunja_io_api_pkg_modules_auth.Token:
properties:
token:
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
type: string
type: object
files.File:
properties:
created:
@@ -1785,7 +1785,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/code_vikunja_io_api_pkg_modules_auth.Token'
$ref: '#/definitions/auth.Token'
"500":
description: Internal error
schema:
@@ -2226,7 +2226,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/code_vikunja_io_api_pkg_modules_auth.Token'
$ref: '#/definitions/auth.Token'
"400":
description: Invalid user password model.
schema:
@@ -4337,7 +4337,7 @@ paths:
"200":
description: The valid jwt auth token.
schema:
$ref: '#/definitions/code_vikunja_io_api_pkg_modules_auth.Token'
$ref: '#/definitions/auth.Token'
"400":
description: Invalid link share object provided.
schema:
@@ -6607,7 +6607,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/code_vikunja_io_api_pkg_modules_auth.Token'
$ref: '#/definitions/auth.Token'
"400":
description: Only user token are available for renew.
schema:
-53
View File
@@ -532,56 +532,3 @@ func (err *ErrUsernameMustNotContainSpaces) HTTPError() web.HTTPError {
Message: "The username must not contain spaces.",
}
}
// ErrMustNotBeLinkShare represents a "MustNotBeLinkShare" kind of error.
type ErrMustNotBeLinkShare struct{}
// IsErrMustNotBeLinkShare checks if an error is a ErrMustNotBeLinkShare.
func IsErrMustNotBeLinkShare(err error) bool {
_, ok := err.(*ErrMustNotBeLinkShare)
return ok
}
func (err *ErrMustNotBeLinkShare) Error() string {
return "user must be a *User, not a *models.LinkSharing"
}
// ErrCodeMustNotBeLinkShare holds the unique world-error code of this error
const ErrCodeMustNotBeLinkShare = 1023
// HTTPError holds the http error description
func (err *ErrMustNotBeLinkShare) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusForbidden,
Code: ErrCodeMustNotBeLinkShare,
Message: "You can't do that as a link share.",
}
}
// ErrInvalidClaimData represents a "InvalidClaimData" kind of error.
type ErrInvalidClaimData struct {
Field string
Type string
}
// IsErrInvalidClaimData checks if an error is a ErrInvalidClaimData.
func IsErrInvalidClaimData(err error) bool {
_, ok := err.(*ErrInvalidClaimData)
return ok
}
func (err *ErrInvalidClaimData) Error() string {
return fmt.Sprintf("invalid claim data for field %s of type %s", err.Field, err.Type)
}
// ErrCodeInvalidClaimData holds the unique world-error code of this error
const ErrCodeInvalidClaimData = 1024
// HTTPError holds the http error description
func (err *ErrInvalidClaimData) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusBadRequest,
Code: ErrCodeInvalidClaimData,
Message: fmt.Sprintf("Invalid claim data for field %s of type %s", err.Field, err.Type),
}
}
+8 -35
View File
@@ -186,11 +186,7 @@ func (u *User) GetFailedPasswordAttemptsKey() string {
func GetFromAuth(a web.Auth) (*User, error) {
u, is := a.(*User)
if !is {
typ := reflect.TypeOf(a)
if typ.String() == "*models.LinkSharing" {
return nil, &ErrMustNotBeLinkShare{}
}
return &User{}, fmt.Errorf("user is not user element, is %s", typ)
return &User{}, fmt.Errorf("user is not user element, is %s", reflect.TypeOf(a))
}
return u, nil
}
@@ -446,39 +442,16 @@ func GetCurrentUser(c echo.Context) (user *User, err error) {
func GetUserFromClaims(claims jwt.MapClaims) (user *User, err error) {
userID, ok := claims["id"].(float64)
if !ok {
return user, &ErrInvalidClaimData{
Field: "id",
Type: reflect.TypeOf(claims["id"]).String(),
}
return user, ErrCouldNotGetUserID{}
}
email, ok := claims["email"].(string)
if !ok {
return nil, &ErrInvalidClaimData{
Field: "email",
Type: reflect.TypeOf(claims["email"]).String(),
}
}
username, ok := claims["username"].(string)
if !ok {
return nil, &ErrInvalidClaimData{
Field: "username",
Type: reflect.TypeOf(claims["username"]).String(),
}
}
name, ok := claims["name"].(string)
if !ok {
return nil, &ErrInvalidClaimData{
Field: "name",
Type: reflect.TypeOf(claims["name"]).String(),
}
user = &User{
ID: int64(userID),
Email: claims["email"].(string),
Username: claims["username"].(string),
Name: claims["name"].(string),
}
return &User{
ID: int64(userID),
Email: email,
Username: username,
Name: name,
}, nil
return
}
// UpdateUser updates a user