Compare commits

..

1 Commits

Author SHA1 Message Date
d933ae28dc fix(deps): update dependency @infectoone/vue-ganttastic to v2.2.0
Some checks failed
continuous-integration/drone/pr Build is failing
2023-08-21 11:08:37 +00:00
91 changed files with 968 additions and 3682 deletions

View File

@ -42,7 +42,7 @@ steps:
# - .cache
- name: dependencies
image: node:20.5-alpine
image: node:20-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -55,7 +55,7 @@ steps:
# - restore-cache
- name: lint
image: node:20.5-alpine
image: node:20-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -66,7 +66,7 @@ steps:
- dependencies
- name: build-prod
image: node:20.5-alpine
image: node:20-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -77,7 +77,7 @@ steps:
- dependencies
- name: test-unit
image: node:20.5-alpine
image: node:20-alpine
pull: always
commands:
- corepack enable && pnpm config set store-dir .cache/pnpm
@ -87,7 +87,7 @@ steps:
- name: typecheck
failure: ignore
image: node:20.5-alpine
image: node:20-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -202,7 +202,7 @@ steps:
# - .cache
- name: build
image: node:20.5-alpine
image: node:20-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -210,7 +210,6 @@ steps:
from_secret: sentry_auth_token
SENTRY_ORG: vikunja
SENTRY_PROJECT: frontend-oss
PUPPETEER_SKIP_DOWNLOAD: true
commands:
- apk add git
- corepack enable && pnpm config set store-dir .cache/pnpm
@ -226,7 +225,6 @@ steps:
image: kolaente/zip
pull: always
commands:
- cp src/version.json dist
- cd dist
- zip -r ../vikunja-frontend-unstable.zip *
- cd ..
@ -285,7 +283,7 @@ steps:
# - .cache
- name: build
image: node:20.5-alpine
image: node:20-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -308,7 +306,6 @@ steps:
image: kolaente/zip
pull: always
commands:
- cp src/version.json dist
- cd dist
- zip -r ../vikunja-frontend-${DRONE_TAG##v}.zip *
- cd ..
@ -475,25 +472,24 @@ name: update-translations
trigger:
branch:
include:
- main
- main
event:
include:
- cron
- cron
cron:
- update_translations
steps:
- name: download
pull: always
image: git.lcomrade.su/root/drone-crowdin-v2
image: jonasfranz/crowdin
settings:
crowdin_key:
download: true
export_dir: src/i18n/lang/
ignore_branch: true
project_identifier: vikunja
environment:
CROWDIN_KEY:
from_secret: crowdin_key
project_id: 462614
target: download
download_to: src/i18n/lang/
download_export_approved_only: true
- name: move-files
pull: always
@ -520,18 +516,19 @@ steps:
- name: upload
pull: always
image: git.lcomrade.su/root/drone-crowdin-v2
image: jonasfranz/crowdin
depends_on:
- clone
settings:
crowdin_key:
from_secret: crowdin_key
project_id: 462614
target: upload
upload_files:
src/i18n/lang/en.json: en.json
files:
en.json: src/i18n/lang/en.json
ignore_branch: true
project_identifier: vikunja
environment:
CROWDIN_KEY:
from_secret: crowdin_key
---
kind: signature
hmac: c5517d5fc49e327984177144aa195d4418a5769c25deb40f1c211e05735bc863
hmac: 6a566550cac03e9f3f9bbccab95fda4b342233bd63a1409cb5f634b1c744c326
...

View File

@ -3,14 +3,13 @@
# │─││ │││ │ │
# ┘─┘┘─┘┘┘─┘┘─┘
FROM --platform=$BUILDPLATFORM node:20.5-alpine AS builder
FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
WORKDIR /build
ARG USE_RELEASE=false
ARG RELEASE_VERSION=unstable
ENV PNPM_CACHE_FOLDER .cache/pnpm/
ENV PUPPETEER_SKIP_DOWNLOAD true
COPY package.json ./
COPY pnpm-lock.yaml ./
@ -58,7 +57,6 @@ ENV VIKUNJA_SENTRY_ENABLED false
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
ENV VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED false
ENV VIKUNJA_ALLOW_ICON_CHANGES true
ENV VIKUNJA_CUSTOM_LOGO_URL "''"
COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh
COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh

0
crowdin.cli Normal file
View File

View File

@ -1,5 +1,4 @@
import {UserFactory} from '../../factories/user'
import {ProjectFactory} from '../../factories/project'
const testAndAssertFailed = fixture => {
cy.intercept(Cypress.env('API_URL') + '/login*').as('login')
@ -14,28 +13,26 @@ const testAndAssertFailed = fixture => {
cy.get('div.message.danger').contains('Wrong username or password.')
}
const credentials = {
username: 'test',
password: '1234',
}
function login() {
cy.get('input[id=username]').type(credentials.username)
cy.get('input[id=password]').type(credentials.password)
cy.get('.button').contains('Login').click()
cy.url().should('include', '/')
}
const username = 'test'
context('Login', () => {
beforeEach(() => {
UserFactory.create(1, {username: credentials.username})
UserFactory.create(1, {username})
})
it('Should log in with the right credentials', () => {
const fixture = {
username: 'test',
password: '1234',
}
cy.visit('/login')
login()
cy.get('input[id=username]').type(fixture.username)
cy.get('input[id=password]').type(fixture.password)
cy.get('.button').contains('Login').click()
cy.url().should('include', '/')
cy.clock(1625656161057) // 13:00
cy.get('h2').should('contain', `Hi ${credentials.username}!`)
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
})
it('Should fail with a bad password', () => {
@ -60,15 +57,4 @@ context('Login', () => {
cy.visit('/')
cy.url().should('include', '/login')
})
it('Should redirect to the previous route after logging in', () => {
const projects = ProjectFactory.create(1)
cy.visit(`/projects/${projects[0].id}/list`)
cy.url().should('include', '/login')
login()
cy.url().should('include', `/projects/${projects[0].id}/list`)
})
})

View File

@ -17,7 +17,7 @@ context('Registration', () => {
it('Should work without issues', () => {
const fixture = {
username: 'testuser',
password: '12345678',
password: '123456',
email: 'testuser@example.com',
}
@ -31,10 +31,10 @@ context('Registration', () => {
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
})
it('Should fail', () => {
it.only('Should fail', () => {
const fixture = {
username: 'test',
password: '12345678',
password: '123456',
email: 'testuser@example.com',
}

View File

@ -13,6 +13,5 @@ sed -ri "s:^(\s*window.SENTRY_ENABLED\s*=)\s*.+:\1 ${VIKUNJA_SENTRY_ENABLED}:g"
sed -ri "s:^(\s*window.SENTRY_DSN\s*=)\s*.+:\1 '${VIKUNJA_SENTRY_DSN}':g" /usr/share/nginx/html/index.html
sed -ri "s:^(\s*window.PROJECT_INFINITE_NESTING_ENABLED\s*=)\s*.+:\1 '${VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED}':g" /usr/share/nginx/html/index.html
sed -ri "s:^(\s*window.ALLOW_ICON_CHANGES\s*=)\s*.+:\1 ${VIKUNJA_ALLOW_ICON_CHANGES}:g" /usr/share/nginx/html/index.html
sed -ri "s:^(\s*window.CUSTOM_LOGO_URL\s*=)\s*.+:\1 ${VIKUNJA_CUSTOM_LOGO_URL}:g" /usr/share/nginx/html/index.html
date -uIseconds | xargs echo 'info: started at'

View File

@ -32,8 +32,6 @@
window.PROJECT_INFINITE_NESTING_ENABLED = false
// Allow changing the logo and other icons based on various occasions throughout the year.
window.ALLOW_ICON_CHANGES = true
// Allow using a custom logo via external URL.
window.CUSTOM_LOGO_URL = ''
</script>
</body>
</html>

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@8.7.4",
"packageManager": "pnpm@8.6.12",
"keywords": [
"todo",
"productivity",
@ -51,17 +51,16 @@
"@fortawesome/vue-fontawesome": "3.0.3",
"@github/hotkey": "2.0.1",
"@infectoone/vue-ganttastic": "2.2.0",
"@intlify/unplugin-vue-i18n": "0.13.0",
"@intlify/unplugin-vue-i18n": "0.12.2",
"@kyvg/vue3-notification": "2.9.1",
"@sentry/tracing": "7.68.0",
"@sentry/vue": "7.68.0",
"@vueuse/core": "10.4.1",
"@vueuse/router": "10.4.1",
"axios": "1.5.0",
"@sentry/tracing": "7.60.0",
"@sentry/vue": "7.60.0",
"@vueuse/core": "10.3.0",
"axios": "1.4.0",
"blurhash": "2.0.5",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"codemirror": "5.65.15",
"codemirror": "5.65.14",
"date-fns": "2.30.0",
"dayjs": "1.11.9",
"dompurify": "3.0.5",
@ -74,12 +73,12 @@
"is-touch-device": "1.0.1",
"klona": "2.0.6",
"lodash.debounce": "4.0.8",
"marked": "5.1.2",
"marked": "5.1.1",
"pinia": "2.1.6",
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"sortablejs": "1.15.0",
"ufo": "1.3.0",
"ufo": "1.2.0",
"vue": "3.3.4",
"vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.3",
@ -93,53 +92,53 @@
"@cypress/vite-dev-server": "5.0.5",
"@cypress/vue": "5.0.5",
"@faker-js/faker": "8.0.2",
"@histoire/plugin-screenshot": "0.17.0",
"@histoire/plugin-vue": "0.17.1",
"@histoire/plugin-screenshot": "0.16.5",
"@histoire/plugin-vue": "0.16.5",
"@rushstack/eslint-patch": "1.3.3",
"@tsconfig/node18": "18.2.1",
"@types/codemirror": "5.60.9",
"@tsconfig/node18": "18.2.0",
"@types/codemirror": "5.60.8",
"@types/dompurify": "3.0.2",
"@types/flexsearch": "0.7.3",
"@types/is-touch-device": "1.0.0",
"@types/lodash.debounce": "4.0.7",
"@types/marked": "5.0.1",
"@types/node": "18.17.12",
"@types/node": "18.17.6",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.2",
"@typescript-eslint/eslint-plugin": "6.5.0",
"@typescript-eslint/parser": "6.5.0",
"@types/sortablejs": "1.15.1",
"@typescript-eslint/eslint-plugin": "6.4.0",
"@typescript-eslint/parser": "6.4.0",
"@vitejs/plugin-legacy": "4.1.1",
"@vitejs/plugin-vue": "4.3.4",
"@vitejs/plugin-vue": "4.3.2",
"@vue/eslint-config-typescript": "11.0.3",
"@vue/test-utils": "2.4.1",
"@vue/tsconfig": "0.4.0",
"autoprefixer": "10.4.15",
"browserslist": "4.21.10",
"caniuse-lite": "1.0.30001524",
"caniuse-lite": "1.0.30001522",
"css-has-pseudo": "6.0.0",
"csstype": "3.1.2",
"cypress": "12.17.4",
"esbuild": "0.19.2",
"eslint": "8.48.0",
"eslint": "8.47.0",
"eslint-plugin-vue": "9.17.0",
"happy-dom": "10.11.1",
"histoire": "0.17.0",
"happy-dom": "10.10.4",
"histoire": "0.16.5",
"postcss": "8.4.28",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-focus-within": "8.0.0",
"postcss-preset-env": "9.1.2",
"rollup": "3.28.1",
"postcss-preset-env": "9.1.1",
"rollup": "3.28.0",
"rollup-plugin-visualizer": "5.9.2",
"sass": "1.66.1",
"start-server-and-test": "2.0.0",
"typescript": "5.2.2",
"typescript": "5.1.6",
"vite": "4.4.9",
"vite-plugin-inject-preload": "1.3.3",
"vite-plugin-inject-preload": "1.3.2",
"vite-plugin-pwa": "0.16.4",
"vite-plugin-sentry": "1.3.0",
"vite-svg-loader": "4.0.0",
"vitest": "0.34.3",
"vitest": "0.34.2",
"vue-tsc": "1.8.8",
"wait-on": "7.0.1",
"workbox-cli": "7.0.0"

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,6 @@
<AddToHomeScreen/>
<UpdateNotification/>
<Notification/>
<DemoMode/>
</Teleport>
</ready>
</template>
@ -46,7 +45,6 @@ import {useBaseStore} from '@/stores/base'
import {useColorScheme} from '@/composables/useColorScheme'
import {useBodyClass} from '@/composables/useBodyClass'
import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue'
import DemoMode from '@/components/home/DemoMode.vue'
const baseStore = useBaseStore()
const authStore = useAuthStore()

View File

@ -1,49 +0,0 @@
<script setup lang="ts">
import {computed, ref} from 'vue'
import {useConfigStore} from '@/stores/config'
import BaseButton from '@/components/base/BaseButton.vue'
const configStore = useConfigStore()
const hide = ref(false)
const enabled = computed(() => configStore.demoModeEnabled && !hide.value)
</script>
<template>
<div
v-if="enabled"
class="demo-mode-banner"
>
<p>
{{ $t('demo.title') }}
<strong class="is-uppercase">{{ $t('demo.everythingWillBeDeleted') }}</strong>
</p>
<BaseButton @click="() => hide = true" class="hide-button">
<icon icon="times"/>
</BaseButton>
</div>
</template>
<style scoped lang="scss">
.demo-mode-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--danger);
z-index: 100;
padding: .5rem;
text-align: center;
&, strong {
color: hsl(220, 13%, 91%) !important; // --grey-200 in light mode, hardcoded because the color should not change
}
}
.hide-button {
padding: .25rem .5rem;
cursor: pointer;
position: absolute;
right: .5rem;
top: .25rem;
}
</style>

View File

@ -9,21 +9,15 @@ import {MILLISECONDS_A_HOUR} from '@/constants/date'
const now = useNow({
interval: MILLISECONDS_A_HOUR,
})
const Logo = computed(() => window.ALLOW_ICON_CHANGES && now.value.getMonth() === 6 ? LogoFullPride : LogoFull)
const CustomLogo = computed(() => window.CUSTOM_LOGO_URL)
const Logo = computed(() => window.ALLOW_ICON_CHANGES && now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
</script>
<template>
<div>
<Logo v-if="!CustomLogo" alt="Vikunja" class="logo" />
<img v-show="CustomLogo" :src="CustomLogo" alt="Vikunja" class="logo" />
</div>
<Logo alt="Vikunja" class="logo" />
</template>
<style lang="scss" scoped>
.logo {
color: var(--logo-text-color);
max-width: 168px;
max-height: 48px;
}
</style>

View File

@ -60,14 +60,6 @@
:can-collapse="false"
/>
</nav>
<nav class="menu" v-if="savedFilterProjects">
<ProjectsNavigation
:model-value="savedFilterProjects"
:can-edit-order="false"
:can-collapse="false"
/>
</nav>
<nav class="menu">
<ProjectsNavigation
@ -99,7 +91,6 @@ const projectStore = useProjectStore()
const projects = computed(() => projectStore.notArchivedRootProjects)
const favoriteProjects = computed(() => projectStore.favoriteProjects)
const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
</script>
<style lang="scss" scoped>

View File

@ -11,12 +11,7 @@
class="input-wrapper input"
:class="{'has-multiple': hasMultiple}"
>
<slot
v-if="Array.isArray(internalValue)"
name="items"
:items="internalValue"
:remove="remove"
>
<template v-if="Array.isArray(internalValue)">
<template v-for="(item, key) in internalValue">
<slot name="tag" :item="item">
<span :key="`item${key}`" class="tag ml-2 mt-2">
@ -25,7 +20,7 @@
</span>
</slot>
</template>
</slot>
</template>
<input
type="text"
@ -90,9 +85,7 @@
</template>
<script setup lang="ts">
import {
computed, onBeforeUnmount, onMounted, ref, toRefs, watch, type ComponentPublicInstance, type PropType,
} from 'vue'
import {computed, onBeforeUnmount, onMounted, ref, toRefs, watch, type ComponentPublicInstance, type PropType} from 'vue'
import {useI18n} from 'vue-i18n'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'

View File

@ -2,7 +2,6 @@ import {library} from '@fortawesome/fontawesome-svg-core'
import {
faAlignLeft,
faAngleRight,
faAnglesUp,
faArchive,
faArrowLeft,
faArrowUpFromBracket,
@ -143,7 +142,6 @@ library.add(faUser)
library.add(faUsers)
library.add(faArrowUpFromBracket)
library.add(faX)
library.add(faAnglesUp)
// overwriting the wrong types
export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes

View File

@ -1,26 +1,21 @@
<template>
<BaseButton class="dropdown-item">
<span
v-if="icon"
class="icon is-small"
:class="iconClass"
>
<span class="icon" v-if="icon">
<Icon :icon="icon"/>
</span>
<span>
<slot/>
<slot />
</span>
</BaseButton>
</template>
<script lang="ts" setup>
import BaseButton, {type BaseButtonProps} from '@/components/base//BaseButton.vue'
import BaseButton, { type BaseButtonProps } from '@/components/base//BaseButton.vue'
import Icon from '@/components/misc/Icon'
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
export interface DropDownItemProps extends /* @vue-ignore */ BaseButtonProps {
icon?: IconProp,
iconClass?: object | string,
}
defineProps<DropDownItemProps>()
@ -29,6 +24,7 @@ defineProps<DropDownItemProps>()
<style scoped lang="scss">
.dropdown-item {
color: var(--text);
display: block;
font-size: 0.875rem;
line-height: 1.5;
padding: $item-padding;
@ -56,7 +52,10 @@ defineProps<DropDownItemProps>()
.icon {
padding-right: .5rem;
color: var(--grey-300);
&:not(.has-text-success) {
color: var(--grey-300) !important;
}
}
.has-text-danger .icon {

View File

@ -1,10 +1,10 @@
<template>
<input
type="text"
data-input
:disabled="disabled"
data-input
:disabled="disabled"
v-bind="attrs"
ref="root"
ref="root"
/>
</template>
@ -20,39 +20,39 @@ type Options = flatpickr.Options.Options
type DateOption = flatpickr.Options.DateOption
function camelToKebab(string: string) {
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
}
function arrayify<T = unknown>(obj: T) {
return obj instanceof Array
return obj instanceof Array
? obj
: [obj]
}
function nullify<T = unknown>(value: T) {
return (value && (value as unknown[]).length)
return (value && (value as unknown[]).length)
? value
: null
}
// Events to emit, copied from flatpickr source
const includedEvents = [
'onChange',
'onClose',
'onDestroy',
'onMonthChange',
'onOpen',
'onYearChange',
'onChange',
'onClose',
'onDestroy',
'onMonthChange',
'onOpen',
'onYearChange',
] as HookKey[]
// Let's not emit these events by default
const excludedEvents = [
'onValueUpdate',
'onDayCreate',
'onParseConfig',
'onReady',
'onPreCalendarPosition',
'onKeyDown',
'onValueUpdate',
'onDayCreate',
'onParseConfig',
'onReady',
'onPreCalendarPosition',
'onKeyDown',
] as HookKey[]
// Keep a copy of all events for later use
@ -100,19 +100,19 @@ const attrs = useAttrs()
const root = ref<HTMLInputElement | null>(null)
const fp = ref<flatpickr.Instance | null>(null)
const safeConfig = ref<Options>({...props.config})
const safeConfig = ref<Options>({ ...props.config })
function prepareConfig() {
// Don't mutate original object on parent component
const newConfig: Options = {...props.config}
const newConfig: Options = { ...props.config }
props.events.forEach((hook) => {
// Respect global callbacks registered via setDefault() method
const globalCallbacks = flatpickr.defaultConfig[hook] || []
// Inject our own method along with user callback
const localCallback: Hook = (...args) => emit(camelToKebab(hook), ...args)
// Overwrite with merged array
newConfig[hook] = arrayify(newConfig[hook] || []).concat(
globalCallbacks,
@ -147,9 +147,9 @@ onMounted(() => {
prepareConfig()
/**
* Get the HTML node where flatpickr to be attached
* Bind on parent element if wrap is true
*/
* Get the HTML node where flatpickr to be attached
* Bind on parent element if wrap is true
*/
const element = props.config.wrap
? root.value.parentNode
: root.value
@ -179,7 +179,7 @@ watch(config, () => {
fp.value.set(name, safeConfig.value[name])
}
})
}, {deep: true})
}, {deep:true})
const fpInput = computed(() => {
if (!fp.value) return
@ -198,8 +198,8 @@ watchEffect(() => fpInput.value?.addEventListener('blur', onBlur))
onBeforeUnmount(() => fpInput.value?.removeEventListener('blur', onBlur))
/**
* Watch for the disabled property and sets the value to the real input.
*/
* Watch for the disabled property and sets the value to the real input.
*/
watchEffect(() => {
if (disabled.value) {
fpInput.value?.setAttribute('disabled', '')

View File

@ -48,14 +48,13 @@ import Message from '@/components/misc/message.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
import {ERROR_NO_API_URL, InvalidApiUrlProvidedError, NoApiUrlProvidedError} from '@/helpers/checkAndSetApiUrl'
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
import {useOnline} from '@/composables/useOnline'
import {getAuthForRoute} from '@/router'
import {useBaseStore} from '@/stores/base'
import {useAuthStore} from '@/stores/auth'
import {useI18n} from 'vue-i18n'
const router = useRouter()
const route = useRoute()
@ -69,8 +68,6 @@ const online = useOnline()
const error = ref('')
const showLoading = computed(() => !ready.value && error.value === '')
const {t} = useI18n()
async function load() {
try {
await baseStore.loadApp()
@ -80,15 +77,7 @@ async function load() {
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)
error.value = String(e)
}
}

View File

@ -33,7 +33,7 @@
}"
/>
<BaseButton
v-if="!project.isArchived && project.id > -1"
v-if="!project.isArchived"
class="favorite"
:class="{'is-favorite': project.isFavorite}"
@click.prevent.stop="projectStore.toggleProjectFavorite(project)"

View File

@ -157,7 +157,7 @@
<template
v-if="['filters.create', 'project.edit', 'filter.settings.edit'].includes($route.name as string)">
<div class="field">
<label class="label">{{ $t('project.projects') }}</label>
<label class="label">{{ $t('project.lists') }}</label>
<div class="control">
<SelectProject
v-model="entities.projects"

View File

@ -24,7 +24,7 @@
{{ hintText }}
</div>
<quick-add-magic v-if="isNewTaskCommand"/>
<quick-add-magic class="p-2 modal-container-smaller" v-if="isNewTaskCommand"/>
<div class="results" v-if="selectedCmd === null">
<div v-for="(r, k) in results" :key="k" class="result">
@ -44,18 +44,7 @@
@keyup.prevent.enter="doAction(r.type, i)"
@keyup.prevent.esc="searchInput?.focus()"
>
<template v-if="r.type === ACTION_TYPE.LABELS">
<x-label :label="i"/>
</template>
<template v-else-if="r.type === ACTION_TYPE.TASK">
<single-task-inline-readonly
:task="i"
:show-project="true"
/>
</template>
<template v-else>
{{ i.title }}
</template>
{{ i.title }}
</BaseButton>
</div>
</div>
@ -77,8 +66,6 @@ import ProjectModel from '@/models/project'
import BaseButton from '@/components/base/BaseButton.vue'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
import XLabel from '@/components/tasks/partials/label.vue'
import SingleTaskInlineReadonly from '@/components/tasks/partials/singleTaskInlineReadonly.vue'
import {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects'
@ -110,7 +97,6 @@ enum ACTION_TYPE {
TASK = 'task',
PROJECT = 'project',
TEAM = 'team',
LABELS = 'labels',
}
enum COMMAND_TYPE {
@ -148,38 +134,24 @@ function closeQuickActions() {
}
const foundProjects = computed(() => {
const {project, text, labels, assignees} = parsedQuery.value
if (project !== null) {
return projectStore.searchProject(project ?? text)
.filter(p => Boolean(p))
}
if (labels.length > 0 || assignees.length > 0) {
const { project } = parsedQuery.value
if (
searchMode.value === SEARCH_MODE.ALL ||
searchMode.value === SEARCH_MODE.PROJECTS ||
project === null
) {
return []
}
if (text === '') {
const history = getHistory()
return history.map((p) => projectStore.projects[p.id])
.filter(p => Boolean(p))
}
const history = getHistory()
const allProjects = [
...new Set([
...history.map((l) => projectStore.projects[l.id]),
...projectStore.searchProject(project),
]),
]
return projectStore.searchProject(project ?? text)
.filter(p => Boolean(p))
})
const foundLabels = computed(() => {
const {labels, text} = parsedQuery.value
if (text === '' && labels.length === 0) {
return []
}
if (labels.length > 0) {
return labelStore.filterLabelsByQuery([], labels[0])
}
return labelStore.filterLabelsByQuery([], text)
return allProjects.filter(l => Boolean(l))
})
// FIXME: use fuzzysearch
@ -200,20 +172,15 @@ const results = computed<Result[]>(() => {
title: t('quickActions.commands'),
items: foundCommands.value,
},
{
type: ACTION_TYPE.PROJECT,
title: t('quickActions.projects'),
items: foundProjects.value,
},
{
type: ACTION_TYPE.TASK,
title: t('quickActions.tasks'),
items: foundTasks.value,
},
{
type: ACTION_TYPE.LABELS,
title: t('quickActions.labels'),
items: foundLabels.value,
type: ACTION_TYPE.PROJECT,
title: t('quickActions.projects'),
items: foundProjects.value,
},
{
type: ACTION_TYPE.TEAM,
@ -223,7 +190,7 @@ const results = computed<Result[]>(() => {
].filter((i) => i.items.length > 0)
})
const loading = computed(() =>
const loading = computed(() =>
taskService.loading ||
projectStore.isLoading ||
teamService.loading,
@ -295,12 +262,10 @@ const searchMode = computed(() => {
if (query.value === '') {
return SEARCH_MODE.ALL
}
const {text, project, labels, assignees} = parsedQuery.value
const { text, project, labels, assignees } = parsedQuery.value
if (assignees.length === 0 && text !== '') {
return SEARCH_MODE.TASKS
}
if (
assignees.length === 0 &&
project !== null &&
@ -309,7 +274,6 @@ const searchMode = computed(() => {
) {
return SEARCH_MODE.PROJECTS
}
if (
assignees.length > 0 &&
project === null &&
@ -318,7 +282,6 @@ const searchMode = computed(() => {
) {
return SEARCH_MODE.TEAMS
}
return SEARCH_MODE.ALL
})
@ -329,12 +292,12 @@ const isNewTaskCommand = computed(() => (
const taskSearchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
type Filter = { by: string, value: string | number, comparator: string }
type Filter = {by: string, value: string | number, comparator: string}
function filtersToParams(filters: Filter[]) {
const filter_by: Filter['by'][] = []
const filter_value: Filter['value'][] = []
const filter_comparator: Filter['comparator'][] = []
const filter_by : Filter['by'][] = []
const filter_value : Filter['value'][] = []
const filter_comparator : Filter['comparator'][] = []
filters.forEach(({by, value, comparator}) => {
filter_by.push(by)
@ -352,8 +315,7 @@ function filtersToParams(filters: Filter[]) {
function searchTasks() {
if (
searchMode.value !== SEARCH_MODE.ALL &&
searchMode.value !== SEARCH_MODE.TASKS &&
searchMode.value !== SEARCH_MODE.PROJECTS
searchMode.value !== SEARCH_MODE.TASKS
) {
foundTasks.value = []
return
@ -368,7 +330,7 @@ function searchTasks() {
taskSearchTimeout.value = null
}
const {text, project: projectName, labels} = parsedQuery.value
const { text, project: projectName, labels } = parsedQuery.value
const filters: Filter[] = []
@ -387,9 +349,8 @@ function searchTasks() {
if (projectName !== null) {
const project = projectStore.findProjectByExactname(projectName)
console.log({project})
if (project !== null) {
addFilter('project_id', project.id, 'equals')
addFilter('projectId', project.id, 'equals')
}
}
@ -400,16 +361,19 @@ function searchTasks() {
}
}
const params = {
s: text,
sort_by: 'done',
...filtersToParams(filters),
}
const params = {
s: text,
...filtersToParams(filters),
}
taskSearchTimeout.value = setTimeout(async () => {
const r = await taskService.getAll({}, params) as DoAction<ITask>[]
const r = await taskService.getAll({}, params) as DoAction<ITask>[]
foundTasks.value = r.map((t) => {
t.type = ACTION_TYPE.TASK
const project = projectStore.projects[t.projectId]
if (project !== null) {
t.title = `${t.title} (${project.title})`
}
return t
})
}, 150)
@ -432,10 +396,10 @@ function searchTeams() {
clearTimeout(teamSearchTimeout.value)
teamSearchTimeout.value = null
}
const {assignees} = parsedQuery.value
const { assignees } = parsedQuery.value
teamSearchTimeout.value = setTimeout(async () => {
const teamSearchPromises = assignees.map((t) =>
teamService.getAll({}, {s: t}),
teamService.getAll({}, { s: t }),
)
const teamsResult = await Promise.all(teamSearchPromises)
foundTeams.value = teamsResult.flat().map((team) => {
@ -458,21 +422,21 @@ async function doAction(type: ACTION_TYPE, item: DoAction) {
closeQuickActions()
await router.push({
name: 'project.index',
params: {projectId: (item as DoAction<IProject>).id},
params: { projectId: (item as DoAction<IProject>).id },
})
break
case ACTION_TYPE.TASK:
closeQuickActions()
await router.push({
name: 'task.detail',
params: {id: (item as DoAction<ITask>).id},
params: { id: (item as DoAction<ITask>).id },
})
break
case ACTION_TYPE.TEAM:
closeQuickActions()
await router.push({
name: 'teams.edit',
params: {id: (item as DoAction<ITeam>).id},
params: { id: (item as DoAction<ITeam>).id },
})
break
case ACTION_TYPE.CMD:
@ -480,11 +444,6 @@ async function doAction(type: ACTION_TYPE, item: DoAction) {
selectedCmd.value = item as DoAction<Command>
searchInput.value?.focus()
break
case ACTION_TYPE.LABELS:
query.value = '*' + item.title
searchInput.value?.focus()
searchTasks()
break
}
}
@ -511,8 +470,8 @@ async function newTask() {
title: query.value,
projectId: currentProject.value.id,
})
success({message: t('task.createSuccess')})
await router.push({name: 'task.detail', params: {id: task.id}})
success({ message: t('task.createSuccess') })
await router.push({ name: 'task.detail', params: { id: task.id } })
}
async function newProject() {
@ -522,17 +481,17 @@ async function newProject() {
await projectStore.createProject(new ProjectModel({
title: query.value,
}))
success({message: t('project.create.createdSuccess')})
success({ message: t('project.create.createdSuccess')})
}
async function newTeam() {
const newTeam = new TeamModel({name: query.value})
const newTeam = new TeamModel({ name: query.value })
const team = await teamService.create(newTeam)
await router.push({
name: 'teams.edit',
params: {id: team.id},
params: { id: team.id },
})
success({message: t('team.create.success')})
success({ message: t('team.create.success') })
}
type BaseButtonInstance = InstanceType<typeof BaseButton>
@ -543,7 +502,7 @@ function setResultRefs(el: Element | ComponentPublicInstance | null, index: numb
resultRefs.value[index] = []
}
resultRefs.value[index][key] = el as (BaseButtonInstance | null)
resultRefs.value[index][key] = el as (BaseButtonInstance | null)
}
function select(parentIndex: number, index: number) {
@ -588,7 +547,7 @@ function reset() {
<style lang="scss" scoped>
.quick-actions {
overflow: hidden;
// FIXME: changed position should be an option of the modal
:deep(.modal-content) {
top: 3rem;
@ -610,7 +569,6 @@ function reset() {
}
}
.active-cmd {
font-size: 1.25rem;
margin-left: .5rem;
@ -656,4 +614,10 @@ function reset() {
background: var(--grey-100);
}
}
// HACK:
// FIXME:
.modal-container-smaller :deep(.hint-modal .modal-container) {
height: calc(100vh - 5rem);
}
</style>

View File

@ -1,93 +0,0 @@
<script setup lang="ts">
import type {IUser} from '@/modelTypes/IUser'
import BaseButton from '@/components/base/BaseButton.vue'
import User from '@/components/misc/user.vue'
import {computed} from 'vue'
type removeFunction = (item: any) => void
const {
assignees,
remove,
disabled,
avatarSize = 30,
inline = false,
} = defineProps<{
assignees: IUser[],
remove?: removeFunction,
disabled?: boolean,
avatarSize?: number,
inline?: boolean,
}>()
const hasDelete = computed(() => typeof remove !== 'undefined' && !disabled)
</script>
<template>
<div class="assignees-list" :class="{'is-inline': inline}">
<span
v-for="user in assignees"
class="assignee"
:key="user.id"
>
<User
:key="'user'+user.id"
:avatar-size="avatarSize"
:show-username="false"
:user="user"
:class="{'m-2': hasDelete, 'mr-3': !hasDelete}"
/>
<BaseButton
:key="'delete'+user.id"
v-if="hasDelete"
@click="remove(user)"
class="remove-assignee"
>
<icon icon="times"/>
</BaseButton>
</span>
</div>
</template>
<style scoped lang="scss">
.assignees-list {
display: flex;
&.is-inline :deep(.user) {
display: inline;
}
&:hover .assignee:not(:first-child) {
margin-left: -1rem;
}
}
.assignee {
position: relative;
transition: all $transition;
&:not(:first-child) {
margin-left: -1.5rem;
}
:deep(.user img) {
border: 2px solid var(--white);
margin-right: 0;
}
}
.remove-assignee {
position: absolute;
top: 4px;
left: 2px;
color: var(--danger);
background: var(--white);
padding: 0 4px;
display: block;
border-radius: 100%;
font-size: .75rem;
width: 18px;
height: 18px;
z-index: 100;
}
</style>

View File

@ -11,8 +11,13 @@
v-model="assignees"
:autocomplete-enabled="false"
>
<template #items="{items}">
<assignee-list :assignees="items" :remove="removeAssignee"/>
<template #tag="{item: user}">
<span class="assignee">
<user :avatar-size="32" :show-username="false" :user="user" class="m-2"/>
<BaseButton @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled">
<icon icon="times"/>
</BaseButton>
</span>
</template>
<template #searchResult="{option: user}">
<user :avatar-size="24" :show-username="true" :user="user"/>
@ -26,6 +31,7 @@ import {useI18n} from 'vue-i18n'
import User from '@/components/misc/user.vue'
import Multiselect from '@/components/input/multiselect.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import {includesById} from '@/helpers/utils'
import ProjectUserService from '@/services/projectUsers'
@ -34,7 +40,6 @@ import {useTaskStore} from '@/stores/tasks'
import type {IUser} from '@/modelTypes/IUser'
import {getDisplayName} from '@/models/user'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
const props = defineProps({
taskId: {
@ -115,3 +120,34 @@ async function findUser(query: string) {
})
}
</script>
<style lang="scss" scoped>
.assignee {
position: relative;
&:not(:first-child) {
margin-left: -1.5rem;
}
:deep(.user img) {
border: 2px solid var(--white);
margin-right: 0;
}
}
.remove-assignee {
position: absolute;
top: 4px;
left: 2px;
color: var(--danger);
background: var(--white);
padding: 0 4px;
display: block;
border-radius: 100%;
font-size: .75rem;
width: 18px;
height: 18px;
z-index: 100;
}
</style>

View File

@ -48,14 +48,16 @@
</progress>
<div class="footer">
<labels :labels="task.labels"/>
<priority-label :priority="task.priority" :done="task.done" class="is-inline-flex is-align-items-center"/>
<assignee-list
v-if="task.assignees.length > 0"
:assignees="task.assignees"
:avatar-size="24"
class="ml-1"
:inline="true"
/>
<priority-label :priority="task.priority" :done="task.done"/>
<div class="assignees" v-if="task.assignees.length > 0">
<user
v-for="u in task.assignees"
:avatar-size="24"
:key="task.id + 'assignee' + u.id"
:show-username="false"
:user="u"
/>
</div>
<checklist-summary :task="task"/>
<span class="icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
@ -76,6 +78,7 @@ import {ref, computed, watch} from 'vue'
import {useRouter} from 'vue-router'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
import User from '@/components/misc/user.vue'
import Done from '@/components/misc/Done.vue'
import Labels from '@/components/tasks/partials/labels.vue'
import ChecklistSummary from './checklist-summary.vue'
@ -88,9 +91,6 @@ import AttachmentService from '@/services/attachment'
import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate'
import {colorIsDark} from '@/helpers/color/colorIsDark'
import {useTaskStore} from '@/stores/tasks'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
import {useAuthStore} from '@/stores/auth'
import {playPopSound} from '@/helpers/playPop'
const router = useRouter()
@ -109,14 +109,10 @@ const color = computed(() => getHexColor(task.hexColor))
async function toggleTaskDone(task: ITask) {
loadingInternal.value = true
try {
const updatedTask = await useTaskStore().update({
await useTaskStore().update({
...task,
done: !task.done,
})
if (updatedTask.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
playPopSound()
}
} finally {
loadingInternal.value = false
}
@ -242,7 +238,7 @@ $task-background: var(--white);
.priority-label {
font-size: .75rem;
padding: 0 .5rem 0 .25rem;
height: 2rem;
.icon {
height: 1rem;

View File

@ -1,25 +0,0 @@
<script setup lang="ts">
import type {ILabel} from '@/modelTypes/ILabel'
defineProps<{
label: ILabel
}>()
</script>
<template>
<span
:key="label.id"
:style="{'background': label.hexColor, 'color': label.textColor}"
class="tag"
>
<span>{{ label.title }}</span>
</span>
</template>
<style scoped lang="scss">
.tag {
& + & {
margin-left: 0.5rem;
}
}
</style>

View File

@ -1,10 +1,12 @@
<template>
<div class="label-wrapper">
<XLabel
v-for="label in labels"
:label="label"
<span
:key="label.id"
/>
:style="{'background': label.hexColor, 'color': label.textColor}"
class="tag"
v-for="label in labels">
<span>{{ label.title }}</span>
</span>
</div>
</template>
@ -12,8 +14,6 @@
import type {PropType} from 'vue'
import type {ILabel} from '@/modelTypes/ILabel'
import XLabel from '@/components/tasks/partials/label.vue'
defineProps({
labels: {
type: Array as PropType<ILabel[]>,
@ -26,4 +26,10 @@ defineProps({
.label-wrapper {
display: inline;
}
.tag {
& + & {
margin-left: 0.5rem;
}
}
</style>

View File

@ -40,12 +40,17 @@ defineProps({
</script>
<style lang="scss" scoped>
.priority-label {
display: inline-flex;
align-items: center;
}
span.high-priority {
color: var(--danger);
width: auto !important; // To override the width set in tasks
.icon {
vertical-align: top;
vertical-align: middle;
width: auto !important;
padding: 0 .5rem;
}

View File

@ -108,7 +108,7 @@ const visible = ref(false)
const mode = computed(() => authStore.settings.frontendSettings.quickAddMagicMode)
defineProps<{
highlightHintIcon?: boolean,
highlightHintIcon: boolean,
}>()
const prefixes = computed(() => PREFIXES[mode.value])

View File

@ -9,7 +9,6 @@
variant="secondary"
icon="plus"
:shadow="false"
id="showRelatedTasksFormButton"
/>
<transition-group name="fade">
<template v-if="editEnabled && showCreate">
@ -162,8 +161,6 @@ import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import {error, success} from '@/message'
import {useTaskStore} from '@/stores/tasks'
import {useProjectStore} from '@/stores/projects'
import {useAuthStore} from '@/stores/auth'
import {playPopSound} from '@/helpers/playPop'
const props = defineProps({
taskId: {
@ -332,10 +329,6 @@ async function createAndRelateTask(title: string) {
async function toggleTaskDone(task: ITask) {
await taskStore.update(task)
if (task.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
playPopSound()
}
// Find the task in the project and update it so that it is correctly strike through
Object.entries(relatedTasks.value).some(([kind, tasks]) => {
return (tasks as ITask[]).some((t, key) => {

View File

@ -7,8 +7,8 @@
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">
{{ $t('task.repeat.everyWeek') }}
</x-button>
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(30, 'days')">
{{ $t('task.repeat.every30d') }}
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">
{{ $t('task.repeat.everyMonth') }}
</x-button>
</div>
<div class="is-flex is-align-items-center mb-2">
@ -51,6 +51,8 @@
<option value="hours">{{ $t('task.repeat.hours') }}</option>
<option value="days">{{ $t('task.repeat.days') }}</option>
<option value="weeks">{{ $t('task.repeat.weeks') }}</option>
<option value="months">{{ $t('task.repeat.months') }}</option>
<option value="years">{{ $t('task.repeat.years') }}</option>
</select>
</div>
</div>

View File

@ -32,8 +32,6 @@
:color="getHexColor(task.hexColor)"
class="mr-1"
/>
<priority-label :priority="task.priority" :done="task.done"/>
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
<span class="parent-tasks" v-if="typeof task.relatedTasks?.parenttask !== 'undefined'">
@ -51,12 +49,14 @@
:labels="task.labels"
/>
<assignee-list
v-if="task.assignees.length > 0"
:assignees="task.assignees"
:avatar-size="25"
class="ml-1"
:inline="true"
<User
v-for="(a, i) in task.assignees"
:avatar-size="27"
:is-inline="true"
:key="task.id + 'assignee' + a.id + i"
:show-username="false"
:user="a"
class="m-2"
/>
<!-- FIXME: use popup -->
@ -72,13 +72,15 @@
class="is-italic"
:aria-expanded="showDefer ? 'true' : 'false'"
>
{{ $t('task.detail.due', {at: dueDateFormatted}) }}
{{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
</time>
</BaseButton>
<CustomTransition name="fade">
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
</CustomTransition>
<priority-label :priority="task.priority" :done="task.done"/>
<span>
<span class="project-task-icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
@ -119,7 +121,7 @@
<icon icon="star" v-if="task.isFavorite"/>
<icon :icon="['far', 'star']" v-else/>
</BaseButton>
<slot/>
<slot />
</div>
</template>
@ -127,7 +129,7 @@
import {ref, watch, shallowReactive, onMounted, onBeforeUnmount, computed} from 'vue'
import {useI18n} from 'vue-i18n'
import TaskModel, {getHexColor} from '@/models/task'
import TaskModel, { getHexColor } from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
@ -135,6 +137,7 @@ import Labels from '@/components/tasks/partials//labels.vue'
import DeferTask from '@/components/tasks/partials//defer-task.vue'
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
import User from '@/components/misc/user.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import ColorBubble from '@/components/misc/colorBubble.vue'
@ -149,10 +152,6 @@ import {success} from '@/message'
import {useProjectStore} from '@/stores/projects'
import {useBaseStore} from '@/stores/base'
import {useTaskStore} from '@/stores/tasks'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
import {useIntervalFn} from '@vueuse/core'
import {playPopSound} from '@/helpers/playPop'
import {useAuthStore} from '@/stores/auth'
const {
theTask,
@ -215,28 +214,11 @@ const taskDetailRoute = computed(() => ({
// state: { backdropView: router.currentRoute.value.fullPath },
}))
function updateDueDate() {
if (!task.value.dueDate) {
return
}
dueDateFormatted.value = formatDateSince(task.value.dueDate)
}
const dueDateFormatted = ref('')
useIntervalFn(updateDueDate, 60_000, {
immediateCallback: true,
})
onMounted(updateDueDate)
async function markAsDone(checked: boolean) {
const updateFunc = async () => {
const newTask = await taskStore.update(task.value)
task.value = newTask
if (checked && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
playPopSound()
}
emit('task-updated', newTask)
success({
message: task.value.done ?
@ -266,7 +248,6 @@ async function toggleFavorite() {
}
const deferDueDate = ref<typeof DeferTask | null>(null)
function hideDeferDueDatePopup(e) {
if (!showDefer.value) {
return
@ -302,7 +283,7 @@ function hideDeferDueDatePopup(e) {
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1 0 50%;
.dueDate {
@ -412,7 +393,7 @@ function hideDeferDueDatePopup(e) {
color: var(--danger);
}
input[type='checkbox'] {
input[type="checkbox"] {
vertical-align: middle;
}

View File

@ -1,190 +0,0 @@
<template>
<div class="task">
<span>
<span
v-if="showProject && typeof project !== 'undefined'"
class="task-project"
:class="{'mr-2': task.hexColor !== ''}"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
>
{{ project.title }}
</span>
<ColorBubble
v-if="task.hexColor !== ''"
:color="getHexColor(task.hexColor)"
class="mr-1"
/>
<priority-label :priority="task.priority" :done="task.done"/>
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
<span class="parent-tasks" v-if="typeof task.relatedTasks?.parenttask !== 'undefined'">
<template v-for="(pt, i) in task.relatedTasks.parenttask">
{{ pt.title }}<template v-if="(i + 1) < task.relatedTasks.parenttask.length">,&nbsp;</template>
</template>
&rsaquo;
</span>
{{ task.title }}
</span>
<labels
v-if="task.labels.length > 0"
class="labels ml-2 mr-1"
:labels="task.labels"
/>
<assignee-list
v-if="task.assignees.length > 0"
:assignees="task.assignees"
:avatar-size="20"
class="ml-1"
:inline="true"
/>
<span
v-if="+new Date(task.dueDate) > 0"
class="dueDate"
v-tooltip="formatDateLong(task.dueDate)"
>
<time
:datetime="formatISO(task.dueDate)"
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="is-italic"
>
{{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
</time>
</span>
<span>
<span class="project-task-icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
</span>
<span class="project-task-icon" v-if="task.description">
<icon icon="align-left"/>
</span>
<span class="project-task-icon" v-if="task.repeatAfter.amount > 0">
<icon icon="history"/>
</span>
</span>
<checklist-summary :task="task"/>
<progress
class="progress is-small"
v-if="task.percentDone > 0"
:value="task.percentDone * 100" max="100"
>
{{ task.percentDone * 100 }}%
</progress>
</div>
</template>
<script setup lang="ts">
import {computed} from 'vue'
import {getHexColor} from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
import Labels from '@/components/tasks/partials//labels.vue'
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
import ColorBubble from '@/components/misc/colorBubble.vue'
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
import {useProjectStore} from '@/stores/projects'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
const {
task,
showProject = false,
} = defineProps<{
task: ITask,
showProject?: boolean,
}>()
const projectStore = useProjectStore()
const project = computed(() => projectStore.projects[task.projectId])
</script>
<style lang="scss" scoped>
.task {
display: flex;
flex-wrap: wrap;
transition: background-color $transition;
align-items: center;
cursor: pointer;
border-radius: $radius;
border: 2px solid transparent;
text-overflow: ellipsis;
word-wrap: break-word;
word-break: break-word;
//display: -webkit-box;
hyphens: auto;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
//flex: 1 0 50%;
.dueDate {
display: inline-block;
margin-left: 5px;
}
.overdue {
color: var(--danger);
}
.task-project {
width: auto;
color: var(--grey-400);
font-size: .9rem;
white-space: nowrap;
}
.avatar {
border-radius: 50%;
vertical-align: bottom;
margin-left: .5rem;
height: 21px;
width: 21px;
}
.project-task-icon {
margin-left: 6px;
&:not(:first-of-type) {
margin-left: 8px;
}
}
a {
color: var(--text);
transition: color ease $transition-duration;
&:hover {
color: var(--grey-900);
}
}
.tasktext.done {
text-decoration: line-through;
color: var(--grey-500);
}
span.parent-tasks {
color: var(--grey-500);
width: auto;
}
.progress {
margin-bottom: 0;
}
}
</style>

View File

@ -17,7 +17,7 @@ export function useRouteFilters<CurrentFilters extends Filters>(
const routeFromFiltersFullPath = computed(() => router.resolve(filtersToRoute(filters.value)).fullPath)
watch(
route.value,
route,
(route, oldRoute) => {
if (
route?.name !== oldRoute?.name ||

View File

@ -32,11 +32,6 @@ export function useRouteWithModal() {
: routePropsOption
: {}
if (typeof routeProps === 'undefined') {
currentModal.value = undefined
return
}
routeProps.backdropView = backdropView.value
const component = route.matched[0]?.components?.default

View File

@ -1,6 +1,5 @@
import {ref, shallowReactive, watch, computed, type ComputedGetter} from 'vue'
import {useRoute} from 'vue-router'
import {useRouteQuery} from '@vueuse/router'
import TaskCollectionService from '@/services/taskCollection'
import type {ITask} from '@/modelTypes/ITask'
@ -69,33 +68,23 @@ export function useTaskList(projectIdGetter: ComputedGetter<IProject['id']>, sor
const params = ref({...getDefaultParams()})
const search = ref('')
const page = useRouteQuery('page', '1', { transform: Number })
const page = ref(1)
const sortBy = ref({ ...sortByDefault })
const allParams = computed(() => {
const loadParams = {...params.value}
const getAllTasksParams = computed(() => {
let loadParams = {...params.value}
if (search.value !== '') {
loadParams.s = search.value
}
return formatSortOrder(sortBy.value, loadParams)
})
watch(
() => allParams.value,
() => {
// When parameters change, the page should always be the first
page.value = 1
},
)
loadParams = formatSortOrder(sortBy.value, loadParams)
const getAllTasksParams = computed(() => {
return [
{projectId: projectId.value},
allParams.value,
page.value,
loadParams,
page.value || 1,
]
})

View File

@ -4,27 +4,8 @@ const API_DEFAULT_PORT = '3456'
export const ERROR_NO_API_URL = 'noApiUrlProvided'
export class NoApiUrlProvidedError extends Error {
constructor() {
super()
this.message = 'No API URL provided'
this.name = 'NoApiUrlProvidedError'
}
}
export class InvalidApiUrlProvidedError extends Error {
constructor() {
super()
this.message = 'The provided API URL is invalid.'
this.name = 'InvalidApiUrlProvidedError'
}
}
export const checkAndSetApiUrl = (url: string | undefined | null): Promise<string> => {
if (url === '' || url === null || typeof url === 'undefined') {
throw new NoApiUrlProvidedError()
}
export const checkAndSetApiUrl = (url: string): Promise<string> => {
if (url.startsWith('/')) {
url = window.location.host + url
}
@ -36,14 +17,8 @@ export const checkAndSetApiUrl = (url: string | undefined | null): Promise<strin
) {
url = `${window.location.protocol}//${url}`
}
let urlToCheck: URL
try {
urlToCheck = new URL(url)
} catch (e) {
throw new InvalidApiUrlProvidedError()
}
const urlToCheck: URL = new URL(url)
const origUrlToCheck = urlToCheck
const oldUrl = window.API_URL
@ -111,6 +86,6 @@ export const checkAndSetApiUrl = (url: string | undefined | null): Promise<strin
return window.API_URL
}
throw new InvalidApiUrlProvidedError()
throw new Error(ERROR_NO_API_URL)
})
}

View File

@ -4,7 +4,7 @@ import {PrefixMode} from '@/modules/parseTaskText'
describe('Parse Subtasks via Relation', () => {
it('Should not return a parent for a single task', () => {
const tasks = parseSubtasksViaIndention('single task', PrefixMode.Default)
const tasks = parseSubtasksViaIndention('single task')
expect(tasks).to.have.length(1)
expect(tasks[0].parent).toBeNull()
@ -118,52 +118,4 @@ task two`, PrefixMode.Default)
expect(tasks[1].project).to.eq('list')
expect(tasks[2].project).to.eq('list')
})
it('Should clean the indention if there is indention on the first line', () => {
const tasks = parseSubtasksViaIndention(
` parent task
sub task one
sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task one')
expect(tasks[1].parent).toBeNull()
expect(tasks[2].title).to.eq('sub task two')
expect(tasks[2].parent).to.eq('sub task one')
})
it('Should clean the indention if there is indention on the first line but not for subsequent tasks', () => {
const tasks = parseSubtasksViaIndention(
` parent task
sub task one
first level task one
sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(4)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task one')
expect(tasks[1].parent).toBeNull()
expect(tasks[2].title).to.eq('first level task one')
expect(tasks[2].parent).toBeNull()
expect(tasks[3].title).to.eq('sub task two')
expect(tasks[3].parent).to.eq('first level task one')
})
it('Should clean the indention if there is indention on the first line for subsequent tasks with less indention', () => {
const tasks = parseSubtasksViaIndention(
` parent task
sub task one
first level task one
sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(4)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task one')
expect(tasks[1].parent).toBeNull()
expect(tasks[2].title).to.eq('first level task one')
expect(tasks[2].parent).toBeNull()
expect(tasks[3].title).to.eq('sub task two')
expect(tasks[3].parent).to.eq('first level task one')
})
})

View File

@ -17,29 +17,7 @@ const spaceRegex = /^ */
* relation between each other.
*/
export function parseSubtasksViaIndention(taskTitles: string, prefixMode: PrefixMode): TaskWithParent[] {
let titles = taskTitles.split(/[\r\n]+/)
if (titles.length == 0) {
return []
}
const spaceOnFirstLine = /^(\t| )+/
const spaces = spaceOnFirstLine.exec(titles[0])
if (spaces !== null) {
let spacesToCut = spaces[0].length
titles = titles.map(title => {
const spacesOnThisLine = spaceOnFirstLine.exec(title)
if (spacesOnThisLine === null) {
// This means the current task title does not start with indention, but the very first one did
// To prevent cutting actual task data we now need to update the number of spaces to cut
spacesToCut = 0
}
if (spacesOnThisLine !== null && spacesOnThisLine[0].length < spacesToCut) {
spacesToCut = spacesOnThisLine[0].length
}
return title.substring(spacesToCut)
})
}
const titles = taskTitles.split(/[\r\n]+/)
return titles.map((title, index) => {
const task: TaskWithParent = {
@ -54,7 +32,7 @@ export function parseSubtasksViaIndention(taskTitles: string, prefixMode: Prefix
return task
}
const matched = spaceRegex.exec(task.title)
const matched = spaceRegex.exec(title)
const matchedSpaces = matched ? matched[0].length : 0
if (matchedSpaces > 0) {
@ -67,7 +45,7 @@ export function parseSubtasksViaIndention(taskTitles: string, prefixMode: Prefix
const parentMatched = spaceRegex.exec(task.parent)
parentSpaces = parentMatched ? parentMatched[0].length : 0
} while (parentSpaces >= matchedSpaces)
task.title = cleanupTitle(task.title.replace(spaceRegex, ''))
task.title = cleanupTitle(title.replace(spaceRegex, ''))
task.parent = task.parent.replace(spaceRegex, '')
if (task.project === null) {
// This allows to specify a project once for the parent task and inherit it to all subtasks

View File

@ -1,10 +1,6 @@
const LAST_VISITED_KEY = 'lastVisited'
export const saveLastVisited = (name: string | undefined, params: object, query: object) => {
if (typeof name === 'undefined') {
return
}
export const saveLastVisited = (name: string, params: object, query: object) => {
localStorage.setItem(LAST_VISITED_KEY, JSON.stringify({name, params, query}))
}
@ -13,7 +9,7 @@ export const getLastVisited = () => {
if (lastVisited === null) {
return null
}
return JSON.parse(lastVisited)
}

View File

@ -5,8 +5,8 @@ import {format, formatDistanceToNow} from 'date-fns'
import {enGB, de, fr, ru} from 'date-fns/locale'
import {i18n} from '@/i18n'
import {createSharedComposable, type MaybeRef} from '@vueuse/core'
import {computed, unref} from 'vue'
import { createSharedComposable, type MaybeRef } from '@vueuse/core'
import { computed, unref } from 'vue'
const locales = {en: enGB, de, ch: de, fr, ru}
@ -62,7 +62,7 @@ export const useDateTimeFormatter = createSharedComposable((options?: MaybeRef<I
})
export function useWeekDayFromDate() {
const dateTimeFormatter = useDateTimeFormatter({weekday: 'short'})
const dateTimeFormatter = useDateTimeFormatter({ weekday: 'short' })
return computed(() => (date: Date) => dateTimeFormatter.value.format(date))
}

View File

@ -16,6 +16,10 @@ export function secondsToPeriod(seconds: number): { unit: PeriodUnit, amount: nu
if (seconds % SECONDS_A_DAY === 0) {
if (seconds % SECONDS_A_WEEK === 0) {
return {unit: 'weeks', amount: seconds / SECONDS_A_WEEK}
} else if (seconds % SECONDS_A_MONTH === 0) {
return {unit: 'days', amount: seconds / SECONDS_A_MONTH * 30}
} else if (seconds % SECONDS_A_YEAR === 0) {
return {unit: 'years', amount: seconds / SECONDS_A_YEAR}
} else {
return {unit: 'days', amount: seconds / SECONDS_A_DAY}
}

View File

@ -18,7 +18,6 @@ export const SUPPORTED_LOCALES = {
'es-ES': 'Español',
'da-DK': 'Dansk',
'ja-JP': '日本語',
'hu-HU': 'Magyar',
} as const
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES
@ -48,13 +47,8 @@ export async function setLanguage(lang: SupportedLocale): Promise<SupportedLocal
// If the language hasn't been loaded yet
if (!i18n.global.availableLocales.includes(lang)) {
try {
const messages = await import(`./lang/${lang}.json`)
i18n.global.setLocaleMessage(lang, messages.default)
} catch (e) {
console.error(`Failed to load language ${lang}:`, e)
return setLanguage(getBrowserLanguage())
}
const messages = await import(`./lang/${lang}.json`)
i18n.global.setLocaleMessage(lang, messages.default)
}
i18n.global.locale.value = lang

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Not found",
"text": "The page you requested does not exist."
@ -144,30 +139,6 @@
"system": "System",
"dark": "Dark"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"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.",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -934,7 +902,6 @@
"tasks": "Tasks",
"projects": "Projects",
"teams": "Teams",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Nenalezeno",
"text": "Požadovaná stránka neexistuje."
@ -144,30 +139,6 @@
"system": "Systém",
"dark": "Tmavý"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "např. https://localhost:3456",
"change": "změnit",
"use": "Používá se instalace Vikunja v {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.",
"error": "Nelze najít nebo použít instalaci Vikunja na \"{domain}\". Zkuste prosím jinou url.",
"success": "Pomocí instalace Vikunja na \"{domain}\".",
"urlRequired": "Je vyžadována adresa URL."
},
@ -934,7 +902,6 @@
"tasks": "Úkoly",
"projects": "Projects",
"teams": "Týmy",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Zadejte název nového úkolu…",
"newTeam": "Zadejte název nového týmu…",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Ikke fundet",
"text": "Den ønskede side findes ikke."
@ -144,30 +139,6 @@
"system": "System",
"dark": "Mørk"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "f.eks. https://localhost:3456",
"change": "ændr",
"use": "Brug Vikunja-installationen på {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.",
"error": "Kunne ikke finde eller bruge Vikunja-installationen på \"{domain}\". Prøv venligst en anden url.",
"success": "Bruger Vikunja-installationen på \"{domain}\".",
"urlRequired": "En url er påkrævet."
},
@ -934,7 +902,6 @@
"tasks": "Opgaver",
"projects": "Projects",
"teams": "Hold",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Indtast titlen på den nye opgave…",
"newTeam": "Indtast navnet på det nye hold…",

View File

@ -11,11 +11,6 @@
"import": "Importiere deine Daten in Vikunja"
}
},
"demo": {
"title": "Diese Instanz ist im Demo-Modus. Verwende sie nicht mit echten Daten!",
"everythingWillBeDeleted": "Alles wird in regelmäßigen Abständen gelöscht!",
"accountWillBeDeleted": "Dein Account wird gelöscht, einschließlich aller Projekte, Aufgaben und Anhänge, die du möglicherweise erstellst."
},
"404": {
"title": "Nicht gefunden",
"text": "Die angeforderte Seite existiert nicht."
@ -144,30 +139,6 @@
"system": "System",
"dark": "Dunkel"
}
},
"apiTokens": {
"title": "API-Tokens",
"general": "Mit API-Token kannst du die API von Vikunja ohne Login-Daten verwenden.",
"apiDocs": "Schaue dir die API-Dokumentation an",
"createAToken": "Token erstellen",
"createToken": "Token erstellen",
"30d": "30 Tage",
"60d": "60 Tage",
"90d": "90 Tage",
"permissionExplanation": "Mit Berechtigungen kannst du einschränken, was ein API-Token tun darf.",
"titleRequired": "Titel ist erforderlich",
"expired": "Dieses Token ist {ago} abgelaufen.",
"delete": {
"header": "Dieses Token löschen",
"text1": "Bist Du sicher, dass Du das Token \"{token}\" löschen möchtest?",
"text2": "Dies wird den Zugriff des Tokens auf alle Anwendungen oder Integrationen aufheben. Du kannst dies nicht rückgängig machen."
},
"attributes": {
"title": "Titel",
"titlePlaceholder": "Gib einen Titel ein, den du später erkennen wirst",
"expiresAt": "Läuft ab am",
"permissions": "Berechtigungen"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "Alle Aufgaben, die in diese Spalte verschoben werden, werden automatisch als erledigt markiert.",
"doneBucketHintExtended": "Alle Aufgaben, die in die Erledigt Spalte verschoben wurden, werden automatisch als erledigt markiert. Aufgaben, die in einer anderen Spalte als Erledigt markiert wurden, werden auch in diese Spalte verschoben.",
"doneBucketSavedSuccess": "Erledigt Spalte gespeichert.",
"defaultBucket": "Standard-Spalte",
"defaultBucketHint": "Wenn Aufgaben ohne Angabe einer Spalte erstellt werden, werden sie zu dieser Spalte hinzugefügt.",
"defaultBucketSavedSuccess": "Die Standardspalte wurde erfolgreich gespeichert.",
"deleteLast": "Du kannst die letzte Spalte nicht entfernen.",
"addTaskPlaceholder": "Gebe einen Aufgabentitel ein …",
"addTask": "Eine Aufgabe hinzufügen",
@ -913,7 +881,7 @@
"urlPlaceholder": "z.B. https://localhost:3456",
"change": "ändern",
"use": "Verwende die Vikunja-Installation unter „{0}“",
"error": "Vikunja Installation unter \"{domain}\" konnte nicht gefunden oder verwendet werden. Bitte prüfe, ob die URL das richtige Format hat und direkt darauf zugreifen kannst und versuche es erneut.",
"error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.",
"success": "Verwende die Vikunja-Installation unter „{domain}“.",
"urlRequired": "Eine Url ist erforderlich."
},
@ -934,7 +902,6 @@
"tasks": "Aufgaben",
"projects": "Projekte",
"teams": "Teams",
"labels": "Labels",
"newProject": "Gib den Titel des neuen Projekts ein…",
"newTask": "Gib den Titel der neuen Aufgabe ein …",
"newTeam": "Gib den Namen des neuen Teams ein …",

View File

@ -11,11 +11,6 @@
"import": "Importiere deine Daten in Vikunja"
}
},
"demo": {
"title": "Diese Instanz ist im Demo-Modus. Verwende sie nicht mit echten Daten!",
"everythingWillBeDeleted": "Alles wird in regelmäßigen Abständen gelöscht!",
"accountWillBeDeleted": "Dein Account wird gelöscht, einschließlich aller Projekte, Aufgaben und Anhänge, die du möglicherweise erstellst."
},
"404": {
"title": "Nid gfunde",
"text": "Dini gsuechti Siite giz nid."
@ -144,30 +139,6 @@
"system": "System",
"dark": "Dunkel"
}
},
"apiTokens": {
"title": "API-Tokens",
"general": "Mit API-Token kannst du die API von Vikunja ohne Login-Daten verwenden.",
"apiDocs": "Schaue dir die API-Dokumentation an",
"createAToken": "Token erstellen",
"createToken": "Token erstellen",
"30d": "30 Tage",
"60d": "60 Tage",
"90d": "90 Tage",
"permissionExplanation": "Mit Berechtigungen kannst du einschränken, was ein API-Token tun darf.",
"titleRequired": "Titel ist erforderlich",
"expired": "Dieses Token ist {ago} abgelaufen.",
"delete": {
"header": "Dieses Token löschen",
"text1": "Bist Du sicher, dass Du das Token \"{token}\" löschen möchtest?",
"text2": "Dies wird den Zugriff des Tokens auf alle Anwendungen oder Integrationen aufheben. Du kannst dies nicht rückgängig machen."
},
"attributes": {
"title": "Titel",
"titlePlaceholder": "Gib einen Titel ein, den du später erkennen wirst",
"expiresAt": "Läuft ab am",
"permissions": "Berechtigungen"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "Alle Aufgaben, die in diese Spalte verschoben werden, werden automatisch als erledigt markiert.",
"doneBucketHintExtended": "Alle Aufgaben, die in die Erledigt Spalte verschoben wurden, werden automatisch als erledigt markiert. Aufgaben, die in einer anderen Spalte als Erledigt markiert wurden, werden auch in diese Spalte verschoben.",
"doneBucketSavedSuccess": "Erledigt Spalte gespeichert.",
"defaultBucket": "Standard-Spalte",
"defaultBucketHint": "Wenn Aufgaben ohne Angabe einer Spalte erstellt werden, werden sie zu dieser Spalte hinzugefügt.",
"defaultBucketSavedSuccess": "Die Standardspalte wurde erfolgreich gespeichert.",
"deleteLast": "Du kannst die letzte Spalte nicht entfernen.",
"addTaskPlaceholder": "Gebe einen Aufgabentitel ein …",
"addTask": "Eine Aufgabe hinzufügen",
@ -913,7 +881,7 @@
"urlPlaceholder": "z.B. https://localhost:3456",
"change": "ändere",
"use": "Verwende die Vikunja-Installation unter „{0}“",
"error": "Vikunja Installation unter \"{domain}\" konnte nicht gefunden oder verwendet werden. Bitte prüfe, ob die URL das richtige Format hat und direkt darauf zugreifen kannst und versuche es erneut.",
"error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.",
"success": "Benutze d'Vikunja Installation uf \"{domain}\".",
"urlRequired": "Eine Url ist erforderlich."
},
@ -934,7 +902,6 @@
"tasks": "Uufgabe",
"projects": "Projekte",
"teams": "Teams",
"labels": "Labels",
"newProject": "Gib den Titel des neuen Projekts ein…",
"newTask": "Gib en Titl für die neu Uufgab iih…",
"newTeam": "Gib en Name für da neui Team iih…",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Not found",
"text": "The page you requested does not exist."
@ -144,32 +139,6 @@
"system": "System",
"dark": "Dark"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -336,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -770,7 +736,7 @@
"repeat": {
"everyDay": "Every Day",
"everyWeek": "Every Week",
"every30d": "Every 30 Days",
"everyMonth": "Every Month",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From Current Date",
@ -918,7 +884,7 @@
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"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.",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -939,7 +905,6 @@
"tasks": "Tasks",
"projects": "Projects",
"teams": "Teams",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Not found",
"text": "The page you requested does not exist."
@ -144,30 +139,6 @@
"system": "System",
"dark": "Dark"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"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.",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -934,7 +902,6 @@
"tasks": "Tasks",
"projects": "Projects",
"teams": "Teams",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -11,11 +11,6 @@
"import": "Importa tus datos a Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "No encontrado",
"text": "La página solicitada no existe."
@ -144,30 +139,6 @@
"system": "Sistema",
"dark": "Oscuro"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "Todas las tareas movidas a este contenedor se marcarán automáticamente como finalizadas.",
"doneBucketHintExtended": "Todas las tareas movidas al contenedor completado se marcarán como finalizadas automáticamente. Todas las tareas marcadas como finalizadas desde otro lugar también se moverán.",
"doneBucketSavedSuccess": "El contenedor completado se ha guardado correctamente.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "No puedes eliminar el último contenedor.",
"addTaskPlaceholder": "Introduce el nuevo título de la tarea…",
"addTask": "Añadir una tarea",
@ -913,7 +881,7 @@
"urlPlaceholder": "ej. https://localhost:3456",
"change": "cambiar",
"use": "Utilizando la instalación de Vikunja en {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.",
"error": "No se pudo encontrar o usar la instalación de Vikunja en \"{domain}\". Por favor, prueba con una url diferente.",
"success": "Usando la instalación de Vikunja en \"{domain}\".",
"urlRequired": "Se requiere una url."
},
@ -934,7 +902,6 @@
"tasks": "Tareas",
"projects": "Proyectos",
"teams": "Equipos",
"labels": "Labels",
"newProject": "Introduzca el título del nuevo proyecto…",
"newTask": "Introduzca el título de la nueva tarea…",
"newTeam": "Introduzca el nombre del nuevo equipo…",

View File

@ -11,11 +11,6 @@
"import": "Importer vos données dans Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Non trouvé",
"text": "La page que vous avez demandée nexiste pas."
@ -144,30 +139,6 @@
"system": "Système",
"dark": "Sombre"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "Toute tâche déplacée dans cette colonne sera automatiquement marquée comme terminée.",
"doneBucketHintExtended": "Toute tâche déplacée dans cette colonne sera automatiquement marquée comme terminée. Toute tâche marquée comme terminée ailleurs sera également déplacée.",
"doneBucketSavedSuccess": "La colonne des tâches terminées a bien été enregistrée.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "Vous ne pouvez pas retirer la dernière colonne.",
"addTaskPlaceholder": "Saisir le nouveau nom de la tâche…",
"addTask": "Ajouter une tâche",
@ -913,7 +881,7 @@
"urlPlaceholder": "Par exemple : https://localhost:3456",
"change": "changer",
"use": "Utiliser linstallation de Vikunja à {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.",
"error": "Impossible de trouver ou d'utiliser l'installation de Vikunja sur « {domain} ». Veuillez essayer une autre adresse.",
"success": "Utilisation de linstallation Vikunja sur « {domain} ».",
"urlRequired": "Une adresse est requise."
},
@ -934,7 +902,6 @@
"tasks": "Tâches",
"projects": "Projets",
"teams": "Équipes",
"labels": "Labels",
"newProject": "Saisissez le nom du nouveau projet…",
"newTask": "Saisir le nom de la nouvelle tâche…",
"newTeam": "Saisir le nom de la nouvelle équipe…",

File diff suppressed because it is too large Load Diff

View File

@ -5,17 +5,12 @@
"welcomeDay": "Ciao {username}!",
"welcomeEvening": "Buonasera {username}!",
"lastViewed": "Ultima visualizzazione",
"addToHomeScreen": "Aggiungi questa app alla tua schermata iniziale per un accesso più veloce e un'esperienza migliore.",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"project": {
"importText": "Import your projects and tasks from other services into Vikunja:",
"import": "Importa i tuoi dati in Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Non trovato",
"text": "La pagina richiesta non esiste."
@ -144,30 +139,6 @@
"system": "Sistema",
"dark": "Scuro"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -195,8 +166,8 @@
},
"project": {
"archivedMessage": "This project is archived. It is not possible to create new or edit tasks for it.",
"archived": "Archiviati",
"showArchived": "Mostra Archiviati",
"archived": "Archived",
"showArchived": "Show Archived",
"title": "Titolo Progetto",
"color": "Colore",
"projects": "Progetti",
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "es. http://localhost:8080",
"change": "modifica",
"use": "Usa l'installazione di Vikunja a {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.",
"error": "Impossibile trovare o usare l'installazione di Vikunja su \"{domain}\". Prova per favore con un altro Url.",
"success": "Utilizzando l'installazione di Vikunja su \"{domain}\".",
"urlRequired": "L'URL è obbligatorio."
},
@ -934,7 +902,6 @@
"tasks": "Attivitá",
"projects": "Projects",
"teams": "Gruppi",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Inserisci il titolo della nuova attività…",
"newTeam": "Inserisci il nome del nuovo gruppo…",

View File

@ -11,11 +11,6 @@
"import": "Vikunjaへのデータのインポート"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Not found",
"text": "リクエストされたページは存在しません。"
@ -144,30 +139,6 @@
"system": "システム既定",
"dark": "ダーク"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "タスクの追加",
@ -361,12 +329,12 @@
"title": "絞り込み",
"clear": "絞り込みの解除",
"attributes": {
"title": "絞り込み条件名",
"titlePlaceholder": "絞り込み条件名を入力…",
"title": "条件名",
"titlePlaceholder": "条件名を入力…",
"description": "説明",
"descriptionPlaceholder": "絞り込み条件の説明を入力…",
"includeNulls": "値が設定されていないタスクを含める",
"requireAll": "すべての条件に一致するタスクのみ表示",
"descriptionPlaceholder": "説明を入力…",
"includeNulls": "値を設定していないタスクを含める",
"requireAll": "Require all filters to be true for a task to show up",
"showDoneTasks": "完了したタスクを表示",
"sortAlphabetically": "アルファベット順に並べ替える",
"enablePriority": "優先度による絞り込みを有効化",
@ -378,18 +346,18 @@
},
"create": {
"title": "新しい絞り込み条件の作成",
"description": "絞り込み条件は、複数の条件を組み合わせて保存できる仮想のプロジェクトです。",
"description": "A saved filter is a virtual project which is computed from a set of filters each time it is accessed.",
"action": "新しい絞り込み条件を作成",
"titleRequired": "絞り込み条件名を入力してください。"
"titleRequired": "Please provide a title for the filter."
},
"delete": {
"header": "絞り込み条件の削除",
"text": "絞り込み条件を削除して本当によろしいですか?",
"success": "絞り込み条件は正常に削除されました。"
"header": "Delete this saved filter",
"text": "Are you sure you want to delete this saved filter?",
"success": "The filter was deleted successfully."
},
"edit": {
"title": "絞り込み条件の編集",
"success": "絞り込み条件は正常に保存されました。"
"title": "Edit This Saved Filter",
"success": "The filter was saved successfully."
}
},
"migrate": {
@ -847,7 +815,7 @@
"namePlaceholder": "The team's name goes here…",
"nameRequired": "Please specify a name.",
"description": "説明",
"descriptionPlaceholder": "チームの説明を入力…",
"descriptionPlaceholder": "The teams description goes here…",
"admin": "管理者",
"member": "メンバー"
}
@ -913,7 +881,7 @@
"urlPlaceholder": "例: https://localhost:3456",
"change": "変更",
"use": "{0} に設置されたVikunjaを使用します。",
"error": "\"{domain}\" にはVikunjaは存在しないか使用できない状態です。URLの形式が正しいかどうか、そして直接アクセスして到達きるかどうかを確認し、もう一度お試しください。",
"error": "\"{domain}\" にはVikunjaは存在しないか使用できない状態です。別のURLでお試しください。",
"success": "\"{domain}\" に設置されたVikunjaを使用します。",
"urlRequired": "URLは必須です。"
},
@ -934,7 +902,6 @@
"tasks": "タスク",
"projects": "プロジェクト",
"teams": "チーム",
"labels": "Labels",
"newProject": "新しいプロジェクト名を入力…",
"newTask": "新しいタスク名を入力…",
"newTeam": "新しいチーム名を入力…",
@ -1016,7 +983,7 @@
"10004": "You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold.",
"10005": "There can be only one done bucket per project.",
"11001": "The saved filter does not exist.",
"11002": "絞り込み条件はリンクの共有には使用できません。",
"11002": "Saved filters are not available for link shares.",
"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.",

View File

@ -11,11 +11,6 @@
"import": "데이터를 Vikunja로 가져오기"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "찾을 수 없습니다.",
"text": "요청하신 페이지가 존재하지 않습니다."
@ -144,30 +139,6 @@
"system": "시스템",
"dark": "어두운 테마"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "작업 추가",
@ -913,7 +881,7 @@
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"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.",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -934,7 +902,6 @@
"tasks": "Tasks",
"projects": "Projects",
"teams": "Teams",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Niet gevonden",
"text": "De opgevraagde pagina bestaat niet."
@ -144,30 +139,6 @@
"system": "Systeem",
"dark": "Donker"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "bv. https://localhost:3456",
"change": "wijzigen",
"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.",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -934,7 +902,6 @@
"tasks": "Taken",
"projects": "Projects",
"teams": "Teams",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -11,11 +11,6 @@
"import": "Importer dine data til Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Ikke funnet",
"text": "Siden du ba om, finnes ikke."
@ -144,30 +139,6 @@
"system": "System",
"dark": "Mørk"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "Alle oppgaver som flyttet til denne bøtte vil automatisk bli markert som ferdig.",
"doneBucketHintExtended": "Alle oppgaver som er flyttet inn i den utførte bøtten, vil bli merket som utført automatisk. Alle oppgaver merket som gjort fra andre steder vil også bli flyttet.",
"doneBucketSavedSuccess": "Bøtten er lagret.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "Du kan ikke fjerne den siste bøtten.",
"addTaskPlaceholder": "Angi den nye oppgavens tittel…",
"addTask": "Legg til oppgave",
@ -913,7 +881,7 @@
"urlPlaceholder": "f.eks. http://localhost:3456",
"change": "endre",
"use": "Bruker Vikunja installasjonen på {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.",
"error": "Kunne ikke finne eller bruke Vikunja installasjon på{domain}\". Prøv en annen Url.",
"success": "Bruker Vikunja installasjonen på \"{domain}.",
"urlRequired": "Url'en er tom, vennligst legg til."
},
@ -934,7 +902,6 @@
"tasks": "Oppgaver",
"projects": "Prosjekter",
"teams": "Grupper",
"labels": "Labels",
"newProject": "Skriv tittelen på det nye prosjektet…",
"newTask": "Skriv tittelen på den nye oppgaven…",
"newTeam": "Skriv inn navnet på den nye gruppen…",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Nie znaleziono",
"text": "Żądana strona nie istnieje."
@ -144,30 +139,6 @@
"system": "Systemowy",
"dark": "Ciemny"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "np. https://localhost:3456",
"change": "zmień",
"use": "Użyj instalacji Vikunji z {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.",
"error": "Nie można znaleźć lub użyć instalacji Vikunji z \"{domain}\". Wypróbuj inny adres URL.",
"success": "Używasz instalacji Vikunji z \"{domain}\".",
"urlRequired": "URL jest wymagany."
},
@ -934,7 +902,6 @@
"tasks": "Zadania",
"projects": "Projects",
"teams": "Zespoły",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Wpisz tytuł nowego zadania…",
"newTeam": "Wpisz nazwę nowego zespołu…",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Não encontrado",
"text": "The page you requested does not exist."
@ -144,30 +139,6 @@
"system": "System",
"dark": "Dark"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"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.",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Usando a instalação Vikunja em \"{domain}\".",
"urlRequired": "Uma url é necessária."
},
@ -934,7 +902,6 @@
"tasks": "Tarefas",
"projects": "Projects",
"teams": "Equipes",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -11,11 +11,6 @@
"import": "Importar os teus dados para o Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Não encontrado",
"text": "A página solicitada não existe."
@ -144,30 +139,6 @@
"system": "Sistema",
"dark": "Escuro"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "Todas as tarefas movidas para este conjunto serão automaticamente marcadas como concluídas.",
"doneBucketHintExtended": "Todas as tarefas movidas para o conjunto concluído serão marcadas automaticamente como concluídas. Todas as tarefas marcadas como concluídas em outro lugar também serão movidas.",
"doneBucketSavedSuccess": "O conjunto concluído foi salvo com sucesso.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "Não podes remover o ultimo conjunto.",
"addTaskPlaceholder": "Introduz o título da nova tarefa…",
"addTask": "Adicionar uma tarefa",
@ -913,7 +881,7 @@
"urlPlaceholder": "ex.: https://localhost:3456",
"change": "alterar",
"use": "A utilizar a instalação do Vikunja em {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.",
"error": "Não foi possível encontrar ou utilizar a instalação do Vikunja em \"{domain}\". Por favor, tenta um url diferente.",
"success": "A utilizar a instalação do Vikunja em \"{domain}\".",
"urlRequired": "É necessário um url."
},
@ -934,7 +902,6 @@
"tasks": "Tarefas",
"projects": "Projetos",
"teams": "Equipas",
"labels": "Etiquetas",
"newProject": "Insere o título do novo espaço…",
"newTask": "Insere o título da nova tarefa…",
"newTeam": "Insere o nome da nova equipa…",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Not found",
"text": "The page you requested does not exist."
@ -144,30 +139,6 @@
"system": "System",
"dark": "Dark"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"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.",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -934,7 +902,6 @@
"tasks": "Tasks",
"projects": "Projects",
"teams": "Teams",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -11,11 +11,6 @@
"import": "Импорт данных в Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Не найдено",
"text": "Запрашиваемая страница не существует."
@ -144,30 +139,6 @@
"system": "Системная",
"dark": "Тёмная"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "Все задачи, помещённые в эту колонку, автоматически отмечаются как завершённые.",
"doneBucketHintExtended": "Все задачи, перенесённые в колонку завершённых, будут помечены как завершённые. Все задачи, помеченные как завершённые, также будут перемещены в эту колонку.",
"doneBucketSavedSuccess": "Колонка завершённых была успешно сохранена.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "Нельзя удалить последнюю колонку.",
"addTaskPlaceholder": "Введите название задачи…",
"addTask": "Добавить задачу",
@ -913,7 +881,7 @@
"urlPlaceholder": "напр. https://localhost:3456",
"change": "изменить",
"use": "Используется Vikunja на {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.",
"error": "Не удалось подключиться к Vikunja по адресу \"{domain}\". Попробуйте указать другой url.",
"success": "Используется Vikunja на \"{domain}\".",
"urlRequired": "Требуется url."
},
@ -934,7 +902,6 @@
"tasks": "Задачи",
"projects": "Проекты",
"teams": "Команды",
"labels": "Labels",
"newProject": "Введите название проекта…",
"newTask": "Введите название задачи…",
"newTeam": "Введите название новой команды…",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Not found",
"text": "The page you requested does not exist."
@ -144,30 +139,6 @@
"system": "System",
"dark": "Dark"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"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.",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -934,7 +902,6 @@
"tasks": "Tasks",
"projects": "Projects",
"teams": "Teams",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Not found",
"text": "The page you requested does not exist."
@ -144,30 +139,6 @@
"system": "System",
"dark": "Dark"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"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.",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -934,7 +902,6 @@
"tasks": "Tasks",
"projects": "Projects",
"teams": "Teams",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -11,11 +11,6 @@
"import": "Importera din data till Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Hittades inte",
"text": "The page you requested does not exist."
@ -144,30 +139,6 @@
"system": "System",
"dark": "Mörkt"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 dagar",
"60d": "60 dagar",
"90d": "90 dagar",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -201,8 +172,8 @@
"color": "Färg",
"projects": "Projekt",
"parent": "Parent Project",
"search": "Skriv för att söka efter ett projekt…",
"searchSelect": "Klicka eller tryck på enter för att välja detta projekt",
"search": "Type to search for a project…",
"searchSelect": "Click or press enter to select this project",
"shared": "Delade projekt",
"noDescriptionAvailable": "No project description is available.",
"inboxTitle": "Inkorg",
@ -323,7 +294,7 @@
"noDates": "This task has no dates set."
},
"table": {
"title": "Tabell",
"title": "Table",
"columns": "Kolumner"
},
"kanban": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Lägg till en uppgift",
@ -913,7 +881,7 @@
"urlPlaceholder": "t. ex. https://localhost:3456",
"change": "change",
"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.",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -934,7 +902,6 @@
"tasks": "Uppgifter",
"projects": "Projekt",
"teams": "Teams",
"labels": "Etiketter",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Not found",
"text": "The page you requested does not exist."
@ -144,30 +139,6 @@
"system": "System",
"dark": "Dark"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"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.",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -934,7 +902,6 @@
"tasks": "Tasks",
"projects": "Projects",
"teams": "Teams",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Không tìm thấy gì cả",
"text": "Trang bạn yêu cầu không tồn tại."
@ -144,30 +139,6 @@
"system": "Hệ thống",
"dark": "Tối"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "ví dụ: https://localhost:3456",
"change": "thay đổi",
"use": "Sử dụng cài đặt Vikunja tại {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.",
"error": "Không thể tìm thấy hoặc sử dụng cài đặt Vikunja tại \"{domain}\". Vui lòng thử một url khác.",
"success": "Sử dụng cài đặt Vikunja tại \"{domain}\".",
"urlRequired": "Cần có một url."
},
@ -934,7 +902,6 @@
"tasks": "Tác vụ",
"projects": "Projects",
"teams": "Team",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Đặt tên cho tác vụ mới…",
"newTeam": "Đặt tên cho đội nhóm mới…",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "未找到数据",
"text": "您请求的页面不存在。"
@ -144,30 +139,6 @@
"system": "跟随系统",
"dark": "暗色"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "例如: http://localhost:3456",
"change": "换一换",
"use": "在 {0} 使用 Vikunja 安装程序",
"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.",
"error": "无法在 “{domain}” 上找到或使用 Vikunja 安装程序。 请尝试其他网址。",
"success": "在 “{domain}” 上使用 Vikunja 安装程序。",
"urlRequired": "Url 是必需的。"
},
@ -934,7 +902,6 @@
"tasks": "任务",
"projects": "Projects",
"teams": "团队",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "输入新任务的标题...",
"newTeam": "输入新团队的名称...",

View File

@ -11,11 +11,6 @@
"import": "Import your data into Vikunja"
}
},
"demo": {
"title": "This instance is in demo mode. Do not use this for real data!",
"everythingWillBeDeleted": "Everything will be deleted in regular intervals!",
"accountWillBeDeleted": "Your account will be deleted, including all projects, tasks and attachments you might create."
},
"404": {
"title": "Not found",
"text": "The page you requested does not exist."
@ -144,30 +139,6 @@
"system": "System",
"dark": "Dark"
}
},
"apiTokens": {
"title": "API Tokens",
"general": "API tokens allow you to use Vikunja's API without user credentials.",
"apiDocs": "Check out the api docs",
"createAToken": "Create a token",
"createToken": "Create token",
"30d": "30 Days",
"60d": "60 Days",
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"expired": "This token has expired {ago}.",
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",
"text2": "This will revoke access to all applications or integrations using it. You cannot undo this."
},
"attributes": {
"title": "Title",
"titlePlaceholder": "Enter a title you will recognize later",
"expiresAt": "Expires at",
"permissions": "Permissions"
}
}
},
"deletion": {
@ -334,9 +305,6 @@
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"defaultBucket": "Default bucket",
"defaultBucketHint": "When creating tasks without specifying a bucket, they will be added to this bucket.",
"defaultBucketSavedSuccess": "The default bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
@ -913,7 +881,7 @@
"urlPlaceholder": "eg. https://localhost:3456",
"change": "change",
"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.",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
},
@ -934,7 +902,6 @@
"tasks": "Tasks",
"projects": "Projects",
"teams": "Teams",
"labels": "Labels",
"newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…",
"newTeam": "Enter the name of the new team…",

View File

@ -23,7 +23,6 @@ declare global {
SENTRY_DSN: string;
PROJECT_INFINITE_NESTING_ENABLED: boolean;
ALLOW_ICON_CHANGES: boolean;
CUSTOM_LOGO_URL?: string;
}
}
@ -36,8 +35,8 @@ if (apiUrlFromStorage !== null) {
}
// Make sure the api url does not contain a / at the end
if (window.API_URL.endsWith('/')) {
window.API_URL = window.API_URL.slice(0, -1)
if (window.API_URL.slice(window.API_URL.length - 1, window.API_URL.length) === '/') {
window.API_URL = window.API_URL.slice(0, window.API_URL.length - 1)
}
// directives

View File

@ -1,14 +0,0 @@
import type {IAbstract} from '@/modelTypes/IAbstract'
export interface IApiPermission {
[key: string]: string[]
}
export interface IApiToken extends IAbstract {
id: number
title: string
token: string
permissions: IApiPermission
expiresAt: Date
created: Date
}

View File

@ -8,6 +8,7 @@ export interface IBucket extends IAbstract {
projectId: number
limit: number
tasks: ITask[]
isDoneBucket: boolean
position: number
count: number

View File

@ -19,8 +19,6 @@ export interface IProject extends IAbstract {
position: number
backgroundBlurHash: string
parentProjectId: number
doneBucketId: number
defaultBucketId: number
created: Date
updated: Date

View File

@ -1,21 +0,0 @@
import AbstractModel from '@/models/abstractModel'
import type {IApiToken} from '@/modelTypes/IApiToken'
export default class ApiTokenModel extends AbstractModel<IApiToken> {
id = 0
title = ''
token = ''
permissions = null
expiresAt: Date = null
created: Date = null
constructor(data: Partial<IApiToken> = {}) {
super()
this.assignData(data)
this.expiresAt = new Date(this.expiresAt)
this.created = new Date(this.created)
this.updated = new Date(this.updated)
}
}

View File

@ -12,6 +12,7 @@ export default class BucketModel extends AbstractModel<IBucket> implements IBuck
projectId = ''
limit = 0
tasks: ITask[] = []
isDoneBucket = false
position = 0
count = 0

View File

@ -23,8 +23,6 @@ export default class ProjectModel extends AbstractModel<IProject> implements IPr
position = 0
backgroundBlurHash = ''
parentProjectId = 0
doneBucketId = 0
defaultBucketId = 0
created: Date = null
updated: Date = null

View File

@ -85,6 +85,7 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
index = 0
isFavorite = false
subscription: ISubscription = null
coverImageAttachmentId: IAttachment['id'] = null
position = 0
kanbanPosition = 0

View File

@ -65,7 +65,6 @@ const UserSettingsEmailUpdateComponent = () => import('@/views/user/settings/Ema
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')
@ -184,11 +183,6 @@ const router = createRouter({
name: 'user.settings.totp',
component: UserSettingsTOTPComponent,
},
{
path: '/user/settings/api-tokens',
name: 'user.settings.apiTokens',
component: UserSettingsApiTokensComponent,
},
],
},
{
@ -454,9 +448,16 @@ export async function getAuthForRoute(to: RouteLocation, authStore) {
return
}
// Check if the route the user wants to go to is a route which needs authentication. We use this to
// redirect the user after successful login.
const isValidUserAppRoute = ![
const baseStore = useBaseStore()
// When trying this before the current user was fully loaded we might get a flash of the login screen
// in the user shell. To make shure this does not happen we check if everything is ready before trying.
if (!baseStore.ready) {
return
}
// Check if the user is already logged in and redirect them to the home page if not
if (
![
'user.login',
'user.password-reset.request',
'user.password-reset.reset',
@ -467,19 +468,8 @@ export async function getAuthForRoute(to: RouteLocation, authStore) {
localStorage.getItem('passwordResetToken') === null &&
localStorage.getItem('emailConfirmToken') === null &&
!(to.name === 'home' && (typeof to.query.userPasswordReset !== 'undefined' || typeof to.query.userEmailConfirm !== 'undefined'))
if (isValidUserAppRoute) {
) {
saveLastVisited(to.name as string, to.params, to.query)
}
const baseStore = useBaseStore()
// When trying this before the current user was fully loaded we might get a flash of the login screen
// in the user shell. To make sure this does not happen we check if everything is ready before trying.
if (!baseStore.ready) {
return
}
if (isValidUserAppRoute) {
return {name: 'user.login'}
}

View File

@ -1,36 +0,0 @@
import AbstractService from '@/services/abstractService'
import type {IApiToken} from '@/modelTypes/IApiToken'
import ApiTokenModel from '@/models/apiTokenModel'
export default class ApiTokenService extends AbstractService<IApiToken> {
constructor() {
super({
create: '/tokens',
getAll: '/tokens',
delete: '/tokens/{id}',
})
}
processModel(model: IApiToken) {
return {
...model,
expiresAt: new Date(model.expiresAt).toISOString(),
created: new Date(model.created).toISOString(),
}
}
modelFactory(data: Partial<IApiToken>) {
return new ApiTokenModel(data)
}
async getAvailableRoutes() {
const cancel = this.setLoading()
try {
const response = await this.http.get('/routes')
return response.data
} finally {
cancel()
}
}
}

View File

@ -18,7 +18,7 @@ const parseDate = date => {
export default class TaskService extends AbstractService<ITask> {
constructor() {
super({
create: '/projects/{projectId}/tasks',
create: '/projects/{projectId}',
getAll: '/tasks/all',
get: '/tasks/{id}',
update: '/tasks/{id}',
@ -81,6 +81,12 @@ export default class TaskService extends AbstractService<ITask> {
case 'weeks':
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_WEEK
break
case 'months':
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_MONTH
break
case 'years':
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_YEAR
break
}
}
model.repeatAfter = repeatAfterSeconds

View File

@ -26,7 +26,6 @@ export interface ConfigState {
caldavEnabled: boolean,
userDeletionEnabled: boolean,
taskCommentsEnabled: boolean,
demoModeEnabled: boolean,
auth: {
local: {
enabled: boolean,
@ -59,7 +58,6 @@ export const useConfigStore = defineStore('config', () => {
caldavEnabled: false,
userDeletionEnabled: true,
taskCommentsEnabled: true,
demoModeEnabled: false,
auth: {
local: {
enabled: true,

View File

@ -19,7 +19,7 @@ import {success} from '@/message'
import {useBaseStore} from '@/stores/base'
import {getSavedFilterIdFromProjectId} from '@/services/savedFilter'
const {add, remove, search, update} = createNewIndexer('projects', ['title', 'description'])
const {remove, search, update} = createNewIndexer('projects', ['title', 'description'])
export interface ProjectState {
[id: IProject['id']]: IProject
@ -36,11 +36,9 @@ export const useProjectStore = defineStore('project', () => {
const projectsArray = computed(() => Object.values(projects.value)
.sort((a, b) => a.position - b.position))
const notArchivedRootProjects = computed(() => projectsArray.value
.filter(p => p.parentProjectId === 0 && !p.isArchived && p.id > 0))
.filter(p => p.parentProjectId === 0 && !p.isArchived))
const favoriteProjects = computed(() => projectsArray.value
.filter(p => !p.isArchived && p.isFavorite))
const savedFilterProjects = computed(() => projectsArray.value
.filter(p => !p.isArchived && p.id < -1))
const hasProjects = computed(() => projectsArray.value.length > 0)
const getChildProjects = computed(() => {
@ -174,7 +172,6 @@ export const useProjectStore = defineStore('project', () => {
const loadedProjects = await projectService.getAll({}, {is_archived: true}) as IProject[]
projects.value = {}
setProjects(loadedProjects)
loadedProjects.forEach(p => add(p))
return loadedProjects
} finally {
@ -201,7 +198,6 @@ export const useProjectStore = defineStore('project', () => {
notArchivedRootProjects: readonly(notArchivedRootProjects),
favoriteProjects: readonly(favoriteProjects),
hasProjects: readonly(hasProjects),
savedFilterProjects: readonly(savedFilterProjects),
getChildProjects,
findProjectByExactname,

View File

@ -6,6 +6,7 @@ import TaskService from '@/services/task'
import TaskAssigneeService from '@/services/taskAssignee'
import LabelTaskService from '@/services/labelTask'
import {playPopSound} from '@/helpers/playPop'
import {cleanupItemText, parseTaskText, PREFIXES} from '@/modules/parseTaskText'
import TaskAssigneeModel from '@/models/taskAssignee'
@ -148,6 +149,9 @@ export const useTaskStore = defineStore('task', () => {
try {
const updatedTask = await taskService.update(task)
kanbanStore.setTaskInBucket(updatedTask)
if (task.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
playPopSound()
}
return updatedTask
} finally {
cancel()

View File

@ -40,7 +40,7 @@
:default-task-end-date="defaultTaskEndDate"
@update:task="updateTask"
/>
<TaskForm v-if="canWrite" @create-task="addGanttTask"/>
<TaskForm v-if="canWrite" @create-task="addGanttTask" />
</card>
</div>
</template>
@ -115,7 +115,7 @@ const flatPickerDateRange = computed<Date[]>({
]),
set(newVal) {
const [dateFrom, dateTo] = newVal.map((date) => date?.toISOString())
// only set after whole range has been selected
if (!dateTo) return

View File

@ -37,7 +37,7 @@
>
<div class="bucket-header" @click="() => unCollapseBucket(bucket)">
<span
v-if="project.doneBucketId === bucket.id"
v-if="bucket.isDoneBucket"
class="icon is-small has-text-success mr-2"
v-tooltip="$t('project.kanban.doneBucketHint')"
>
@ -97,32 +97,26 @@
<dropdown-item
@click.stop="toggleDoneBucket(bucket)"
v-tooltip="$t('project.kanban.doneBucketHintExtended')"
:icon-class="{'has-text-success': bucket.id === project.doneBucketId}"
icon="check-double"
>
<span class="icon is-small" :class="{'has-text-success': bucket.isDoneBucket}">
<icon icon="check-double"/>
</span>
{{ $t('project.kanban.doneBucket') }}
</dropdown-item>
<dropdown-item
@click.stop="toggleDefaultBucket(bucket)"
v-tooltip="$t('project.kanban.defaultBucketHint')"
:icon-class="{'has-text-primary': bucket.id === project.defaultBucketId}"
icon="th"
>
{{ $t('project.kanban.defaultBucket') }}
</dropdown-item>
<dropdown-item
@click.stop="() => collapseBucket(bucket)"
icon="angles-up"
>
{{ $t('project.kanban.collapse') }}
</dropdown-item>
<dropdown-item
:class="{'is-disabled': buckets.length <= 1}"
@click.stop="() => deleteBucketModal(bucket.id)"
class="has-text-danger"
v-tooltip="buckets.length <= 1 ? $t('project.kanban.deleteLast') : ''"
icon-class="has-text-danger"
icon="trash-alt"
>
<span class="icon is-small">
<icon icon="trash-alt"/>
</span>
{{ $t('misc.delete') }}
</dropdown-item>
</dropdown>
@ -257,7 +251,6 @@ import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {isSavedFilter} from '@/services/savedFilter'
import {success} from '@/message'
import {useProjectStore} from '@/stores/projects'
const DRAG_OPTIONS = {
// sortable options
@ -275,7 +268,6 @@ const {t} = useI18n({useScope: 'global'})
const baseStore = useBaseStore()
const kanbanStore = useKanbanStore()
const taskStore = useTaskStore()
const projectStore = useProjectStore()
const taskContainerRefs = ref<{[id: IBucket['id']]: HTMLElement}>({})
@ -430,9 +422,10 @@ async function updateTaskPosition(e) {
)
if (
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
newBucket.id !== oldBucket.id
newBucket.id !== oldBucket.id &&
newBucket.isDoneBucket !== oldBucket.isDoneBucket
) {
newTask.done = project.value.doneBucketId === newBucket.id
newTask.done = newBucket.isDoneBucket
}
if (
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
@ -603,26 +596,10 @@ function dragstart(bucket: IBucket) {
sourceBucket.value = bucket.id
}
async function toggleDefaultBucket(bucket: IBucket) {
const defaultBucketId = project.value.defaultBucketId === bucket.id
? 0
: bucket.id
await projectStore.updateProject({
...project.value,
defaultBucketId,
})
success({message: t('project.kanban.defaultBucketSavedSuccess')})
}
async function toggleDoneBucket(bucket: IBucket) {
const doneBucketId = project.value.doneBucketId === bucket.id
? 0
: bucket.id
await projectStore.updateProject({
...project.value,
doneBucketId,
await kanbanStore.updateBucket({
...bucket,
isDoneBucket: !bucket.isDoneBucket,
})
success({message: t('project.kanban.doneBucketSavedSuccess')})
}

View File

@ -63,7 +63,7 @@
<nothing v-if="ctaVisible && tasks.length === 0 && !loading">
{{ $t('project.list.empty') }}
<ButtonLink @click="focusNewTaskInput()" v-if="project.id > 0">
<ButtonLink @click="focusNewTaskInput()">
{{ $t('project.list.newTaskCta') }}
</ButtonLink>
</nothing>

View File

@ -143,12 +143,13 @@
<labels :labels="t.labels"/>
</td>
<td v-if="activeColumns.assignees">
<assignee-list
v-if="t.assignees.length > 0"
:assignees="t.assignees"
:avatar-size="28"
class="ml-1"
:inline="true"
<user
:avatar-size="27"
:is-inline="true"
:key="t.id + 'assignee' + a.id + i"
:show-username="false"
:user="a"
v-for="(a, i) in t.assignees"
/>
</td>
<date-table-cell :date="t.dueDate" v-if="activeColumns.dueDate"/>
@ -200,7 +201,6 @@ import {useTaskList} from '@/composables/useTaskList'
import type {SortBy} from '@/composables/useTaskList'
import type {ITask} from '@/modelTypes/ITask'
import type {IProject} from '@/modelTypes/IProject'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
const ACTIVE_COLUMNS_DEFAULT = {
index: true,

View File

@ -378,7 +378,7 @@
{{ $t('task.detail.actions.attachments') }}
</x-button>
<x-button
@click="setRelatedTasksActive()"
@click="setFieldActive('relatedTasks')"
variant="secondary"
icon="sitemap"
v-shortcut="'r'"
@ -447,7 +447,7 @@
</template>
<script lang="ts" setup>
import {ref, reactive, toRef, shallowReactive, computed, watch, nextTick} from 'vue'
import {ref, reactive, toRef, shallowReactive, computed, watch, watchEffect, nextTick} from 'vue'
import {useRouter, type RouteLocation} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {unrefElement} from '@vueuse/core'
@ -488,6 +488,7 @@ import {uploadFile} from '@/helpers/attachments'
import {getProjectTitle} from '@/helpers/getProjectTitle'
import {scrollIntoView} from '@/helpers/scrollIntoView'
import {useBaseStore} from '@/stores/base'
import {useAttachmentStore} from '@/stores/attachments'
import {useTaskStore} from '@/stores/tasks'
import {useKanbanStore} from '@/stores/kanban'
@ -498,8 +499,6 @@ import {success} from '@/message'
import type {Action as MessageAction} from '@/message'
import {useProjectStore} from '@/stores/projects'
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
import {useAuthStore} from '@/stores/auth'
import {playPopSound} from '@/helpers/playPop'
const {
taskId,
@ -514,6 +513,7 @@ defineEmits(['close'])
const router = useRouter()
const {t} = useI18n({useScope: 'global'})
const baseStore = useBaseStore()
const projectStore = useProjectStore()
const attachmentStore = useAttachmentStore()
const taskStore = useTaskStore()
@ -534,6 +534,17 @@ const taskColor = ref<ITask['hexColor']>('')
const visible = ref(false)
const project = computed(() => projectStore.projects[task.value.projectId])
watchEffect(() => {
if (typeof project.value === 'undefined') {
// assuming the task has not been loaded completely and thus the project id is 0.
// This avoids flickering between a project background and none when opening the task detail view from
// any the project views.
return
}
baseStore.handleSetCurrentProject({
project: project.value,
})
})
const canWrite = computed(() => (
task.value.maxRight !== null &&
@ -724,10 +735,6 @@ function toggleTaskDone() {
done: !task.value.done,
}
if (newTask.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
playPopSound()
}
saveTask(
newTask,
toggleTaskDone,
@ -770,19 +777,6 @@ async function removeRepeatAfter() {
task.value.repeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
await saveTask()
}
function setRelatedTasksActive() {
setFieldActive('relatedTasks')
// If the related tasks are already available, show the form again
const el = activeFieldElements['relatedTasks']
for (const child in el?.children) {
if (el?.children[child]?.id === 'showRelatedTasksFormButton') {
el?.children[child]?.click()
break
}
}
}
</script>
<style lang="scss" scoped>

View File

@ -58,17 +58,6 @@
>
{{ $t('user.auth.createAccount') }}
</x-button>
<message
v-if="configStore.demoModeEnabled"
variant="warning"
class="mt-4"
>
{{ $t('demo.title') }}
{{ $t('demo.accountWillBeDeleted') }}<br/>
<strong class="is-uppercase">{{ $t('demo.everythingWillBeDeleted') }}</strong>
</message>
<p class="mt-2">
{{ $t('user.auth.alreadyHaveAnAccount') }}
<router-link :to="{ name: 'user.login' }">
@ -89,10 +78,8 @@ import {isEmail} from '@/helpers/isEmail'
import Password from '@/components/input/password.vue'
import {useAuthStore} from '@/stores/auth'
import {useConfigStore} from '@/stores/config'
const authStore = useAuthStore()
const configStore = useConfigStore()
// FIXME: use the `beforeEnter` hook of vue-router
// Check if the user is already logged in, if so, redirect them to the homepage

View File

@ -75,10 +75,6 @@ const navigationItems = computed(() => {
routeName: 'user.settings.caldav',
condition: caldavEnabled.value,
},
{
title: t('user.settings.apiTokens.title'),
routeName: 'user.settings.apiTokens',
},
{
title: t('user.deletion.title'),
routeName: 'user.settings.deletion',

View File

@ -1,262 +0,0 @@
<script setup lang="ts">
import ApiTokenService from '@/services/apiToken'
import {computed, onMounted, ref} from 'vue'
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
import XButton from '@/components/input/button.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import ApiTokenModel from '@/models/apiTokenModel'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import {MILLISECONDS_A_DAY} from '@/constants/date'
import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import {useI18n} from 'vue-i18n'
import {useAuthStore} from '@/stores/auth'
import Message from '@/components/misc/message.vue'
const service = new ApiTokenService()
const tokens = ref([])
const apiDocsUrl = window.API_URL + '/docs'
const showCreateForm = ref(false)
const availableRoutes = ref(null)
const newToken = ref(new ApiTokenModel())
const newTokenExpiry = ref<string | number>(30)
const newTokenExpiryCustom = ref(new Date())
const newTokenPermissions = ref({})
const newTokenTitleValid = ref(true)
const apiTokenTitle = ref()
const tokenCreatedSuccessMessage = ref('')
const showDeleteModal = ref(false)
const tokenToDelete = ref(null)
const {t} = useI18n()
const authStore = useAuthStore()
const now = new Date()
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
locale: {
firstDayOfWeek: authStore.settings.weekStart,
},
minDate: now,
}))
onMounted(async () => {
tokens.value = await service.getAll()
availableRoutes.value = await service.getAvailableRoutes()
resetPermissions()
})
function resetPermissions() {
newTokenPermissions.value = {}
Object.entries(availableRoutes.value).forEach(entry => {
const [group, routes] = entry
newTokenPermissions.value[group] = {}
Object.keys(routes).forEach(r => {
newTokenPermissions.value[group][r] = false
})
})
}
async function deleteToken() {
await service.delete(tokenToDelete.value)
showDeleteModal.value = false
tokenToDelete.value = null
const index = tokens.value.findIndex(el => el.id === tokenToDelete.value.id)
if (index === -1) {
return
}
tokens.value.splice(index, 1)
}
async function createToken() {
if (!newTokenTitleValid.value) {
apiTokenTitle.value.focus()
return
}
const expiry = Number(newTokenExpiry.value)
if (!isNaN(expiry)) {
// if it's a number, we assume it's the number of days in the future
newToken.value.expiresAt = new Date((+new Date()) + expiry * MILLISECONDS_A_DAY)
} else {
newToken.value.expiresAt = new Date(newTokenExpiryCustom.value)
}
newToken.value.permissions = {}
Object.entries(newTokenPermissions.value).forEach(([key, ps]) => {
const all = Object.entries(ps)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.filter(([_, v]) => v)
.map(p => p[0])
if (all.length > 0) {
newToken.value.permissions[key] = all
}
})
const token = await service.create(newToken.value)
tokenCreatedSuccessMessage.value = t('user.settings.apiTokens.tokenCreatedSuccess', {token: token.token})
newToken.value = new ApiTokenModel()
newTokenExpiry.value = 30
newTokenExpiryCustom.value = new Date()
resetPermissions()
tokens.value.push(token)
showCreateForm.value = false
}
function formatPermissionTitle(title: string): string {
return title.replaceAll('_', ' ')
}
</script>
<template>
<card :title="$t('user.settings.apiTokens.title')">
<message v-if="tokenCreatedSuccessMessage !== ''" class="has-text-centered mb-4">
{{ tokenCreatedSuccessMessage }}<br/>
{{ $t('user.settings.apiTokens.tokenCreatedNotSeeAgain') }}
</message>
<p>
{{ $t('user.settings.apiTokens.general') }}
<BaseButton :href="apiDocsUrl">{{ $t('user.settings.apiTokens.apiDocs') }}</BaseButton>
.
</p>
<table class="table" v-if="tokens.length > 0">
<tr>
<th>{{ $t('misc.id') }}</th>
<th>{{ $t('user.settings.apiTokens.attributes.title') }}</th>
<th>{{ $t('user.settings.apiTokens.attributes.permissions') }}</th>
<th>{{ $t('user.settings.apiTokens.attributes.expiresAt') }}</th>
<th>{{ $t('misc.created') }}</th>
<th class="has-text-right">{{ $t('misc.actions') }}</th>
</tr>
<tr v-for="tk in tokens" :key="tk.id">
<td>{{ tk.id }}</td>
<td>{{ tk.title }}</td>
<td class="is-capitalized">
<template v-for="(v, p) in tk.permissions" :key="'permission-' + p">
<strong>{{ formatPermissionTitle(p) }}:</strong>
{{ v.map(formatPermissionTitle).join(', ') }}
<br/>
</template>
</td>
<td>
{{ formatDateShort(tk.expiresAt) }}
<p v-if="tk.expiresAt < new Date()" class="has-text-danger">
{{ $t('user.settings.apiTokens.expired', {ago: formatDateSince(tk.expiresAt)}) }}
</p>
</td>
<td>{{ formatDateShort(tk.created) }}</td>
<td class="has-text-right">
<x-button variant="secondary" @click="() => {tokenToDelete = tk; showDeleteModal = true}">
{{ $t('misc.delete') }}
</x-button>
</td>
</tr>
</table>
<form
v-if="showCreateForm"
@submit.prevent="createToken"
>
<!-- Title -->
<div class="field">
<label class="label" for="apiTokenTitle">{{ $t('user.settings.apiTokens.attributes.title') }}</label>
<div class="control">
<input
class="input"
id="apiTokenTitle"
ref="apiTokenTitle"
type="text"
v-focus
:placeholder="$t('user.settings.apiTokens.attributes.titlePlaceholder')"
v-model="newToken.title"
@keyup="() => newTokenTitleValid = newToken.title !== ''"
@focusout="() => newTokenTitleValid = newToken.title !== ''"
/>
</div>
<p class="help is-danger" v-if="!newTokenTitleValid">
{{ $t('user.settings.apiTokens.titleRequired') }}
</p>
</div>
<!-- Expiry -->
<div class="field">
<label class="label" for="apiTokenExpiry">
{{ $t('user.settings.apiTokens.attributes.expiresAt') }}
</label>
<div class="is-flex">
<div class="control select">
<select class="select" v-model="newTokenExpiry" id="apiTokenExpiry">
<option value="30">{{ $t('user.settings.apiTokens.30d') }}</option>
<option value="60">{{ $t('user.settings.apiTokens.60d') }}</option>
<option value="90">{{ $t('user.settings.apiTokens.90d') }}</option>
<option value="custom">{{ $t('misc.custom') }}</option>
</select>
</div>
<flat-pickr
v-if="newTokenExpiry === 'custom'"
class="ml-2"
:config="flatPickerConfig"
v-model="newTokenExpiryCustom"
/>
</div>
</div>
<!-- Permissions -->
<div class="field">
<label class="label">{{ $t('user.settings.apiTokens.attributes.permissions') }}</label>
<p>{{ $t('user.settings.apiTokens.permissionExplanation') }}</p>
<div v-for="(routes, group) in availableRoutes" class="mb-2" :key="group">
<strong class="is-capitalized">{{ formatPermissionTitle(group) }}</strong><br/>
<fancycheckbox
v-for="(paths, route) in routes"
:key="group+'-'+route"
class="mr-2 is-capitalized"
v-model="newTokenPermissions[group][route]"
>
{{ formatPermissionTitle(route) }}
</fancycheckbox>
<br/>
</div>
</div>
<x-button :loading="service.loading" @click="createToken">
{{ $t('user.settings.apiTokens.createToken') }}
</x-button>
</form>
<x-button
v-else
icon="plus"
class="mb-4"
@click="() => showCreateForm = true"
:loading="service.loading"
>
{{ $t('user.settings.apiTokens.createAToken') }}
</x-button>
<modal
:enabled="showDeleteModal"
@close="showDeleteModal = false"
@submit="deleteToken()"
>
<template #header>
{{ $t('user.settings.apiTokens.delete.header') }}
</template>
<template #text>
<p>
{{ $t('user.settings.apiTokens.delete.text1', {token: tokenToDelete.title}) }}<br/>
{{ $t('user.settings.apiTokens.delete.text2') }}
</p>
</template>
</modal>
</card>
</template>