Compare commits
85 Commits
d933ae28dc
...
b0102eb039
Author | SHA1 | Date | |
---|---|---|---|
b0102eb039 | |||
3fec92283b | |||
beb016400e | |||
7746d39161 | |||
b187e8c1b6 | |||
0ecda46af9 | |||
59dc927b5c | |||
a13953ee14 | |||
a4b836d395 | |||
16b46b0f4d | |||
184110b986 | |||
1918947c0b | |||
4e5823183e | |||
b9e17ea870 | |||
|
a8a6ec5ab0 | ||
|
3e9b872894 | ||
c4adcf4655 | |||
b1fe3fe29b | |||
5720a86bc3 | |||
86eff7d49e | |||
7a9aa7771b | |||
abbc11528e | |||
725fd1ad46 | |||
44754fac0f | |||
7f2d92138e | |||
95be0d1d32 | |||
f63c39a578 | |||
270e32290a | |||
9cf8696b84 | |||
|
b97e13b6b4 | ||
04ba1011cc | |||
52c0efe0ce | |||
c803020537 | |||
3373b5fc45 | |||
f6d1db3595 | |||
|
ce6f099912 | ||
ed8fb71ff0 | |||
28f2551d87 | |||
cec480ad80 | |||
830a3745ba | |||
49104c65b6 | |||
984978fe6d | |||
bd7b973559 | |||
0bb85870db | |||
021f92303d | |||
e47ad021a3 | |||
a20eef2453 | |||
|
7b57b10804 | ||
|
83a7032b6f | ||
49261a6fcc | |||
5630c90dee | |||
47d589002c | |||
99e2161c09 | |||
20f61baf03 | |||
4e6b99544e | |||
d57e1909c4 | |||
99d8fbdfa7 | |||
442d0342a9 | |||
a4b369470a | |||
0ca73e0851 | |||
9fc829115f | |||
1e19548563 | |||
c327d86a71 | |||
3044560759 | |||
c3f85fcb19 | |||
53434952d3 | |||
e9b0640660 | |||
ae57e5d314 | |||
6e7928b2e4 | |||
47639b00f8 | |||
|
e63cecceca | ||
|
55e2e323ed | ||
f7e22c8c56 | |||
a9fb306e46 | |||
58a1f46668 | |||
6cbbe17bd8 | |||
c01957aae2 | |||
1ad03877fb | |||
fc72a82a2a | |||
63ef09b020 | |||
|
311b1d7594 | ||
cade3df3e9 | |||
37975c1931 | |||
0d500182e7 | |||
|
f647d6e9b4 |
53
.drone.yml
53
.drone.yml
|
@ -42,7 +42,7 @@ steps:
|
||||||
# - .cache
|
# - .cache
|
||||||
|
|
||||||
- name: dependencies
|
- name: dependencies
|
||||||
image: node:20-alpine
|
image: node:20.5-alpine
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||||
|
@ -55,7 +55,7 @@ steps:
|
||||||
# - restore-cache
|
# - restore-cache
|
||||||
|
|
||||||
- name: lint
|
- name: lint
|
||||||
image: node:20-alpine
|
image: node:20.5-alpine
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||||
|
@ -66,7 +66,7 @@ steps:
|
||||||
- dependencies
|
- dependencies
|
||||||
|
|
||||||
- name: build-prod
|
- name: build-prod
|
||||||
image: node:20-alpine
|
image: node:20.5-alpine
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||||
|
@ -77,7 +77,7 @@ steps:
|
||||||
- dependencies
|
- dependencies
|
||||||
|
|
||||||
- name: test-unit
|
- name: test-unit
|
||||||
image: node:20-alpine
|
image: node:20.5-alpine
|
||||||
pull: always
|
pull: always
|
||||||
commands:
|
commands:
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||||
|
@ -87,7 +87,7 @@ steps:
|
||||||
|
|
||||||
- name: typecheck
|
- name: typecheck
|
||||||
failure: ignore
|
failure: ignore
|
||||||
image: node:20-alpine
|
image: node:20.5-alpine
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||||
|
@ -202,7 +202,7 @@ steps:
|
||||||
# - .cache
|
# - .cache
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
image: node:20-alpine
|
image: node:20.5-alpine
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||||
|
@ -210,6 +210,7 @@ steps:
|
||||||
from_secret: sentry_auth_token
|
from_secret: sentry_auth_token
|
||||||
SENTRY_ORG: vikunja
|
SENTRY_ORG: vikunja
|
||||||
SENTRY_PROJECT: frontend-oss
|
SENTRY_PROJECT: frontend-oss
|
||||||
|
PUPPETEER_SKIP_DOWNLOAD: true
|
||||||
commands:
|
commands:
|
||||||
- apk add git
|
- apk add git
|
||||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||||
|
@ -225,6 +226,7 @@ steps:
|
||||||
image: kolaente/zip
|
image: kolaente/zip
|
||||||
pull: always
|
pull: always
|
||||||
commands:
|
commands:
|
||||||
|
- cp src/version.json dist
|
||||||
- cd dist
|
- cd dist
|
||||||
- zip -r ../vikunja-frontend-unstable.zip *
|
- zip -r ../vikunja-frontend-unstable.zip *
|
||||||
- cd ..
|
- cd ..
|
||||||
|
@ -283,7 +285,7 @@ steps:
|
||||||
# - .cache
|
# - .cache
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
image: node:20-alpine
|
image: node:20.5-alpine
|
||||||
pull: always
|
pull: always
|
||||||
environment:
|
environment:
|
||||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||||
|
@ -306,6 +308,7 @@ steps:
|
||||||
image: kolaente/zip
|
image: kolaente/zip
|
||||||
pull: always
|
pull: always
|
||||||
commands:
|
commands:
|
||||||
|
- cp src/version.json dist
|
||||||
- cd dist
|
- cd dist
|
||||||
- zip -r ../vikunja-frontend-${DRONE_TAG##v}.zip *
|
- zip -r ../vikunja-frontend-${DRONE_TAG##v}.zip *
|
||||||
- cd ..
|
- cd ..
|
||||||
|
@ -472,24 +475,25 @@ name: update-translations
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branch:
|
branch:
|
||||||
- main
|
include:
|
||||||
|
- main
|
||||||
event:
|
event:
|
||||||
- cron
|
include:
|
||||||
|
- cron
|
||||||
cron:
|
cron:
|
||||||
- update_translations
|
- update_translations
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: download
|
- name: download
|
||||||
pull: always
|
pull: always
|
||||||
image: jonasfranz/crowdin
|
image: git.lcomrade.su/root/drone-crowdin-v2
|
||||||
settings:
|
settings:
|
||||||
download: true
|
crowdin_key:
|
||||||
export_dir: src/i18n/lang/
|
|
||||||
ignore_branch: true
|
|
||||||
project_identifier: vikunja
|
|
||||||
environment:
|
|
||||||
CROWDIN_KEY:
|
|
||||||
from_secret: crowdin_key
|
from_secret: crowdin_key
|
||||||
|
project_id: 462614
|
||||||
|
target: download
|
||||||
|
download_to: src/i18n/lang/
|
||||||
|
download_export_approved_only: true
|
||||||
|
|
||||||
- name: move-files
|
- name: move-files
|
||||||
pull: always
|
pull: always
|
||||||
|
@ -516,19 +520,18 @@ steps:
|
||||||
|
|
||||||
- name: upload
|
- name: upload
|
||||||
pull: always
|
pull: always
|
||||||
image: jonasfranz/crowdin
|
image: git.lcomrade.su/root/drone-crowdin-v2
|
||||||
depends_on:
|
depends_on:
|
||||||
- clone
|
- clone
|
||||||
settings:
|
settings:
|
||||||
files:
|
crowdin_key:
|
||||||
en.json: src/i18n/lang/en.json
|
from_secret: crowdin_key
|
||||||
ignore_branch: true
|
project_id: 462614
|
||||||
project_identifier: vikunja
|
target: upload
|
||||||
environment:
|
upload_files:
|
||||||
CROWDIN_KEY:
|
src/i18n/lang/en.json: en.json
|
||||||
from_secret: crowdin_key
|
|
||||||
---
|
---
|
||||||
kind: signature
|
kind: signature
|
||||||
hmac: 6a566550cac03e9f3f9bbccab95fda4b342233bd63a1409cb5f634b1c744c326
|
hmac: c5517d5fc49e327984177144aa195d4418a5769c25deb40f1c211e05735bc863
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
# │─││ │││ │ │
|
# │─││ │││ │ │
|
||||||
# ┘─┘┘─┘┘┘─┘┘─┘
|
# ┘─┘┘─┘┘┘─┘┘─┘
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
|
FROM --platform=$BUILDPLATFORM node:20.5-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
ARG USE_RELEASE=false
|
ARG USE_RELEASE=false
|
||||||
ARG RELEASE_VERSION=unstable
|
ARG RELEASE_VERSION=unstable
|
||||||
ENV PNPM_CACHE_FOLDER .cache/pnpm/
|
ENV PNPM_CACHE_FOLDER .cache/pnpm/
|
||||||
|
ENV PUPPETEER_SKIP_DOWNLOAD true
|
||||||
|
|
||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
COPY pnpm-lock.yaml ./
|
COPY pnpm-lock.yaml ./
|
||||||
|
@ -57,6 +58,7 @@ ENV VIKUNJA_SENTRY_ENABLED false
|
||||||
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
|
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
|
||||||
ENV VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED false
|
ENV VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED false
|
||||||
ENV VIKUNJA_ALLOW_ICON_CHANGES true
|
ENV VIKUNJA_ALLOW_ICON_CHANGES true
|
||||||
|
ENV VIKUNJA_CUSTOM_LOGO_URL "''"
|
||||||
|
|
||||||
COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh
|
COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh
|
||||||
COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh
|
COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {UserFactory} from '../../factories/user'
|
import {UserFactory} from '../../factories/user'
|
||||||
|
import {ProjectFactory} from '../../factories/project'
|
||||||
|
|
||||||
const testAndAssertFailed = fixture => {
|
const testAndAssertFailed = fixture => {
|
||||||
cy.intercept(Cypress.env('API_URL') + '/login*').as('login')
|
cy.intercept(Cypress.env('API_URL') + '/login*').as('login')
|
||||||
|
@ -13,26 +14,28 @@ const testAndAssertFailed = fixture => {
|
||||||
cy.get('div.message.danger').contains('Wrong username or password.')
|
cy.get('div.message.danger').contains('Wrong username or password.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = 'test'
|
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', '/')
|
||||||
|
}
|
||||||
|
|
||||||
context('Login', () => {
|
context('Login', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
UserFactory.create(1, {username})
|
UserFactory.create(1, {username: credentials.username})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should log in with the right credentials', () => {
|
it('Should log in with the right credentials', () => {
|
||||||
const fixture = {
|
|
||||||
username: 'test',
|
|
||||||
password: '1234',
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.visit('/login')
|
cy.visit('/login')
|
||||||
cy.get('input[id=username]').type(fixture.username)
|
login()
|
||||||
cy.get('input[id=password]').type(fixture.password)
|
|
||||||
cy.get('.button').contains('Login').click()
|
|
||||||
cy.url().should('include', '/')
|
|
||||||
cy.clock(1625656161057) // 13:00
|
cy.clock(1625656161057) // 13:00
|
||||||
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
|
cy.get('h2').should('contain', `Hi ${credentials.username}!`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with a bad password', () => {
|
it('Should fail with a bad password', () => {
|
||||||
|
@ -57,4 +60,15 @@ context('Login', () => {
|
||||||
cy.visit('/')
|
cy.visit('/')
|
||||||
cy.url().should('include', '/login')
|
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', () => {
|
it('Should work without issues', () => {
|
||||||
const fixture = {
|
const fixture = {
|
||||||
username: 'testuser',
|
username: 'testuser',
|
||||||
password: '123456',
|
password: '12345678',
|
||||||
email: 'testuser@example.com',
|
email: 'testuser@example.com',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,10 +31,10 @@ context('Registration', () => {
|
||||||
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
|
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.only('Should fail', () => {
|
it('Should fail', () => {
|
||||||
const fixture = {
|
const fixture = {
|
||||||
username: 'test',
|
username: 'test',
|
||||||
password: '123456',
|
password: '12345678',
|
||||||
email: 'testuser@example.com',
|
email: 'testuser@example.com',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,5 +13,6 @@ 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.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.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.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'
|
date -uIseconds | xargs echo 'info: started at'
|
||||||
|
|
|
@ -32,6 +32,8 @@
|
||||||
window.PROJECT_INFINITE_NESTING_ENABLED = false
|
window.PROJECT_INFINITE_NESTING_ENABLED = false
|
||||||
// Allow changing the logo and other icons based on various occasions throughout the year.
|
// Allow changing the logo and other icons based on various occasions throughout the year.
|
||||||
window.ALLOW_ICON_CHANGES = true
|
window.ALLOW_ICON_CHANGES = true
|
||||||
|
// Allow using a custom logo via external URL.
|
||||||
|
window.CUSTOM_LOGO_URL = ''
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
55
package.json
55
package.json
|
@ -13,7 +13,7 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://vikunja.io/",
|
"homepage": "https://vikunja.io/",
|
||||||
"funding": "https://opencollective.com/vikunja",
|
"funding": "https://opencollective.com/vikunja",
|
||||||
"packageManager": "pnpm@8.6.12",
|
"packageManager": "pnpm@8.7.4",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"todo",
|
"todo",
|
||||||
"productivity",
|
"productivity",
|
||||||
|
@ -51,16 +51,17 @@
|
||||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||||
"@github/hotkey": "2.0.1",
|
"@github/hotkey": "2.0.1",
|
||||||
"@infectoone/vue-ganttastic": "2.2.0",
|
"@infectoone/vue-ganttastic": "2.2.0",
|
||||||
"@intlify/unplugin-vue-i18n": "0.12.2",
|
"@intlify/unplugin-vue-i18n": "0.13.0",
|
||||||
"@kyvg/vue3-notification": "2.9.1",
|
"@kyvg/vue3-notification": "2.9.1",
|
||||||
"@sentry/tracing": "7.60.0",
|
"@sentry/tracing": "7.68.0",
|
||||||
"@sentry/vue": "7.60.0",
|
"@sentry/vue": "7.68.0",
|
||||||
"@vueuse/core": "10.3.0",
|
"@vueuse/core": "10.4.1",
|
||||||
"axios": "1.4.0",
|
"@vueuse/router": "10.4.1",
|
||||||
|
"axios": "1.5.0",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"bulma-css-variables": "0.9.33",
|
"bulma-css-variables": "0.9.33",
|
||||||
"camel-case": "4.1.2",
|
"camel-case": "4.1.2",
|
||||||
"codemirror": "5.65.14",
|
"codemirror": "5.65.15",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"dayjs": "1.11.9",
|
"dayjs": "1.11.9",
|
||||||
"dompurify": "3.0.5",
|
"dompurify": "3.0.5",
|
||||||
|
@ -73,12 +74,12 @@
|
||||||
"is-touch-device": "1.0.1",
|
"is-touch-device": "1.0.1",
|
||||||
"klona": "2.0.6",
|
"klona": "2.0.6",
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"marked": "5.1.1",
|
"marked": "5.1.2",
|
||||||
"pinia": "2.1.6",
|
"pinia": "2.1.6",
|
||||||
"register-service-worker": "1.7.2",
|
"register-service-worker": "1.7.2",
|
||||||
"snake-case": "3.0.4",
|
"snake-case": "3.0.4",
|
||||||
"sortablejs": "1.15.0",
|
"sortablejs": "1.15.0",
|
||||||
"ufo": "1.2.0",
|
"ufo": "1.3.0",
|
||||||
"vue": "3.3.4",
|
"vue": "3.3.4",
|
||||||
"vue-advanced-cropper": "2.8.8",
|
"vue-advanced-cropper": "2.8.8",
|
||||||
"vue-flatpickr-component": "11.0.3",
|
"vue-flatpickr-component": "11.0.3",
|
||||||
|
@ -92,53 +93,53 @@
|
||||||
"@cypress/vite-dev-server": "5.0.5",
|
"@cypress/vite-dev-server": "5.0.5",
|
||||||
"@cypress/vue": "5.0.5",
|
"@cypress/vue": "5.0.5",
|
||||||
"@faker-js/faker": "8.0.2",
|
"@faker-js/faker": "8.0.2",
|
||||||
"@histoire/plugin-screenshot": "0.16.5",
|
"@histoire/plugin-screenshot": "0.17.0",
|
||||||
"@histoire/plugin-vue": "0.16.5",
|
"@histoire/plugin-vue": "0.17.1",
|
||||||
"@rushstack/eslint-patch": "1.3.3",
|
"@rushstack/eslint-patch": "1.3.3",
|
||||||
"@tsconfig/node18": "18.2.0",
|
"@tsconfig/node18": "18.2.1",
|
||||||
"@types/codemirror": "5.60.8",
|
"@types/codemirror": "5.60.9",
|
||||||
"@types/dompurify": "3.0.2",
|
"@types/dompurify": "3.0.2",
|
||||||
"@types/flexsearch": "0.7.3",
|
"@types/flexsearch": "0.7.3",
|
||||||
"@types/is-touch-device": "1.0.0",
|
"@types/is-touch-device": "1.0.0",
|
||||||
"@types/lodash.debounce": "4.0.7",
|
"@types/lodash.debounce": "4.0.7",
|
||||||
"@types/marked": "5.0.1",
|
"@types/marked": "5.0.1",
|
||||||
"@types/node": "18.17.6",
|
"@types/node": "18.17.12",
|
||||||
"@types/postcss-preset-env": "7.7.0",
|
"@types/postcss-preset-env": "7.7.0",
|
||||||
"@types/sortablejs": "1.15.1",
|
"@types/sortablejs": "1.15.2",
|
||||||
"@typescript-eslint/eslint-plugin": "6.4.0",
|
"@typescript-eslint/eslint-plugin": "6.5.0",
|
||||||
"@typescript-eslint/parser": "6.4.0",
|
"@typescript-eslint/parser": "6.5.0",
|
||||||
"@vitejs/plugin-legacy": "4.1.1",
|
"@vitejs/plugin-legacy": "4.1.1",
|
||||||
"@vitejs/plugin-vue": "4.3.2",
|
"@vitejs/plugin-vue": "4.3.4",
|
||||||
"@vue/eslint-config-typescript": "11.0.3",
|
"@vue/eslint-config-typescript": "11.0.3",
|
||||||
"@vue/test-utils": "2.4.1",
|
"@vue/test-utils": "2.4.1",
|
||||||
"@vue/tsconfig": "0.4.0",
|
"@vue/tsconfig": "0.4.0",
|
||||||
"autoprefixer": "10.4.15",
|
"autoprefixer": "10.4.15",
|
||||||
"browserslist": "4.21.10",
|
"browserslist": "4.21.10",
|
||||||
"caniuse-lite": "1.0.30001522",
|
"caniuse-lite": "1.0.30001524",
|
||||||
"css-has-pseudo": "6.0.0",
|
"css-has-pseudo": "6.0.0",
|
||||||
"csstype": "3.1.2",
|
"csstype": "3.1.2",
|
||||||
"cypress": "12.17.4",
|
"cypress": "12.17.4",
|
||||||
"esbuild": "0.19.2",
|
"esbuild": "0.19.2",
|
||||||
"eslint": "8.47.0",
|
"eslint": "8.48.0",
|
||||||
"eslint-plugin-vue": "9.17.0",
|
"eslint-plugin-vue": "9.17.0",
|
||||||
"happy-dom": "10.10.4",
|
"happy-dom": "10.11.1",
|
||||||
"histoire": "0.16.5",
|
"histoire": "0.17.0",
|
||||||
"postcss": "8.4.28",
|
"postcss": "8.4.28",
|
||||||
"postcss-easing-gradients": "3.0.1",
|
"postcss-easing-gradients": "3.0.1",
|
||||||
"postcss-easings": "4.0.0",
|
"postcss-easings": "4.0.0",
|
||||||
"postcss-focus-within": "8.0.0",
|
"postcss-focus-within": "8.0.0",
|
||||||
"postcss-preset-env": "9.1.1",
|
"postcss-preset-env": "9.1.2",
|
||||||
"rollup": "3.28.0",
|
"rollup": "3.28.1",
|
||||||
"rollup-plugin-visualizer": "5.9.2",
|
"rollup-plugin-visualizer": "5.9.2",
|
||||||
"sass": "1.66.1",
|
"sass": "1.66.1",
|
||||||
"start-server-and-test": "2.0.0",
|
"start-server-and-test": "2.0.0",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.2.2",
|
||||||
"vite": "4.4.9",
|
"vite": "4.4.9",
|
||||||
"vite-plugin-inject-preload": "1.3.2",
|
"vite-plugin-inject-preload": "1.3.3",
|
||||||
"vite-plugin-pwa": "0.16.4",
|
"vite-plugin-pwa": "0.16.4",
|
||||||
"vite-plugin-sentry": "1.3.0",
|
"vite-plugin-sentry": "1.3.0",
|
||||||
"vite-svg-loader": "4.0.0",
|
"vite-svg-loader": "4.0.0",
|
||||||
"vitest": "0.34.2",
|
"vitest": "0.34.3",
|
||||||
"vue-tsc": "1.8.8",
|
"vue-tsc": "1.8.8",
|
||||||
"wait-on": "7.0.1",
|
"wait-on": "7.0.1",
|
||||||
"workbox-cli": "7.0.0"
|
"workbox-cli": "7.0.0"
|
||||||
|
|
1006
pnpm-lock.yaml
1006
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -15,6 +15,7 @@
|
||||||
<AddToHomeScreen/>
|
<AddToHomeScreen/>
|
||||||
<UpdateNotification/>
|
<UpdateNotification/>
|
||||||
<Notification/>
|
<Notification/>
|
||||||
|
<DemoMode/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</ready>
|
</ready>
|
||||||
</template>
|
</template>
|
||||||
|
@ -45,6 +46,7 @@ import {useBaseStore} from '@/stores/base'
|
||||||
import {useColorScheme} from '@/composables/useColorScheme'
|
import {useColorScheme} from '@/composables/useColorScheme'
|
||||||
import {useBodyClass} from '@/composables/useBodyClass'
|
import {useBodyClass} from '@/composables/useBodyClass'
|
||||||
import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue'
|
import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue'
|
||||||
|
import DemoMode from '@/components/home/DemoMode.vue'
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
49
src/components/home/DemoMode.vue
Normal file
49
src/components/home/DemoMode.vue
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<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,15 +9,21 @@ import {MILLISECONDS_A_HOUR} from '@/constants/date'
|
||||||
const now = useNow({
|
const now = useNow({
|
||||||
interval: MILLISECONDS_A_HOUR,
|
interval: MILLISECONDS_A_HOUR,
|
||||||
})
|
})
|
||||||
const Logo = computed(() => window.ALLOW_ICON_CHANGES && now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
|
const Logo = computed(() => window.ALLOW_ICON_CHANGES && now.value.getMonth() === 6 ? LogoFullPride : LogoFull)
|
||||||
|
const CustomLogo = computed(() => window.CUSTOM_LOGO_URL)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Logo alt="Vikunja" class="logo" />
|
<div>
|
||||||
|
<Logo v-if="!CustomLogo" alt="Vikunja" class="logo" />
|
||||||
|
<img v-show="CustomLogo" :src="CustomLogo" alt="Vikunja" class="logo" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.logo {
|
.logo {
|
||||||
color: var(--logo-text-color);
|
color: var(--logo-text-color);
|
||||||
|
max-width: 168px;
|
||||||
|
max-height: 48px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -60,6 +60,14 @@
|
||||||
:can-collapse="false"
|
:can-collapse="false"
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<nav class="menu" v-if="savedFilterProjects">
|
||||||
|
<ProjectsNavigation
|
||||||
|
:model-value="savedFilterProjects"
|
||||||
|
:can-edit-order="false"
|
||||||
|
:can-collapse="false"
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<nav class="menu">
|
<nav class="menu">
|
||||||
<ProjectsNavigation
|
<ProjectsNavigation
|
||||||
|
@ -91,6 +99,7 @@ const projectStore = useProjectStore()
|
||||||
|
|
||||||
const projects = computed(() => projectStore.notArchivedRootProjects)
|
const projects = computed(() => projectStore.notArchivedRootProjects)
|
||||||
const favoriteProjects = computed(() => projectStore.favoriteProjects)
|
const favoriteProjects = computed(() => projectStore.favoriteProjects)
|
||||||
|
const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -11,7 +11,12 @@
|
||||||
class="input-wrapper input"
|
class="input-wrapper input"
|
||||||
:class="{'has-multiple': hasMultiple}"
|
:class="{'has-multiple': hasMultiple}"
|
||||||
>
|
>
|
||||||
<template v-if="Array.isArray(internalValue)">
|
<slot
|
||||||
|
v-if="Array.isArray(internalValue)"
|
||||||
|
name="items"
|
||||||
|
:items="internalValue"
|
||||||
|
:remove="remove"
|
||||||
|
>
|
||||||
<template v-for="(item, key) in internalValue">
|
<template v-for="(item, key) in internalValue">
|
||||||
<slot name="tag" :item="item">
|
<slot name="tag" :item="item">
|
||||||
<span :key="`item${key}`" class="tag ml-2 mt-2">
|
<span :key="`item${key}`" class="tag ml-2 mt-2">
|
||||||
|
@ -20,7 +25,7 @@
|
||||||
</span>
|
</span>
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</slot>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -85,7 +90,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {library} from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faAlignLeft,
|
faAlignLeft,
|
||||||
faAngleRight,
|
faAngleRight,
|
||||||
|
faAnglesUp,
|
||||||
faArchive,
|
faArchive,
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faArrowUpFromBracket,
|
faArrowUpFromBracket,
|
||||||
|
@ -142,6 +143,7 @@ library.add(faUser)
|
||||||
library.add(faUsers)
|
library.add(faUsers)
|
||||||
library.add(faArrowUpFromBracket)
|
library.add(faArrowUpFromBracket)
|
||||||
library.add(faX)
|
library.add(faX)
|
||||||
|
library.add(faAnglesUp)
|
||||||
|
|
||||||
// overwriting the wrong types
|
// overwriting the wrong types
|
||||||
export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes
|
export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes
|
|
@ -1,21 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<BaseButton class="dropdown-item">
|
<BaseButton class="dropdown-item">
|
||||||
<span class="icon" v-if="icon">
|
<span
|
||||||
|
v-if="icon"
|
||||||
|
class="icon is-small"
|
||||||
|
:class="iconClass"
|
||||||
|
>
|
||||||
<Icon :icon="icon"/>
|
<Icon :icon="icon"/>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<slot />
|
<slot/>
|
||||||
</span>
|
</span>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 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 {
|
export interface DropDownItemProps extends /* @vue-ignore */ BaseButtonProps {
|
||||||
icon?: IconProp,
|
icon?: IconProp,
|
||||||
|
iconClass?: object | string,
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<DropDownItemProps>()
|
defineProps<DropDownItemProps>()
|
||||||
|
@ -24,7 +29,6 @@ defineProps<DropDownItemProps>()
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
display: block;
|
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
padding: $item-padding;
|
padding: $item-padding;
|
||||||
|
@ -52,10 +56,7 @@ defineProps<DropDownItemProps>()
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
padding-right: .5rem;
|
padding-right: .5rem;
|
||||||
|
color: var(--grey-300);
|
||||||
&:not(.has-text-success) {
|
|
||||||
color: var(--grey-300) !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-text-danger .icon {
|
.has-text-danger .icon {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
data-input
|
data-input
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
ref="root"
|
ref="root"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -20,39 +20,39 @@ type Options = flatpickr.Options.Options
|
||||||
type DateOption = flatpickr.Options.DateOption
|
type DateOption = flatpickr.Options.DateOption
|
||||||
|
|
||||||
function camelToKebab(string: string) {
|
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) {
|
function arrayify<T = unknown>(obj: T) {
|
||||||
return obj instanceof Array
|
return obj instanceof Array
|
||||||
? obj
|
? obj
|
||||||
: [obj]
|
: [obj]
|
||||||
}
|
}
|
||||||
|
|
||||||
function nullify<T = unknown>(value: T) {
|
function nullify<T = unknown>(value: T) {
|
||||||
return (value && (value as unknown[]).length)
|
return (value && (value as unknown[]).length)
|
||||||
? value
|
? value
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events to emit, copied from flatpickr source
|
// Events to emit, copied from flatpickr source
|
||||||
const includedEvents = [
|
const includedEvents = [
|
||||||
'onChange',
|
'onChange',
|
||||||
'onClose',
|
'onClose',
|
||||||
'onDestroy',
|
'onDestroy',
|
||||||
'onMonthChange',
|
'onMonthChange',
|
||||||
'onOpen',
|
'onOpen',
|
||||||
'onYearChange',
|
'onYearChange',
|
||||||
] as HookKey[]
|
] as HookKey[]
|
||||||
|
|
||||||
// Let's not emit these events by default
|
// Let's not emit these events by default
|
||||||
const excludedEvents = [
|
const excludedEvents = [
|
||||||
'onValueUpdate',
|
'onValueUpdate',
|
||||||
'onDayCreate',
|
'onDayCreate',
|
||||||
'onParseConfig',
|
'onParseConfig',
|
||||||
'onReady',
|
'onReady',
|
||||||
'onPreCalendarPosition',
|
'onPreCalendarPosition',
|
||||||
'onKeyDown',
|
'onKeyDown',
|
||||||
] as HookKey[]
|
] as HookKey[]
|
||||||
|
|
||||||
// Keep a copy of all events for later use
|
// Keep a copy of all events for later use
|
||||||
|
@ -100,19 +100,19 @@ const attrs = useAttrs()
|
||||||
|
|
||||||
const root = ref<HTMLInputElement | null>(null)
|
const root = ref<HTMLInputElement | null>(null)
|
||||||
const fp = ref<flatpickr.Instance | null>(null)
|
const fp = ref<flatpickr.Instance | null>(null)
|
||||||
const safeConfig = ref<Options>({ ...props.config })
|
const safeConfig = ref<Options>({...props.config})
|
||||||
|
|
||||||
function prepareConfig() {
|
function prepareConfig() {
|
||||||
// Don't mutate original object on parent component
|
// Don't mutate original object on parent component
|
||||||
const newConfig: Options = { ...props.config }
|
const newConfig: Options = {...props.config}
|
||||||
|
|
||||||
props.events.forEach((hook) => {
|
props.events.forEach((hook) => {
|
||||||
// Respect global callbacks registered via setDefault() method
|
// Respect global callbacks registered via setDefault() method
|
||||||
const globalCallbacks = flatpickr.defaultConfig[hook] || []
|
const globalCallbacks = flatpickr.defaultConfig[hook] || []
|
||||||
|
|
||||||
// Inject our own method along with user callback
|
// Inject our own method along with user callback
|
||||||
const localCallback: Hook = (...args) => emit(camelToKebab(hook), ...args)
|
const localCallback: Hook = (...args) => emit(camelToKebab(hook), ...args)
|
||||||
|
|
||||||
// Overwrite with merged array
|
// Overwrite with merged array
|
||||||
newConfig[hook] = arrayify(newConfig[hook] || []).concat(
|
newConfig[hook] = arrayify(newConfig[hook] || []).concat(
|
||||||
globalCallbacks,
|
globalCallbacks,
|
||||||
|
@ -147,9 +147,9 @@ onMounted(() => {
|
||||||
prepareConfig()
|
prepareConfig()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the HTML node where flatpickr to be attached
|
* Get the HTML node where flatpickr to be attached
|
||||||
* Bind on parent element if wrap is true
|
* Bind on parent element if wrap is true
|
||||||
*/
|
*/
|
||||||
const element = props.config.wrap
|
const element = props.config.wrap
|
||||||
? root.value.parentNode
|
? root.value.parentNode
|
||||||
: root.value
|
: root.value
|
||||||
|
@ -179,7 +179,7 @@ watch(config, () => {
|
||||||
fp.value.set(name, safeConfig.value[name])
|
fp.value.set(name, safeConfig.value[name])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, {deep:true})
|
}, {deep: true})
|
||||||
|
|
||||||
const fpInput = computed(() => {
|
const fpInput = computed(() => {
|
||||||
if (!fp.value) return
|
if (!fp.value) return
|
||||||
|
@ -198,8 +198,8 @@ watchEffect(() => fpInput.value?.addEventListener('blur', onBlur))
|
||||||
onBeforeUnmount(() => fpInput.value?.removeEventListener('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(() => {
|
watchEffect(() => {
|
||||||
if (disabled.value) {
|
if (disabled.value) {
|
||||||
fpInput.value?.setAttribute('disabled', '')
|
fpInput.value?.setAttribute('disabled', '')
|
||||||
|
|
|
@ -48,13 +48,14 @@ import Message from '@/components/misc/message.vue'
|
||||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||||
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
|
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
|
||||||
|
|
||||||
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
|
import {ERROR_NO_API_URL, InvalidApiUrlProvidedError, NoApiUrlProvidedError} from '@/helpers/checkAndSetApiUrl'
|
||||||
import {useOnline} from '@/composables/useOnline'
|
import {useOnline} from '@/composables/useOnline'
|
||||||
|
|
||||||
import {getAuthForRoute} from '@/router'
|
import {getAuthForRoute} from '@/router'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
@ -68,6 +69,8 @@ const online = useOnline()
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const showLoading = computed(() => !ready.value && error.value === '')
|
const showLoading = computed(() => !ready.value && error.value === '')
|
||||||
|
|
||||||
|
const {t} = useI18n()
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
await baseStore.loadApp()
|
await baseStore.loadApp()
|
||||||
|
@ -77,7 +80,15 @@ async function load() {
|
||||||
await router.push(redirectTo)
|
await router.push(redirectTo)
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
error.value = String(e)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="!project.isArchived"
|
v-if="!project.isArchived && project.id > -1"
|
||||||
class="favorite"
|
class="favorite"
|
||||||
:class="{'is-favorite': project.isFavorite}"
|
:class="{'is-favorite': project.isFavorite}"
|
||||||
@click.prevent.stop="projectStore.toggleProjectFavorite(project)"
|
@click.prevent.stop="projectStore.toggleProjectFavorite(project)"
|
||||||
|
|
|
@ -157,7 +157,7 @@
|
||||||
<template
|
<template
|
||||||
v-if="['filters.create', 'project.edit', 'filter.settings.edit'].includes($route.name as string)">
|
v-if="['filters.create', 'project.edit', 'filter.settings.edit'].includes($route.name as string)">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ $t('project.lists') }}</label>
|
<label class="label">{{ $t('project.projects') }}</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<SelectProject
|
<SelectProject
|
||||||
v-model="entities.projects"
|
v-model="entities.projects"
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
{{ hintText }}
|
{{ hintText }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<quick-add-magic class="p-2 modal-container-smaller" v-if="isNewTaskCommand"/>
|
<quick-add-magic v-if="isNewTaskCommand"/>
|
||||||
|
|
||||||
<div class="results" v-if="selectedCmd === null">
|
<div class="results" v-if="selectedCmd === null">
|
||||||
<div v-for="(r, k) in results" :key="k" class="result">
|
<div v-for="(r, k) in results" :key="k" class="result">
|
||||||
|
@ -44,7 +44,18 @@
|
||||||
@keyup.prevent.enter="doAction(r.type, i)"
|
@keyup.prevent.enter="doAction(r.type, i)"
|
||||||
@keyup.prevent.esc="searchInput?.focus()"
|
@keyup.prevent.esc="searchInput?.focus()"
|
||||||
>
|
>
|
||||||
{{ i.title }}
|
<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>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,6 +77,8 @@ import ProjectModel from '@/models/project'
|
||||||
|
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
import BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.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 {useBaseStore} from '@/stores/base'
|
||||||
import {useProjectStore} from '@/stores/projects'
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
@ -97,6 +110,7 @@ enum ACTION_TYPE {
|
||||||
TASK = 'task',
|
TASK = 'task',
|
||||||
PROJECT = 'project',
|
PROJECT = 'project',
|
||||||
TEAM = 'team',
|
TEAM = 'team',
|
||||||
|
LABELS = 'labels',
|
||||||
}
|
}
|
||||||
|
|
||||||
enum COMMAND_TYPE {
|
enum COMMAND_TYPE {
|
||||||
|
@ -134,24 +148,38 @@ function closeQuickActions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const foundProjects = computed(() => {
|
const foundProjects = computed(() => {
|
||||||
const { project } = parsedQuery.value
|
const {project, text, labels, assignees} = parsedQuery.value
|
||||||
if (
|
|
||||||
searchMode.value === SEARCH_MODE.ALL ||
|
if (project !== null) {
|
||||||
searchMode.value === SEARCH_MODE.PROJECTS ||
|
return projectStore.searchProject(project ?? text)
|
||||||
project === null
|
.filter(p => Boolean(p))
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
if (labels.length > 0 || assignees.length > 0) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const history = getHistory()
|
if (text === '') {
|
||||||
const allProjects = [
|
const history = getHistory()
|
||||||
...new Set([
|
return history.map((p) => projectStore.projects[p.id])
|
||||||
...history.map((l) => projectStore.projects[l.id]),
|
.filter(p => Boolean(p))
|
||||||
...projectStore.searchProject(project),
|
}
|
||||||
]),
|
|
||||||
]
|
|
||||||
|
|
||||||
return allProjects.filter(l => Boolean(l))
|
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)
|
||||||
})
|
})
|
||||||
|
|
||||||
// FIXME: use fuzzysearch
|
// FIXME: use fuzzysearch
|
||||||
|
@ -172,15 +200,20 @@ const results = computed<Result[]>(() => {
|
||||||
title: t('quickActions.commands'),
|
title: t('quickActions.commands'),
|
||||||
items: foundCommands.value,
|
items: foundCommands.value,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: ACTION_TYPE.PROJECT,
|
||||||
|
title: t('quickActions.projects'),
|
||||||
|
items: foundProjects.value,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: ACTION_TYPE.TASK,
|
type: ACTION_TYPE.TASK,
|
||||||
title: t('quickActions.tasks'),
|
title: t('quickActions.tasks'),
|
||||||
items: foundTasks.value,
|
items: foundTasks.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ACTION_TYPE.PROJECT,
|
type: ACTION_TYPE.LABELS,
|
||||||
title: t('quickActions.projects'),
|
title: t('quickActions.labels'),
|
||||||
items: foundProjects.value,
|
items: foundLabels.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ACTION_TYPE.TEAM,
|
type: ACTION_TYPE.TEAM,
|
||||||
|
@ -190,7 +223,7 @@ const results = computed<Result[]>(() => {
|
||||||
].filter((i) => i.items.length > 0)
|
].filter((i) => i.items.length > 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = computed(() =>
|
const loading = computed(() =>
|
||||||
taskService.loading ||
|
taskService.loading ||
|
||||||
projectStore.isLoading ||
|
projectStore.isLoading ||
|
||||||
teamService.loading,
|
teamService.loading,
|
||||||
|
@ -262,10 +295,12 @@ const searchMode = computed(() => {
|
||||||
if (query.value === '') {
|
if (query.value === '') {
|
||||||
return SEARCH_MODE.ALL
|
return SEARCH_MODE.ALL
|
||||||
}
|
}
|
||||||
const { text, project, labels, assignees } = parsedQuery.value
|
|
||||||
|
const {text, project, labels, assignees} = parsedQuery.value
|
||||||
if (assignees.length === 0 && text !== '') {
|
if (assignees.length === 0 && text !== '') {
|
||||||
return SEARCH_MODE.TASKS
|
return SEARCH_MODE.TASKS
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
assignees.length === 0 &&
|
assignees.length === 0 &&
|
||||||
project !== null &&
|
project !== null &&
|
||||||
|
@ -274,6 +309,7 @@ const searchMode = computed(() => {
|
||||||
) {
|
) {
|
||||||
return SEARCH_MODE.PROJECTS
|
return SEARCH_MODE.PROJECTS
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
assignees.length > 0 &&
|
assignees.length > 0 &&
|
||||||
project === null &&
|
project === null &&
|
||||||
|
@ -282,6 +318,7 @@ const searchMode = computed(() => {
|
||||||
) {
|
) {
|
||||||
return SEARCH_MODE.TEAMS
|
return SEARCH_MODE.TEAMS
|
||||||
}
|
}
|
||||||
|
|
||||||
return SEARCH_MODE.ALL
|
return SEARCH_MODE.ALL
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -292,12 +329,12 @@ const isNewTaskCommand = computed(() => (
|
||||||
|
|
||||||
const taskSearchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
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[]) {
|
function filtersToParams(filters: Filter[]) {
|
||||||
const filter_by : Filter['by'][] = []
|
const filter_by: Filter['by'][] = []
|
||||||
const filter_value : Filter['value'][] = []
|
const filter_value: Filter['value'][] = []
|
||||||
const filter_comparator : Filter['comparator'][] = []
|
const filter_comparator: Filter['comparator'][] = []
|
||||||
|
|
||||||
filters.forEach(({by, value, comparator}) => {
|
filters.forEach(({by, value, comparator}) => {
|
||||||
filter_by.push(by)
|
filter_by.push(by)
|
||||||
|
@ -315,7 +352,8 @@ function filtersToParams(filters: Filter[]) {
|
||||||
function searchTasks() {
|
function searchTasks() {
|
||||||
if (
|
if (
|
||||||
searchMode.value !== SEARCH_MODE.ALL &&
|
searchMode.value !== SEARCH_MODE.ALL &&
|
||||||
searchMode.value !== SEARCH_MODE.TASKS
|
searchMode.value !== SEARCH_MODE.TASKS &&
|
||||||
|
searchMode.value !== SEARCH_MODE.PROJECTS
|
||||||
) {
|
) {
|
||||||
foundTasks.value = []
|
foundTasks.value = []
|
||||||
return
|
return
|
||||||
|
@ -330,7 +368,7 @@ function searchTasks() {
|
||||||
taskSearchTimeout.value = null
|
taskSearchTimeout.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { text, project: projectName, labels } = parsedQuery.value
|
const {text, project: projectName, labels} = parsedQuery.value
|
||||||
|
|
||||||
const filters: Filter[] = []
|
const filters: Filter[] = []
|
||||||
|
|
||||||
|
@ -349,8 +387,9 @@ function searchTasks() {
|
||||||
|
|
||||||
if (projectName !== null) {
|
if (projectName !== null) {
|
||||||
const project = projectStore.findProjectByExactname(projectName)
|
const project = projectStore.findProjectByExactname(projectName)
|
||||||
|
console.log({project})
|
||||||
if (project !== null) {
|
if (project !== null) {
|
||||||
addFilter('projectId', project.id, 'equals')
|
addFilter('project_id', project.id, 'equals')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -361,19 +400,16 @@ function searchTasks() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
s: text,
|
s: text,
|
||||||
...filtersToParams(filters),
|
sort_by: 'done',
|
||||||
}
|
...filtersToParams(filters),
|
||||||
|
}
|
||||||
|
|
||||||
taskSearchTimeout.value = setTimeout(async () => {
|
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) => {
|
foundTasks.value = r.map((t) => {
|
||||||
t.type = ACTION_TYPE.TASK
|
t.type = ACTION_TYPE.TASK
|
||||||
const project = projectStore.projects[t.projectId]
|
|
||||||
if (project !== null) {
|
|
||||||
t.title = `${t.title} (${project.title})`
|
|
||||||
}
|
|
||||||
return t
|
return t
|
||||||
})
|
})
|
||||||
}, 150)
|
}, 150)
|
||||||
|
@ -396,10 +432,10 @@ function searchTeams() {
|
||||||
clearTimeout(teamSearchTimeout.value)
|
clearTimeout(teamSearchTimeout.value)
|
||||||
teamSearchTimeout.value = null
|
teamSearchTimeout.value = null
|
||||||
}
|
}
|
||||||
const { assignees } = parsedQuery.value
|
const {assignees} = parsedQuery.value
|
||||||
teamSearchTimeout.value = setTimeout(async () => {
|
teamSearchTimeout.value = setTimeout(async () => {
|
||||||
const teamSearchPromises = assignees.map((t) =>
|
const teamSearchPromises = assignees.map((t) =>
|
||||||
teamService.getAll({}, { s: t }),
|
teamService.getAll({}, {s: t}),
|
||||||
)
|
)
|
||||||
const teamsResult = await Promise.all(teamSearchPromises)
|
const teamsResult = await Promise.all(teamSearchPromises)
|
||||||
foundTeams.value = teamsResult.flat().map((team) => {
|
foundTeams.value = teamsResult.flat().map((team) => {
|
||||||
|
@ -422,21 +458,21 @@ async function doAction(type: ACTION_TYPE, item: DoAction) {
|
||||||
closeQuickActions()
|
closeQuickActions()
|
||||||
await router.push({
|
await router.push({
|
||||||
name: 'project.index',
|
name: 'project.index',
|
||||||
params: { projectId: (item as DoAction<IProject>).id },
|
params: {projectId: (item as DoAction<IProject>).id},
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case ACTION_TYPE.TASK:
|
case ACTION_TYPE.TASK:
|
||||||
closeQuickActions()
|
closeQuickActions()
|
||||||
await router.push({
|
await router.push({
|
||||||
name: 'task.detail',
|
name: 'task.detail',
|
||||||
params: { id: (item as DoAction<ITask>).id },
|
params: {id: (item as DoAction<ITask>).id},
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case ACTION_TYPE.TEAM:
|
case ACTION_TYPE.TEAM:
|
||||||
closeQuickActions()
|
closeQuickActions()
|
||||||
await router.push({
|
await router.push({
|
||||||
name: 'teams.edit',
|
name: 'teams.edit',
|
||||||
params: { id: (item as DoAction<ITeam>).id },
|
params: {id: (item as DoAction<ITeam>).id},
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case ACTION_TYPE.CMD:
|
case ACTION_TYPE.CMD:
|
||||||
|
@ -444,6 +480,11 @@ async function doAction(type: ACTION_TYPE, item: DoAction) {
|
||||||
selectedCmd.value = item as DoAction<Command>
|
selectedCmd.value = item as DoAction<Command>
|
||||||
searchInput.value?.focus()
|
searchInput.value?.focus()
|
||||||
break
|
break
|
||||||
|
case ACTION_TYPE.LABELS:
|
||||||
|
query.value = '*' + item.title
|
||||||
|
searchInput.value?.focus()
|
||||||
|
searchTasks()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -470,8 +511,8 @@ async function newTask() {
|
||||||
title: query.value,
|
title: query.value,
|
||||||
projectId: currentProject.value.id,
|
projectId: currentProject.value.id,
|
||||||
})
|
})
|
||||||
success({ message: t('task.createSuccess') })
|
success({message: t('task.createSuccess')})
|
||||||
await router.push({ name: 'task.detail', params: { id: task.id } })
|
await router.push({name: 'task.detail', params: {id: task.id}})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function newProject() {
|
async function newProject() {
|
||||||
|
@ -481,17 +522,17 @@ async function newProject() {
|
||||||
await projectStore.createProject(new ProjectModel({
|
await projectStore.createProject(new ProjectModel({
|
||||||
title: query.value,
|
title: query.value,
|
||||||
}))
|
}))
|
||||||
success({ message: t('project.create.createdSuccess')})
|
success({message: t('project.create.createdSuccess')})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function newTeam() {
|
async function newTeam() {
|
||||||
const newTeam = new TeamModel({ name: query.value })
|
const newTeam = new TeamModel({name: query.value})
|
||||||
const team = await teamService.create(newTeam)
|
const team = await teamService.create(newTeam)
|
||||||
await router.push({
|
await router.push({
|
||||||
name: 'teams.edit',
|
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>
|
type BaseButtonInstance = InstanceType<typeof BaseButton>
|
||||||
|
@ -502,7 +543,7 @@ function setResultRefs(el: Element | ComponentPublicInstance | null, index: numb
|
||||||
resultRefs.value[index] = []
|
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) {
|
function select(parentIndex: number, index: number) {
|
||||||
|
@ -547,7 +588,7 @@ function reset() {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.quick-actions {
|
.quick-actions {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
// FIXME: changed position should be an option of the modal
|
// FIXME: changed position should be an option of the modal
|
||||||
:deep(.modal-content) {
|
:deep(.modal-content) {
|
||||||
top: 3rem;
|
top: 3rem;
|
||||||
|
@ -569,6 +610,7 @@ function reset() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-cmd {
|
.active-cmd {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
margin-left: .5rem;
|
margin-left: .5rem;
|
||||||
|
@ -614,10 +656,4 @@ function reset() {
|
||||||
background: var(--grey-100);
|
background: var(--grey-100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HACK:
|
|
||||||
// FIXME:
|
|
||||||
.modal-container-smaller :deep(.hint-modal .modal-container) {
|
|
||||||
height: calc(100vh - 5rem);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
93
src/components/tasks/partials/assigneeList.vue
Normal file
93
src/components/tasks/partials/assigneeList.vue
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
<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,13 +11,8 @@
|
||||||
v-model="assignees"
|
v-model="assignees"
|
||||||
:autocomplete-enabled="false"
|
:autocomplete-enabled="false"
|
||||||
>
|
>
|
||||||
<template #tag="{item: user}">
|
<template #items="{items}">
|
||||||
<span class="assignee">
|
<assignee-list :assignees="items" :remove="removeAssignee"/>
|
||||||
<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>
|
||||||
<template #searchResult="{option: user}">
|
<template #searchResult="{option: user}">
|
||||||
<user :avatar-size="24" :show-username="true" :user="user"/>
|
<user :avatar-size="24" :show-username="true" :user="user"/>
|
||||||
|
@ -31,7 +26,6 @@ import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
import User from '@/components/misc/user.vue'
|
import User from '@/components/misc/user.vue'
|
||||||
import Multiselect from '@/components/input/multiselect.vue'
|
import Multiselect from '@/components/input/multiselect.vue'
|
||||||
import BaseButton from '@/components/base/BaseButton.vue'
|
|
||||||
|
|
||||||
import {includesById} from '@/helpers/utils'
|
import {includesById} from '@/helpers/utils'
|
||||||
import ProjectUserService from '@/services/projectUsers'
|
import ProjectUserService from '@/services/projectUsers'
|
||||||
|
@ -40,6 +34,7 @@ import {useTaskStore} from '@/stores/tasks'
|
||||||
|
|
||||||
import type {IUser} from '@/modelTypes/IUser'
|
import type {IUser} from '@/modelTypes/IUser'
|
||||||
import {getDisplayName} from '@/models/user'
|
import {getDisplayName} from '@/models/user'
|
||||||
|
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
taskId: {
|
taskId: {
|
||||||
|
@ -120,34 +115,3 @@ async function findUser(query: string) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</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,16 +48,14 @@
|
||||||
</progress>
|
</progress>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<labels :labels="task.labels"/>
|
<labels :labels="task.labels"/>
|
||||||
<priority-label :priority="task.priority" :done="task.done"/>
|
<priority-label :priority="task.priority" :done="task.done" class="is-inline-flex is-align-items-center"/>
|
||||||
<div class="assignees" v-if="task.assignees.length > 0">
|
<assignee-list
|
||||||
<user
|
v-if="task.assignees.length > 0"
|
||||||
v-for="u in task.assignees"
|
:assignees="task.assignees"
|
||||||
:avatar-size="24"
|
:avatar-size="24"
|
||||||
:key="task.id + 'assignee' + u.id"
|
class="ml-1"
|
||||||
:show-username="false"
|
:inline="true"
|
||||||
:user="u"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<checklist-summary :task="task"/>
|
<checklist-summary :task="task"/>
|
||||||
<span class="icon" v-if="task.attachments.length > 0">
|
<span class="icon" v-if="task.attachments.length > 0">
|
||||||
<icon icon="paperclip"/>
|
<icon icon="paperclip"/>
|
||||||
|
@ -78,7 +76,6 @@ import {ref, computed, watch} from 'vue'
|
||||||
import {useRouter} from 'vue-router'
|
import {useRouter} from 'vue-router'
|
||||||
|
|
||||||
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
|
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
|
||||||
import User from '@/components/misc/user.vue'
|
|
||||||
import Done from '@/components/misc/Done.vue'
|
import Done from '@/components/misc/Done.vue'
|
||||||
import Labels from '@/components/tasks/partials/labels.vue'
|
import Labels from '@/components/tasks/partials/labels.vue'
|
||||||
import ChecklistSummary from './checklist-summary.vue'
|
import ChecklistSummary from './checklist-summary.vue'
|
||||||
|
@ -91,6 +88,9 @@ import AttachmentService from '@/services/attachment'
|
||||||
import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate'
|
import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate'
|
||||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||||
import {useTaskStore} from '@/stores/tasks'
|
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()
|
const router = useRouter()
|
||||||
|
|
||||||
|
@ -109,10 +109,14 @@ const color = computed(() => getHexColor(task.hexColor))
|
||||||
async function toggleTaskDone(task: ITask) {
|
async function toggleTaskDone(task: ITask) {
|
||||||
loadingInternal.value = true
|
loadingInternal.value = true
|
||||||
try {
|
try {
|
||||||
await useTaskStore().update({
|
const updatedTask = await useTaskStore().update({
|
||||||
...task,
|
...task,
|
||||||
done: !task.done,
|
done: !task.done,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (updatedTask.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
|
||||||
|
playPopSound()
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loadingInternal.value = false
|
loadingInternal.value = false
|
||||||
}
|
}
|
||||||
|
@ -238,7 +242,7 @@ $task-background: var(--white);
|
||||||
|
|
||||||
.priority-label {
|
.priority-label {
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
height: 2rem;
|
padding: 0 .5rem 0 .25rem;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
|
|
25
src/components/tasks/partials/label.vue
Normal file
25
src/components/tasks/partials/label.vue
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<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,12 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="label-wrapper">
|
<div class="label-wrapper">
|
||||||
<span
|
<XLabel
|
||||||
|
v-for="label in labels"
|
||||||
|
:label="label"
|
||||||
:key="label.id"
|
:key="label.id"
|
||||||
:style="{'background': label.hexColor, 'color': label.textColor}"
|
/>
|
||||||
class="tag"
|
|
||||||
v-for="label in labels">
|
|
||||||
<span>{{ label.title }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -14,6 +12,8 @@
|
||||||
import type {PropType} from 'vue'
|
import type {PropType} from 'vue'
|
||||||
import type {ILabel} from '@/modelTypes/ILabel'
|
import type {ILabel} from '@/modelTypes/ILabel'
|
||||||
|
|
||||||
|
import XLabel from '@/components/tasks/partials/label.vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
labels: {
|
labels: {
|
||||||
type: Array as PropType<ILabel[]>,
|
type: Array as PropType<ILabel[]>,
|
||||||
|
@ -26,10 +26,4 @@ defineProps({
|
||||||
.label-wrapper {
|
.label-wrapper {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
|
||||||
& + & {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
|
@ -40,17 +40,12 @@ defineProps({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.priority-label {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.high-priority {
|
span.high-priority {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
width: auto !important; // To override the width set in tasks
|
width: auto !important; // To override the width set in tasks
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
vertical-align: middle;
|
vertical-align: top;
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
padding: 0 .5rem;
|
padding: 0 .5rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,7 @@ const visible = ref(false)
|
||||||
const mode = computed(() => authStore.settings.frontendSettings.quickAddMagicMode)
|
const mode = computed(() => authStore.settings.frontendSettings.quickAddMagicMode)
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
highlightHintIcon: boolean,
|
highlightHintIcon?: boolean,
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const prefixes = computed(() => PREFIXES[mode.value])
|
const prefixes = computed(() => PREFIXES[mode.value])
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
:shadow="false"
|
:shadow="false"
|
||||||
|
id="showRelatedTasksFormButton"
|
||||||
/>
|
/>
|
||||||
<transition-group name="fade">
|
<transition-group name="fade">
|
||||||
<template v-if="editEnabled && showCreate">
|
<template v-if="editEnabled && showCreate">
|
||||||
|
@ -161,6 +162,8 @@ import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||||
import {error, success} from '@/message'
|
import {error, success} from '@/message'
|
||||||
import {useTaskStore} from '@/stores/tasks'
|
import {useTaskStore} from '@/stores/tasks'
|
||||||
import {useProjectStore} from '@/stores/projects'
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
import {playPopSound} from '@/helpers/playPop'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
taskId: {
|
taskId: {
|
||||||
|
@ -329,6 +332,10 @@ async function createAndRelateTask(title: string) {
|
||||||
async function toggleTaskDone(task: ITask) {
|
async function toggleTaskDone(task: ITask) {
|
||||||
await taskStore.update(task)
|
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
|
// Find the task in the project and update it so that it is correctly strike through
|
||||||
Object.entries(relatedTasks.value).some(([kind, tasks]) => {
|
Object.entries(relatedTasks.value).some(([kind, tasks]) => {
|
||||||
return (tasks as ITask[]).some((t, key) => {
|
return (tasks as ITask[]).some((t, key) => {
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">
|
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">
|
||||||
{{ $t('task.repeat.everyWeek') }}
|
{{ $t('task.repeat.everyWeek') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">
|
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(30, 'days')">
|
||||||
{{ $t('task.repeat.everyMonth') }}
|
{{ $t('task.repeat.every30d') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="is-flex is-align-items-center mb-2">
|
<div class="is-flex is-align-items-center mb-2">
|
||||||
|
@ -51,8 +51,6 @@
|
||||||
<option value="hours">{{ $t('task.repeat.hours') }}</option>
|
<option value="hours">{{ $t('task.repeat.hours') }}</option>
|
||||||
<option value="days">{{ $t('task.repeat.days') }}</option>
|
<option value="days">{{ $t('task.repeat.days') }}</option>
|
||||||
<option value="weeks">{{ $t('task.repeat.weeks') }}</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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -32,6 +32,8 @@
|
||||||
:color="getHexColor(task.hexColor)"
|
:color="getHexColor(task.hexColor)"
|
||||||
class="mr-1"
|
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 -->
|
<!-- 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'">
|
<span class="parent-tasks" v-if="typeof task.relatedTasks?.parenttask !== 'undefined'">
|
||||||
|
@ -49,14 +51,12 @@
|
||||||
:labels="task.labels"
|
:labels="task.labels"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<User
|
<assignee-list
|
||||||
v-for="(a, i) in task.assignees"
|
v-if="task.assignees.length > 0"
|
||||||
:avatar-size="27"
|
:assignees="task.assignees"
|
||||||
:is-inline="true"
|
:avatar-size="25"
|
||||||
:key="task.id + 'assignee' + a.id + i"
|
class="ml-1"
|
||||||
:show-username="false"
|
:inline="true"
|
||||||
:user="a"
|
|
||||||
class="m-2"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- FIXME: use popup -->
|
<!-- FIXME: use popup -->
|
||||||
|
@ -72,15 +72,13 @@
|
||||||
class="is-italic"
|
class="is-italic"
|
||||||
:aria-expanded="showDefer ? 'true' : 'false'"
|
:aria-expanded="showDefer ? 'true' : 'false'"
|
||||||
>
|
>
|
||||||
– {{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
|
– {{ $t('task.detail.due', {at: dueDateFormatted}) }}
|
||||||
</time>
|
</time>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
<CustomTransition name="fade">
|
<CustomTransition name="fade">
|
||||||
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
|
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
|
||||||
</CustomTransition>
|
</CustomTransition>
|
||||||
|
|
||||||
<priority-label :priority="task.priority" :done="task.done"/>
|
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<span class="project-task-icon" v-if="task.attachments.length > 0">
|
<span class="project-task-icon" v-if="task.attachments.length > 0">
|
||||||
<icon icon="paperclip"/>
|
<icon icon="paperclip"/>
|
||||||
|
@ -121,7 +119,7 @@
|
||||||
<icon icon="star" v-if="task.isFavorite"/>
|
<icon icon="star" v-if="task.isFavorite"/>
|
||||||
<icon :icon="['far', 'star']" v-else/>
|
<icon :icon="['far', 'star']" v-else/>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
<slot />
|
<slot/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -129,7 +127,7 @@
|
||||||
import {ref, watch, shallowReactive, onMounted, onBeforeUnmount, computed} from 'vue'
|
import {ref, watch, shallowReactive, onMounted, onBeforeUnmount, computed} from 'vue'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
import TaskModel, { getHexColor } from '@/models/task'
|
import TaskModel, {getHexColor} from '@/models/task'
|
||||||
import type {ITask} from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
|
|
||||||
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
|
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
|
||||||
|
@ -137,7 +135,6 @@ import Labels from '@/components/tasks/partials//labels.vue'
|
||||||
import DeferTask from '@/components/tasks/partials//defer-task.vue'
|
import DeferTask from '@/components/tasks/partials//defer-task.vue'
|
||||||
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.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 BaseButton from '@/components/base/BaseButton.vue'
|
||||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||||
import ColorBubble from '@/components/misc/colorBubble.vue'
|
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||||
|
@ -152,6 +149,10 @@ import {success} from '@/message'
|
||||||
import {useProjectStore} from '@/stores/projects'
|
import {useProjectStore} from '@/stores/projects'
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {useTaskStore} from '@/stores/tasks'
|
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 {
|
const {
|
||||||
theTask,
|
theTask,
|
||||||
|
@ -214,11 +215,28 @@ const taskDetailRoute = computed(() => ({
|
||||||
// state: { backdropView: router.currentRoute.value.fullPath },
|
// 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) {
|
async function markAsDone(checked: boolean) {
|
||||||
const updateFunc = async () => {
|
const updateFunc = async () => {
|
||||||
const newTask = await taskStore.update(task.value)
|
const newTask = await taskStore.update(task.value)
|
||||||
task.value = newTask
|
task.value = newTask
|
||||||
|
if (checked && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
|
||||||
|
playPopSound()
|
||||||
|
}
|
||||||
emit('task-updated', newTask)
|
emit('task-updated', newTask)
|
||||||
success({
|
success({
|
||||||
message: task.value.done ?
|
message: task.value.done ?
|
||||||
|
@ -248,6 +266,7 @@ async function toggleFavorite() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const deferDueDate = ref<typeof DeferTask | null>(null)
|
const deferDueDate = ref<typeof DeferTask | null>(null)
|
||||||
|
|
||||||
function hideDeferDueDatePopup(e) {
|
function hideDeferDueDatePopup(e) {
|
||||||
if (!showDefer.value) {
|
if (!showDefer.value) {
|
||||||
return
|
return
|
||||||
|
@ -283,7 +302,7 @@ function hideDeferDueDatePopup(e) {
|
||||||
-webkit-line-clamp: 4;
|
-webkit-line-clamp: 4;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
flex: 1 0 50%;
|
flex: 1 0 50%;
|
||||||
|
|
||||||
.dueDate {
|
.dueDate {
|
||||||
|
@ -393,7 +412,7 @@ function hideDeferDueDatePopup(e) {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"] {
|
input[type='checkbox'] {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
190
src/components/tasks/partials/singleTaskInlineReadonly.vue
Normal file
190
src/components/tasks/partials/singleTaskInlineReadonly.vue
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
<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)
|
const routeFromFiltersFullPath = computed(() => router.resolve(filtersToRoute(filters.value)).fullPath)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
route,
|
route.value,
|
||||||
(route, oldRoute) => {
|
(route, oldRoute) => {
|
||||||
if (
|
if (
|
||||||
route?.name !== oldRoute?.name ||
|
route?.name !== oldRoute?.name ||
|
||||||
|
|
|
@ -32,6 +32,11 @@ export function useRouteWithModal() {
|
||||||
: routePropsOption
|
: routePropsOption
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
|
if (typeof routeProps === 'undefined') {
|
||||||
|
currentModal.value = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
routeProps.backdropView = backdropView.value
|
routeProps.backdropView = backdropView.value
|
||||||
|
|
||||||
const component = route.matched[0]?.components?.default
|
const component = route.matched[0]?.components?.default
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {ref, shallowReactive, watch, computed, type ComputedGetter} from 'vue'
|
import {ref, shallowReactive, watch, computed, type ComputedGetter} from 'vue'
|
||||||
import {useRoute} from 'vue-router'
|
import {useRoute} from 'vue-router'
|
||||||
|
import {useRouteQuery} from '@vueuse/router'
|
||||||
|
|
||||||
import TaskCollectionService from '@/services/taskCollection'
|
import TaskCollectionService from '@/services/taskCollection'
|
||||||
import type {ITask} from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
|
@ -68,23 +69,33 @@ export function useTaskList(projectIdGetter: ComputedGetter<IProject['id']>, sor
|
||||||
const params = ref({...getDefaultParams()})
|
const params = ref({...getDefaultParams()})
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const page = ref(1)
|
const page = useRouteQuery('page', '1', { transform: Number })
|
||||||
|
|
||||||
const sortBy = ref({ ...sortByDefault })
|
const sortBy = ref({ ...sortByDefault })
|
||||||
|
|
||||||
const getAllTasksParams = computed(() => {
|
const allParams = computed(() => {
|
||||||
let loadParams = {...params.value}
|
const loadParams = {...params.value}
|
||||||
|
|
||||||
if (search.value !== '') {
|
if (search.value !== '') {
|
||||||
loadParams.s = search.value
|
loadParams.s = search.value
|
||||||
}
|
}
|
||||||
|
|
||||||
loadParams = formatSortOrder(sortBy.value, loadParams)
|
return formatSortOrder(sortBy.value, loadParams)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => allParams.value,
|
||||||
|
() => {
|
||||||
|
// When parameters change, the page should always be the first
|
||||||
|
page.value = 1
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const getAllTasksParams = computed(() => {
|
||||||
return [
|
return [
|
||||||
{projectId: projectId.value},
|
{projectId: projectId.value},
|
||||||
loadParams,
|
allParams.value,
|
||||||
page.value || 1,
|
page.value,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,27 @@ const API_DEFAULT_PORT = '3456'
|
||||||
|
|
||||||
export const ERROR_NO_API_URL = 'noApiUrlProvided'
|
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('/')) {
|
if (url.startsWith('/')) {
|
||||||
url = window.location.host + url
|
url = window.location.host + url
|
||||||
}
|
}
|
||||||
|
@ -17,8 +36,14 @@ export const checkAndSetApiUrl = (url: string): Promise<string> => {
|
||||||
) {
|
) {
|
||||||
url = `${window.location.protocol}//${url}`
|
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 origUrlToCheck = urlToCheck
|
||||||
|
|
||||||
const oldUrl = window.API_URL
|
const oldUrl = window.API_URL
|
||||||
|
@ -86,6 +111,6 @@ export const checkAndSetApiUrl = (url: string): Promise<string> => {
|
||||||
return window.API_URL
|
return window.API_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(ERROR_NO_API_URL)
|
throw new InvalidApiUrlProvidedError()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {PrefixMode} from '@/modules/parseTaskText'
|
||||||
|
|
||||||
describe('Parse Subtasks via Relation', () => {
|
describe('Parse Subtasks via Relation', () => {
|
||||||
it('Should not return a parent for a single task', () => {
|
it('Should not return a parent for a single task', () => {
|
||||||
const tasks = parseSubtasksViaIndention('single task')
|
const tasks = parseSubtasksViaIndention('single task', PrefixMode.Default)
|
||||||
|
|
||||||
expect(tasks).to.have.length(1)
|
expect(tasks).to.have.length(1)
|
||||||
expect(tasks[0].parent).toBeNull()
|
expect(tasks[0].parent).toBeNull()
|
||||||
|
@ -118,4 +118,52 @@ task two`, PrefixMode.Default)
|
||||||
expect(tasks[1].project).to.eq('list')
|
expect(tasks[1].project).to.eq('list')
|
||||||
expect(tasks[2].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,7 +17,29 @@ const spaceRegex = /^ */
|
||||||
* relation between each other.
|
* relation between each other.
|
||||||
*/
|
*/
|
||||||
export function parseSubtasksViaIndention(taskTitles: string, prefixMode: PrefixMode): TaskWithParent[] {
|
export function parseSubtasksViaIndention(taskTitles: string, prefixMode: PrefixMode): TaskWithParent[] {
|
||||||
const titles = taskTitles.split(/[\r\n]+/)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return titles.map((title, index) => {
|
return titles.map((title, index) => {
|
||||||
const task: TaskWithParent = {
|
const task: TaskWithParent = {
|
||||||
|
@ -32,7 +54,7 @@ export function parseSubtasksViaIndention(taskTitles: string, prefixMode: Prefix
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
const matched = spaceRegex.exec(title)
|
const matched = spaceRegex.exec(task.title)
|
||||||
const matchedSpaces = matched ? matched[0].length : 0
|
const matchedSpaces = matched ? matched[0].length : 0
|
||||||
|
|
||||||
if (matchedSpaces > 0) {
|
if (matchedSpaces > 0) {
|
||||||
|
@ -45,7 +67,7 @@ export function parseSubtasksViaIndention(taskTitles: string, prefixMode: Prefix
|
||||||
const parentMatched = spaceRegex.exec(task.parent)
|
const parentMatched = spaceRegex.exec(task.parent)
|
||||||
parentSpaces = parentMatched ? parentMatched[0].length : 0
|
parentSpaces = parentMatched ? parentMatched[0].length : 0
|
||||||
} while (parentSpaces >= matchedSpaces)
|
} while (parentSpaces >= matchedSpaces)
|
||||||
task.title = cleanupTitle(title.replace(spaceRegex, ''))
|
task.title = cleanupTitle(task.title.replace(spaceRegex, ''))
|
||||||
task.parent = task.parent.replace(spaceRegex, '')
|
task.parent = task.parent.replace(spaceRegex, '')
|
||||||
if (task.project === null) {
|
if (task.project === null) {
|
||||||
// This allows to specify a project once for the parent task and inherit it to all subtasks
|
// This allows to specify a project once for the parent task and inherit it to all subtasks
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
const LAST_VISITED_KEY = 'lastVisited'
|
const LAST_VISITED_KEY = 'lastVisited'
|
||||||
|
|
||||||
export const saveLastVisited = (name: string, params: object, query: object) => {
|
export const saveLastVisited = (name: string | undefined, params: object, query: object) => {
|
||||||
|
if (typeof name === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem(LAST_VISITED_KEY, JSON.stringify({name, params, query}))
|
localStorage.setItem(LAST_VISITED_KEY, JSON.stringify({name, params, query}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +13,7 @@ export const getLastVisited = () => {
|
||||||
if (lastVisited === null) {
|
if (lastVisited === null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parse(lastVisited)
|
return JSON.parse(lastVisited)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,8 @@ import {format, formatDistanceToNow} from 'date-fns'
|
||||||
import {enGB, de, fr, ru} from 'date-fns/locale'
|
import {enGB, de, fr, ru} from 'date-fns/locale'
|
||||||
|
|
||||||
import {i18n} from '@/i18n'
|
import {i18n} from '@/i18n'
|
||||||
import { createSharedComposable, type MaybeRef } from '@vueuse/core'
|
import {createSharedComposable, type MaybeRef} from '@vueuse/core'
|
||||||
import { computed, unref } from 'vue'
|
import {computed, unref} from 'vue'
|
||||||
|
|
||||||
const locales = {en: enGB, de, ch: de, fr, ru}
|
const locales = {en: enGB, de, ch: de, fr, ru}
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ export const useDateTimeFormatter = createSharedComposable((options?: MaybeRef<I
|
||||||
})
|
})
|
||||||
|
|
||||||
export function useWeekDayFromDate() {
|
export function useWeekDayFromDate() {
|
||||||
const dateTimeFormatter = useDateTimeFormatter({ weekday: 'short' })
|
const dateTimeFormatter = useDateTimeFormatter({weekday: 'short'})
|
||||||
|
|
||||||
return computed(() => (date: Date) => dateTimeFormatter.value.format(date))
|
return computed(() => (date: Date) => dateTimeFormatter.value.format(date))
|
||||||
}
|
}
|
|
@ -16,10 +16,6 @@ export function secondsToPeriod(seconds: number): { unit: PeriodUnit, amount: nu
|
||||||
if (seconds % SECONDS_A_DAY === 0) {
|
if (seconds % SECONDS_A_DAY === 0) {
|
||||||
if (seconds % SECONDS_A_WEEK === 0) {
|
if (seconds % SECONDS_A_WEEK === 0) {
|
||||||
return {unit: 'weeks', amount: seconds / SECONDS_A_WEEK}
|
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 {
|
} else {
|
||||||
return {unit: 'days', amount: seconds / SECONDS_A_DAY}
|
return {unit: 'days', amount: seconds / SECONDS_A_DAY}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ export const SUPPORTED_LOCALES = {
|
||||||
'es-ES': 'Español',
|
'es-ES': 'Español',
|
||||||
'da-DK': 'Dansk',
|
'da-DK': 'Dansk',
|
||||||
'ja-JP': '日本語',
|
'ja-JP': '日本語',
|
||||||
|
'hu-HU': 'Magyar',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES
|
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES
|
||||||
|
@ -47,8 +48,13 @@ export async function setLanguage(lang: SupportedLocale): Promise<SupportedLocal
|
||||||
|
|
||||||
// If the language hasn't been loaded yet
|
// If the language hasn't been loaded yet
|
||||||
if (!i18n.global.availableLocales.includes(lang)) {
|
if (!i18n.global.availableLocales.includes(lang)) {
|
||||||
const messages = await import(`./lang/${lang}.json`)
|
try {
|
||||||
i18n.global.setLocaleMessage(lang, messages.default)
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i18n.global.locale.value = lang
|
i18n.global.locale.value = lang
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Not found",
|
"title": "Not found",
|
||||||
"text": "The page you requested does not exist."
|
"text": "The page you requested does not exist."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Dark"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "eg. https://localhost:3456",
|
"urlPlaceholder": "eg. https://localhost:3456",
|
||||||
"change": "change",
|
"change": "change",
|
||||||
"use": "Using Vikunja installation at {0}",
|
"use": "Using Vikunja installation at {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Using Vikunja installation at \"{domain}\".",
|
"success": "Using Vikunja installation at \"{domain}\".",
|
||||||
"urlRequired": "A url is required."
|
"urlRequired": "A url is required."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Nenalezeno",
|
"title": "Nenalezeno",
|
||||||
"text": "Požadovaná stránka neexistuje."
|
"text": "Požadovaná stránka neexistuje."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "Systém",
|
"system": "Systém",
|
||||||
"dark": "Tmavý"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "např. https://localhost:3456",
|
"urlPlaceholder": "např. https://localhost:3456",
|
||||||
"change": "změnit",
|
"change": "změnit",
|
||||||
"use": "Používá se instalace Vikunja v {0}",
|
"use": "Používá se instalace Vikunja v {0}",
|
||||||
"error": "Nelze najít nebo použít instalaci Vikunja na \"{domain}\". Zkuste prosím jinou url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Pomocí instalace Vikunja na \"{domain}\".",
|
"success": "Pomocí instalace Vikunja na \"{domain}\".",
|
||||||
"urlRequired": "Je vyžadována adresa URL."
|
"urlRequired": "Je vyžadována adresa URL."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Úkoly",
|
"tasks": "Úkoly",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Týmy",
|
"teams": "Týmy",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Zadejte název nového úkolu…",
|
"newTask": "Zadejte název nového úkolu…",
|
||||||
"newTeam": "Zadejte název nového týmu…",
|
"newTeam": "Zadejte název nového týmu…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Ikke fundet",
|
"title": "Ikke fundet",
|
||||||
"text": "Den ønskede side findes ikke."
|
"text": "Den ønskede side findes ikke."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Mørk"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "f.eks. https://localhost:3456",
|
"urlPlaceholder": "f.eks. https://localhost:3456",
|
||||||
"change": "ændr",
|
"change": "ændr",
|
||||||
"use": "Brug Vikunja-installationen på {0}",
|
"use": "Brug Vikunja-installationen på {0}",
|
||||||
"error": "Kunne ikke finde eller bruge Vikunja-installationen på \"{domain}\". Prøv venligst en anden url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Bruger Vikunja-installationen på \"{domain}\".",
|
"success": "Bruger Vikunja-installationen på \"{domain}\".",
|
||||||
"urlRequired": "En url er påkrævet."
|
"urlRequired": "En url er påkrævet."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Opgaver",
|
"tasks": "Opgaver",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Hold",
|
"teams": "Hold",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Indtast titlen på den nye opgave…",
|
"newTask": "Indtast titlen på den nye opgave…",
|
||||||
"newTeam": "Indtast navnet på det nye hold…",
|
"newTeam": "Indtast navnet på det nye hold…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Importiere deine Daten in Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Nicht gefunden",
|
"title": "Nicht gefunden",
|
||||||
"text": "Die angeforderte Seite existiert nicht."
|
"text": "Die angeforderte Seite existiert nicht."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Dunkel"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "Alle Aufgaben, die in diese Spalte verschoben werden, werden automatisch als erledigt markiert.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "Du kannst die letzte Spalte nicht entfernen.",
|
||||||
"addTaskPlaceholder": "Gebe einen Aufgabentitel ein …",
|
"addTaskPlaceholder": "Gebe einen Aufgabentitel ein …",
|
||||||
"addTask": "Eine Aufgabe hinzufügen",
|
"addTask": "Eine Aufgabe hinzufügen",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "z.B. https://localhost:3456",
|
"urlPlaceholder": "z.B. https://localhost:3456",
|
||||||
"change": "ändern",
|
"change": "ändern",
|
||||||
"use": "Verwende die Vikunja-Installation unter „{0}“",
|
"use": "Verwende die Vikunja-Installation unter „{0}“",
|
||||||
"error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.",
|
"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.",
|
||||||
"success": "Verwende die Vikunja-Installation unter „{domain}“.",
|
"success": "Verwende die Vikunja-Installation unter „{domain}“.",
|
||||||
"urlRequired": "Eine Url ist erforderlich."
|
"urlRequired": "Eine Url ist erforderlich."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Aufgaben",
|
"tasks": "Aufgaben",
|
||||||
"projects": "Projekte",
|
"projects": "Projekte",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Gib den Titel des neuen Projekts ein…",
|
"newProject": "Gib den Titel des neuen Projekts ein…",
|
||||||
"newTask": "Gib den Titel der neuen Aufgabe ein …",
|
"newTask": "Gib den Titel der neuen Aufgabe ein …",
|
||||||
"newTeam": "Gib den Namen des neuen Teams ein …",
|
"newTeam": "Gib den Namen des neuen Teams ein …",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Importiere deine Daten in Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Nid gfunde",
|
"title": "Nid gfunde",
|
||||||
"text": "Dini gsuechti Siite giz nid."
|
"text": "Dini gsuechti Siite giz nid."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Dunkel"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "Alle Aufgaben, die in diese Spalte verschoben werden, werden automatisch als erledigt markiert.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "Du kannst die letzte Spalte nicht entfernen.",
|
||||||
"addTaskPlaceholder": "Gebe einen Aufgabentitel ein …",
|
"addTaskPlaceholder": "Gebe einen Aufgabentitel ein …",
|
||||||
"addTask": "Eine Aufgabe hinzufügen",
|
"addTask": "Eine Aufgabe hinzufügen",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "z.B. https://localhost:3456",
|
"urlPlaceholder": "z.B. https://localhost:3456",
|
||||||
"change": "ändere",
|
"change": "ändere",
|
||||||
"use": "Verwende die Vikunja-Installation unter „{0}“",
|
"use": "Verwende die Vikunja-Installation unter „{0}“",
|
||||||
"error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.",
|
"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.",
|
||||||
"success": "Benutze d'Vikunja Installation uf \"{domain}\".",
|
"success": "Benutze d'Vikunja Installation uf \"{domain}\".",
|
||||||
"urlRequired": "Eine Url ist erforderlich."
|
"urlRequired": "Eine Url ist erforderlich."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Uufgabe",
|
"tasks": "Uufgabe",
|
||||||
"projects": "Projekte",
|
"projects": "Projekte",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Gib den Titel des neuen Projekts ein…",
|
"newProject": "Gib den Titel des neuen Projekts ein…",
|
||||||
"newTask": "Gib en Titl für die neu Uufgab iih…",
|
"newTask": "Gib en Titl für die neu Uufgab iih…",
|
||||||
"newTeam": "Gib en Name für da neui Team iih…",
|
"newTeam": "Gib en Name für da neui Team iih…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Not found",
|
"title": "Not found",
|
||||||
"text": "The page you requested does not exist."
|
"text": "The page you requested does not exist."
|
||||||
|
@ -139,6 +144,32 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Dark"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +336,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -736,7 +770,7 @@
|
||||||
"repeat": {
|
"repeat": {
|
||||||
"everyDay": "Every Day",
|
"everyDay": "Every Day",
|
||||||
"everyWeek": "Every Week",
|
"everyWeek": "Every Week",
|
||||||
"everyMonth": "Every Month",
|
"every30d": "Every 30 Days",
|
||||||
"mode": "Repeat mode",
|
"mode": "Repeat mode",
|
||||||
"monthly": "Monthly",
|
"monthly": "Monthly",
|
||||||
"fromCurrentDate": "From Current Date",
|
"fromCurrentDate": "From Current Date",
|
||||||
|
@ -884,7 +918,7 @@
|
||||||
"urlPlaceholder": "eg. https://localhost:3456",
|
"urlPlaceholder": "eg. https://localhost:3456",
|
||||||
"change": "change",
|
"change": "change",
|
||||||
"use": "Using Vikunja installation at {0}",
|
"use": "Using Vikunja installation at {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Using Vikunja installation at \"{domain}\".",
|
"success": "Using Vikunja installation at \"{domain}\".",
|
||||||
"urlRequired": "A url is required."
|
"urlRequired": "A url is required."
|
||||||
},
|
},
|
||||||
|
@ -905,6 +939,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Not found",
|
"title": "Not found",
|
||||||
"text": "The page you requested does not exist."
|
"text": "The page you requested does not exist."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Dark"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "eg. https://localhost:3456",
|
"urlPlaceholder": "eg. https://localhost:3456",
|
||||||
"change": "change",
|
"change": "change",
|
||||||
"use": "Using Vikunja installation at {0}",
|
"use": "Using Vikunja installation at {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Using Vikunja installation at \"{domain}\".",
|
"success": "Using Vikunja installation at \"{domain}\".",
|
||||||
"urlRequired": "A url is required."
|
"urlRequired": "A url is required."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Importa tus datos a Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "No encontrado",
|
"title": "No encontrado",
|
||||||
"text": "La página solicitada no existe."
|
"text": "La página solicitada no existe."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "Sistema",
|
"system": "Sistema",
|
||||||
"dark": "Oscuro"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "Todas las tareas movidas a este contenedor se marcarán automáticamente como finalizadas.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "No puedes eliminar el último contenedor.",
|
||||||
"addTaskPlaceholder": "Introduce el nuevo título de la tarea…",
|
"addTaskPlaceholder": "Introduce el nuevo título de la tarea…",
|
||||||
"addTask": "Añadir una tarea",
|
"addTask": "Añadir una tarea",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "ej. https://localhost:3456",
|
"urlPlaceholder": "ej. https://localhost:3456",
|
||||||
"change": "cambiar",
|
"change": "cambiar",
|
||||||
"use": "Utilizando la instalación de Vikunja en {0}",
|
"use": "Utilizando la instalación de Vikunja en {0}",
|
||||||
"error": "No se pudo encontrar o usar la instalación de Vikunja en \"{domain}\". Por favor, prueba con una url diferente.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Usando la instalación de Vikunja en \"{domain}\".",
|
"success": "Usando la instalación de Vikunja en \"{domain}\".",
|
||||||
"urlRequired": "Se requiere una url."
|
"urlRequired": "Se requiere una url."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Tareas",
|
"tasks": "Tareas",
|
||||||
"projects": "Proyectos",
|
"projects": "Proyectos",
|
||||||
"teams": "Equipos",
|
"teams": "Equipos",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Introduzca el título del nuevo proyecto…",
|
"newProject": "Introduzca el título del nuevo proyecto…",
|
||||||
"newTask": "Introduzca el título de la nueva tarea…",
|
"newTask": "Introduzca el título de la nueva tarea…",
|
||||||
"newTeam": "Introduzca el nombre del nuevo equipo…",
|
"newTeam": "Introduzca el nombre del nuevo equipo…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Importer vos données dans Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Non trouvé",
|
"title": "Non trouvé",
|
||||||
"text": "La page que vous avez demandée n’existe pas."
|
"text": "La page que vous avez demandée n’existe pas."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "Système",
|
"system": "Système",
|
||||||
"dark": "Sombre"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "Toute tâche déplacée dans cette colonne sera automatiquement marquée comme terminée.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "Vous ne pouvez pas retirer la dernière colonne.",
|
||||||
"addTaskPlaceholder": "Saisir le nouveau nom de la tâche…",
|
"addTaskPlaceholder": "Saisir le nouveau nom de la tâche…",
|
||||||
"addTask": "Ajouter une tâche",
|
"addTask": "Ajouter une tâche",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "Par exemple : https://localhost:3456",
|
"urlPlaceholder": "Par exemple : https://localhost:3456",
|
||||||
"change": "changer",
|
"change": "changer",
|
||||||
"use": "Utiliser l’installation de Vikunja à {0}",
|
"use": "Utiliser l’installation de Vikunja à {0}",
|
||||||
"error": "Impossible de trouver ou d'utiliser l'installation de Vikunja sur « {domain} ». Veuillez essayer une autre adresse.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Utilisation de l’installation Vikunja sur « {domain} ».",
|
"success": "Utilisation de l’installation Vikunja sur « {domain} ».",
|
||||||
"urlRequired": "Une adresse est requise."
|
"urlRequired": "Une adresse est requise."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Tâches",
|
"tasks": "Tâches",
|
||||||
"projects": "Projets",
|
"projects": "Projets",
|
||||||
"teams": "Équipes",
|
"teams": "Équipes",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Saisissez le nom du nouveau projet…",
|
"newProject": "Saisissez le nom du nouveau projet…",
|
||||||
"newTask": "Saisir le nom de la nouvelle tâche…",
|
"newTask": "Saisir le nom de la nouvelle tâche…",
|
||||||
"newTeam": "Saisir le nom de la nouvelle équipe…",
|
"newTeam": "Saisir le nom de la nouvelle équipe…",
|
||||||
|
|
1041
src/i18n/lang/hu-HU.json
Normal file
1041
src/i18n/lang/hu-HU.json
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -5,12 +5,17 @@
|
||||||
"welcomeDay": "Ciao {username}!",
|
"welcomeDay": "Ciao {username}!",
|
||||||
"welcomeEvening": "Buonasera {username}!",
|
"welcomeEvening": "Buonasera {username}!",
|
||||||
"lastViewed": "Ultima visualizzazione",
|
"lastViewed": "Ultima visualizzazione",
|
||||||
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
|
"addToHomeScreen": "Aggiungi questa app alla tua schermata iniziale per un accesso più veloce e un'esperienza migliore.",
|
||||||
"project": {
|
"project": {
|
||||||
"importText": "Import your projects and tasks from other services into Vikunja:",
|
"importText": "Import your projects and tasks from other services into Vikunja:",
|
||||||
"import": "Importa i tuoi dati in 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": {
|
"404": {
|
||||||
"title": "Non trovato",
|
"title": "Non trovato",
|
||||||
"text": "La pagina richiesta non esiste."
|
"text": "La pagina richiesta non esiste."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "Sistema",
|
"system": "Sistema",
|
||||||
"dark": "Scuro"
|
"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": {
|
"deletion": {
|
||||||
|
@ -166,8 +195,8 @@
|
||||||
},
|
},
|
||||||
"project": {
|
"project": {
|
||||||
"archivedMessage": "This project is archived. It is not possible to create new or edit tasks for it.",
|
"archivedMessage": "This project is archived. It is not possible to create new or edit tasks for it.",
|
||||||
"archived": "Archived",
|
"archived": "Archiviati",
|
||||||
"showArchived": "Show Archived",
|
"showArchived": "Mostra Archiviati",
|
||||||
"title": "Titolo Progetto",
|
"title": "Titolo Progetto",
|
||||||
"color": "Colore",
|
"color": "Colore",
|
||||||
"projects": "Progetti",
|
"projects": "Progetti",
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "es. http://localhost:8080",
|
"urlPlaceholder": "es. http://localhost:8080",
|
||||||
"change": "modifica",
|
"change": "modifica",
|
||||||
"use": "Usa l'installazione di Vikunja a {0}",
|
"use": "Usa l'installazione di Vikunja a {0}",
|
||||||
"error": "Impossibile trovare o usare l'installazione di Vikunja su \"{domain}\". Prova per favore con un altro Url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Utilizzando l'installazione di Vikunja su \"{domain}\".",
|
"success": "Utilizzando l'installazione di Vikunja su \"{domain}\".",
|
||||||
"urlRequired": "L'URL è obbligatorio."
|
"urlRequired": "L'URL è obbligatorio."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Attivitá",
|
"tasks": "Attivitá",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Gruppi",
|
"teams": "Gruppi",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Inserisci il titolo della nuova attività…",
|
"newTask": "Inserisci il titolo della nuova attività…",
|
||||||
"newTeam": "Inserisci il nome del nuovo gruppo…",
|
"newTeam": "Inserisci il nome del nuovo gruppo…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Vikunjaへのデータのインポート"
|
"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": {
|
"404": {
|
||||||
"title": "Not found",
|
"title": "Not found",
|
||||||
"text": "リクエストされたページは存在しません。"
|
"text": "リクエストされたページは存在しません。"
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "タスクの追加",
|
"addTask": "タスクの追加",
|
||||||
|
@ -329,12 +361,12 @@
|
||||||
"title": "絞り込み",
|
"title": "絞り込み",
|
||||||
"clear": "絞り込みの解除",
|
"clear": "絞り込みの解除",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"title": "条件名",
|
"title": "絞り込み条件名",
|
||||||
"titlePlaceholder": "条件名を入力…",
|
"titlePlaceholder": "絞り込み条件名を入力…",
|
||||||
"description": "説明",
|
"description": "説明",
|
||||||
"descriptionPlaceholder": "説明を入力…",
|
"descriptionPlaceholder": "絞り込み条件の説明を入力…",
|
||||||
"includeNulls": "値を設定していないタスクを含める",
|
"includeNulls": "値が設定されていないタスクを含める",
|
||||||
"requireAll": "Require all filters to be true for a task to show up",
|
"requireAll": "すべての条件に一致するタスクのみ表示",
|
||||||
"showDoneTasks": "完了したタスクを表示",
|
"showDoneTasks": "完了したタスクを表示",
|
||||||
"sortAlphabetically": "アルファベット順に並べ替える",
|
"sortAlphabetically": "アルファベット順に並べ替える",
|
||||||
"enablePriority": "優先度による絞り込みを有効化",
|
"enablePriority": "優先度による絞り込みを有効化",
|
||||||
|
@ -346,18 +378,18 @@
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "新しい絞り込み条件の作成",
|
"title": "新しい絞り込み条件の作成",
|
||||||
"description": "A saved filter is a virtual project which is computed from a set of filters each time it is accessed.",
|
"description": "絞り込み条件は、複数の条件を組み合わせて保存できる仮想のプロジェクトです。",
|
||||||
"action": "新しい絞り込み条件を作成",
|
"action": "新しい絞り込み条件を作成",
|
||||||
"titleRequired": "Please provide a title for the filter."
|
"titleRequired": "絞り込み条件名を入力してください。"
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"header": "Delete this saved filter",
|
"header": "絞り込み条件の削除",
|
||||||
"text": "Are you sure you want to delete this saved filter?",
|
"text": "絞り込み条件を削除して本当によろしいですか?",
|
||||||
"success": "The filter was deleted successfully."
|
"success": "絞り込み条件は正常に削除されました。"
|
||||||
},
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"title": "Edit This Saved Filter",
|
"title": "絞り込み条件の編集",
|
||||||
"success": "The filter was saved successfully."
|
"success": "絞り込み条件は正常に保存されました。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"migrate": {
|
"migrate": {
|
||||||
|
@ -815,7 +847,7 @@
|
||||||
"namePlaceholder": "The team's name goes here…",
|
"namePlaceholder": "The team's name goes here…",
|
||||||
"nameRequired": "Please specify a name.",
|
"nameRequired": "Please specify a name.",
|
||||||
"description": "説明",
|
"description": "説明",
|
||||||
"descriptionPlaceholder": "The teams description goes here…",
|
"descriptionPlaceholder": "チームの説明を入力…",
|
||||||
"admin": "管理者",
|
"admin": "管理者",
|
||||||
"member": "メンバー"
|
"member": "メンバー"
|
||||||
}
|
}
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "例: https://localhost:3456",
|
"urlPlaceholder": "例: https://localhost:3456",
|
||||||
"change": "変更",
|
"change": "変更",
|
||||||
"use": "{0} に設置されたVikunjaを使用します。",
|
"use": "{0} に設置されたVikunjaを使用します。",
|
||||||
"error": "\"{domain}\" にはVikunjaは存在しないか使用できない状態です。別のURLでお試しください。",
|
"error": "\"{domain}\" にはVikunjaは存在しないか使用できない状態です。URLの形式が正しいかどうか、そして直接アクセスして到達できるかどうかを確認し、もう一度お試しください。",
|
||||||
"success": "\"{domain}\" に設置されたVikunjaを使用します。",
|
"success": "\"{domain}\" に設置されたVikunjaを使用します。",
|
||||||
"urlRequired": "URLは必須です。"
|
"urlRequired": "URLは必須です。"
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "タスク",
|
"tasks": "タスク",
|
||||||
"projects": "プロジェクト",
|
"projects": "プロジェクト",
|
||||||
"teams": "チーム",
|
"teams": "チーム",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "新しいプロジェクト名を入力…",
|
"newProject": "新しいプロジェクト名を入力…",
|
||||||
"newTask": "新しいタスク名を入力…",
|
"newTask": "新しいタスク名を入力…",
|
||||||
"newTeam": "新しいチーム名を入力…",
|
"newTeam": "新しいチーム名を入力…",
|
||||||
|
@ -983,7 +1016,7 @@
|
||||||
"10004": "You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold.",
|
"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.",
|
"10005": "There can be only one done bucket per project.",
|
||||||
"11001": "The saved filter does not exist.",
|
"11001": "The saved filter does not exist.",
|
||||||
"11002": "Saved filters are not available for link shares.",
|
"11002": "絞り込み条件はリンクの共有には使用できません。",
|
||||||
"12001": "The subscription entity type is invalid.",
|
"12001": "The subscription entity type is invalid.",
|
||||||
"12002": "You are already subscribed to the entity itself or a parent entity.",
|
"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.",
|
"13001": "This link share requires a password for authentication, but none was provided.",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "데이터를 Vikunja로 가져오기"
|
"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": {
|
"404": {
|
||||||
"title": "찾을 수 없습니다.",
|
"title": "찾을 수 없습니다.",
|
||||||
"text": "요청하신 페이지가 존재하지 않습니다."
|
"text": "요청하신 페이지가 존재하지 않습니다."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "작업 추가",
|
"addTask": "작업 추가",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "eg. https://localhost:3456",
|
"urlPlaceholder": "eg. https://localhost:3456",
|
||||||
"change": "change",
|
"change": "change",
|
||||||
"use": "Using Vikunja installation at {0}",
|
"use": "Using Vikunja installation at {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Using Vikunja installation at \"{domain}\".",
|
"success": "Using Vikunja installation at \"{domain}\".",
|
||||||
"urlRequired": "A url is required."
|
"urlRequired": "A url is required."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Niet gevonden",
|
"title": "Niet gevonden",
|
||||||
"text": "De opgevraagde pagina bestaat niet."
|
"text": "De opgevraagde pagina bestaat niet."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "Systeem",
|
"system": "Systeem",
|
||||||
"dark": "Donker"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "bv. https://localhost:3456",
|
"urlPlaceholder": "bv. https://localhost:3456",
|
||||||
"change": "wijzigen",
|
"change": "wijzigen",
|
||||||
"use": "Using Vikunja installation at {0}",
|
"use": "Using Vikunja installation at {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Using Vikunja installation at \"{domain}\".",
|
"success": "Using Vikunja installation at \"{domain}\".",
|
||||||
"urlRequired": "A url is required."
|
"urlRequired": "A url is required."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Taken",
|
"tasks": "Taken",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Importer dine data til Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Ikke funnet",
|
"title": "Ikke funnet",
|
||||||
"text": "Siden du ba om, finnes ikke."
|
"text": "Siden du ba om, finnes ikke."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Mørk"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "Alle oppgaver som flyttet til denne bøtte vil automatisk bli markert som ferdig.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "Du kan ikke fjerne den siste bøtten.",
|
||||||
"addTaskPlaceholder": "Angi den nye oppgavens tittel…",
|
"addTaskPlaceholder": "Angi den nye oppgavens tittel…",
|
||||||
"addTask": "Legg til oppgave",
|
"addTask": "Legg til oppgave",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "f.eks. http://localhost:3456",
|
"urlPlaceholder": "f.eks. http://localhost:3456",
|
||||||
"change": "endre",
|
"change": "endre",
|
||||||
"use": "Bruker Vikunja installasjonen på {0}",
|
"use": "Bruker Vikunja installasjonen på {0}",
|
||||||
"error": "Kunne ikke finne eller bruke Vikunja installasjon på{domain}\". Prøv en annen Url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Bruker Vikunja installasjonen på \"{domain}.",
|
"success": "Bruker Vikunja installasjonen på \"{domain}.",
|
||||||
"urlRequired": "Url'en er tom, vennligst legg til."
|
"urlRequired": "Url'en er tom, vennligst legg til."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Oppgaver",
|
"tasks": "Oppgaver",
|
||||||
"projects": "Prosjekter",
|
"projects": "Prosjekter",
|
||||||
"teams": "Grupper",
|
"teams": "Grupper",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Skriv tittelen på det nye prosjektet…",
|
"newProject": "Skriv tittelen på det nye prosjektet…",
|
||||||
"newTask": "Skriv tittelen på den nye oppgaven…",
|
"newTask": "Skriv tittelen på den nye oppgaven…",
|
||||||
"newTeam": "Skriv inn navnet på den nye gruppen…",
|
"newTeam": "Skriv inn navnet på den nye gruppen…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Nie znaleziono",
|
"title": "Nie znaleziono",
|
||||||
"text": "Żądana strona nie istnieje."
|
"text": "Żądana strona nie istnieje."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "Systemowy",
|
"system": "Systemowy",
|
||||||
"dark": "Ciemny"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "np. https://localhost:3456",
|
"urlPlaceholder": "np. https://localhost:3456",
|
||||||
"change": "zmień",
|
"change": "zmień",
|
||||||
"use": "Użyj instalacji Vikunji z {0}",
|
"use": "Użyj instalacji Vikunji z {0}",
|
||||||
"error": "Nie można znaleźć lub użyć instalacji Vikunji z \"{domain}\". Wypróbuj inny adres URL.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Używasz instalacji Vikunji z \"{domain}\".",
|
"success": "Używasz instalacji Vikunji z \"{domain}\".",
|
||||||
"urlRequired": "URL jest wymagany."
|
"urlRequired": "URL jest wymagany."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Zadania",
|
"tasks": "Zadania",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Zespoły",
|
"teams": "Zespoły",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Wpisz tytuł nowego zadania…",
|
"newTask": "Wpisz tytuł nowego zadania…",
|
||||||
"newTeam": "Wpisz nazwę nowego zespołu…",
|
"newTeam": "Wpisz nazwę nowego zespołu…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Não encontrado",
|
"title": "Não encontrado",
|
||||||
"text": "The page you requested does not exist."
|
"text": "The page you requested does not exist."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Dark"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "eg. https://localhost:3456",
|
"urlPlaceholder": "eg. https://localhost:3456",
|
||||||
"change": "change",
|
"change": "change",
|
||||||
"use": "Using Vikunja installation at {0}",
|
"use": "Using Vikunja installation at {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Usando a instalação Vikunja em \"{domain}\".",
|
"success": "Usando a instalação Vikunja em \"{domain}\".",
|
||||||
"urlRequired": "Uma url é necessária."
|
"urlRequired": "Uma url é necessária."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Tarefas",
|
"tasks": "Tarefas",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Equipes",
|
"teams": "Equipes",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Importar os teus dados para o Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Não encontrado",
|
"title": "Não encontrado",
|
||||||
"text": "A página solicitada não existe."
|
"text": "A página solicitada não existe."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "Sistema",
|
"system": "Sistema",
|
||||||
"dark": "Escuro"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "Todas as tarefas movidas para este conjunto serão automaticamente marcadas como concluídas.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "Não podes remover o ultimo conjunto.",
|
||||||
"addTaskPlaceholder": "Introduz o título da nova tarefa…",
|
"addTaskPlaceholder": "Introduz o título da nova tarefa…",
|
||||||
"addTask": "Adicionar uma tarefa",
|
"addTask": "Adicionar uma tarefa",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "ex.: https://localhost:3456",
|
"urlPlaceholder": "ex.: https://localhost:3456",
|
||||||
"change": "alterar",
|
"change": "alterar",
|
||||||
"use": "A utilizar a instalação do Vikunja em {0}",
|
"use": "A utilizar a instalação do Vikunja em {0}",
|
||||||
"error": "Não foi possível encontrar ou utilizar a instalação do Vikunja em \"{domain}\". Por favor, tenta um url diferente.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "A utilizar a instalação do Vikunja em \"{domain}\".",
|
"success": "A utilizar a instalação do Vikunja em \"{domain}\".",
|
||||||
"urlRequired": "É necessário um url."
|
"urlRequired": "É necessário um url."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Tarefas",
|
"tasks": "Tarefas",
|
||||||
"projects": "Projetos",
|
"projects": "Projetos",
|
||||||
"teams": "Equipas",
|
"teams": "Equipas",
|
||||||
|
"labels": "Etiquetas",
|
||||||
"newProject": "Insere o título do novo espaço…",
|
"newProject": "Insere o título do novo espaço…",
|
||||||
"newTask": "Insere o título da nova tarefa…",
|
"newTask": "Insere o título da nova tarefa…",
|
||||||
"newTeam": "Insere o nome da nova equipa…",
|
"newTeam": "Insere o nome da nova equipa…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Not found",
|
"title": "Not found",
|
||||||
"text": "The page you requested does not exist."
|
"text": "The page you requested does not exist."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Dark"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "eg. https://localhost:3456",
|
"urlPlaceholder": "eg. https://localhost:3456",
|
||||||
"change": "change",
|
"change": "change",
|
||||||
"use": "Using Vikunja installation at {0}",
|
"use": "Using Vikunja installation at {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Using Vikunja installation at \"{domain}\".",
|
"success": "Using Vikunja installation at \"{domain}\".",
|
||||||
"urlRequired": "A url is required."
|
"urlRequired": "A url is required."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Импорт данных в Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Не найдено",
|
"title": "Не найдено",
|
||||||
"text": "Запрашиваемая страница не существует."
|
"text": "Запрашиваемая страница не существует."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "Все задачи, помещённые в эту колонку, автоматически отмечаются как завершённые.",
|
"doneBucketHint": "Все задачи, помещённые в эту колонку, автоматически отмечаются как завершённые.",
|
||||||
"doneBucketHintExtended": "Все задачи, перенесённые в колонку завершённых, будут помечены как завершённые. Все задачи, помеченные как завершённые, также будут перемещены в эту колонку.",
|
"doneBucketHintExtended": "Все задачи, перенесённые в колонку завершённых, будут помечены как завершённые. Все задачи, помеченные как завершённые, также будут перемещены в эту колонку.",
|
||||||
"doneBucketSavedSuccess": "Колонка завершённых была успешно сохранена.",
|
"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": "Нельзя удалить последнюю колонку.",
|
"deleteLast": "Нельзя удалить последнюю колонку.",
|
||||||
"addTaskPlaceholder": "Введите название задачи…",
|
"addTaskPlaceholder": "Введите название задачи…",
|
||||||
"addTask": "Добавить задачу",
|
"addTask": "Добавить задачу",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "напр. https://localhost:3456",
|
"urlPlaceholder": "напр. https://localhost:3456",
|
||||||
"change": "изменить",
|
"change": "изменить",
|
||||||
"use": "Используется Vikunja на {0}",
|
"use": "Используется Vikunja на {0}",
|
||||||
"error": "Не удалось подключиться к Vikunja по адресу \"{domain}\". Попробуйте указать другой url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Используется Vikunja на \"{domain}\".",
|
"success": "Используется Vikunja на \"{domain}\".",
|
||||||
"urlRequired": "Требуется url."
|
"urlRequired": "Требуется url."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Задачи",
|
"tasks": "Задачи",
|
||||||
"projects": "Проекты",
|
"projects": "Проекты",
|
||||||
"teams": "Команды",
|
"teams": "Команды",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Введите название проекта…",
|
"newProject": "Введите название проекта…",
|
||||||
"newTask": "Введите название задачи…",
|
"newTask": "Введите название задачи…",
|
||||||
"newTeam": "Введите название новой команды…",
|
"newTeam": "Введите название новой команды…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Not found",
|
"title": "Not found",
|
||||||
"text": "The page you requested does not exist."
|
"text": "The page you requested does not exist."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Dark"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "eg. https://localhost:3456",
|
"urlPlaceholder": "eg. https://localhost:3456",
|
||||||
"change": "change",
|
"change": "change",
|
||||||
"use": "Using Vikunja installation at {0}",
|
"use": "Using Vikunja installation at {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Using Vikunja installation at \"{domain}\".",
|
"success": "Using Vikunja installation at \"{domain}\".",
|
||||||
"urlRequired": "A url is required."
|
"urlRequired": "A url is required."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Not found",
|
"title": "Not found",
|
||||||
"text": "The page you requested does not exist."
|
"text": "The page you requested does not exist."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Dark"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "eg. https://localhost:3456",
|
"urlPlaceholder": "eg. https://localhost:3456",
|
||||||
"change": "change",
|
"change": "change",
|
||||||
"use": "Using Vikunja installation at {0}",
|
"use": "Using Vikunja installation at {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Using Vikunja installation at \"{domain}\".",
|
"success": "Using Vikunja installation at \"{domain}\".",
|
||||||
"urlRequired": "A url is required."
|
"urlRequired": "A url is required."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Importera din data till Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Hittades inte",
|
"title": "Hittades inte",
|
||||||
"text": "The page you requested does not exist."
|
"text": "The page you requested does not exist."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Mörkt"
|
"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": {
|
"deletion": {
|
||||||
|
@ -172,8 +201,8 @@
|
||||||
"color": "Färg",
|
"color": "Färg",
|
||||||
"projects": "Projekt",
|
"projects": "Projekt",
|
||||||
"parent": "Parent Project",
|
"parent": "Parent Project",
|
||||||
"search": "Type to search for a project…",
|
"search": "Skriv för att söka efter ett projekt…",
|
||||||
"searchSelect": "Click or press enter to select this project",
|
"searchSelect": "Klicka eller tryck på enter för att välja detta projekt",
|
||||||
"shared": "Delade projekt",
|
"shared": "Delade projekt",
|
||||||
"noDescriptionAvailable": "No project description is available.",
|
"noDescriptionAvailable": "No project description is available.",
|
||||||
"inboxTitle": "Inkorg",
|
"inboxTitle": "Inkorg",
|
||||||
|
@ -294,7 +323,7 @@
|
||||||
"noDates": "This task has no dates set."
|
"noDates": "This task has no dates set."
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"title": "Table",
|
"title": "Tabell",
|
||||||
"columns": "Kolumner"
|
"columns": "Kolumner"
|
||||||
},
|
},
|
||||||
"kanban": {
|
"kanban": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Lägg till en uppgift",
|
"addTask": "Lägg till en uppgift",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "t. ex. https://localhost:3456",
|
"urlPlaceholder": "t. ex. https://localhost:3456",
|
||||||
"change": "change",
|
"change": "change",
|
||||||
"use": "Using Vikunja installation at {0}",
|
"use": "Using Vikunja installation at {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Using Vikunja installation at \"{domain}\".",
|
"success": "Using Vikunja installation at \"{domain}\".",
|
||||||
"urlRequired": "A url is required."
|
"urlRequired": "A url is required."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Uppgifter",
|
"tasks": "Uppgifter",
|
||||||
"projects": "Projekt",
|
"projects": "Projekt",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Etiketter",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Not found",
|
"title": "Not found",
|
||||||
"text": "The page you requested does not exist."
|
"text": "The page you requested does not exist."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Dark"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "eg. https://localhost:3456",
|
"urlPlaceholder": "eg. https://localhost:3456",
|
||||||
"change": "change",
|
"change": "change",
|
||||||
"use": "Using Vikunja installation at {0}",
|
"use": "Using Vikunja installation at {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Using Vikunja installation at \"{domain}\".",
|
"success": "Using Vikunja installation at \"{domain}\".",
|
||||||
"urlRequired": "A url is required."
|
"urlRequired": "A url is required."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Không tìm thấy gì cả",
|
"title": "Không tìm thấy gì cả",
|
||||||
"text": "Trang bạn yêu cầu không tồn tại."
|
"text": "Trang bạn yêu cầu không tồn tại."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "Hệ thống",
|
"system": "Hệ thống",
|
||||||
"dark": "Tối"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "ví dụ: https://localhost:3456",
|
"urlPlaceholder": "ví dụ: https://localhost:3456",
|
||||||
"change": "thay đổi",
|
"change": "thay đổi",
|
||||||
"use": "Sử dụng cài đặt Vikunja tại {0}",
|
"use": "Sử dụng cài đặt Vikunja tại {0}",
|
||||||
"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.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Sử dụng cài đặt Vikunja tại \"{domain}\".",
|
"success": "Sử dụng cài đặt Vikunja tại \"{domain}\".",
|
||||||
"urlRequired": "Cần có một url."
|
"urlRequired": "Cần có một url."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Tác vụ",
|
"tasks": "Tác vụ",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Team",
|
"teams": "Team",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Đặt tên cho tác vụ mới…",
|
"newTask": "Đặt tên cho tác vụ mới…",
|
||||||
"newTeam": "Đặt tên cho đội nhóm mới…",
|
"newTeam": "Đặt tên cho đội nhóm mới…",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "未找到数据",
|
"title": "未找到数据",
|
||||||
"text": "您请求的页面不存在。"
|
"text": "您请求的页面不存在。"
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "例如: http://localhost:3456",
|
"urlPlaceholder": "例如: http://localhost:3456",
|
||||||
"change": "换一换",
|
"change": "换一换",
|
||||||
"use": "在 {0} 使用 Vikunja 安装程序",
|
"use": "在 {0} 使用 Vikunja 安装程序",
|
||||||
"error": "无法在 “{domain}” 上找到或使用 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.",
|
||||||
"success": "在 “{domain}” 上使用 Vikunja 安装程序。",
|
"success": "在 “{domain}” 上使用 Vikunja 安装程序。",
|
||||||
"urlRequired": "Url 是必需的。"
|
"urlRequired": "Url 是必需的。"
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "任务",
|
"tasks": "任务",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "团队",
|
"teams": "团队",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "输入新任务的标题...",
|
"newTask": "输入新任务的标题...",
|
||||||
"newTeam": "输入新团队的名称...",
|
"newTeam": "输入新团队的名称...",
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
"import": "Import your data into Vikunja"
|
"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": {
|
"404": {
|
||||||
"title": "Not found",
|
"title": "Not found",
|
||||||
"text": "The page you requested does not exist."
|
"text": "The page you requested does not exist."
|
||||||
|
@ -139,6 +144,30 @@
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"dark": "Dark"
|
"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": {
|
"deletion": {
|
||||||
|
@ -305,6 +334,9 @@
|
||||||
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
|
"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.",
|
"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.",
|
"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.",
|
"deleteLast": "You cannot remove the last bucket.",
|
||||||
"addTaskPlaceholder": "Enter the new task title…",
|
"addTaskPlaceholder": "Enter the new task title…",
|
||||||
"addTask": "Add a task",
|
"addTask": "Add a task",
|
||||||
|
@ -881,7 +913,7 @@
|
||||||
"urlPlaceholder": "eg. https://localhost:3456",
|
"urlPlaceholder": "eg. https://localhost:3456",
|
||||||
"change": "change",
|
"change": "change",
|
||||||
"use": "Using Vikunja installation at {0}",
|
"use": "Using Vikunja installation at {0}",
|
||||||
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
|
"error": "Could not find or use Vikunja installation at \"{domain}\". Please check if the url has the correct format and you can reach it when accessing it directly and try again.",
|
||||||
"success": "Using Vikunja installation at \"{domain}\".",
|
"success": "Using Vikunja installation at \"{domain}\".",
|
||||||
"urlRequired": "A url is required."
|
"urlRequired": "A url is required."
|
||||||
},
|
},
|
||||||
|
@ -902,6 +934,7 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
|
"labels": "Labels",
|
||||||
"newProject": "Enter the title of the new project…",
|
"newProject": "Enter the title of the new project…",
|
||||||
"newTask": "Enter the title of the new task…",
|
"newTask": "Enter the title of the new task…",
|
||||||
"newTeam": "Enter the name of the new team…",
|
"newTeam": "Enter the name of the new team…",
|
||||||
|
|
|
@ -23,6 +23,7 @@ declare global {
|
||||||
SENTRY_DSN: string;
|
SENTRY_DSN: string;
|
||||||
PROJECT_INFINITE_NESTING_ENABLED: boolean;
|
PROJECT_INFINITE_NESTING_ENABLED: boolean;
|
||||||
ALLOW_ICON_CHANGES: boolean;
|
ALLOW_ICON_CHANGES: boolean;
|
||||||
|
CUSTOM_LOGO_URL?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +36,8 @@ if (apiUrlFromStorage !== null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the api url does not contain a / at the end
|
// Make sure the api url does not contain a / at the end
|
||||||
if (window.API_URL.slice(window.API_URL.length - 1, window.API_URL.length) === '/') {
|
if (window.API_URL.endsWith('/')) {
|
||||||
window.API_URL = window.API_URL.slice(0, window.API_URL.length - 1)
|
window.API_URL = window.API_URL.slice(0, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// directives
|
// directives
|
||||||
|
|
14
src/modelTypes/IApiToken.ts
Normal file
14
src/modelTypes/IApiToken.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
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,7 +8,6 @@ export interface IBucket extends IAbstract {
|
||||||
projectId: number
|
projectId: number
|
||||||
limit: number
|
limit: number
|
||||||
tasks: ITask[]
|
tasks: ITask[]
|
||||||
isDoneBucket: boolean
|
|
||||||
position: number
|
position: number
|
||||||
count: number
|
count: number
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,8 @@ export interface IProject extends IAbstract {
|
||||||
position: number
|
position: number
|
||||||
backgroundBlurHash: string
|
backgroundBlurHash: string
|
||||||
parentProjectId: number
|
parentProjectId: number
|
||||||
|
doneBucketId: number
|
||||||
|
defaultBucketId: number
|
||||||
|
|
||||||
created: Date
|
created: Date
|
||||||
updated: Date
|
updated: Date
|
||||||
|
|
21
src/models/apiTokenModel.ts
Normal file
21
src/models/apiTokenModel.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
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,7 +12,6 @@ export default class BucketModel extends AbstractModel<IBucket> implements IBuck
|
||||||
projectId = ''
|
projectId = ''
|
||||||
limit = 0
|
limit = 0
|
||||||
tasks: ITask[] = []
|
tasks: ITask[] = []
|
||||||
isDoneBucket = false
|
|
||||||
position = 0
|
position = 0
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,8 @@ export default class ProjectModel extends AbstractModel<IProject> implements IPr
|
||||||
position = 0
|
position = 0
|
||||||
backgroundBlurHash = ''
|
backgroundBlurHash = ''
|
||||||
parentProjectId = 0
|
parentProjectId = 0
|
||||||
|
doneBucketId = 0
|
||||||
|
defaultBucketId = 0
|
||||||
|
|
||||||
created: Date = null
|
created: Date = null
|
||||||
updated: Date = null
|
updated: Date = null
|
||||||
|
|
|
@ -85,7 +85,6 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
|
||||||
index = 0
|
index = 0
|
||||||
isFavorite = false
|
isFavorite = false
|
||||||
subscription: ISubscription = null
|
subscription: ISubscription = null
|
||||||
coverImageAttachmentId: IAttachment['id'] = null
|
|
||||||
|
|
||||||
position = 0
|
position = 0
|
||||||
kanbanPosition = 0
|
kanbanPosition = 0
|
||||||
|
|
|
@ -65,6 +65,7 @@ const UserSettingsEmailUpdateComponent = () => import('@/views/user/settings/Ema
|
||||||
const UserSettingsGeneralComponent = () => import('@/views/user/settings/General.vue')
|
const UserSettingsGeneralComponent = () => import('@/views/user/settings/General.vue')
|
||||||
const UserSettingsPasswordUpdateComponent = () => import('@/views/user/settings/PasswordUpdate.vue')
|
const UserSettingsPasswordUpdateComponent = () => import('@/views/user/settings/PasswordUpdate.vue')
|
||||||
const UserSettingsTOTPComponent = () => import('@/views/user/settings/TOTP.vue')
|
const UserSettingsTOTPComponent = () => import('@/views/user/settings/TOTP.vue')
|
||||||
|
const UserSettingsApiTokensComponent = () => import('@/views/user/settings/ApiTokens.vue')
|
||||||
|
|
||||||
// Project Handling
|
// Project Handling
|
||||||
const NewProjectComponent = () => import('@/views/project/NewProject.vue')
|
const NewProjectComponent = () => import('@/views/project/NewProject.vue')
|
||||||
|
@ -183,6 +184,11 @@ const router = createRouter({
|
||||||
name: 'user.settings.totp',
|
name: 'user.settings.totp',
|
||||||
component: UserSettingsTOTPComponent,
|
component: UserSettingsTOTPComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/user/settings/api-tokens',
|
||||||
|
name: 'user.settings.apiTokens',
|
||||||
|
component: UserSettingsApiTokensComponent,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -448,16 +454,9 @@ export async function getAuthForRoute(to: RouteLocation, authStore) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
// Check if the route the user wants to go to is a route which needs authentication. We use this to
|
||||||
// When trying this before the current user was fully loaded we might get a flash of the login screen
|
// redirect the user after successful login.
|
||||||
// in the user shell. To make shure this does not happen we check if everything is ready before trying.
|
const isValidUserAppRoute = ![
|
||||||
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.login',
|
||||||
'user.password-reset.request',
|
'user.password-reset.request',
|
||||||
'user.password-reset.reset',
|
'user.password-reset.reset',
|
||||||
|
@ -468,8 +467,19 @@ export async function getAuthForRoute(to: RouteLocation, authStore) {
|
||||||
localStorage.getItem('passwordResetToken') === null &&
|
localStorage.getItem('passwordResetToken') === null &&
|
||||||
localStorage.getItem('emailConfirmToken') === null &&
|
localStorage.getItem('emailConfirmToken') === null &&
|
||||||
!(to.name === 'home' && (typeof to.query.userPasswordReset !== 'undefined' || typeof to.query.userEmailConfirm !== 'undefined'))
|
!(to.name === 'home' && (typeof to.query.userPasswordReset !== 'undefined' || typeof to.query.userEmailConfirm !== 'undefined'))
|
||||||
) {
|
|
||||||
|
if (isValidUserAppRoute) {
|
||||||
saveLastVisited(to.name as string, to.params, to.query)
|
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'}
|
return {name: 'user.login'}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
36
src/services/apiToken.ts
Normal file
36
src/services/apiToken.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
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> {
|
export default class TaskService extends AbstractService<ITask> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
create: '/projects/{projectId}',
|
create: '/projects/{projectId}/tasks',
|
||||||
getAll: '/tasks/all',
|
getAll: '/tasks/all',
|
||||||
get: '/tasks/{id}',
|
get: '/tasks/{id}',
|
||||||
update: '/tasks/{id}',
|
update: '/tasks/{id}',
|
||||||
|
@ -81,12 +81,6 @@ export default class TaskService extends AbstractService<ITask> {
|
||||||
case 'weeks':
|
case 'weeks':
|
||||||
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_WEEK
|
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_WEEK
|
||||||
break
|
break
|
||||||
case 'months':
|
|
||||||
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_MONTH
|
|
||||||
break
|
|
||||||
case 'years':
|
|
||||||
repeatAfterSeconds = model.repeatAfter.amount * SECONDS_A_YEAR
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
model.repeatAfter = repeatAfterSeconds
|
model.repeatAfter = repeatAfterSeconds
|
||||||
|
|
|
@ -26,6 +26,7 @@ export interface ConfigState {
|
||||||
caldavEnabled: boolean,
|
caldavEnabled: boolean,
|
||||||
userDeletionEnabled: boolean,
|
userDeletionEnabled: boolean,
|
||||||
taskCommentsEnabled: boolean,
|
taskCommentsEnabled: boolean,
|
||||||
|
demoModeEnabled: boolean,
|
||||||
auth: {
|
auth: {
|
||||||
local: {
|
local: {
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
|
@ -58,6 +59,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||||
caldavEnabled: false,
|
caldavEnabled: false,
|
||||||
userDeletionEnabled: true,
|
userDeletionEnabled: true,
|
||||||
taskCommentsEnabled: true,
|
taskCommentsEnabled: true,
|
||||||
|
demoModeEnabled: false,
|
||||||
auth: {
|
auth: {
|
||||||
local: {
|
local: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {success} from '@/message'
|
||||||
import {useBaseStore} from '@/stores/base'
|
import {useBaseStore} from '@/stores/base'
|
||||||
import {getSavedFilterIdFromProjectId} from '@/services/savedFilter'
|
import {getSavedFilterIdFromProjectId} from '@/services/savedFilter'
|
||||||
|
|
||||||
const {remove, search, update} = createNewIndexer('projects', ['title', 'description'])
|
const {add, remove, search, update} = createNewIndexer('projects', ['title', 'description'])
|
||||||
|
|
||||||
export interface ProjectState {
|
export interface ProjectState {
|
||||||
[id: IProject['id']]: IProject
|
[id: IProject['id']]: IProject
|
||||||
|
@ -36,9 +36,11 @@ export const useProjectStore = defineStore('project', () => {
|
||||||
const projectsArray = computed(() => Object.values(projects.value)
|
const projectsArray = computed(() => Object.values(projects.value)
|
||||||
.sort((a, b) => a.position - b.position))
|
.sort((a, b) => a.position - b.position))
|
||||||
const notArchivedRootProjects = computed(() => projectsArray.value
|
const notArchivedRootProjects = computed(() => projectsArray.value
|
||||||
.filter(p => p.parentProjectId === 0 && !p.isArchived))
|
.filter(p => p.parentProjectId === 0 && !p.isArchived && p.id > 0))
|
||||||
const favoriteProjects = computed(() => projectsArray.value
|
const favoriteProjects = computed(() => projectsArray.value
|
||||||
.filter(p => !p.isArchived && p.isFavorite))
|
.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 hasProjects = computed(() => projectsArray.value.length > 0)
|
||||||
|
|
||||||
const getChildProjects = computed(() => {
|
const getChildProjects = computed(() => {
|
||||||
|
@ -172,6 +174,7 @@ export const useProjectStore = defineStore('project', () => {
|
||||||
const loadedProjects = await projectService.getAll({}, {is_archived: true}) as IProject[]
|
const loadedProjects = await projectService.getAll({}, {is_archived: true}) as IProject[]
|
||||||
projects.value = {}
|
projects.value = {}
|
||||||
setProjects(loadedProjects)
|
setProjects(loadedProjects)
|
||||||
|
loadedProjects.forEach(p => add(p))
|
||||||
|
|
||||||
return loadedProjects
|
return loadedProjects
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -198,6 +201,7 @@ export const useProjectStore = defineStore('project', () => {
|
||||||
notArchivedRootProjects: readonly(notArchivedRootProjects),
|
notArchivedRootProjects: readonly(notArchivedRootProjects),
|
||||||
favoriteProjects: readonly(favoriteProjects),
|
favoriteProjects: readonly(favoriteProjects),
|
||||||
hasProjects: readonly(hasProjects),
|
hasProjects: readonly(hasProjects),
|
||||||
|
savedFilterProjects: readonly(savedFilterProjects),
|
||||||
|
|
||||||
getChildProjects,
|
getChildProjects,
|
||||||
findProjectByExactname,
|
findProjectByExactname,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import TaskService from '@/services/task'
|
||||||
import TaskAssigneeService from '@/services/taskAssignee'
|
import TaskAssigneeService from '@/services/taskAssignee'
|
||||||
import LabelTaskService from '@/services/labelTask'
|
import LabelTaskService from '@/services/labelTask'
|
||||||
|
|
||||||
import {playPopSound} from '@/helpers/playPop'
|
|
||||||
import {cleanupItemText, parseTaskText, PREFIXES} from '@/modules/parseTaskText'
|
import {cleanupItemText, parseTaskText, PREFIXES} from '@/modules/parseTaskText'
|
||||||
|
|
||||||
import TaskAssigneeModel from '@/models/taskAssignee'
|
import TaskAssigneeModel from '@/models/taskAssignee'
|
||||||
|
@ -149,9 +148,6 @@ export const useTaskStore = defineStore('task', () => {
|
||||||
try {
|
try {
|
||||||
const updatedTask = await taskService.update(task)
|
const updatedTask = await taskService.update(task)
|
||||||
kanbanStore.setTaskInBucket(updatedTask)
|
kanbanStore.setTaskInBucket(updatedTask)
|
||||||
if (task.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
|
|
||||||
playPopSound()
|
|
||||||
}
|
|
||||||
return updatedTask
|
return updatedTask
|
||||||
} finally {
|
} finally {
|
||||||
cancel()
|
cancel()
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
:default-task-end-date="defaultTaskEndDate"
|
:default-task-end-date="defaultTaskEndDate"
|
||||||
@update:task="updateTask"
|
@update:task="updateTask"
|
||||||
/>
|
/>
|
||||||
<TaskForm v-if="canWrite" @create-task="addGanttTask" />
|
<TaskForm v-if="canWrite" @create-task="addGanttTask"/>
|
||||||
</card>
|
</card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -115,7 +115,7 @@ const flatPickerDateRange = computed<Date[]>({
|
||||||
]),
|
]),
|
||||||
set(newVal) {
|
set(newVal) {
|
||||||
const [dateFrom, dateTo] = newVal.map((date) => date?.toISOString())
|
const [dateFrom, dateTo] = newVal.map((date) => date?.toISOString())
|
||||||
|
|
||||||
// only set after whole range has been selected
|
// only set after whole range has been selected
|
||||||
if (!dateTo) return
|
if (!dateTo) return
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
>
|
>
|
||||||
<div class="bucket-header" @click="() => unCollapseBucket(bucket)">
|
<div class="bucket-header" @click="() => unCollapseBucket(bucket)">
|
||||||
<span
|
<span
|
||||||
v-if="bucket.isDoneBucket"
|
v-if="project.doneBucketId === bucket.id"
|
||||||
class="icon is-small has-text-success mr-2"
|
class="icon is-small has-text-success mr-2"
|
||||||
v-tooltip="$t('project.kanban.doneBucketHint')"
|
v-tooltip="$t('project.kanban.doneBucketHint')"
|
||||||
>
|
>
|
||||||
|
@ -97,26 +97,32 @@
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
@click.stop="toggleDoneBucket(bucket)"
|
@click.stop="toggleDoneBucket(bucket)"
|
||||||
v-tooltip="$t('project.kanban.doneBucketHintExtended')"
|
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') }}
|
{{ $t('project.kanban.doneBucket') }}
|
||||||
</dropdown-item>
|
</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
|
<dropdown-item
|
||||||
@click.stop="() => collapseBucket(bucket)"
|
@click.stop="() => collapseBucket(bucket)"
|
||||||
|
icon="angles-up"
|
||||||
>
|
>
|
||||||
{{ $t('project.kanban.collapse') }}
|
{{ $t('project.kanban.collapse') }}
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
<dropdown-item
|
<dropdown-item
|
||||||
:class="{'is-disabled': buckets.length <= 1}"
|
:class="{'is-disabled': buckets.length <= 1}"
|
||||||
@click.stop="() => deleteBucketModal(bucket.id)"
|
@click.stop="() => deleteBucketModal(bucket.id)"
|
||||||
class="has-text-danger"
|
|
||||||
v-tooltip="buckets.length <= 1 ? $t('project.kanban.deleteLast') : ''"
|
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') }}
|
{{ $t('misc.delete') }}
|
||||||
</dropdown-item>
|
</dropdown-item>
|
||||||
</dropdown>
|
</dropdown>
|
||||||
|
@ -251,6 +257,7 @@ import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
||||||
|
|
||||||
import {isSavedFilter} from '@/services/savedFilter'
|
import {isSavedFilter} from '@/services/savedFilter'
|
||||||
import {success} from '@/message'
|
import {success} from '@/message'
|
||||||
|
import {useProjectStore} from '@/stores/projects'
|
||||||
|
|
||||||
const DRAG_OPTIONS = {
|
const DRAG_OPTIONS = {
|
||||||
// sortable options
|
// sortable options
|
||||||
|
@ -268,6 +275,7 @@ const {t} = useI18n({useScope: 'global'})
|
||||||
const baseStore = useBaseStore()
|
const baseStore = useBaseStore()
|
||||||
const kanbanStore = useKanbanStore()
|
const kanbanStore = useKanbanStore()
|
||||||
const taskStore = useTaskStore()
|
const taskStore = useTaskStore()
|
||||||
|
const projectStore = useProjectStore()
|
||||||
|
|
||||||
const taskContainerRefs = ref<{[id: IBucket['id']]: HTMLElement}>({})
|
const taskContainerRefs = ref<{[id: IBucket['id']]: HTMLElement}>({})
|
||||||
|
|
||||||
|
@ -422,10 +430,9 @@ async function updateTaskPosition(e) {
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
|
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 = newBucket.isDoneBucket
|
newTask.done = project.value.doneBucketId === newBucket.id
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
|
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
|
||||||
|
@ -596,10 +603,26 @@ function dragstart(bucket: IBucket) {
|
||||||
sourceBucket.value = bucket.id
|
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) {
|
async function toggleDoneBucket(bucket: IBucket) {
|
||||||
await kanbanStore.updateBucket({
|
const doneBucketId = project.value.doneBucketId === bucket.id
|
||||||
...bucket,
|
? 0
|
||||||
isDoneBucket: !bucket.isDoneBucket,
|
: bucket.id
|
||||||
|
|
||||||
|
await projectStore.updateProject({
|
||||||
|
...project.value,
|
||||||
|
doneBucketId,
|
||||||
})
|
})
|
||||||
success({message: t('project.kanban.doneBucketSavedSuccess')})
|
success({message: t('project.kanban.doneBucketSavedSuccess')})
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
|
|
||||||
<nothing v-if="ctaVisible && tasks.length === 0 && !loading">
|
<nothing v-if="ctaVisible && tasks.length === 0 && !loading">
|
||||||
{{ $t('project.list.empty') }}
|
{{ $t('project.list.empty') }}
|
||||||
<ButtonLink @click="focusNewTaskInput()">
|
<ButtonLink @click="focusNewTaskInput()" v-if="project.id > 0">
|
||||||
{{ $t('project.list.newTaskCta') }}
|
{{ $t('project.list.newTaskCta') }}
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
</nothing>
|
</nothing>
|
||||||
|
|
|
@ -143,13 +143,12 @@
|
||||||
<labels :labels="t.labels"/>
|
<labels :labels="t.labels"/>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="activeColumns.assignees">
|
<td v-if="activeColumns.assignees">
|
||||||
<user
|
<assignee-list
|
||||||
:avatar-size="27"
|
v-if="t.assignees.length > 0"
|
||||||
:is-inline="true"
|
:assignees="t.assignees"
|
||||||
:key="t.id + 'assignee' + a.id + i"
|
:avatar-size="28"
|
||||||
:show-username="false"
|
class="ml-1"
|
||||||
:user="a"
|
:inline="true"
|
||||||
v-for="(a, i) in t.assignees"
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<date-table-cell :date="t.dueDate" v-if="activeColumns.dueDate"/>
|
<date-table-cell :date="t.dueDate" v-if="activeColumns.dueDate"/>
|
||||||
|
@ -201,6 +200,7 @@ import {useTaskList} from '@/composables/useTaskList'
|
||||||
import type {SortBy} from '@/composables/useTaskList'
|
import type {SortBy} from '@/composables/useTaskList'
|
||||||
import type {ITask} from '@/modelTypes/ITask'
|
import type {ITask} from '@/modelTypes/ITask'
|
||||||
import type {IProject} from '@/modelTypes/IProject'
|
import type {IProject} from '@/modelTypes/IProject'
|
||||||
|
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
|
||||||
|
|
||||||
const ACTIVE_COLUMNS_DEFAULT = {
|
const ACTIVE_COLUMNS_DEFAULT = {
|
||||||
index: true,
|
index: true,
|
||||||
|
|
|
@ -378,7 +378,7 @@
|
||||||
{{ $t('task.detail.actions.attachments') }}
|
{{ $t('task.detail.actions.attachments') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button
|
<x-button
|
||||||
@click="setFieldActive('relatedTasks')"
|
@click="setRelatedTasksActive()"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon="sitemap"
|
icon="sitemap"
|
||||||
v-shortcut="'r'"
|
v-shortcut="'r'"
|
||||||
|
@ -447,7 +447,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {ref, reactive, toRef, shallowReactive, computed, watch, watchEffect, nextTick} from 'vue'
|
import {ref, reactive, toRef, shallowReactive, computed, watch, nextTick} from 'vue'
|
||||||
import {useRouter, type RouteLocation} from 'vue-router'
|
import {useRouter, type RouteLocation} from 'vue-router'
|
||||||
import {useI18n} from 'vue-i18n'
|
import {useI18n} from 'vue-i18n'
|
||||||
import {unrefElement} from '@vueuse/core'
|
import {unrefElement} from '@vueuse/core'
|
||||||
|
@ -488,7 +488,6 @@ import {uploadFile} from '@/helpers/attachments'
|
||||||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||||
import {scrollIntoView} from '@/helpers/scrollIntoView'
|
import {scrollIntoView} from '@/helpers/scrollIntoView'
|
||||||
|
|
||||||
import {useBaseStore} from '@/stores/base'
|
|
||||||
import {useAttachmentStore} from '@/stores/attachments'
|
import {useAttachmentStore} from '@/stores/attachments'
|
||||||
import {useTaskStore} from '@/stores/tasks'
|
import {useTaskStore} from '@/stores/tasks'
|
||||||
import {useKanbanStore} from '@/stores/kanban'
|
import {useKanbanStore} from '@/stores/kanban'
|
||||||
|
@ -499,6 +498,8 @@ import {success} from '@/message'
|
||||||
import type {Action as MessageAction} from '@/message'
|
import type {Action as MessageAction} from '@/message'
|
||||||
import {useProjectStore} from '@/stores/projects'
|
import {useProjectStore} from '@/stores/projects'
|
||||||
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
|
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
|
||||||
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
import {playPopSound} from '@/helpers/playPop'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
taskId,
|
taskId,
|
||||||
|
@ -513,7 +514,6 @@ defineEmits(['close'])
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const {t} = useI18n({useScope: 'global'})
|
const {t} = useI18n({useScope: 'global'})
|
||||||
|
|
||||||
const baseStore = useBaseStore()
|
|
||||||
const projectStore = useProjectStore()
|
const projectStore = useProjectStore()
|
||||||
const attachmentStore = useAttachmentStore()
|
const attachmentStore = useAttachmentStore()
|
||||||
const taskStore = useTaskStore()
|
const taskStore = useTaskStore()
|
||||||
|
@ -534,17 +534,6 @@ const taskColor = ref<ITask['hexColor']>('')
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
|
|
||||||
const project = computed(() => projectStore.projects[task.value.projectId])
|
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(() => (
|
const canWrite = computed(() => (
|
||||||
task.value.maxRight !== null &&
|
task.value.maxRight !== null &&
|
||||||
|
@ -735,6 +724,10 @@ function toggleTaskDone() {
|
||||||
done: !task.value.done,
|
done: !task.value.done,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newTask.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
|
||||||
|
playPopSound()
|
||||||
|
}
|
||||||
|
|
||||||
saveTask(
|
saveTask(
|
||||||
newTask,
|
newTask,
|
||||||
toggleTaskDone,
|
toggleTaskDone,
|
||||||
|
@ -777,6 +770,19 @@ async function removeRepeatAfter() {
|
||||||
task.value.repeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
|
task.value.repeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
|
||||||
await saveTask()
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -58,6 +58,17 @@
|
||||||
>
|
>
|
||||||
{{ $t('user.auth.createAccount') }}
|
{{ $t('user.auth.createAccount') }}
|
||||||
</x-button>
|
</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">
|
<p class="mt-2">
|
||||||
{{ $t('user.auth.alreadyHaveAnAccount') }}
|
{{ $t('user.auth.alreadyHaveAnAccount') }}
|
||||||
<router-link :to="{ name: 'user.login' }">
|
<router-link :to="{ name: 'user.login' }">
|
||||||
|
@ -78,8 +89,10 @@ import {isEmail} from '@/helpers/isEmail'
|
||||||
import Password from '@/components/input/password.vue'
|
import Password from '@/components/input/password.vue'
|
||||||
|
|
||||||
import {useAuthStore} from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
import {useConfigStore} from '@/stores/config'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
// FIXME: use the `beforeEnter` hook of vue-router
|
// FIXME: use the `beforeEnter` hook of vue-router
|
||||||
// Check if the user is already logged in, if so, redirect them to the homepage
|
// Check if the user is already logged in, if so, redirect them to the homepage
|
||||||
|
|
|
@ -75,6 +75,10 @@ const navigationItems = computed(() => {
|
||||||
routeName: 'user.settings.caldav',
|
routeName: 'user.settings.caldav',
|
||||||
condition: caldavEnabled.value,
|
condition: caldavEnabled.value,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t('user.settings.apiTokens.title'),
|
||||||
|
routeName: 'user.settings.apiTokens',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('user.deletion.title'),
|
title: t('user.deletion.title'),
|
||||||
routeName: 'user.settings.deletion',
|
routeName: 'user.settings.deletion',
|
||||||
|
|
262
src/views/user/settings/ApiTokens.vue
Normal file
262
src/views/user/settings/ApiTokens.vue
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
<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