Compare commits
1 Commits
b0102eb039
...
d933ae28dc
Author | SHA1 | Date | |
---|---|---|---|
d933ae28dc |
53
.drone.yml
53
.drone.yml
|
@ -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
|
||||
|
||||
...
|
||||
|
|
|
@ -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
0
crowdin.cli
Normal 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`)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
55
package.json
55
package.json
|
@ -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"
|
||||
|
|
1006
pnpm-lock.yaml
1006
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -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()
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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 {
|
||||
|
|
|
@ -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', '')
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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">, </template>
|
||||
</template>
|
||||
›
|
||||
</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>
|
|
@ -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 ||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
})
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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 …",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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 n’existe 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 l’installation 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 l’installation 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
|
@ -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…",
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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": "Введите название новой команды…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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": "输入新团队的名称...",
|
||||
|
|
|
@ -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…",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -8,6 +8,7 @@ export interface IBucket extends IAbstract {
|
|||
projectId: number
|
||||
limit: number
|
||||
tasks: ITask[]
|
||||
isDoneBucket: boolean
|
||||
position: number
|
||||
count: number
|
||||
|
||||
|
|
|
@ -19,8 +19,6 @@ export interface IProject extends IAbstract {
|
|||
position: number
|
||||
backgroundBlurHash: string
|
||||
parentProjectId: number
|
||||
doneBucketId: number
|
||||
defaultBucketId: number
|
||||
|
||||
created: Date
|
||||
updated: Date
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ export default class BucketModel extends AbstractModel<IBucket> implements IBuck
|
|||
projectId = ''
|
||||
limit = 0
|
||||
tasks: ITask[] = []
|
||||
isDoneBucket = false
|
||||
position = 0
|
||||
count = 0
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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')})
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
Reference in New Issue
Block a user