Compare commits

..
75 changed files with 2832 additions and 3096 deletions
+12 -12
View File
@@ -363,7 +363,7 @@ steps:
- api-build
- name: frontend-dependencies
image: node:22.13.0-alpine
image: node:22.12.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.12.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.12.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.12.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.12.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.12.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.12.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.12.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.12.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@@ -1398,6 +1398,6 @@ steps:
- failure
---
kind: signature
hmac: 298674d9b9d8618eef47969038fd4d7516112ecbd8a4d899ec3751d997a34ce4
hmac: 3e5069e96f4fa86dcc8453202f51a7bef36f958fb497477f0d5b5d56b985f3b7
...
+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.12.0-alpine AS frontendbuilder
WORKDIR /build
+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.12.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
+50 -50
View File
@@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@9.15.4",
"packageManager": "pnpm@9.15.1",
"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,31 @@
"@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.2",
"@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-code-block": "2.10.4",
"@tiptap/extension-code-block-lowlight": "2.10.4",
"@tiptap/extension-hard-break": "2.10.4",
"@tiptap/extension-image": "2.10.4",
"@tiptap/extension-link": "2.10.4",
"@tiptap/extension-placeholder": "2.10.4",
"@tiptap/extension-table": "2.10.4",
"@tiptap/extension-table-cell": "2.10.4",
"@tiptap/extension-table-header": "2.10.4",
"@tiptap/extension-table-row": "2.10.4",
"@tiptap/extension-task-item": "2.10.4",
"@tiptap/extension-task-list": "2.10.4",
"@tiptap/extension-typography": "2.10.4",
"@tiptap/extension-underline": "2.10.4",
"@tiptap/pm": "2.10.4",
"@tiptap/starter-kit": "^2.10.4",
"@tiptap/suggestion": "2.10.4",
"@tiptap/vue-3": "2.10.4",
"@vueuse/core": "12.2.0",
"@vueuse/router": "12.2.0",
"axios": "1.7.9",
"blurhash": "2.0.5",
"bulma-css-variables": "0.9.33",
@@ -93,7 +93,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",
@@ -111,50 +111,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.2",
"@typescript-eslint/parser": "8.18.2",
"@vitejs/plugin-vue": "5.2.1",
"@vue/eslint-config-typescript": "14.3.0",
"@vue/eslint-config-typescript": "14.2.0",
"@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.30001690",
"csstype": "3.1.3",
"cypress": "14.0.0",
"cypress": "13.17.0",
"esbuild": "0.24.2",
"eslint": "9.18.0",
"eslint": "9.17.0",
"eslint-plugin-vue": "9.32.0",
"happy-dom": "16.6.0",
"happy-dom": "16.0.0",
"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.29.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.6",
"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",
"vitest": "2.1.8",
"vue-tsc": "2.2.0",
"wait-on": "8.0.2",
"wait-on": "8.0.1",
"workbox-cli": "7.3.0"
},
"pnpm": {
+1284 -1328
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'
@@ -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
+46 -69
View File
@@ -137,7 +137,7 @@
</template>
<script setup lang="ts">
import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch} from 'vue'
import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch, watchEffect} from 'vue'
import {useI18n} from 'vue-i18n'
import {eventToHotkeyString} from '@github/hotkey'
@@ -162,12 +162,10 @@ import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import HardBreak from '@tiptap/extension-hard-break'
import {Node} from '@tiptap/pm/model'
import Commands from './commands'
import suggestionSetup from './suggestion'
import {common, createLowlight} from 'lowlight'
import {lowlight} from 'lowlight'
import type {BottomAction, UploadCallback} from './types'
import type {ITask} from '@/modelTypes/ITask'
@@ -181,17 +179,7 @@ import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
import inputPrompt from '@/helpers/inputPrompt'
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
const {
modelValue,
uploadCallback,
isEditEnabled,
bottomActions,
showSave,
placeholder,
editShortcut,
enableDiscardShortcut,
} = withDefaults(defineProps<{
modelValue: string,
const props = withDefaults(defineProps<{
uploadCallback?: UploadCallback,
isEditEnabled?: boolean,
bottomActions?: BottomAction[],
@@ -200,6 +188,7 @@ const {
editShortcut?: string,
enableDiscardShortcut?: boolean,
}>(), {
uploadCallback: undefined,
isEditEnabled: true,
bottomActions: () => [],
showSave: false,
@@ -208,7 +197,9 @@ const {
enableDiscardShortcut: false,
})
const emit = defineEmits(['update:modelValue', 'save'])
const emit = defineEmits(['save'])
const modelValue = defineModel<string>({ default: '' })
const tiptapInstanceRef = ref<HTMLInputElement | null>(null)
@@ -273,7 +264,7 @@ const CustomImage = Image.extend({
nextTick(async () => {
const img = document.getElementById(id)
const img = document.getElementById(id) as HTMLImageElement | null
if (!img) return
@@ -304,13 +295,13 @@ const CustomImage = Image.extend({
type Mode = 'edit' | 'preview'
const internalMode = ref<Mode>('preview')
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
const isEditing = computed(() => internalMode.value === 'edit' && props.isEditEnabled)
const contentHasChanged = ref<boolean>(false)
let lastSavedState = ''
watch(
() => modelValue,
modelValue,
(newValue) => {
if (!contentHasChanged.value) {
lastSavedState = newValue
@@ -320,7 +311,7 @@ watch(
)
watch(
() => internalMode.value,
internalMode,
mode => {
if (mode === 'preview') {
contentHasChanged.value = false
@@ -343,7 +334,7 @@ const extensions : Extensions = [
}),
CodeBlockLowlight.configure({
lowlight: createLowlight(common),
lowlight,
}),
HardBreak.extend({
addKeyboardShortcuts() {
@@ -360,25 +351,19 @@ const extensions : Extensions = [
}),
Placeholder.configure({
placeholder: ({editor}) => {
if (!isEditing.value) {
placeholder({editor}) {
if (!isEditing.value || editor.getText() !== '' && !editor.isFocused) {
return ''
}
if (editor.getText() !== '' && !editor.isFocused) {
return ''
}
return placeholder !== ''
? placeholder
: t('input.editor.placeholder')
return props.placeholder || t('input.editor.placeholder')
},
}),
Typography,
Underline,
Link.configure({
openOnClick: false,
validate: (href: string) => (new RegExp(
validate: (href) => (new RegExp(
`^(https?|${additionalLinkProtocols.join('|')}):\\/\\/`,
'i',
)).test(href),
@@ -397,8 +382,8 @@ const extensions : Extensions = [
TaskList,
TaskItem.configure({
nested: true,
onReadOnlyChecked: (node: Node, checked: boolean): boolean => {
if (!isEditEnabled) {
onReadOnlyChecked(node, checked) {
if (!props.isEditEnabled) {
return false
}
@@ -429,7 +414,7 @@ const extensions : Extensions = [
]
// Add a custom extension for the Escape key
if (enableDiscardShortcut) {
if (props.enableDiscardShortcut) {
extensions.push(Extension.create({
name: 'escapeKey',
@@ -448,21 +433,13 @@ const editor = useEditor({
// eslint-disable-next-line vue/no-ref-object-reactivity-loss
editable: isEditing.value,
extensions: extensions,
onUpdate: () => {
bubbleNow()
},
onUpdate: bubbleNow,
})
watch(
() => isEditing.value,
() => {
editor.value?.setEditable(isEditing.value)
},
{immediate: true},
)
watchEffect(() => editor.value?.setEditable(isEditing.value))
watch(
() => modelValue,
modelValue,
value => {
if (!editor?.value) return
@@ -476,13 +453,14 @@ watch(
)
function bubbleNow() {
if (editor.value?.getHTML() === modelValue ||
(editor.value?.getHTML() === '<p></p>') && modelValue === '') {
const editorVal = editor.value!.getHTML()
if (editorVal === modelValue.value ||
(editorVal === '<p></p>') && modelValue.value === '') {
return
}
contentHasChanged.value = true
emit('update:modelValue', editor.value?.getHTML())
modelValue.value = editorVal
}
function bubbleSave() {
@@ -502,7 +480,7 @@ function exitEditMode() {
}
function setEditIfApplicable() {
if (!isEditEnabled) return
if (!props.isEditEnabled) return
if (isEditing.value) return
setEdit()
@@ -519,21 +497,20 @@ onBeforeUnmount(() => editor.value?.destroy())
const uploadInputRef = ref<HTMLInputElement | null>(null)
function uploadAndInsertFiles(files: File[] | FileList) {
uploadCallback(files).then(urls => {
urls?.forEach(url => {
editor.value
?.chain()
.focus()
.setImage({src: url})
.run()
})
bubbleSave()
async function uploadAndInsertFiles(files: File[] | FileList) {
const urls = await props.uploadCallback?.(files)
urls?.forEach(url => {
editor.value
?.chain()
.focus()
.setImage({src: url})
.run()
})
bubbleSave()
}
function triggerImageInput(event) {
if (typeof uploadCallback !== 'undefined') {
if (typeof props.uploadCallback !== 'undefined') {
uploadInputRef.value?.click()
return
}
@@ -543,7 +520,7 @@ function triggerImageInput(event) {
async function addImage(event) {
if (typeof uploadCallback !== 'undefined') {
if (typeof props.uploadCallback !== 'undefined') {
const files = uploadInputRef.value?.files
if (!files || files.length === 0) {
@@ -568,28 +545,28 @@ function setLink(event) {
}
onMounted(async () => {
if (editShortcut !== '') {
if (props.editShortcut !== '') {
document.addEventListener('keydown', setFocusToEditor)
}
await nextTick()
if (typeof uploadCallback !== 'undefined') {
if (typeof props.uploadCallback !== 'undefined') {
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
input?.addEventListener('paste', handleImagePaste)
}
setModeAndValue(modelValue)
setModeAndValue(modelValue.value)
})
onBeforeUnmount(() => {
nextTick(() => {
if (typeof uploadCallback !== 'undefined') {
if (typeof props.uploadCallback !== 'undefined') {
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
input?.removeEventListener('paste', handleImagePaste)
}
})
if (editShortcut !== '') {
if (props.editShortcut !== '') {
document.removeEventListener('keydown', setFocusToEditor)
}
})
@@ -620,7 +597,7 @@ function setFocusToEditor(event) {
const hotkeyString = eventToHotkeyString(event)
if (!hotkeyString) return
if (hotkeyString !== editShortcut ||
if (hotkeyString !== props.editShortcut ||
event.target.tagName.toLowerCase() === 'input' ||
event.target.tagName.toLowerCase() === 'textarea' ||
event.target.contentEditable === 'true') {
@@ -629,7 +606,7 @@ function setFocusToEditor(event) {
event.preventDefault()
if (!isEditing.value && isEditEnabled) {
if (!isEditing.value && props.isEditEnabled) {
internalMode.value = 'edit'
}
@@ -653,7 +630,7 @@ function clickTasklistCheckbox(event) {
}
watch(
() => isEditing.value,
isEditing,
async editing => {
await nextTick()
+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>
@@ -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()
+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 -2
View File
@@ -1203,8 +1203,7 @@
"12001": "نوع الاشتراك غير صالح.",
"12002": "أنت مشترك بالفعل في العنصر نفسه أو في العنصر الرئيس.",
"13001": "رابط المشاركة هذا يتطلب كلمة مرور للتوثيق، ولكن لم يتم توفير أي منها.",
"13002": "كلمة مرور رابط المشاركة غير صالحة.",
"14001": "The provided file is not a valid zip file."
"13002": "كلمة مرور رابط المشاركة غير صالحة."
},
"about": {
"title": "عن البرنامج",
+1 -2
View File
@@ -1203,8 +1203,7 @@
"12001": "Типът на абонамента е невалиден.",
"12002": "Вече сте абонирани за самия обект или за родителски обект.",
"13001": "Тази споделена връзка изисква парола за удостоверяване, но такава не е предоставена.",
"13002": "Предоставената парола за споделената връзка е невалидна.",
"14001": "The provided file is not a valid zip file."
"13002": "Предоставената парола за споделената връзка е невалидна."
},
"about": {
"title": "За приложението",
+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",
+1 -2
View File
@@ -1203,8 +1203,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",
+1 -2
View File
@@ -1203,8 +1203,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",
+1 -2
View File
@@ -1203,8 +1203,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",
+1 -2
View File
@@ -1203,8 +1203,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",
+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",
+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",
+1 -2
View File
@@ -1203,8 +1203,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",
+1 -2
View File
@@ -1203,8 +1203,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",
+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",
+1 -2
View File
@@ -1203,8 +1203,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",
File diff suppressed because it is too large Load Diff
+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": "Vikunjaについて",
+1 -2
View File
@@ -1203,8 +1203,7 @@
"12001": "구독 엔터티 유형이 잘못되었습니다.",
"12002": "이미 엔티티 자체 또는 상위 엔티티에 가입되어 있습니다.",
"13001": "이 링크 공유에는 인증을 위한 비밀번호가 필요하지만 비밀번호가 제공되지 않았습니다.",
"13002": "제공한 링크 공유 비밀번호가 잘못되었습니다.",
"14001": "The provided file is not a valid zip file."
"13002": "제공한 링크 공유 비밀번호가 잘못되었습니다."
},
"about": {
"title": "소개",
+105 -106
View File
@@ -18,71 +18,71 @@
"accountWillBeDeleted": "Tavo paskyra bus ištrinta, įskaitant visus projektus, užduotis ir prisegtukus su tuo susijusius."
},
"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.",
@@ -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}"
},
@@ -366,7 +366,7 @@
"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."
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
},
"pseudo": {
"favorites": {
@@ -463,11 +463,11 @@
"labels": "Etiketės sietinos su užduotimi",
"project": "The project the task belongs to (only available for saved filters, not on a project level)",
"reminders": "The reminders of the task as a date field, will return all tasks with at least one reminder matching the query",
"created": "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ž",
@@ -506,7 +506,7 @@
"inProgress": "Importavimas progrese…",
"alreadyMigrated1": "Atrodo, kad jau importavai savo info is {name} {date}.",
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "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ą.",
@@ -684,34 +684,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 +725,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 +736,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 +752,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 +900,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 +951,13 @@
"hint": "Use magic prefixes to define due dates, assignees and other task properties.",
"title": "Quick Add Magic",
"intro": "When creating a task, you can use special keywords to directly add attributes to the newly created task. This allows to add commonly used attributes to tasks much faster.",
"multiple": "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 +987,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 +1007,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 +1027,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 +1041,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 +1087,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 +1130,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 +1185,7 @@
"6002": "The team does not exist.",
"6004": "The team already has access to that project.",
"6005": "The user is already a member of that team.",
"6006": "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 +1200,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",
+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": "Over",
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -1203,8 +1203,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",
+1 -2
View File
@@ -1203,8 +1203,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",
+1 -2
View File
@@ -1203,8 +1203,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",
+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",
+1 -2
View File
@@ -1203,8 +1203,7 @@
"12001": "Некорректный тип подписки.",
"12002": "Вы уже подписаны на сам объект или родительский объект.",
"13001": "Эта ссылка требует пароля, но он не был предоставлен.",
"13002": "Неверный пароль ссылки для обмена.",
"14001": "The provided file is not a valid zip file."
"13002": "Неверный пароль ссылки для обмена."
},
"about": {
"title": "О Vikunja",
+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",
+2 -3
View File
@@ -366,7 +366,7 @@
"bucketTitleSavedSuccess": "Naslov vedra je bil uspešno shranjen.",
"bucketLimitSavedSuccess": "Omejitev vedra je bila uspešno shranjena.",
"collapse": "Zloži vedro",
"bucketLimitReached": "Dosegli ste omejitev vedra. Odstranite opravila ali povečajte omejitev, da dodate nova opravila."
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
},
"pseudo": {
"favorites": {
@@ -1203,8 +1203,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",
+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",
+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": "Om",
+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",
+1 -2
View File
@@ -1203,8 +1203,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": "Про",
+1 -2
View File
@@ -1203,8 +1203,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",
+1 -2
View File
@@ -1203,8 +1203,7 @@
"12001": "订阅实体类型无效。",
"12002": "你已经订阅实体本身或上级实体。",
"13001": "此链接共享需要密码进行身份验证,但没有提供密码。",
"13002": "提供的链接共享密码无效。",
"14001": "The provided file is not a valid zip file."
"13002": "提供的链接共享密码无效。"
},
"about": {
"title": "关于",
+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",
+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
}
@@ -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,
}))
+32 -33
View File
@@ -18,21 +18,20 @@ 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/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
@@ -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,20 +84,20 @@ require (
)
require (
cel.dev/expr v0.16.1 // indirect
cel.dev/expr v0.16.0 // 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/auth v0.9.9 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
cloud.google.com/go/compute/metadata v0.5.2 // indirect
cloud.google.com/go/iam v1.2.1 // indirect
cloud.google.com/go/longrunning v0.6.1 // indirect
cloud.google.com/go/monitoring v1.21.1 // 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/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
@@ -109,11 +108,11 @@ require (
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/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59 // 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/go-control-plane v0.13.0 // 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
@@ -133,7 +132,7 @@ require (
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/gax-go/v2 v2.13.0 // 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
@@ -196,16 +195,16 @@ require (
go.opentelemetry.io/otel/trace v1.29.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/api v0.203.0 // indirect
google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/protobuf v1.35.1 // 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 +213,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
-66
View File
@@ -1,7 +1,5 @@
cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y=
cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
cel.dev/expr v0.16.1 h1:NR0+oFYzR1CqLFhTAqg3ql59G9VfN8fKq1TCHJ6gq1g=
cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -105,12 +103,8 @@ cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM
cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
cloud.google.com/go/auth v0.9.9 h1:BmtbpNQozo8ZwW2t7QJjnrQtdganSdmqeIBxHxNkEZQ=
cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8=
cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8=
@@ -192,8 +186,6 @@ cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxB
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=
cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck=
cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w=
@@ -329,8 +321,6 @@ cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB
cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU=
cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g=
cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA=
cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY=
cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc=
cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A=
cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk=
@@ -365,8 +355,6 @@ cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc=
cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0=
cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc=
cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI=
cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE=
cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM=
cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA=
@@ -392,8 +380,6 @@ cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtD
cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw=
cloud.google.com/go/monitoring v1.21.1 h1:zWtbIoBMnU5LP9A/fz8LmWMGHpk4skdfeiaa66QdFGc=
cloud.google.com/go/monitoring v1.21.1/go.mod h1:Rj++LKrlht9uBi8+Eb530dIrzG/cU/lB8mt+lbeFK1c=
cloud.google.com/go/monitoring v1.21.2 h1:FChwVtClH19E7pJ+e0xUhJPGksctZNVOk2UhMmblmdU=
cloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU=
cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA=
cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o=
cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM=
@@ -648,8 +634,6 @@ github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 h1:oVLqHXhnYtUwM89y9T1
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1 h1:pB2F2JKCj1Znmp2rwxxt1J0Fg0wezTMgWYk5Mpbi1kg=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1/go.mod h1:itPGVDKf9cC/ov4MdvJ2QZ0khw4bfoo9jzwTJlaxy2k=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
@@ -660,12 +644,6 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/ThreeDotsLabs/watermill v1.4.1 h1:gjP6yZH+otMPjV0KsV07pl9TeMm9UQV/gqiuiuG5Drs=
github.com/ThreeDotsLabs/watermill v1.4.1/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to=
github.com/ThreeDotsLabs/watermill v1.4.2 h1:lX/J79HyUipxZ2VetC7vMPqlw29xreHMxzhPlcZnYoQ=
github.com/ThreeDotsLabs/watermill v1.4.2/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to=
github.com/ThreeDotsLabs/watermill v1.4.3 h1:cRT1v7jlAgoPyEknvz0IFp3EKdSBRD/0Qbtz6KhexG8=
github.com/ThreeDotsLabs/watermill v1.4.3/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to=
github.com/ThreeDotsLabs/watermill v1.4.4 h1:aLClMl6EYIOQy4BML9yb2VpTekbynDatvQbXGp7idCU=
github.com/ThreeDotsLabs/watermill v1.4.4/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to=
github.com/adlio/trello v1.12.0 h1:JqOE2GFHQ9YtEviRRRSnicSxPbt4WFOxhqXzjMOw8lw=
github.com/adlio/trello v1.12.0/go.mod h1:I4Lti4jf2KxjTNgTqs5W3lLuE78QZZdYbbPnQQGwjOo=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
@@ -739,13 +717,9 @@ github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59 h1:fLZ97KE86ELjEYJCEUVzmbhfzDxHHGwBrDVMd4XL6Bs=
github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@@ -787,8 +761,6 @@ github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJ
github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q=
github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les=
github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8=
github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE=
github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
@@ -807,18 +779,12 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/getsentry/sentry-go v0.29.1 h1:DyZuChN8Hz3ARxGVV8ePaNXh1dQ7d76AiB117xcREwA=
github.com/getsentry/sentry-go v0.29.1/go.mod h1:x3AtIzN01d6SiWkderzaH28Tm0lgkafpJ5Bm3li39O0=
github.com/getsentry/sentry-go v0.30.0 h1:lWUwDnY7sKHaVIoZ9wYqRHJ5iEmoc0pqcRqFkosKzBo=
github.com/getsentry/sentry-go v0.30.0/go.mod h1:WU9B9/1/sHDqeV8T+3VwwbjeR5MSXs/6aqG3mqZrezA=
github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4=
github.com/getsentry/sentry-go v0.31.1/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY=
github.com/getsentry/sentry-go/echo v0.31.1 h1:bGY2QrNq5PovERoQBwyfJtQixjptHC06gLiAlF0WUPc=
github.com/getsentry/sentry-go/echo v0.31.1/go.mod h1:2gHa20EVxDNNTJY+Cq4Eqr8A0Z6UEULh4ImSsVMSRUg=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
@@ -1000,8 +966,6 @@ github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57Q
github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/googleapis/go-sql-spanner v1.7.4 h1:pwndJlqgIMOewkORveYQQocaSyOGqaQg8e2Os8hYh00=
github.com/googleapis/go-sql-spanner v1.7.4/go.mod h1:DfuJMbqpcDQwtbol+TnfO+AUyeoW5H+w8Gm216dTPys=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
@@ -1319,8 +1283,6 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
@@ -1375,8 +1337,6 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wneessen/go-mail v0.5.2 h1:MZKwgHJoRboLJ+EHMLuHpZc95wo+u1xViL/4XSswDT8=
github.com/wneessen/go-mail v0.5.2/go.mod h1:kRroJvEq2hOSEPFRiKjN7Csrz0G1w+RpiGR3b6yo+Ck=
github.com/wneessen/go-mail v0.6.0 h1:wO7EeJ8RL6DD+aycFGntil6b11g3FNQpQQQC1gkm97Y=
github.com/wneessen/go-mail v0.6.0/go.mod h1:G702XlFhzHV0Z4w9j2VsH5K9dJDvj0hx+yOOp1oX9vc=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
@@ -1471,8 +1431,6 @@ golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1541,8 +1499,6 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1648,8 +1604,6 @@ golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1773,8 +1727,6 @@ golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -1794,8 +1746,6 @@ golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1904,8 +1854,6 @@ golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1983,8 +1931,6 @@ google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU=
google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI=
google.golang.org/api v0.215.0 h1:jdYF4qnyczlEz2ReWIsosNLDuzXyvFHJtI5gcr0J7t0=
google.golang.org/api v0.215.0/go.mod h1:fta3CVtuJYOEdugLNWm6WodzOS8KdFckABwN4I40hzY=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -2126,16 +2072,10 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE=
google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53/go.mod h1:fheguH3Am2dGp1LfXkrvwqC/KlFq8F0nLq3LryOMrrE=
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc=
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg=
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -2179,8 +2119,6 @@ google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -2203,8 +2141,6 @@ google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6h
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -2307,8 +2243,6 @@ modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+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 -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)
}
-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