Compare commits
171 Commits
da15e410d8
...
507917dd19
Author | SHA1 | Date |
---|---|---|
renovate | 507917dd19 | |
kolaente | 25742385ba | |
renovate | 2fa576d9f5 | |
Frederick [Bot] | 116b909d31 | |
konrad | e95159a33c | |
renovate | b340bb418b | |
waza-ari | 01fb80d7a1 | |
Daniel Herrmann | 6e52db76dc | |
Hangya | c5e8ff66fb | |
kolaente | 009e9b5455 | |
Frederick [Bot] | d963667a29 | |
kolaente | 654e95d99f | |
kolaente | d628471d0e | |
kolaente | 4e6e0608c7 | |
kolaente | 6fea5640e8 | |
kolaente | b874b02412 | |
kolaente | 084a62e835 | |
kolaente | f3e2b1b89b | |
kolaente | 4e26fa0b85 | |
kolaente | 32e1a2018a | |
kolaente | 05d3bb4fb6 | |
kolaente | 38985a8318 | |
kolaente | d0b762d761 | |
kolaente | e0a7f46e5d | |
kolaente | be253333c2 | |
kolaente | 533e778b93 | |
kolaente | 1d2f3ca546 | |
kolaente | 55b806d311 | |
kolaente | 0c947790e8 | |
kolaente | b35eb4adbf | |
kolaente | a22652b737 | |
kolaente | 4dcd3abe9e | |
kolaente | 5a13c2b423 | |
kolaente | 9eac746984 | |
kolaente | b1d9dc6fc3 | |
kolaente | 8fa2f6686a | |
kolaente | 9ade917ac4 | |
kolaente | 7fc1f27ef5 | |
kolaente | 356399f853 | |
kolaente | 9ed93b181d | |
kolaente | 981f2d0e70 | |
kolaente | 2990c01d0a | |
kolaente | 2daecbc2bc | |
kolaente | 35487093c6 | |
kolaente | 571bcf8996 | |
kolaente | 388a3a68ba | |
kolaente | 992d108bfa | |
kolaente | c22daab28c | |
kolaente | 3bd639a110 | |
kolaente | 0d12d72b73 | |
kolaente | 1827102a0a | |
kolaente | 4586e525ce | |
kolaente | c162a5a457 | |
kolaente | b978d344ca | |
kolaente | 28fa2c517a | |
kolaente | bc6d812eb0 | |
kolaente | 87c027aafd | |
kolaente | 65e1357705 | |
kolaente | eebfee73d3 | |
kolaente | ef1cc9720c | |
kolaente | c6b682507a | |
kolaente | 9d3fb6f81d | |
kolaente | 3ea81db836 | |
kolaente | 76ed2cff5f | |
kolaente | e43349618b | |
kolaente | 9624cc9e97 | |
kolaente | 764bc15d49 | |
kolaente | 3fc4aaa2a1 | |
kolaente | 9f73e2c5f9 | |
kolaente | c1e137d8ee | |
kolaente | de320aac72 | |
kolaente | 307ffe11c4 | |
Christoph Ritzer | 86983f50d4 | |
kolaente | e65c3ffe6b | |
renovate | 9ad7bc5932 | |
kolaente | 2101f37aa2 | |
renovate | e8d77e61e4 | |
renovate | 7e69c7cbe0 | |
Frederick [Bot] | f6204c307e | |
kolaente | 28d72d4d47 | |
renovate | 2c5f8126c5 | |
renovate | 9aea5830f3 | |
kolaente | 14452428a2 | |
renovate | b5682ecc18 | |
renovate | 0bec7ee1ab | |
renovate | d1a3eb9701 | |
renovate | cba09c8eb1 | |
kolaente | dc291a51f5 | |
renovate | e5e66d73f5 | |
waza-ari | d69fc28125 | |
renovate | 4793282baf | |
renovate | 43b0689c1a | |
renovate | 48d1937bd4 | |
renovate | 3e5d55d9e8 | |
waza-ari | a3154e805c | |
renovate | 2414b580c1 | |
renovate | 87ac9e261f | |
renovate | 63e9f8f682 | |
renovate | 5a9de579cc | |
renovate | 42e660c3d6 | |
renovate | 6533e75496 | |
renovate | f8c5e314df | |
renovate | 429b140cad | |
renovate | dc7ee851ec | |
waza-ari | 92d9c31101 | |
kolaente | ac8751e1be | |
kolaente | f5b90517c4 | |
kolaente | fe27dd59ad | |
kolaente | 22933dac4a | |
kolaente | fe02f4da2c | |
Frederick [Bot] | 4bb09b69be | |
kolaente | 379b0b24b3 | |
kolaente | 6db8728420 | |
kolaente | a34ca20c1a | |
kolaente | a4a0ea973a | |
kolaente | fc4303a778 | |
kolaente | 4f1f96f1e9 | |
kolaente | 10ff864e0c | |
kolaente | 89b01e86bc | |
kolaente | a3932a0a19 | |
kolaente | e42a605597 | |
kolaente | 67f55510bf | |
kolaente | 178cd8c392 | |
viehlieb | ed4da96ab1 | |
kolaente | f18cde269b | |
kolaente | 09d446765d | |
renovate | a99e7f9aa3 | |
renovate | e93e48c4c2 | |
renovate | 06688fa8d4 | |
renovate | 1a15c865e2 | |
renovate | 7b9a72ebe1 | |
renovate | a0f4bdbdad | |
renovate | eea356e570 | |
kolaente | 5b70609ba7 | |
kolaente | 6b1e67485b | |
kolaente | 5d127c2897 | |
kolaente | 1275dfc260 | |
kolaente | 997fb6bc54 | |
kolaente | b2e5de88ff | |
kolaente | baa5d14ca6 | |
kolaente | 2d5c496397 | |
kolaente | b8533d2bfc | |
kolaente | 8a82093233 | |
kolaente | 3d39fc3960 | |
kolaente | 43e13d9cdd | |
kolaente | a0e812395f | |
kolaente | 2e5c19352e | |
kolaente | 1ffb93b63c | |
renovate | 1bf8659423 | |
renovate | 7629c8426e | |
renovate | a83acc0300 | |
kolaente | 837360f122 | |
renovate | 34c31e2f03 | |
renovate | 829f504d3b | |
renovate | 899bc67853 | |
renovate | b419df3156 | |
renovate | 52d0930034 | |
renovate | 318f00d252 | |
kolaente | 4d11dd0383 | |
kolaente | e40a0043d4 | |
kolaente | e532979101 | |
renovate | 503036abff | |
renovate | 70265e176a | |
renovate | 68873e1d0d | |
renovate | be66ec8608 | |
renovate | 814c142b71 | |
renovate | ea75657d45 | |
kolaente | ff1730e323 | |
kolaente | f120d72211 | |
renovate | a2951570a7 | |
kolaente | f4efdaa5de |
|
@ -1,6 +1,7 @@
|
|||
files/
|
||||
dist/
|
||||
logs/
|
||||
docs/
|
||||
|
||||
Dockerfile
|
||||
docker-manifest.tmpl
|
||||
|
|
142
.drone.yml
142
.drone.yml
|
@ -1,7 +1,12 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-and-test-api
|
||||
name: build-and-test
|
||||
|
||||
trigger:
|
||||
event:
|
||||
exclude:
|
||||
- cron
|
||||
|
||||
workspace:
|
||||
base: /go
|
||||
|
@ -122,7 +127,7 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: build
|
||||
- name: api-build
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -133,7 +138,7 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: lint
|
||||
- name: api-lint
|
||||
image: golangci/golangci-lint:v1.55.2
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -156,7 +161,9 @@ steps:
|
|||
- name: test-migration-sqlite
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
depends_on: [ test-migration-prepare, build ]
|
||||
depends_on:
|
||||
- test-migration-prepare
|
||||
- api-build
|
||||
environment:
|
||||
VIKUNJA_DATABASE_TYPE: sqlite
|
||||
VIKUNJA_DATABASE_PATH: /db/vikunja-migration-test.db
|
||||
|
@ -175,7 +182,9 @@ steps:
|
|||
- name: test-migration-mysql
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
depends_on: [ test-migration-prepare, build ]
|
||||
depends_on:
|
||||
- test-migration-prepare
|
||||
- api-build
|
||||
environment:
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_HOST: test-mysql-migration
|
||||
|
@ -194,7 +203,9 @@ steps:
|
|||
- name: test-migration-psql
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
depends_on: [ test-migration-prepare, build ]
|
||||
depends_on:
|
||||
- test-migration-prepare
|
||||
- api-build
|
||||
environment:
|
||||
VIKUNJA_DATABASE_TYPE: postgres
|
||||
VIKUNJA_DATABASE_HOST: test-postgres-migration
|
||||
|
@ -211,7 +222,7 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: test
|
||||
- name: api-test-unit
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -222,7 +233,7 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: test-sqlite
|
||||
- name: api-test-unit-sqlite
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -239,7 +250,7 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: test-mysql
|
||||
- name: api-test-unit-mysql
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -256,7 +267,7 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: test-postgres
|
||||
- name: api-test-unit-postgres
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
environment:
|
||||
|
@ -337,32 +348,23 @@ steps:
|
|||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-and-test-frontend
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
include:
|
||||
- main
|
||||
event:
|
||||
include:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
services:
|
||||
- name: api
|
||||
image: vikunja/vikunja:unstable
|
||||
- name: test-api-run
|
||||
image: vikunja/golang-build:latest
|
||||
pull: always
|
||||
environment:
|
||||
VIKUNJA_SERVICE_TESTINGTOKEN: averyLongSecretToSe33dtheDB
|
||||
VIKUNJA_LOG_LEVEL: DEBUG
|
||||
VIKUNJA_CORS_ENABLE: 1
|
||||
VIKUNJA_DATABASE_PATH: memory
|
||||
VIKUNJA_DATABASE_TYPE: sqlite
|
||||
commands:
|
||||
- ./vikunja
|
||||
detach: true
|
||||
depends_on:
|
||||
- api-build
|
||||
|
||||
steps:
|
||||
- name: dependencies
|
||||
image: node:20.11.0-alpine
|
||||
- name: frontend-dependencies
|
||||
image: node:20.11.1-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -375,8 +377,8 @@ steps:
|
|||
# depends_on:
|
||||
# - restore-cache
|
||||
|
||||
- name: lint
|
||||
image: node:20.11.0-alpine
|
||||
- name: frontend-lint
|
||||
image: node:20.11.1-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -385,33 +387,33 @@ steps:
|
|||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm run lint
|
||||
depends_on:
|
||||
- dependencies
|
||||
- frontend-dependencies
|
||||
|
||||
- name: build-prod
|
||||
image: node:20.11.0-alpine
|
||||
- name: frontend-build-prod
|
||||
image: node:20.11.1-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
commands:
|
||||
- cd frontend
|
||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm run build
|
||||
- pnpm run build:test
|
||||
depends_on:
|
||||
- dependencies
|
||||
- frontend-dependencies
|
||||
|
||||
- name: test-unit
|
||||
image: node:20.11.0-alpine
|
||||
- name: frontend-test-unit
|
||||
image: node:20.11.1-alpine
|
||||
pull: always
|
||||
commands:
|
||||
- cd frontend
|
||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm run test:unit
|
||||
depends_on:
|
||||
- dependencies
|
||||
- frontend-dependencies
|
||||
|
||||
- name: typecheck
|
||||
- name: frontend-typecheck
|
||||
failure: ignore
|
||||
image: node:20.11.0-alpine
|
||||
image: node:20.11.1-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -420,13 +422,13 @@ steps:
|
|||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm run typecheck
|
||||
depends_on:
|
||||
- dependencies
|
||||
- frontend-dependencies
|
||||
|
||||
- name: test-frontend
|
||||
- name: frontend-test
|
||||
image: cypress/browsers:node18.12.0-chrome107
|
||||
pull: always
|
||||
environment:
|
||||
CYPRESS_API_URL: http://api:3456/api/v1
|
||||
CYPRESS_API_URL: http://test-api-run:3456/api/v1
|
||||
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
CYPRESS_CACHE_FOLDER: .cache/cypress
|
||||
|
@ -435,14 +437,15 @@ steps:
|
|||
from_secret: cypress_project_key
|
||||
commands:
|
||||
- cd frontend
|
||||
- sed -i 's/localhost/api/g' dist/index.html
|
||||
- sed -i 's/localhost/test-api-run/g' dist-test/index.html
|
||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm cypress install
|
||||
- pnpm run test:e2e-record
|
||||
- pnpm run test:e2e-record-test
|
||||
depends_on:
|
||||
- build-prod
|
||||
- frontend-build-prod
|
||||
- test-api-run
|
||||
|
||||
- name: deploy-preview
|
||||
- name: frontend-deploy-preview
|
||||
image: williamjackson/netlify-cli
|
||||
pull: always
|
||||
user: root # The rest runs as root and thus the permissions wouldn't work
|
||||
|
@ -455,7 +458,7 @@ steps:
|
|||
from_secret: gitea_token
|
||||
commands:
|
||||
- cd frontend
|
||||
- cp -r dist dist-preview
|
||||
- cp -r dist-test dist-preview
|
||||
# Override the default api url used for preview
|
||||
- sed -i 's|http://localhost:3456|https://try.vikunja.io|g' dist-preview/index.html
|
||||
- apk add --no-cache perl-utils
|
||||
|
@ -464,7 +467,7 @@ steps:
|
|||
- shasum -a 384 -c ./scripts/deploy-preview-netlify.mjs.sha384
|
||||
- node ./scripts/deploy-preview-netlify.mjs
|
||||
depends_on:
|
||||
- build-prod
|
||||
- frontend-build-prod
|
||||
when:
|
||||
event:
|
||||
include:
|
||||
|
@ -476,7 +479,7 @@ type: docker
|
|||
name: generate-swagger-docs
|
||||
|
||||
depends_on:
|
||||
- build-and-test-api
|
||||
- build-and-test
|
||||
|
||||
workspace:
|
||||
base: /go
|
||||
|
@ -520,8 +523,7 @@ type: docker
|
|||
name: release
|
||||
|
||||
depends_on:
|
||||
- build-and-test-api
|
||||
- build-and-test-frontend
|
||||
- build-and-test
|
||||
|
||||
workspace:
|
||||
base: /source
|
||||
|
@ -531,6 +533,9 @@ trigger:
|
|||
ref:
|
||||
- refs/heads/main
|
||||
- "refs/tags/**"
|
||||
event:
|
||||
exclude:
|
||||
- cron
|
||||
|
||||
steps:
|
||||
# Needed to get the versions right as they depend on tags
|
||||
|
@ -540,7 +545,7 @@ steps:
|
|||
- git fetch --tags
|
||||
|
||||
- name: frontend-dependencies
|
||||
image: node:20.11.0-alpine
|
||||
image: node:20.11.1-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -552,7 +557,7 @@ steps:
|
|||
- pnpm install --fetch-timeout 100000
|
||||
|
||||
- name: frontend-build
|
||||
image: node:20.11.0-alpine
|
||||
image: node:20.11.1-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -805,13 +810,15 @@ type: docker
|
|||
name: docker-release
|
||||
|
||||
depends_on:
|
||||
- build-and-test-api
|
||||
- build-and-test-frontend
|
||||
- build-and-test
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
- refs/heads/main
|
||||
- "refs/tags/**"
|
||||
event:
|
||||
exclude:
|
||||
- cron
|
||||
|
||||
steps:
|
||||
- name: fetch-tags
|
||||
|
@ -880,7 +887,7 @@ type: docker
|
|||
name: frontend-release-unstable
|
||||
|
||||
depends_on:
|
||||
- build-and-test-frontend
|
||||
- build-and-test
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
|
@ -895,7 +902,7 @@ steps:
|
|||
- git fetch --tags
|
||||
|
||||
- name: build
|
||||
image: node:20.11.0-alpine
|
||||
image: node:20.11.1-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -943,7 +950,7 @@ type: docker
|
|||
name: frontend-release-version
|
||||
|
||||
depends_on:
|
||||
- build-and-test-frontend
|
||||
- build-and-test
|
||||
|
||||
trigger:
|
||||
event:
|
||||
|
@ -956,7 +963,7 @@ steps:
|
|||
- git fetch --tags
|
||||
|
||||
- name: build
|
||||
image: node:20.11.0-alpine
|
||||
image: node:20.11.1-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -1149,6 +1156,9 @@ trigger:
|
|||
ref:
|
||||
- refs/heads/main
|
||||
- "refs/tags/**"
|
||||
event:
|
||||
exclude:
|
||||
- cron
|
||||
|
||||
steps:
|
||||
- name: fetch-tags
|
||||
|
@ -1364,10 +1374,12 @@ trigger:
|
|||
ref:
|
||||
- refs/heads/main
|
||||
- "refs/tags/**"
|
||||
event:
|
||||
exclude:
|
||||
- cron
|
||||
|
||||
depends_on:
|
||||
- build-and-test-api
|
||||
- build-and-test-frontend
|
||||
- build-and-test
|
||||
- release
|
||||
- deploy-docs
|
||||
- docker-release
|
||||
|
@ -1389,6 +1401,6 @@ steps:
|
|||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: aa9bd51fc7d73686ee169060dcb4d6540214825a0d5134035f477a97f77dd24d
|
||||
hmac: bd616ecf66fe95bd25c5f4bd73fc9ccfc20601a87a4f8dd4574d66393eacd077
|
||||
|
||||
...
|
||||
|
|
19
Dockerfile
19
Dockerfile
|
@ -1,5 +1,5 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
FROM --platform=$BUILDPLATFORM node:20.11.0-alpine AS frontendbuilder
|
||||
FROM --platform=$BUILDPLATFORM node:20.11.1-alpine AS frontendbuilder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
|
@ -33,22 +33,15 @@ RUN export PATH=$PATH:$GOPATH/bin && \
|
|||
# ┘└┘┘─┘┘└┘┘└┘┴─┘┘└┘
|
||||
|
||||
# The actual image
|
||||
# Note: I wanted to use the scratch image here, but unfortunatly the go-sqlite bindings require cgo and
|
||||
# because of this, the container would not start when I compiled the image without cgo.
|
||||
FROM alpine:3.19 AS runner
|
||||
FROM scratch
|
||||
LABEL maintainer="maintainers@vikunja.io"
|
||||
WORKDIR /app/vikunja
|
||||
ENTRYPOINT [ "/sbin/tini", "-g", "--", "/entrypoint.sh" ]
|
||||
ENTRYPOINT [ "/app/vikunja/vikunja" ]
|
||||
EXPOSE 3456
|
||||
USER 1000
|
||||
|
||||
ENV VIKUNJA_SERVICE_ROOTPATH=/app/vikunja/
|
||||
ENV PUID 1000
|
||||
ENV PGID 1000
|
||||
|
||||
RUN apk --update --no-cache add tzdata tini shadow && \
|
||||
addgroup vikunja && \
|
||||
adduser -s /bin/sh -D -G vikunja vikunja -h /app/vikunja -H
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod 0755 /entrypoint.sh && mkdir files
|
||||
ENV VIKUNJA_DATABASE_PATH=/db/vikunja.db
|
||||
|
||||
COPY --from=apibuilder /build/vikunja-* vikunja
|
||||
COPY --from=apibuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
|
|
@ -11,6 +11,9 @@
|
|||
|
||||
> The Todo-app to organize your life.
|
||||
|
||||
If Vikunja is useful to you, please consider [buying me a coffee](https://www.buymeacoffee.com/kolaente), [sponsoring me on GitHub](https://github.com/sponsors/kolaente) or buying [a sticker pack](https://vikunja.cloud/stickers).
|
||||
I'm also offering [a hosted version of Vikunja](https://vikunja.cloud/) if you want a hassle-free solution for yourself or your team.
|
||||
|
||||
# Table of contents
|
||||
|
||||
* [Security Reports](#security-reports)
|
||||
|
|
|
@ -6,7 +6,7 @@ service:
|
|||
# The duration of the issued JWT tokens in seconds.
|
||||
# The default is 259200 seconds (3 Days).
|
||||
jwtttl: 259200
|
||||
# The duration of the "remember me" time in seconds. When the login request is made with
|
||||
# The duration of the "remember me" time in seconds. When the login request is made with
|
||||
# the long param set, the token returned will be valid for this period.
|
||||
# The default is 2592000 seconds (30 Days).
|
||||
jwtttllong: 2592000
|
||||
|
@ -48,7 +48,7 @@ service:
|
|||
# If enabled, vikunja will send an email to everyone who is either assigned to a task or created it when a task reminder
|
||||
# is due.
|
||||
enableemailreminders: true
|
||||
# If true, will allow users to request the complete deletion of their account. When using external authentication methods
|
||||
# If true, will allow users to request the complete deletion of their account. When using external authentication methods
|
||||
# it may be required to coordinate with them in order to delete the account. This setting will not affect the cli commands
|
||||
# for user deletion.
|
||||
enableuserdeletion: true
|
||||
|
@ -76,7 +76,7 @@ sentry:
|
|||
frontenddsn: "https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480"
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql, postgres and sqlite.
|
||||
# Database type to use. Supported values are mysql, postgres and sqlite. Vikunja is able to run with MySQL 8.0+, Mariadb 10.2+, PostgreSQL 12+, and sqlite.
|
||||
type: "sqlite"
|
||||
# Database user which is used to connect to the database.
|
||||
user: "vikunja"
|
||||
|
@ -109,7 +109,7 @@ database:
|
|||
typesense:
|
||||
# Whether to enable the Typesense integration. If true, all tasks will be synced to the configured Typesense
|
||||
# instance and all search and filtering will run through Typesense instead of only through the database.
|
||||
# Typesense allows fast fulltext search including fuzzy matching support. It may return different results than
|
||||
# Typesense allows fast fulltext search including fuzzy matching support. It may return different results than
|
||||
# what you'd get with a database-only search.
|
||||
enabled: false
|
||||
# The url to the Typesense instance you want to use. Can be hosted locally or in Typesense Cloud as long
|
||||
|
@ -203,7 +203,7 @@ ratelimit:
|
|||
# Possible values are "keyvalue", "memory" or "redis".
|
||||
# When choosing "keyvalue" this setting follows the one configured in the "keyvalue" section.
|
||||
store: keyvalue
|
||||
# The number of requests a user can make from the same IP to all unauthenticated routes (login, register,
|
||||
# The number of requests a user can make from the same IP to all unauthenticated routes (login, register,
|
||||
# password confirmation, email verification, password reset request) per minute. This limit cannot be disabled.
|
||||
# You should only change this if you know what you're doing.
|
||||
noauthlimit: 10
|
||||
|
@ -301,13 +301,11 @@ auth:
|
|||
enabled: true
|
||||
# OpenID configuration will allow users to authenticate through a third-party OpenID Connect compatible provider.<br/>
|
||||
# The provider needs to support the `openid`, `profile` and `email` scopes.<br/>
|
||||
# **Note:** Some openid providers (like gitlab) only make the email of the user available through openid claims if they have set it to be publicly visible.
|
||||
# **Note:** Some openid providers (like Gitlab) only make the email of the user available through OpenID if they have set it to be publicly visible.
|
||||
# If the email is not public in those cases, authenticating will fail.
|
||||
# **Note 2:** The frontend expects to be redirected after authentication by the third party
|
||||
# to <frontend-url>/auth/openid/<auth key>. Please make sure to configure the redirect url in your third party
|
||||
# auth service accordingly if you're using the default vikunja frontend.
|
||||
# The frontend will automatically provide the api with the redirect url, composed from the current url where it's hosted.
|
||||
# If you want to use the desktop client with openid, make sure to allow redirects to `127.0.0.1`.
|
||||
# **Note 2:** The frontend expects the third party to rediect the user <frontend-url>/auth/openid/<auth key> after authentication. Please make sure to configure the redirect url in your third party auth service accordingly if you're using the default vikunja frontend.
|
||||
# The frontend will automatically provide the API with the redirect url, composed from the current url where it's hosted.
|
||||
# If you want to use the desktop client with OpenID, make sure to allow redirects to `127.0.0.1`.
|
||||
# Take a look at the [default config file](https://kolaente.dev/vikunja/vikunja/src/branch/main/config.yml.sample) for more information about how to configure openid authentication.
|
||||
openid:
|
||||
# Enable or disable OpenID Connect authentication
|
||||
|
@ -325,6 +323,10 @@ auth:
|
|||
clientid:
|
||||
# The client secret used to authenticate Vikunja at the OpenID Connect provider.
|
||||
clientsecret:
|
||||
# The scope necessary to use oidc.
|
||||
# If you want to use the Feature to create and assign to vikunja teams via oidc, you have to add the custom "vikunja_scope" and check [openid.md](https://vikunja.io/docs/openid/).
|
||||
# e.g. scope: openid email profile vikunja_scope
|
||||
scope: openid email profile
|
||||
|
||||
# Prometheus metrics endpoint
|
||||
metrics:
|
||||
|
|
|
@ -51,11 +51,11 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "28.2.2",
|
||||
"electron-builder": "24.9.1"
|
||||
"electron": "29.1.1",
|
||||
"electron-builder": "24.13.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"express": "4.18.2"
|
||||
"express": "4.18.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,10 @@
|
|||
optionalDependencies:
|
||||
global-agent "^3.0.0"
|
||||
|
||||
"@electron/notarize@2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@electron/notarize/-/notarize-2.1.0.tgz#76aaec10c8687225e8d0a427cc9df67611c46ff3"
|
||||
integrity sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA==
|
||||
"@electron/notarize@2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@electron/notarize/-/notarize-2.2.1.tgz#d0aa6bc43cba830c41bfd840b85dbe0e273f59fe"
|
||||
integrity sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
fs-extra "^9.0.1"
|
||||
|
@ -61,10 +61,10 @@
|
|||
minimist "^1.2.6"
|
||||
plist "^3.0.5"
|
||||
|
||||
"@electron/universal@1.4.1":
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-1.4.1.tgz#3fbda2a5ed9ff9f3304c8e8316b94c1e3a7b3785"
|
||||
integrity sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ==
|
||||
"@electron/universal@1.5.1":
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-1.5.1.tgz#f338bc5bcefef88573cf0ab1d5920fac10d06ee5"
|
||||
integrity sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==
|
||||
dependencies:
|
||||
"@electron/asar" "^3.2.1"
|
||||
"@malept/cross-spawn-promise" "^1.1.0"
|
||||
|
@ -166,10 +166,12 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806"
|
||||
integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw==
|
||||
|
||||
"@types/node@^18.11.18":
|
||||
version "18.15.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f"
|
||||
integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==
|
||||
"@types/node@^20.9.0":
|
||||
version "20.11.19"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.19.tgz#b466de054e9cb5b3831bee38938de64ac7f81195"
|
||||
integrity sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/plist@^3.0.1":
|
||||
version "3.0.2"
|
||||
|
@ -251,26 +253,25 @@ app-builder-bin@4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-4.0.0.tgz#1df8e654bd1395e4a319d82545c98667d7eed2f0"
|
||||
integrity sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==
|
||||
|
||||
app-builder-lib@24.9.1:
|
||||
version "24.9.1"
|
||||
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-24.9.1.tgz#bf3568529298b4de8595ed1acbb351fe27db5ba4"
|
||||
integrity sha512-Q1nYxZcio4r+W72cnIRVYofEAyjBd3mG47o+zms8HlD51zWtA/YxJb01Jei5F+jkWhge/PTQK+uldsPh6d0/4g==
|
||||
app-builder-lib@24.13.3:
|
||||
version "24.13.3"
|
||||
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-24.13.3.tgz#36e47b65fecb8780bb73bff0fee4e0480c28274b"
|
||||
integrity sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==
|
||||
dependencies:
|
||||
"7zip-bin" "~5.2.0"
|
||||
"@develar/schema-utils" "~2.6.5"
|
||||
"@electron/notarize" "2.1.0"
|
||||
"@electron/notarize" "2.2.1"
|
||||
"@electron/osx-sign" "1.0.5"
|
||||
"@electron/universal" "1.4.1"
|
||||
"@electron/universal" "1.5.1"
|
||||
"@malept/flatpak-bundler" "^0.4.0"
|
||||
"@types/fs-extra" "9.0.13"
|
||||
async-exit-hook "^2.0.1"
|
||||
bluebird-lst "^1.0.9"
|
||||
builder-util "24.8.1"
|
||||
builder-util-runtime "9.2.3"
|
||||
builder-util "24.13.1"
|
||||
builder-util-runtime "9.2.4"
|
||||
chromium-pickle-js "^0.2.0"
|
||||
debug "^4.3.4"
|
||||
ejs "^3.1.8"
|
||||
electron-publish "24.8.1"
|
||||
electron-publish "24.13.1"
|
||||
form-data "^4.0.0"
|
||||
fs-extra "^10.1.0"
|
||||
hosted-git-info "^4.1.0"
|
||||
|
@ -347,13 +348,13 @@ bluebird@^3.5.5:
|
|||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||
|
||||
body-parser@1.20.1:
|
||||
version "1.20.1"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
|
||||
integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
|
||||
body-parser@1.20.2:
|
||||
version "1.20.2"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
|
||||
integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
content-type "~1.0.4"
|
||||
content-type "~1.0.5"
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
destroy "1.2.0"
|
||||
|
@ -361,7 +362,7 @@ body-parser@1.20.1:
|
|||
iconv-lite "0.4.24"
|
||||
on-finished "2.4.1"
|
||||
qs "6.11.0"
|
||||
raw-body "2.5.1"
|
||||
raw-body "2.5.2"
|
||||
type-is "~1.6.18"
|
||||
unpipe "1.0.0"
|
||||
|
||||
|
@ -408,24 +409,24 @@ buffer@^5.1.0:
|
|||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
builder-util-runtime@9.2.3:
|
||||
version "9.2.3"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz#0a82c7aca8eadef46d67b353c638f052c206b83c"
|
||||
integrity sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==
|
||||
builder-util-runtime@9.2.4:
|
||||
version "9.2.4"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz#13cd1763da621e53458739a1e63f7fcba673c42a"
|
||||
integrity sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
sax "^1.2.4"
|
||||
|
||||
builder-util@24.8.1:
|
||||
version "24.8.1"
|
||||
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-24.8.1.tgz#594d45b0c86d1d17f5c7bebbb77405080b2571c2"
|
||||
integrity sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw==
|
||||
builder-util@24.13.1:
|
||||
version "24.13.1"
|
||||
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-24.13.1.tgz#4a4c4f9466b016b85c6990a0ea15aa14edec6816"
|
||||
integrity sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==
|
||||
dependencies:
|
||||
"7zip-bin" "~5.2.0"
|
||||
"@types/debug" "^4.1.6"
|
||||
app-builder-bin "4.0.0"
|
||||
bluebird-lst "^1.0.9"
|
||||
builder-util-runtime "9.2.3"
|
||||
builder-util-runtime "9.2.4"
|
||||
chalk "^4.1.2"
|
||||
cross-spawn "^7.0.3"
|
||||
debug "^4.3.4"
|
||||
|
@ -570,7 +571,7 @@ content-disposition@0.5.4:
|
|||
dependencies:
|
||||
safe-buffer "5.2.1"
|
||||
|
||||
content-type@~1.0.4:
|
||||
content-type@~1.0.4, content-type@~1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
|
||||
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
|
||||
|
@ -688,14 +689,14 @@ dir-compare@^3.0.0:
|
|||
buffer-equal "^1.0.0"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
dmg-builder@24.9.1:
|
||||
version "24.9.1"
|
||||
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-24.9.1.tgz#04bf6c0dcd235f6214511f2358a78ed2b9379421"
|
||||
integrity sha512-huC+O6hvHd24Ubj3cy2GMiGLe2xGFKN3klqVMLAdcbB6SWMd1yPSdZvV8W1O01ICzCCRlZDHiv4VrNUgnPUfbQ==
|
||||
dmg-builder@24.13.3:
|
||||
version "24.13.3"
|
||||
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-24.13.3.tgz#95d5b99c587c592f90d168a616d7ec55907c7e55"
|
||||
integrity sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==
|
||||
dependencies:
|
||||
app-builder-lib "24.9.1"
|
||||
builder-util "24.8.1"
|
||||
builder-util-runtime "9.2.3"
|
||||
app-builder-lib "24.13.3"
|
||||
builder-util "24.13.1"
|
||||
builder-util-runtime "9.2.4"
|
||||
fs-extra "^10.1.0"
|
||||
iconv-lite "^0.6.2"
|
||||
js-yaml "^4.1.0"
|
||||
|
@ -738,16 +739,16 @@ ejs@^3.1.8:
|
|||
dependencies:
|
||||
jake "^10.8.5"
|
||||
|
||||
electron-builder@24.9.1:
|
||||
version "24.9.1"
|
||||
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-24.9.1.tgz#4aee03947963b829a7f48a850fe02c219311ef63"
|
||||
integrity sha512-v7BuakDuY6sKMUYM8mfQGrwyjBpZ/ObaqnenU0H+igEL10nc6ht049rsCw2HghRBdEwJxGIBuzs3jbEhNaMDmg==
|
||||
electron-builder@24.13.3:
|
||||
version "24.13.3"
|
||||
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-24.13.3.tgz#c506dfebd36d9a50a83ee8aa32d803d83dbe4616"
|
||||
integrity sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==
|
||||
dependencies:
|
||||
app-builder-lib "24.9.1"
|
||||
builder-util "24.8.1"
|
||||
builder-util-runtime "9.2.3"
|
||||
app-builder-lib "24.13.3"
|
||||
builder-util "24.13.1"
|
||||
builder-util-runtime "9.2.4"
|
||||
chalk "^4.1.2"
|
||||
dmg-builder "24.9.1"
|
||||
dmg-builder "24.13.3"
|
||||
fs-extra "^10.1.0"
|
||||
is-ci "^3.0.0"
|
||||
lazy-val "^1.0.5"
|
||||
|
@ -755,26 +756,26 @@ electron-builder@24.9.1:
|
|||
simple-update-notifier "2.0.0"
|
||||
yargs "^17.6.2"
|
||||
|
||||
electron-publish@24.8.1:
|
||||
version "24.8.1"
|
||||
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-24.8.1.tgz#4216740372bf4297a429543402a1a15ce8c3560b"
|
||||
integrity sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw==
|
||||
electron-publish@24.13.1:
|
||||
version "24.13.1"
|
||||
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-24.13.1.tgz#57289b2f7af18737dc2ad134668cdd4a1b574a0c"
|
||||
integrity sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==
|
||||
dependencies:
|
||||
"@types/fs-extra" "^9.0.11"
|
||||
builder-util "24.8.1"
|
||||
builder-util-runtime "9.2.3"
|
||||
builder-util "24.13.1"
|
||||
builder-util-runtime "9.2.4"
|
||||
chalk "^4.1.2"
|
||||
fs-extra "^10.1.0"
|
||||
lazy-val "^1.0.5"
|
||||
mime "^2.5.2"
|
||||
|
||||
electron@28.2.2:
|
||||
version "28.2.2"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.2.tgz#d5aa4a33c00927d83ca893f8726f7c62aad98c41"
|
||||
integrity sha512-8UcvIGFcjplHdjPFNAHVFg5bS0atDyT3Zx21WwuE4iLfxcAMsyMEOgrQX3im5LibA8srwsUZs7Cx0JAUfcQRpw==
|
||||
electron@29.1.1:
|
||||
version "29.1.1"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-29.1.1.tgz#e9cb11311324e4b43a3e73667cd2b65a30e8fa34"
|
||||
integrity sha512-cXN15NgCi7MkzGo5/23ZQbii+0UfhmUiDjACunmzcUofYCjF42XhFbL7JZnwgI0qtBCCeJU8qZNZt9lU91gUFw==
|
||||
dependencies:
|
||||
"@electron/get" "^2.0.0"
|
||||
"@types/node" "^18.11.18"
|
||||
"@types/node" "^20.9.0"
|
||||
extract-zip "^2.0.1"
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
|
@ -829,14 +830,14 @@ etag@~1.8.1:
|
|||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
||||
|
||||
express@4.18.2:
|
||||
version "4.18.2"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
|
||||
integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
|
||||
express@4.18.3:
|
||||
version "4.18.3"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.18.3.tgz#6870746f3ff904dee1819b82e4b51509afffb0d4"
|
||||
integrity sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==
|
||||
dependencies:
|
||||
accepts "~1.3.8"
|
||||
array-flatten "1.1.1"
|
||||
body-parser "1.20.1"
|
||||
body-parser "1.20.2"
|
||||
content-disposition "0.5.4"
|
||||
content-type "~1.0.4"
|
||||
cookie "0.5.0"
|
||||
|
@ -1568,10 +1569,10 @@ range-parser@~1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||
|
||||
raw-body@2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
|
||||
integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
|
||||
raw-body@2.5.2:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
|
||||
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
http-errors "2.0.0"
|
||||
|
@ -1903,6 +1904,11 @@ typescript@^4.0.2:
|
|||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
|
||||
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
|
||||
universalify@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
if [ -n "$PUID" ] && [ "$PUID" -ne 0 ] && \
|
||||
[ -n "$PGID" ] && [ "$PGID" -ne 0 ] ; then
|
||||
echo "info: creating the new user vikunja with $PUID:$PGID"
|
||||
groupmod -g "$PGID" -o vikunja
|
||||
usermod -u "$PUID" -o vikunja
|
||||
chown -R vikunja:vikunja ./files/
|
||||
chown vikunja:vikunja .
|
||||
exec su vikunja -c /app/vikunja/vikunja "$@"
|
||||
else
|
||||
echo "info: creation of non-root user is skipped"
|
||||
exec /app/vikunja/vikunja "$@"
|
||||
fi
|
|
@ -94,7 +94,7 @@ Environment path: `VIKUNJA_SERVICE_JWTTTL`
|
|||
|
||||
### jwtttllong
|
||||
|
||||
The duration of the "remember me" time in seconds. When the login request is made with
|
||||
The duration of the "remember me" time in seconds. When the login request is made with
|
||||
the long param set, the token returned will be valid for this period.
|
||||
The default is 2592000 seconds (30 Days).
|
||||
|
||||
|
@ -289,7 +289,7 @@ Environment path: `VIKUNJA_SERVICE_ENABLEEMAILREMINDERS`
|
|||
|
||||
### enableuserdeletion
|
||||
|
||||
If true, will allow users to request the complete deletion of their account. When using external authentication methods
|
||||
If true, will allow users to request the complete deletion of their account. When using external authentication methods
|
||||
it may be required to coordinate with them in order to delete the account. This setting will not affect the cli commands
|
||||
for user deletion.
|
||||
|
||||
|
@ -406,7 +406,7 @@ Environment path: `VIKUNJA_SENTRY_FRONTENDDSN`
|
|||
|
||||
### type
|
||||
|
||||
Database type to use. Supported types are mysql, postgres and sqlite.
|
||||
Database type to use. Supported values are mysql, postgres and sqlite. Vikunja is able to run with MySQL 8.0+, Mariadb 10.2+, PostgreSQL 12+, and sqlite.
|
||||
|
||||
Default: `sqlite`
|
||||
|
||||
|
@ -569,7 +569,7 @@ Environment path: `VIKUNJA_DATABASE_TLS`
|
|||
|
||||
Whether to enable the Typesense integration. If true, all tasks will be synced to the configured Typesense
|
||||
instance and all search and filtering will run through Typesense instead of only through the database.
|
||||
Typesense allows fast fulltext search including fuzzy matching support. It may return different results than
|
||||
Typesense allows fast fulltext search including fuzzy matching support. It may return different results than
|
||||
what you'd get with a database-only search.
|
||||
|
||||
Default: `false`
|
||||
|
@ -1024,7 +1024,7 @@ Environment path: `VIKUNJA_RATELIMIT_STORE`
|
|||
|
||||
### noauthlimit
|
||||
|
||||
The number of requests a user can make from the same IP to all unauthenticated routes (login, register,
|
||||
The number of requests a user can make from the same IP to all unauthenticated routes (login, register,
|
||||
password confirmation, email verification, password reset request) per minute. This limit cannot be disabled.
|
||||
You should only change this if you know what you're doing.
|
||||
|
||||
|
@ -1209,13 +1209,11 @@ Environment path: `VIKUNJA_AUTH_LOCAL`
|
|||
|
||||
OpenID configuration will allow users to authenticate through a third-party OpenID Connect compatible provider.<br/>
|
||||
The provider needs to support the `openid`, `profile` and `email` scopes.<br/>
|
||||
**Note:** Some openid providers (like gitlab) only make the email of the user available through openid claims if they have set it to be publicly visible.
|
||||
**Note:** Some openid providers (like Gitlab) only make the email of the user available through OpenID if they have set it to be publicly visible.
|
||||
If the email is not public in those cases, authenticating will fail.
|
||||
**Note 2:** The frontend expects to be redirected after authentication by the third party
|
||||
to <frontend-url>/auth/openid/<auth key>. Please make sure to configure the redirect url in your third party
|
||||
auth service accordingly if you're using the default vikunja frontend.
|
||||
The frontend will automatically provide the api with the redirect url, composed from the current url where it's hosted.
|
||||
If you want to use the desktop client with openid, make sure to allow redirects to `127.0.0.1`.
|
||||
**Note 2:** The frontend expects the third party to rediect the user <frontend-url>/auth/openid/<auth key> after authentication. Please make sure to configure the redirect url in your third party auth service accordingly if you're using the default vikunja frontend.
|
||||
The frontend will automatically provide the API with the redirect url, composed from the current url where it's hosted.
|
||||
If you want to use the desktop client with OpenID, make sure to allow redirects to `127.0.0.1`.
|
||||
Take a look at the [default config file](https://kolaente.dev/vikunja/vikunja/src/branch/main/config.yml.sample) for more information about how to configure openid authentication.
|
||||
|
||||
Default: `<empty>`
|
||||
|
|
|
@ -27,7 +27,6 @@ Create a directory for the project where all data and the compose file will live
|
|||
|
||||
Create a `docker-compose.yml` file with the following contents in your directory:
|
||||
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
|
@ -80,6 +79,18 @@ The number before the colon is the host port - This is where you can reach vikun
|
|||
|
||||
You'll need to change the value of the `VIKUNJA_SERVICE_PUBLICURL` environment variable to the public port or hostname where Vikunja is reachable.
|
||||
|
||||
## Ensure adequate file permissions
|
||||
|
||||
Vikunja runs as user `1000` and no group by default.
|
||||
|
||||
To be able to upload task attachments or change the background of project, Vikunja must be able to write into the `files` directory.
|
||||
To do this, create the folder and chown it before starting the stack:
|
||||
|
||||
```
|
||||
mkdir $PWD/files
|
||||
chown 1000 $PWD/files
|
||||
```
|
||||
|
||||
## Run it
|
||||
|
||||
Run `sudo docker-compose up` in your directory and take a look at the output you get.
|
||||
|
|
|
@ -15,8 +15,6 @@ It uses a proxy configuration to make it available under a domain.
|
|||
|
||||
For all available configuration options, see [configuration]({{< ref "config.md">}}).
|
||||
|
||||
Once deployed, you might want to change the [`PUID` and `GUID` settings]({{< ref "install.md">}}#setting-user-and-group-id-of-the-user-running-vikunja) or [set the time zone]({{< ref "config.md">}}#timezone).
|
||||
|
||||
After registering all your users, you might also want to [disable the user registration]({{<ref "config.md">}}#enableregistration).
|
||||
|
||||
<div class="notification is-warning">
|
||||
|
@ -27,6 +25,23 @@ All examples on this page already reflect this and do not require additional wor
|
|||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## File permissions
|
||||
|
||||
Vikunja runs as user `1000` and no group by default.
|
||||
You can use Docker's [`--user`](https://docs.docker.com/engine/reference/run/#user) flag to change that.
|
||||
|
||||
You must ensure Vikunja is able to write into the `files` directory.
|
||||
To do this, create the folder and chown it before starting the stack:
|
||||
|
||||
```
|
||||
mkdir $PWD/files
|
||||
chown 1000 $PWD/files
|
||||
```
|
||||
|
||||
You'll need to do this before running any of the examples on this page.
|
||||
|
||||
Vikunja will not try to aquire ownership of the files folder, as that would mean it had to run as root.
|
||||
|
||||
## PostgreSQL
|
||||
|
||||
Vikunja supports postgres, mysql and sqlite as a database backend. The examples on this page use mysql with a mariadb container.
|
||||
|
@ -78,6 +93,13 @@ You'll also need to remove or change the `VIKUNJA_DATABASE_TYPE` to `sqlite` on
|
|||
|
||||
You can also remove the db section.
|
||||
|
||||
To run the container, you need to create the directories first and make sure they have all required permissions:
|
||||
|
||||
```
|
||||
mkdir $PWD/files $PWD/db
|
||||
chown 1000 $PWD/files $PWD/db
|
||||
```
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> If you'll use your instance with more than a handful of users, we recommend using mysql or postgres.
|
||||
</div>
|
||||
|
@ -88,8 +110,13 @@ This example lets you host Vikunja without any reverse proxy in front of it.
|
|||
This is the absolute minimum configuration you need to get something up and running.
|
||||
If you want to make Vikunja available on a domain or need tls termination, check out one of the other examples.
|
||||
|
||||
Note that you need to change the [`VIKUNJA_SERVICE_PUBLICURL`]({{< ref "config.md" >}}#publicurl) environment variable to the ip (the docker host you're running this on) is reachable at.
|
||||
Because the browser you'll use to access the Vikunja frontend uses that url to make the requests, it has to be able to reach that ip + port from the outside.
|
||||
Note that you need to change the [`VIKUNJA_SERVICE_PUBLICURL`]({{< ref "config.md" >}}#publicurl) environment variable to the public ip or hostname including the port (the docker host you're running this on) is reachable at, prefixed with `http://`.
|
||||
Because the browser you'll use to access the Vikunja frontend uses that url to make the requests, it has to be able to reach it from the outside.
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> You must ensure Vikunja has write permissions on the `files` directory before starting the stack.
|
||||
To do this, <a href="#file-permissions">check out the related commands here</a>.
|
||||
</div>
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
@ -98,7 +125,7 @@ services:
|
|||
vikunja:
|
||||
image: vikunja/vikunja
|
||||
environment:
|
||||
VIKUNJA_SERVICE_PUBLICURL: http://<the public url where vikunja is reachable>
|
||||
VIKUNJA_SERVICE_PUBLICURL: http://<the public ip or host where vikunja is reachable>
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: changeme
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
|
@ -140,6 +167,11 @@ We also make a few assumptions here which you'll most likely need to adjust for
|
|||
* The entrypoint you want to make vikunja available from is called `https`
|
||||
* The tls cert resolver is called `acme`
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> You must ensure Vikunja has write permissions on the `files` directory before starting the stack.
|
||||
To do this, <a href="#file-permissions">check out the related commands here</a>.
|
||||
</div>
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
|
@ -203,6 +235,11 @@ vikunja.example.com {
|
|||
Note that you need to change the [`VIKUNJA_SERVICE_PUBLICURL`]({{< ref "config.md" >}}#publicurl) environment variable to the ip (the docker host you're running this on) is reachable at.
|
||||
Because the browser you'll use to access the Vikunja frontend uses that url to make the requests, it has to be able to reach that ip + port from the outside.
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> You must ensure Vikunja has write permissions on the `files` directory before starting the stack.
|
||||
To do this, <a href="#file-permissions">check out the related commands here</a>.
|
||||
</div>
|
||||
|
||||
Docker Compose config:
|
||||
|
||||
```yaml
|
||||
|
@ -286,10 +323,13 @@ The docker-compose file we're going to use is exactly the same from the [example
|
|||
|
||||
You may want to change the volumes to match the rest of your setup.
|
||||
|
||||
Once deployed, you might want to change the [`PUID` and `GUID` settings]({{< ref "install.md">}}#setting-user-and-group-id-of-the-user-running-vikunja) or [set the time zone]({{< ref "config.md">}}#timezone).
|
||||
|
||||
After registering all your users, you might also want to [disable the user registration]({{<ref "config.md">}}#enableregistration).
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> You must ensure Vikunja has write permissions on the `files` directory before starting the stack.
|
||||
To do this, <a href="#file-permissions">check out the related commands here</a>.
|
||||
</div>
|
||||
|
||||
## Redis
|
||||
|
||||
While Vikunja has support to use redis as a caching backend, you'll probably not need it unless you're using Vikunja with more than a handful of users.
|
||||
|
|
|
@ -140,17 +140,23 @@ It will automatically run all necessary database migrations.
|
|||
To get up and running quickly, use this command:
|
||||
|
||||
```
|
||||
touch vikunja.db
|
||||
docker run -p 3456:3456 -v $PWD/files:/app/vikunja/files -v $PWD/vikunja.db:/app/vikunja/vikunja.db vikunja/vikunja
|
||||
mkdir $PWD/files $PWD/db
|
||||
chown 1000 $PWD/files $PWD/db
|
||||
docker run -p 3456:3456 -v $PWD/files:/app/vikunja/files -v $PWD/db:/db vikunja/vikunja
|
||||
```
|
||||
|
||||
This will expose vikunja on port `3456` on the host running the container and use sqlite as database backend.
|
||||
|
||||
**Note**: The container runs as the user `1000` and no group by default.
|
||||
You can use Docker's [`--user`](https://docs.docker.com/engine/reference/run/#user) flag to change that.
|
||||
Make sure the new user has required permissions on the `db` and `files` folder.
|
||||
|
||||
You can mount a local configuration like so:
|
||||
|
||||
```
|
||||
touch vikunja.db
|
||||
docker run -p 3456:3456 -v /path/to/config/on/host.yml:/app/vikunja/config.yml:ro -v $PWD/files:/app/vikunja/files -v $PWD/vikunja.db:/app/vikunja/vikunja.db vikunja/vikunja
|
||||
mkdir $PWD/files $PWD/db
|
||||
chown 1000 $PWD/files $PWD/db
|
||||
docker run -p 3456:3456 -v /path/to/config/on/host.yml:/app/vikunja/config.yml:ro -v $PWD/files:/app/vikunja/files -v $PWD/db:/db vikunja/vikunja
|
||||
```
|
||||
|
||||
Though it is recommended to use environment variables or `.env` files to configure Vikunja in docker.
|
||||
|
@ -163,13 +169,6 @@ Check out the [docker examples]({{<ref "full-docker-example.md">}}) for more adv
|
|||
By default, the container stores all files uploaded and used through vikunja inside of `/app/vikunja/files` which is created as a docker volume.
|
||||
You should mount the volume somewhere to the host to permanently store the files and don't lose them if the container restarts.
|
||||
|
||||
### Setting user and group id of the user running vikunja
|
||||
|
||||
You can set the user and group id of the user running vikunja with the `PUID` and `PGID` environment variables.
|
||||
This follows the pattern used by [the linuxserver.io](https://docs.linuxserver.io/general/understanding-puid-and-pgid) docker images.
|
||||
|
||||
This is useful to solve general permission problems when host-mounting volumes such as the volume used for task attachments.
|
||||
|
||||
### Docker compose
|
||||
|
||||
Check out the [docker examples]({{<ref "full-docker-example.md">}}) for more advanced configuration using docker compose.
|
||||
|
|
|
@ -10,7 +10,7 @@ menu:
|
|||
|
||||
# OpenID example configurations
|
||||
|
||||
On this page you will find examples about how to set up Vikunja with a third-party OpenID provider.
|
||||
On this page you will find examples about how to set up Vikunja with a third-party OAuth 2.0 provider using OpenID Connect.
|
||||
To add another example, please [edit this document](https://kolaente.dev/vikunja/vikunja/src/branch/main/docs/content/doc/setup/openid-examples.md) and send a PR.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
@ -67,7 +67,7 @@ Google config:
|
|||
|
||||
Note that there currently seems to be no way to stop creation of new users, even when `enableregistration` is `false` in the configuration. This means that this approach works well only with an "Internal Organization" app for Google Workspace, which limits the allowed users to organizational accounts only. External / public applications will potentially allow every Google user to register.
|
||||
|
||||
## Keycloak
|
||||
## Keycloak
|
||||
|
||||
Vikunja Config:
|
||||
```yaml
|
||||
|
@ -111,4 +111,7 @@ auth:
|
|||
clientsecret: "" # copy from Authentik
|
||||
```
|
||||
|
||||
**Note:** The `authurl` that Vikunja requires is not the `Authorize URL` that you can see in the Provider. Vikunja uses Open ID Discovery to find the correct endpoint to use. Vikunja does this by automatically accessing the `OpenID Configuration URL` (usually `https://authentik.mydomain.com/application/o/vikunja/.well-known/openid-configuration`). Use this URL without the `.well-known/openid-configuration` as the `authurl`.
|
||||
**Note:** The `authurl` that Vikunja requires is not the `Authorize URL` that you can see in the Provider.
|
||||
OpenID Discovery is used to find the correct endpoint to use automatically, by accessing the `OpenID Configuration URL` (usually `https://authentik.mydomain.com/application/o/vikunja/.well-known/openid-configuration`).
|
||||
Use this URL without the `.well-known/openid-configuration` as the `authurl`.
|
||||
Typically this URL can be found in the metadata section within your identity provider.
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
---
|
||||
date: "2022-08-09:00:00+02:00"
|
||||
title: "OpenID"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "setup"
|
||||
---
|
||||
|
||||
# OpenID
|
||||
|
||||
Vikunja allows for authentication with an external identity source such as Authentik, Keycloak or similar via the
|
||||
[OpenID Connect](https://openid.net/developers/specs/) standard.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## OpenID Connect Overview
|
||||
|
||||
OpenID Connect is a standardized identity layer built on top of the more generic OAuth 2.0 specification, simplying interaction between the involved parties significantly.
|
||||
While the [OpenID specification](https://openid.net/specs/openid-connect-core-1_0.html#Overview) is worth a read, we summarize the most important basics here.
|
||||
|
||||
The involved parties are:
|
||||
|
||||
- **Resource Owner:** typically the end-user
|
||||
- **Resource Server:** the application server handling requests from the client, the Vikunja API in our case
|
||||
- **Client:** the application or client accessing the RS on behalf of the RO. Vikunja web frontend or any of the apps
|
||||
- **Authorization Server:** the server verifying the user identity and issuing tokens. These docs also use the words `OAuth 2.0 provider`, `Identity Provider` interchangeably.
|
||||
|
||||
After the user is authenticated, the provider issues a token to the user, containing various claims.
|
||||
There's different types of tokens (ID token, access token, refresh token), and all of them are created as [JSON Web Token](https://www.rfc-editor.org/info/rfc7519).
|
||||
Claims in turn are assertions containing information about the token bearer, usually the user.
|
||||
|
||||
**Scopes** are requested by the client when redirecting the end-user to the Authorization Server for authentication, and indirectly control which claims are included in the resulting tokens.
|
||||
There's certain default scopes, but its also possible to define custom scopes, which are used by the feature assigning users to Teams automatically.
|
||||
|
||||
## Configuring OIDC Authentication
|
||||
|
||||
To achieve authentication via an external provider, it is required to (a) configure a confidential Client on your OAuth 2.0 provider and (b) configure Vikunja to authenticate against this provider.
|
||||
[Example configurations]({{< ref "openid-examples.md">}}) are provided for various different identity providers, below you can find generic guides though.
|
||||
|
||||
OpenID Connect defines various flow types indicating how exactly the interaction between the involved parties work, Vikunja makes use of the standard **Authorization Code Flow**.
|
||||
|
||||
### Step 1: Configure your Authorization Server
|
||||
|
||||
The first step is to configure the Authorization Server to correctly handle requests coming from Vikunja.
|
||||
In general, this involves the following steps at a minimum:
|
||||
|
||||
- Create a confidential client and obtain the client ID and client secret
|
||||
- Configure (whitelist) redirect URLs that can be used by Vikunja
|
||||
- Make sure the required scopes (`openid profile email` are the default scopes used by Vikunja) are supported
|
||||
- Optional: configure an additional scope for automatic team assignment, see below for details
|
||||
|
||||
More detailed instructions for various different identity providers can be [found here]({{< ref "openid-examples.md">}})
|
||||
|
||||
### Step 2: Configure Vikunja
|
||||
|
||||
Vikunja has to be configured to use the identity provider. Please note that there is currently no option to configure these settings via environment variables, they have to be defined using the configuration file. The configuration schema is as follows:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
openid:
|
||||
enabled: true
|
||||
redirecturl: https://vikunja.mydomain.com/auth/openid/ <---- slash at the end is important
|
||||
providers:
|
||||
- name: <provider-name>
|
||||
authurl: <auth-url> <----- Used for OIDC Discovery, usually the issuer
|
||||
clientid: <vikunja client-id>
|
||||
clientsecret: <vikunja client-secret>
|
||||
scope: openid profile email
|
||||
```
|
||||
|
||||
The value for `authurl` can be obtained from the metadata of your provider.
|
||||
Note that the `authurl` is used for [OIDC Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html).
|
||||
Typically, you'll want to use the `issuer` URL as found in the provider metadata.
|
||||
|
||||
The values for `clientid` and `clientsecret` are typically obtained when configuring the client.
|
||||
The scope usually doesn't need to be specified or changed, unless you want to configure the automatic team assignment.
|
||||
|
||||
Optionally it is possible to disable local authentication and therefore forcing users to login via OpenID connect:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
local:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
## Automatically assign users to teams
|
||||
|
||||
Vikunja is capable of automatically adding users to a team based on OIDC claims added by the identity provider.
|
||||
If configured, Vikunja will sync teams, automatically create new ones and make sure the members are part of the configured teams.
|
||||
Teams which exist only because they were created from oidc attributes are not editable in Vikunja.
|
||||
|
||||
To distinguish between teams created in Vikunja and teams generated automatically via oidc, generated teams have an `oidcID` assigned internally.
|
||||
Within the UI, the teams created through OIDC get a `(OIDC)` suffix to make them distinguishable from locally created teams.
|
||||
|
||||
On a high level, you need to make sure that the **ID token** issued by your identity provider contains a `vikunja_groups` claim, following the structure defined below.
|
||||
It depends on the provider being used as well as the preferences of the administrator how this is achieved.
|
||||
Typically you'd want to request an additional scope (e.g. `vikunja_scope`) which then triggers the identity provider to add the claim.
|
||||
If the `vikunja_groups` is part of the **ID token**, Vikunja will start the procedure and import teams and team memberships.
|
||||
|
||||
The claim structure expexted by Vikunja is as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"vikunja_groups": [
|
||||
{
|
||||
"name": "team 1",
|
||||
"oidcID": 33349
|
||||
},
|
||||
{
|
||||
"name": "team 2",
|
||||
"oidcID": 35933
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For each team, you need to define a team `name` and an `oidcID`, where the `oidcID` can be any string with a length of less than 250 characters.
|
||||
The `oidcID` is used to uniquely identify the team, so please make sure to keep this unique.
|
||||
|
||||
Below you'll find two example implementations for Authentik and Keycloak.
|
||||
If you've successfully implemented this with another identity provider, please let us know and submit a PR to improve the docs.
|
||||
|
||||
### Setup in Authentik
|
||||
|
||||
To configure automatic team management through Authentik, we assume you have already [set up Authentik]({{< ref "openid-examples.md">}}#authentik) as an OIDC provider for authentication with Vikunja.
|
||||
|
||||
To use Authentik's group assignment feature, follow these steps:
|
||||
|
||||
1. Edit [your config]({{< ref "config.md">}}) to include the following scopes: `openid profile email vikunja_scope`
|
||||
2. Open `<your authentik url>/if/admin/#/core/property-mappings`
|
||||
3. Create a new property mapping called `vikunja_scope` as scope mapping. There is a field `expression` to enter python expressions that will be delivered with the oidc token.
|
||||
4. Write a small script like the following to add group information to `vikunja_scope`:
|
||||
|
||||
```python
|
||||
groupsDict = {"vikunja_groups": []}
|
||||
for group in request.user.ak_groups.all():
|
||||
groupsDict["vikunja_groups"].append({"name": group.name, "oidcID": group.num_pk})
|
||||
return groupsDict
|
||||
```
|
||||
|
||||
5. In Authentik's menu on the left, go to Applications > Providers > Select the Vikunja provider. Then click on "Edit", on the bottom open "Advanced protocol settings", select the newly created property mapping under "Scopes". Save the provider.
|
||||
|
||||
Now when you log into Vikunja via Authentik it will show you a list of scopes you are claiming.
|
||||
You should see the description you entered on the OIDC provider's admin area.
|
||||
|
||||
Proceed to vikunja and open the teams page in the sidebar menu.
|
||||
You should see "(OIDC)" written next to each team you were assigned through OIDC.
|
||||
|
||||
### Setup in Keycloak
|
||||
|
||||
The kind people from Makerspace Darmstadt e.V. have written [a guide on how to create a mapper for Vikunja here](https://github.com/makerspace-darmstadt/keycloak-vikunja-mapper).
|
||||
|
||||
## Use cases
|
||||
|
||||
All examples assume one team called "Team 1" to be configured within your provider.
|
||||
|
||||
* *Token delivers team.name +team.oidcID and Vikunja team does not exist:* \
|
||||
New team will be created called "Team 1" with attribute oidcID: "33929"
|
||||
|
||||
2. *In Vikunja Team with name "team 1" already exists in vikunja, but has no oidcID set:* \
|
||||
new team will be created called "team 1" with attribute oidcID: "33929"
|
||||
|
||||
3. *In Vikunja Team with name "team 1" already exists in vikunja, but has different oidcID set:* \
|
||||
new team will be created called "team 1" with attribute oidcID: "33929"
|
||||
|
||||
4. *In Vikunja Team with oidcID "33929" already exists in vikunja, but has different name than "team1":* \
|
||||
new team will be created called "team 1" with attribute oidcID: "33929"
|
||||
|
||||
5. *Scope vikunja_scope is not set:* \
|
||||
nothing happens
|
||||
|
||||
6. *oidcID is not set:* \
|
||||
You'll get error.
|
||||
Custom Scope malformed
|
||||
"The custom scope set by the OIDC provider is malformed. Please make sure the openid provider sets the data correctly for your scope. Check especially to have set an oidcID."
|
||||
|
||||
7. *In Vikunja I am in "team 3" with oidcID "", but the token does not deliver any data for "team 3":* \
|
||||
You will stay in team 3 since it was not set by the oidc provider
|
||||
|
||||
8. *In Vikunja I am in "team 3" with oidcID "12345", but the token does not deliver any data for "team 3"*:\
|
||||
You will be signed out of all teams, which have an oidcID set and are not contained in the token.
|
||||
Especially if you've been the last team member, the team will be deleted.
|
|
@ -44,6 +44,7 @@ This document describes the different errors Vikunja can return.
|
|||
| 1020 | 412 | This user account is disabled. |
|
||||
| 1021 | 412 | This account is managed by a third-party authentication provider. |
|
||||
| 1021 | 412 | The username must not contain spaces. |
|
||||
| 1022 | 412 | The custom scope set by the OIDC provider is malformed. Please make sure the openid provider sets the data correctly for your scope. Check especially to have set an oidcID. |
|
||||
|
||||
## Validation
|
||||
|
||||
|
@ -72,29 +73,30 @@ This document describes the different errors Vikunja can return.
|
|||
|
||||
| ErrorCode | HTTP Status Code | Description |
|
||||
|-----------|------------------|----------------------------------------------------------------------------|
|
||||
| 4001 | 400 | The project task text cannot be empty. |
|
||||
| 4002 | 404 | The project task does not exist. |
|
||||
| 4003 | 403 | All bulk editing tasks must belong to the same project. |
|
||||
| 4004 | 403 | Need at least one task when bulk editing tasks. |
|
||||
| 4005 | 403 | The user does not have the right to see the task. |
|
||||
| 4006 | 403 | The user tried to set a parent task as the task itself. |
|
||||
| 4007 | 400 | The user tried to create a task relation with an invalid kind of relation. |
|
||||
| 4008 | 409 | The user tried to create a task relation which already exists. |
|
||||
| 4009 | 404 | The task relation does not exist. |
|
||||
| 4010 | 400 | Cannot relate a task with itself. |
|
||||
| 4011 | 404 | The task attachment does not exist. |
|
||||
| 4012 | 400 | The task attachment is too large. |
|
||||
| 4013 | 400 | The task sort param is invalid. |
|
||||
| 4014 | 400 | The task sort order is invalid. |
|
||||
| 4015 | 404 | The task comment does not exist. |
|
||||
| 4016 | 400 | Invalid task field. |
|
||||
| 4017 | 400 | Invalid task filter comparator. |
|
||||
| 4018 | 400 | Invalid task filter concatinator. |
|
||||
| 4019 | 400 | Invalid task filter value. |
|
||||
| 4020 | 400 | The provided attachment does not belong to that task. |
|
||||
| 4021 | 400 | This user is already assigned to that task. |
|
||||
| 4022 | 400 | The task has a relative reminder which does not specify relative to what. |
|
||||
| 4023 | 409 | Tried to create a task relation which would create a cycle. |
|
||||
| 4001 | 400 | The project task text cannot be empty. |
|
||||
| 4002 | 404 | The project task does not exist. |
|
||||
| 4003 | 403 | All bulk editing tasks must belong to the same project. |
|
||||
| 4004 | 403 | Need at least one task when bulk editing tasks. |
|
||||
| 4005 | 403 | The user does not have the right to see the task. |
|
||||
| 4006 | 403 | The user tried to set a parent task as the task itself. |
|
||||
| 4007 | 400 | The user tried to create a task relation with an invalid kind of relation. |
|
||||
| 4008 | 409 | The user tried to create a task relation which already exists. |
|
||||
| 4009 | 404 | The task relation does not exist. |
|
||||
| 4010 | 400 | Cannot relate a task with itself. |
|
||||
| 4011 | 404 | The task attachment does not exist. |
|
||||
| 4012 | 400 | The task attachment is too large. |
|
||||
| 4013 | 400 | The task sort param is invalid. |
|
||||
| 4014 | 400 | The task sort order is invalid. |
|
||||
| 4015 | 404 | The task comment does not exist. |
|
||||
| 4016 | 400 | Invalid task field. |
|
||||
| 4017 | 400 | Invalid task filter comparator. |
|
||||
| 4018 | 400 | Invalid task filter concatinator. |
|
||||
| 4019 | 400 | Invalid task filter value. |
|
||||
| 4020 | 400 | The provided attachment does not belong to that task. |
|
||||
| 4021 | 400 | This user is already assigned to that task. |
|
||||
| 4022 | 400 | The task has a relative reminder which does not specify relative to what. |
|
||||
| 4023 | 409 | Tried to create a task relation which would create a cycle. |
|
||||
| 4024 | 400 | The provided filter expression is invalid. |
|
||||
|
||||
## Team
|
||||
|
||||
|
@ -106,6 +108,9 @@ This document describes the different errors Vikunja can return.
|
|||
| 6005 | 409 | The user is already a member of that team. |
|
||||
| 6006 | 400 | Cannot delete the last team member. |
|
||||
| 6007 | 403 | The team does not have access to the project to perform that action. |
|
||||
| 6008 | 400 | There are no teams found with that team name. |
|
||||
| 6009 | 400 | There is no oidc team with that team name and oidcId. |
|
||||
| 6010 | 400 | There are no oidc teams found for the user. |
|
||||
|
||||
## User Project Access
|
||||
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
title: "Filters"
|
||||
date: 2024-03-09T19:51:32+02:00
|
||||
draft: false
|
||||
type: doc
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
---
|
||||
|
||||
# Filter Syntax
|
||||
|
||||
To filter tasks via the api, you can use a query syntax similar to SQL.
|
||||
|
||||
This document is about filtering via the api. To filter in Vikunja's web ui, check out the help text below the filter query input.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## Available fields
|
||||
|
||||
The available fields for filtering include:
|
||||
|
||||
* `done`: Whether the task is completed or not
|
||||
* `priority`: The priority level of the task (1-5)
|
||||
* `percentDone`: The percentage of completion for the task (0-100)
|
||||
* `dueDate`: The due date of the task
|
||||
* `startDate`: The start date of the task
|
||||
* `endDate`: The end date of the task
|
||||
* `doneAt`: The date and time when the task was completed
|
||||
* `assignees`: The assignees of the task
|
||||
* `labels`: The labels associated with the task
|
||||
* `project`: The project the task belongs to (only available for saved filters, not on a project level)
|
||||
|
||||
You can date math to set relative dates. Click on the date value in a query to find out more.
|
||||
|
||||
All strings must be either single-word or enclosed in `"` or `'`. This extends to date values like `2024-03-11`.
|
||||
|
||||
## Operators
|
||||
|
||||
The available operators for filtering include:
|
||||
|
||||
* `!=`: Not equal to
|
||||
* `=`: Equal to
|
||||
* `>`: Greater than
|
||||
* `>=`: Greater than or equal to
|
||||
* `<`: Less than
|
||||
* `<=`: Less than or equal to
|
||||
* `like`: Matches a pattern (using wildcard `%`)
|
||||
* `in`: Matches any value in a list
|
||||
|
||||
To combine multiple conditions, you can use the following logical operators:
|
||||
|
||||
* `&&`: AND operator, matches if all conditions are true
|
||||
* `||`: OR operator, matches if any of the conditions are true
|
||||
* `(` and `)`: Parentheses for grouping conditions
|
||||
|
||||
## Examples
|
||||
|
||||
Here are some examples of filter queries:
|
||||
|
||||
* `priority = 4`: Matches tasks with priority level 4
|
||||
* `dueDate < now`: Matches tasks with a due date in the past
|
||||
* `done = false && priority >= 3`: Matches undone tasks with priority level 3 or higher
|
||||
* `assignees in [user1, user2]`: Matches tasks assigned to either "user1" or "user2
|
||||
* `(priority = 1 || priority = 2) && dueDate <= now`: Matches tasks with priority level 1 or 2 and a due date in the past
|
||||
|
||||
|
|
@ -1 +1 @@
|
|||
20.11.0
|
||||
20.11.1
|
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@8.15.3",
|
||||
"packageManager": "pnpm@8.15.4",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
|
@ -28,13 +28,15 @@
|
|||
"serve": "pnpm run dev",
|
||||
"preview": "vite preview --port 4173",
|
||||
"preview:dev": "vite preview --outDir dist-dev --mode development --port 4173",
|
||||
"preview:test": "vite preview --port 4173 --outDir dist-test",
|
||||
"build": "vite build && workbox copyLibraries dist/",
|
||||
"build:test": "vite build --outDir dist-test && workbox copyLibraries dist-dev/",
|
||||
"build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/",
|
||||
"build:dev": "vite build --mode development --outDir dist-dev/",
|
||||
"lint": "eslint 'src/**/*.{js,ts,vue}'",
|
||||
"lint:fix": "pnpm run lint --fix",
|
||||
"test:e2e": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome'",
|
||||
"test:e2e-record": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
|
||||
"test:e2e-record-test": "start-server-and-test preview:test http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
|
||||
"test:e2e-dev-dev": "start-server-and-test preview:dev http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
"test:e2e-dev": "start-server-and-test preview http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
"test:unit": "vitest --dir ./src",
|
||||
|
@ -54,54 +56,54 @@
|
|||
"@fortawesome/vue-fontawesome": "3.0.6",
|
||||
"@github/hotkey": "3.1.0",
|
||||
"@infectoone/vue-ganttastic": "2.2.0",
|
||||
"@intlify/unplugin-vue-i18n": "2.0.0",
|
||||
"@kyvg/vue3-notification": "3.1.4",
|
||||
"@sentry/tracing": "7.101.0",
|
||||
"@sentry/vue": "7.101.0",
|
||||
"@tiptap/core": "2.2.2",
|
||||
"@tiptap/extension-blockquote": "2.2.2",
|
||||
"@tiptap/extension-bold": "2.2.2",
|
||||
"@tiptap/extension-bullet-list": "2.2.2",
|
||||
"@tiptap/extension-code": "2.2.2",
|
||||
"@tiptap/extension-code-block-lowlight": "2.2.2",
|
||||
"@tiptap/extension-document": "2.2.2",
|
||||
"@tiptap/extension-dropcursor": "2.2.2",
|
||||
"@tiptap/extension-gapcursor": "2.2.2",
|
||||
"@tiptap/extension-hard-break": "2.2.2",
|
||||
"@tiptap/extension-heading": "2.2.2",
|
||||
"@tiptap/extension-history": "2.2.2",
|
||||
"@tiptap/extension-horizontal-rule": "2.2.2",
|
||||
"@tiptap/extension-image": "2.2.2",
|
||||
"@tiptap/extension-italic": "2.2.2",
|
||||
"@tiptap/extension-link": "2.2.2",
|
||||
"@tiptap/extension-list-item": "2.2.2",
|
||||
"@tiptap/extension-ordered-list": "2.2.2",
|
||||
"@tiptap/extension-paragraph": "2.2.2",
|
||||
"@tiptap/extension-placeholder": "2.2.2",
|
||||
"@tiptap/extension-strike": "2.2.2",
|
||||
"@tiptap/extension-table": "2.2.2",
|
||||
"@tiptap/extension-table-cell": "2.2.2",
|
||||
"@tiptap/extension-table-header": "2.2.2",
|
||||
"@tiptap/extension-table-row": "2.2.2",
|
||||
"@tiptap/extension-task-item": "2.2.2",
|
||||
"@tiptap/extension-task-list": "2.2.2",
|
||||
"@tiptap/extension-text": "2.2.2",
|
||||
"@tiptap/extension-typography": "2.2.2",
|
||||
"@tiptap/extension-underline": "2.2.2",
|
||||
"@tiptap/pm": "2.2.2",
|
||||
"@tiptap/suggestion": "2.2.2",
|
||||
"@tiptap/vue-3": "2.2.2",
|
||||
"@intlify/unplugin-vue-i18n": "3.0.1",
|
||||
"@kyvg/vue3-notification": "3.2.0",
|
||||
"@sentry/tracing": "7.106.0",
|
||||
"@sentry/vue": "7.106.0",
|
||||
"@tiptap/core": "2.2.4",
|
||||
"@tiptap/extension-blockquote": "2.2.4",
|
||||
"@tiptap/extension-bold": "2.2.4",
|
||||
"@tiptap/extension-bullet-list": "2.2.4",
|
||||
"@tiptap/extension-code": "2.2.4",
|
||||
"@tiptap/extension-code-block-lowlight": "2.2.4",
|
||||
"@tiptap/extension-document": "2.2.4",
|
||||
"@tiptap/extension-dropcursor": "2.2.4",
|
||||
"@tiptap/extension-gapcursor": "2.2.4",
|
||||
"@tiptap/extension-hard-break": "2.2.4",
|
||||
"@tiptap/extension-heading": "2.2.4",
|
||||
"@tiptap/extension-history": "2.2.4",
|
||||
"@tiptap/extension-horizontal-rule": "2.2.4",
|
||||
"@tiptap/extension-image": "2.2.4",
|
||||
"@tiptap/extension-italic": "2.2.4",
|
||||
"@tiptap/extension-link": "2.2.4",
|
||||
"@tiptap/extension-list-item": "2.2.4",
|
||||
"@tiptap/extension-ordered-list": "2.2.4",
|
||||
"@tiptap/extension-paragraph": "2.2.4",
|
||||
"@tiptap/extension-placeholder": "2.2.4",
|
||||
"@tiptap/extension-strike": "2.2.4",
|
||||
"@tiptap/extension-table": "2.2.4",
|
||||
"@tiptap/extension-table-cell": "2.2.4",
|
||||
"@tiptap/extension-table-header": "2.2.4",
|
||||
"@tiptap/extension-table-row": "2.2.4",
|
||||
"@tiptap/extension-task-item": "2.2.4",
|
||||
"@tiptap/extension-task-list": "2.2.4",
|
||||
"@tiptap/extension-text": "2.2.4",
|
||||
"@tiptap/extension-typography": "2.2.4",
|
||||
"@tiptap/extension-underline": "2.2.4",
|
||||
"@tiptap/pm": "2.2.4",
|
||||
"@tiptap/suggestion": "2.2.4",
|
||||
"@tiptap/vue-3": "2.2.4",
|
||||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.clonedeep": "4.5.9",
|
||||
"@vueuse/core": "10.7.2",
|
||||
"@vueuse/router": "10.7.2",
|
||||
"@vueuse/core": "10.9.0",
|
||||
"@vueuse/router": "10.9.0",
|
||||
"axios": "1.6.7",
|
||||
"blurhash": "2.0.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"camel-case": "4.1.2",
|
||||
"date-fns": "3.3.1",
|
||||
"dayjs": "1.11.10",
|
||||
"dompurify": "3.0.8",
|
||||
"dompurify": "3.0.9",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.31",
|
||||
|
@ -116,11 +118,11 @@
|
|||
"sortablejs": "1.15.2",
|
||||
"tippy.js": "6.3.7",
|
||||
"ufo": "1.4.0",
|
||||
"vue": "3.4.19",
|
||||
"vue": "3.4.21",
|
||||
"vue-advanced-cropper": "2.8.8",
|
||||
"vue-flatpickr-component": "11.0.3",
|
||||
"vue-i18n": "9.9.1",
|
||||
"vue-router": "4.2.5",
|
||||
"vue-flatpickr-component": "11.0.5",
|
||||
"vue-i18n": "9.10.1",
|
||||
"vue-router": "4.3.0",
|
||||
"workbox-precaching": "7.0.0",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
},
|
||||
|
@ -128,9 +130,9 @@
|
|||
"@4tw/cypress-drag-drop": "2.2.5",
|
||||
"@cypress/vite-dev-server": "5.0.7",
|
||||
"@cypress/vue": "6.0.0",
|
||||
"@faker-js/faker": "8.4.0",
|
||||
"@faker-js/faker": "8.4.1",
|
||||
"@histoire/plugin-screenshot": "0.17.8",
|
||||
"@histoire/plugin-vue": "0.17.9",
|
||||
"@histoire/plugin-vue": "0.17.12",
|
||||
"@rushstack/eslint-patch": "1.7.2",
|
||||
"@tsconfig/node18": "18.2.2",
|
||||
"@types/codemirror": "5.60.15",
|
||||
|
@ -139,44 +141,44 @@
|
|||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.debounce": "4.0.9",
|
||||
"@types/marked": "5.0.2",
|
||||
"@types/node": "20.11.10",
|
||||
"@types/node": "20.11.25",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@types/sortablejs": "1.15.7",
|
||||
"@typescript-eslint/eslint-plugin": "7.0.1",
|
||||
"@typescript-eslint/parser": "7.0.1",
|
||||
"@vitejs/plugin-legacy": "5.3.0",
|
||||
"@vitejs/plugin-vue": "5.0.3",
|
||||
"@vue/eslint-config-typescript": "12.0.0",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "7.1.1",
|
||||
"@typescript-eslint/parser": "7.1.1",
|
||||
"@vitejs/plugin-legacy": "5.3.2",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vue/eslint-config-typescript": "13.0.0",
|
||||
"@vue/test-utils": "2.4.4",
|
||||
"@vue/tsconfig": "0.5.1",
|
||||
"autoprefixer": "10.4.17",
|
||||
"browserslist": "4.22.3",
|
||||
"caniuse-lite": "1.0.30001581",
|
||||
"css-has-pseudo": "6.0.1",
|
||||
"autoprefixer": "10.4.18",
|
||||
"browserslist": "4.23.0",
|
||||
"caniuse-lite": "1.0.30001596",
|
||||
"css-has-pseudo": "6.0.2",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "13.6.3",
|
||||
"esbuild": "0.20.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-vue": "9.20.1",
|
||||
"happy-dom": "13.3.5",
|
||||
"cypress": "13.6.6",
|
||||
"esbuild": "0.20.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-vue": "9.22.0",
|
||||
"happy-dom": "13.7.1",
|
||||
"histoire": "0.17.9",
|
||||
"postcss": "8.4.33",
|
||||
"postcss": "8.4.35",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-easings": "4.0.0",
|
||||
"postcss-focus-within": "8.0.1",
|
||||
"postcss-preset-env": "9.3.0",
|
||||
"rollup": "4.9.6",
|
||||
"postcss-preset-env": "9.5.0",
|
||||
"rollup": "4.12.1",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"sass": "1.70.0",
|
||||
"sass": "1.71.1",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "5.0.12",
|
||||
"typescript": "5.4.2",
|
||||
"vite": "5.1.5",
|
||||
"vite-plugin-inject-preload": "1.3.3",
|
||||
"vite-plugin-pwa": "0.17.5",
|
||||
"vite-plugin-sentry": "1.3.0",
|
||||
"vite-plugin-pwa": "0.19.2",
|
||||
"vite-plugin-sentry": "1.4.0",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "1.2.2",
|
||||
"vue-tsc": "1.8.27",
|
||||
"vitest": "1.3.1",
|
||||
"vue-tsc": "2.0.6",
|
||||
"wait-on": "7.2.0",
|
||||
"workbox-cli": "7.0.0"
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -51,10 +51,12 @@ Hi ${process.env.DRONE_COMMIT_AUTHOR}!
|
|||
|
||||
Thank you for creating a PR!
|
||||
|
||||
I've deployed the changes of this PR on a preview environment under this URL: ${fullPreviewUrl}
|
||||
I've deployed the frontend changes of this PR on a preview environment under this URL: ${fullPreviewUrl}
|
||||
|
||||
You can use this url to view the changes live and test them out.
|
||||
You will need to manually connect this to an api running somehwere. The easiest to use is https://try.vikunja.io/.
|
||||
You will need to manually connect this to an api running somewhere. The easiest to use is https://try.vikunja.io/.
|
||||
|
||||
This preview does not contain any changes made to the api, only the frontend.
|
||||
|
||||
Have a nice day!
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
d58cd9ebc135407aa29d093b046d84b72ec7073b3f08cedfdbb936318a0ad3e272fab921d3ff91a82c1a7059fcdecd7b ./scripts/deploy-preview-netlify.mjs
|
||||
2ba5ae4c831fd749296d92f92c5f89339030e22b80be62b1253dc26982e8fd0082e354f884a3ba15293e0b96317ec758 ./scripts/deploy-preview-netlify.mjs
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
class="base-button"
|
||||
:href="href"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
:target="openExternalInNewTab ? '_blank' : undefined"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
|
@ -69,6 +69,7 @@ export interface BaseButtonProps extends /* @vue-ignore */ HTMLAttributes {
|
|||
disabled?: boolean
|
||||
to?: RouteLocationRaw
|
||||
href?: string
|
||||
openExternalInNewTab?: boolean
|
||||
}
|
||||
|
||||
export interface BaseButtonEmits {
|
||||
|
@ -78,6 +79,7 @@ export interface BaseButtonEmits {
|
|||
const {
|
||||
type = BASE_BUTTON_TYPES_MAP.BUTTON,
|
||||
disabled = false,
|
||||
openExternalInNewTab = true,
|
||||
} = defineProps<BaseButtonProps>()
|
||||
|
||||
const emit = defineEmits<BaseButtonEmits>()
|
||||
|
|
|
@ -19,3 +19,28 @@ export const DATE_RANGES = {
|
|||
'thisYear': ['now/y', 'now/y+1y'],
|
||||
'restOfThisYear': ['now', 'now/y+1y'],
|
||||
}
|
||||
|
||||
export const DATE_VALUES = {
|
||||
'now': 'now',
|
||||
'startOfToday': 'now/d',
|
||||
'endOfToday': 'now/d+1d',
|
||||
|
||||
'beginningOflastWeek': 'now/w-1w',
|
||||
'endOfLastWeek': 'now/w-2w',
|
||||
'beginningOfThisWeek': 'now/w',
|
||||
'endOfThisWeek': 'now/w+1w',
|
||||
'startOfNextWeek': 'now/w+1w',
|
||||
'endOfNextWeek': 'now/w+2w',
|
||||
'in7Days': 'now+7d',
|
||||
|
||||
'beginningOfLastMonth': 'now/M-1M',
|
||||
'endOfLastMonth': 'now/M-2M',
|
||||
'startOfThisMonth': 'now/M',
|
||||
'endOfThisMonth': 'now/M+1M',
|
||||
'startOfNextMonth': 'now/M+1M',
|
||||
'endOfNextMonth': 'now/M+2M',
|
||||
'in30Days': 'now+30d',
|
||||
|
||||
'startOfThisYear': 'now/y',
|
||||
'endOfThisYear': 'now/y+1y',
|
||||
}
|
||||
|
|
|
@ -75,14 +75,15 @@
|
|||
|
||||
<p>
|
||||
{{ $t('input.datemathHelp.canuse') }}
|
||||
<BaseButton
|
||||
class="has-text-primary"
|
||||
@click="showHowItWorks = true"
|
||||
>
|
||||
{{ $t('input.datemathHelp.learnhow') }}
|
||||
</BaseButton>
|
||||
</p>
|
||||
|
||||
<BaseButton
|
||||
class="has-text-primary"
|
||||
@click="showHowItWorks = true"
|
||||
>
|
||||
{{ $t('input.datemathHelp.learnhow') }}
|
||||
</BaseButton>
|
||||
|
||||
<modal
|
||||
:enabled="showHowItWorks"
|
||||
transition-name="fade"
|
||||
|
@ -111,7 +112,7 @@ import Popup from '@/components/misc/popup.vue'
|
|||
import {DATE_RANGES} from '@/components/date/dateRanges'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import DatemathHelp from '@/components/date/datemathHelp.vue'
|
||||
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
|
||||
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
|
|
@ -0,0 +1,256 @@
|
|||
<template>
|
||||
<div class="datepicker-with-range-container">
|
||||
<Popup
|
||||
:open="open"
|
||||
@close="() => emit('close')"
|
||||
>
|
||||
<template #content="{isOpen}">
|
||||
<div
|
||||
class="datepicker-with-range"
|
||||
:class="{'is-open': isOpen}"
|
||||
>
|
||||
<div class="selections">
|
||||
<BaseButton
|
||||
:class="{'is-active': customRangeActive}"
|
||||
@click="setDate(null)"
|
||||
>
|
||||
{{ $t('misc.custom') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-for="(value, text) in DATE_VALUES"
|
||||
:key="text"
|
||||
:class="{'is-active': date === value}"
|
||||
@click="setDate(value)"
|
||||
>
|
||||
{{ $t(`input.datepickerRange.values.${text}`) }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div class="flatpickr-container input-group">
|
||||
<label class="label">
|
||||
{{ $t('input.datepickerRange.date') }}
|
||||
<div class="field has-addons">
|
||||
<div class="control is-fullwidth">
|
||||
<input
|
||||
v-model="date"
|
||||
class="input"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
<x-button
|
||||
icon="calendar"
|
||||
variant="secondary"
|
||||
data-toggle
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<flat-pickr
|
||||
v-model="flatpickrDate"
|
||||
:config="flatPickerConfig"
|
||||
/>
|
||||
|
||||
<p>
|
||||
{{ $t('input.datemathHelp.canuse') }}
|
||||
</p>
|
||||
|
||||
<BaseButton
|
||||
class="has-text-primary"
|
||||
@click="showHowItWorks = true"
|
||||
>
|
||||
{{ $t('input.datemathHelp.learnhow') }}
|
||||
</BaseButton>
|
||||
|
||||
<modal
|
||||
:enabled="showHowItWorks"
|
||||
transition-name="fade"
|
||||
:overflow="true"
|
||||
variant="hint-modal"
|
||||
@close="() => showHowItWorks = false"
|
||||
>
|
||||
<DatemathHelp />
|
||||
</modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, ref, watch} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||
|
||||
import Popup from '@/components/misc/popup.vue'
|
||||
import {DATE_VALUES} from '@/components/date/dateRanges'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import DatemathHelp from '@/components/date/datemathHelp.vue'
|
||||
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const flatPickerConfig = computed(() => ({
|
||||
altFormat: t('date.altFormatLong'),
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d H:i',
|
||||
enableTime: false,
|
||||
wrap: true,
|
||||
locale: getFlatpickrLanguage(),
|
||||
}))
|
||||
|
||||
const showHowItWorks = ref(false)
|
||||
|
||||
const flatpickrDate = ref('')
|
||||
|
||||
const date = ref<string|Date>('')
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
newValue => {
|
||||
date.value = newValue
|
||||
// Only set the date back to flatpickr when it's an actual date.
|
||||
// Otherwise flatpickr runs in an endless loop and slows down the browser.
|
||||
const parsed = parseDateOrString(date.value, false)
|
||||
if (parsed instanceof Date) {
|
||||
flatpickrDate.value = date.value
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function emitChanged() {
|
||||
emit('update:modelValue', date.value === '' ? null : date.value)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => flatpickrDate.value,
|
||||
(newVal: string | null) => {
|
||||
if (newVal === null) {
|
||||
return
|
||||
}
|
||||
|
||||
date.value = newVal
|
||||
|
||||
emitChanged()
|
||||
},
|
||||
)
|
||||
watch(() => date.value, emitChanged)
|
||||
|
||||
function setDate(range: string | null) {
|
||||
if (range === null) {
|
||||
date.value = ''
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
date.value = range
|
||||
}
|
||||
|
||||
const customRangeActive = computed<boolean>(() => {
|
||||
return !Object.values(DATE_VALUES).some(d => date.value === d)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.datepicker-with-range-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.popup) {
|
||||
z-index: 10;
|
||||
margin-top: 1rem;
|
||||
border-radius: $radius;
|
||||
border: 1px solid var(--grey-200);
|
||||
background-color: var(--white);
|
||||
box-shadow: $shadow;
|
||||
|
||||
&.is-open {
|
||||
width: 500px;
|
||||
height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.datepicker-with-range {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
:deep(.flatpickr-calendar) {
|
||||
margin: 0 auto 8px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.flatpickr-container {
|
||||
width: 70%;
|
||||
border-left: 1px solid var(--grey-200);
|
||||
padding: 1rem;
|
||||
font-size: .9rem;
|
||||
|
||||
// Flatpickr has no option to use it without an input field so we're hiding it instead
|
||||
:deep(input.form-control.input) {
|
||||
height: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.field .control :deep(.button) {
|
||||
border: 1px solid var(--input-border-color);
|
||||
height: 2.25rem;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--input-hover-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.label, .input, :deep(.button) {
|
||||
font-size: .9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.selections {
|
||||
width: 30%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: .5rem;
|
||||
overflow-y: scroll;
|
||||
|
||||
button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: .5rem 1rem;
|
||||
transition: $transition;
|
||||
font-size: .9rem;
|
||||
color: var(--text);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.is-active {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&:hover, &.is-active {
|
||||
background-color: var(--grey-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -122,7 +122,7 @@ const labelStore = useLabelStore()
|
|||
labelStore.loadAllLabels()
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
projectStore.loadProjects()
|
||||
projectStore.loadAllProjects()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -0,0 +1,232 @@
|
|||
<script setup lang="ts">
|
||||
import {type ComponentPublicInstance, nextTick, ref, watch} from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
options: any[],
|
||||
suggestion?: string,
|
||||
maxHeight?: number,
|
||||
}>(), {
|
||||
maxHeight: 200,
|
||||
suggestion: '',
|
||||
})
|
||||
|
||||
const emit = defineEmits(['blur'])
|
||||
|
||||
const ESCAPE = 27,
|
||||
ARROW_UP = 38,
|
||||
ARROW_DOWN = 40
|
||||
|
||||
type StateType = 'unfocused' | 'focused'
|
||||
|
||||
const selectedIndex = ref(-1)
|
||||
const state = ref<StateType>('unfocused')
|
||||
const val = ref<string>('')
|
||||
const model = defineModel<string>()
|
||||
|
||||
const suggestionScrollerRef = ref<HTMLInputElement | null>(null)
|
||||
const containerRef = ref<HTMLInputElement | null>(null)
|
||||
const editorRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
watch(
|
||||
() => model.value,
|
||||
newValue => {
|
||||
val.value = newValue
|
||||
},
|
||||
)
|
||||
|
||||
function updateSuggestionScroll() {
|
||||
nextTick(() => {
|
||||
const scroller = suggestionScrollerRef.value
|
||||
const selectedItem = scroller?.querySelector('.selected')
|
||||
scroller.scrollTop = selectedItem ? selectedItem.offsetTop : 0
|
||||
})
|
||||
}
|
||||
|
||||
function setState(stateName: StateType) {
|
||||
state.value = stateName
|
||||
if (stateName === 'unfocused') {
|
||||
emit('blur')
|
||||
}
|
||||
}
|
||||
|
||||
function onFocusField() {
|
||||
setState('focused')
|
||||
}
|
||||
|
||||
function onKeydown(e) {
|
||||
switch (e.keyCode || e.which) {
|
||||
case ESCAPE:
|
||||
e.preventDefault()
|
||||
setState('unfocused')
|
||||
break
|
||||
case ARROW_UP:
|
||||
e.preventDefault()
|
||||
select(-1)
|
||||
break
|
||||
case ARROW_DOWN:
|
||||
e.preventDefault()
|
||||
select(1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const resultRefs = ref<(HTMLElement | null)[]>([])
|
||||
|
||||
function setResultRefs(el: Element | ComponentPublicInstance | null, index: number) {
|
||||
resultRefs.value[index] = el as (HTMLElement | null)
|
||||
}
|
||||
|
||||
function select(offset: number) {
|
||||
|
||||
let index = selectedIndex.value + offset
|
||||
|
||||
if (!isFinite(index)) {
|
||||
index = 0
|
||||
}
|
||||
|
||||
if (index >= props.options.length) {
|
||||
// At the last index, now moving back to the top
|
||||
index = 0
|
||||
}
|
||||
|
||||
if (index < 0) {
|
||||
// Arrow up but we're already at the top
|
||||
index = props.options.length - 1
|
||||
}
|
||||
const elems = resultRefs.value[index]
|
||||
if (
|
||||
typeof elems === 'undefined'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedIndex.value = index
|
||||
updateSuggestionScroll()
|
||||
|
||||
if (Array.isArray(elems)) {
|
||||
elems[0].focus()
|
||||
return
|
||||
}
|
||||
elems?.focus()
|
||||
}
|
||||
|
||||
function onSelectValue(value) {
|
||||
model.value = value
|
||||
selectedIndex.value = 0
|
||||
setState('unfocused')
|
||||
}
|
||||
|
||||
function onUpdateField(e) {
|
||||
setState('focused')
|
||||
model.value = e.currentTarget.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="autocomplete"
|
||||
>
|
||||
<div class="entry-box">
|
||||
<slot
|
||||
name="input"
|
||||
:on-update-field
|
||||
:on-focus-field
|
||||
:on-keydown
|
||||
>
|
||||
<textarea
|
||||
ref="editorRef"
|
||||
class="field"
|
||||
:class="state"
|
||||
:value="val"
|
||||
@input="onUpdateField"
|
||||
@focus="onFocusField"
|
||||
@keydown="onKeydown"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
<div
|
||||
v-if="state === 'focused' && options.length"
|
||||
class="suggestion-list"
|
||||
>
|
||||
<div
|
||||
v-if="options && options.length"
|
||||
class="scroll-list"
|
||||
>
|
||||
<div
|
||||
ref="suggestionScrollerRef"
|
||||
class="items"
|
||||
@keydown="onKeydown"
|
||||
>
|
||||
<button
|
||||
v-for="(item, index) in options"
|
||||
:key="item"
|
||||
:ref="(el: Element | ComponentPublicInstance | null) => setResultRefs(el, index)"
|
||||
class="item"
|
||||
:class="{ selected: index === selectedIndex }"
|
||||
@click="onSelectValue(item)"
|
||||
>
|
||||
<slot
|
||||
name="result"
|
||||
:item
|
||||
:selected="index === selectedIndex"
|
||||
>
|
||||
{{ item }}
|
||||
</slot>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.autocomplete {
|
||||
position: relative;
|
||||
|
||||
.suggestion-list {
|
||||
position: absolute;
|
||||
|
||||
background: var(--white);
|
||||
border-radius: 0 0 var(--input-radius) var(--input-radius);
|
||||
border: 1px solid var(--primary);
|
||||
border-top: none;
|
||||
|
||||
max-height: 50vh;
|
||||
overflow-x: auto;
|
||||
z-index: 100;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
margin-top: -2px;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
font-size: .9rem;
|
||||
width: 100%;
|
||||
color: var(--grey-800);
|
||||
text-align: left;
|
||||
box-shadow: none;
|
||||
text-transform: none;
|
||||
font-family: $family-sans-serif;
|
||||
font-weight: normal;
|
||||
padding: .5rem .75rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
background: var(--grey-100);
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--grey-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -193,7 +193,6 @@ import {mergeAttributes} from '@tiptap/core'
|
|||
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
|
||||
import inputPrompt from '@/helpers/inputPrompt'
|
||||
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
|
||||
import {createRandomID} from '@/helpers/randomId'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
|
@ -389,20 +388,7 @@ const editor = useEditor({
|
|||
CustomImage,
|
||||
|
||||
TaskList,
|
||||
TaskItem.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
id: {
|
||||
default: () => createRandomID(),
|
||||
parseHTML: element => element.getAttribute('data-id'),
|
||||
renderHTML: attributes => ({
|
||||
'data-id': attributes.id,
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
}).configure({
|
||||
TaskItem.configure({
|
||||
nested: true,
|
||||
onReadOnlyChecked: (node: Node, checked: boolean): boolean => {
|
||||
if (!isEditEnabled) {
|
||||
|
@ -414,7 +400,7 @@ const editor = useEditor({
|
|||
// https://github.com/ueberdosis/tiptap/issues/3676
|
||||
|
||||
editor.value!.state.doc.descendants((subnode, pos) => {
|
||||
if (node.attrs.id === subnode.attrs.id) {
|
||||
if (node.eq(subnode)) {
|
||||
const {tr} = editor.value!.state
|
||||
tr.setNodeMarkup(pos, undefined, {
|
||||
...node.attrs,
|
||||
|
@ -422,10 +408,10 @@ const editor = useEditor({
|
|||
})
|
||||
editor.value!.view.dispatch(tr)
|
||||
bubbleSave()
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return true
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -42,6 +42,10 @@ const props = defineProps({
|
|||
modelValue: String,
|
||||
// This prop is a workaround to trigger validation from the outside when the user never had focus in the input.
|
||||
validateInitially: Boolean,
|
||||
validateMinLength: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['submit', 'update:modelValue'])
|
||||
const {t} = useI18n()
|
||||
|
@ -62,12 +66,12 @@ const validate = useDebounceFn(() => {
|
|||
return
|
||||
}
|
||||
|
||||
if (password.value.length < 8) {
|
||||
if (props.validateMinLength && password.value.length < 8) {
|
||||
isValid.value = t('user.auth.passwordNotMin')
|
||||
return
|
||||
}
|
||||
|
||||
if (password.value.length > 250) {
|
||||
if (props.validateMinLength && password.value.length > 250) {
|
||||
isValid.value = t('user.auth.passwordNotMax')
|
||||
return
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<BaseButton class="dropdown-item">
|
||||
<BaseButton
|
||||
class="dropdown-item"
|
||||
:class="{'is-disabled': disabled}"
|
||||
>
|
||||
<span
|
||||
v-if="icon"
|
||||
class="icon is-small"
|
||||
|
@ -21,6 +24,7 @@ import type {IconProp} from '@fortawesome/fontawesome-svg-core'
|
|||
export interface DropDownItemProps extends /* @vue-ignore */ BaseButtonProps {
|
||||
icon?: IconProp,
|
||||
iconClass?: object | string,
|
||||
disabled?: boolean,
|
||||
}
|
||||
|
||||
defineProps<DropDownItemProps>()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<slot
|
||||
name="trigger"
|
||||
:is-open="open"
|
||||
:is-open="openValue"
|
||||
:toggle="toggle"
|
||||
:close="close"
|
||||
/>
|
||||
|
@ -9,13 +9,13 @@
|
|||
ref="popup"
|
||||
class="popup"
|
||||
:class="{
|
||||
'is-open': open,
|
||||
'has-overflow': props.hasOverflow && open
|
||||
'is-open': openValue,
|
||||
'has-overflow': props.hasOverflow && openValue
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
name="content"
|
||||
:is-open="open"
|
||||
:is-open="openValue"
|
||||
:toggle="toggle"
|
||||
:close="close"
|
||||
/>
|
||||
|
@ -23,7 +23,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import {ref, watch} from 'vue'
|
||||
import {onClickOutside} from '@vueuse/core'
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -31,24 +31,35 @@ const props = defineProps({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const open = ref(false)
|
||||
watch(
|
||||
() => props.open,
|
||||
nowOpen => {
|
||||
openValue.value = nowOpen
|
||||
},
|
||||
)
|
||||
|
||||
const openValue = ref(false)
|
||||
const popup = ref<HTMLElement | null>(null)
|
||||
|
||||
function close() {
|
||||
open.value = false
|
||||
openValue.value = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
open.value = !open.value
|
||||
openValue.value = !openValue.value
|
||||
}
|
||||
|
||||
onClickOutside(popup, () => {
|
||||
if (!open.value) {
|
||||
if (!openValue.value) {
|
||||
return
|
||||
}
|
||||
close()
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import FilterInput from '@/components/project/partials/FilterInput.vue'
|
||||
|
||||
function initState(value: string) {
|
||||
return {
|
||||
value,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Filter Input">
|
||||
<Variant
|
||||
title="With date values"
|
||||
:init-state="initState('dueDate < now && done = false && dueDate > now/w+1w')"
|
||||
>
|
||||
<template #default="{state}">
|
||||
<FilterInput v-model="state.value" />
|
||||
</template>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
|
@ -0,0 +1,354 @@
|
|||
<script setup lang="ts">
|
||||
import {computed, nextTick, ref, watch} from 'vue'
|
||||
import {useAutoHeightTextarea} from '@/composables/useAutoHeightTextarea'
|
||||
import DatepickerWithValues from '@/components/date/datepickerWithValues.vue'
|
||||
import UserService from '@/services/user'
|
||||
import AutocompleteDropdown from '@/components/input/AutocompleteDropdown.vue'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
import XLabel from '@/components/tasks/partials/label.vue'
|
||||
import User from '@/components/misc/user.vue'
|
||||
import ProjectUserService from '@/services/projectUsers'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {
|
||||
DATE_FIELDS,
|
||||
ASSIGNEE_FIELDS,
|
||||
AUTOCOMPLETE_FIELDS,
|
||||
AVAILABLE_FILTER_FIELDS,
|
||||
FILTER_JOIN_OPERATOR,
|
||||
FILTER_OPERATORS,
|
||||
FILTER_OPERATORS_REGEX, LABEL_FIELDS,
|
||||
} from '@/helpers/filters'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
projectId,
|
||||
} = defineProps<{
|
||||
modelValue: string,
|
||||
projectId?: number,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'blur'])
|
||||
|
||||
const filterQuery = ref<string>('')
|
||||
const {
|
||||
textarea: filterInput,
|
||||
height,
|
||||
} = useAutoHeightTextarea(filterQuery)
|
||||
|
||||
watch(
|
||||
() => modelValue,
|
||||
() => {
|
||||
filterQuery.value = modelValue
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => filterQuery.value,
|
||||
() => {
|
||||
if (filterQuery.value !== modelValue) {
|
||||
emit('update:modelValue', filterQuery.value)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const userService = new UserService()
|
||||
const projectUserService = new ProjectUserService()
|
||||
|
||||
function escapeHtml(unsafe: string): string {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function unEscapeHtml(unsafe: string): string {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, '\'')
|
||||
}
|
||||
|
||||
const highlightedFilterQuery = computed(() => {
|
||||
let highlighted = escapeHtml(filterQuery.value)
|
||||
DATE_FIELDS
|
||||
.forEach(o => {
|
||||
const pattern = new RegExp(o + '(\\s*)' + FILTER_OPERATORS_REGEX + '(\\s*)([\'"]?)([^\'"\\s]+\\1?)?', 'ig')
|
||||
highlighted = highlighted.replaceAll(pattern, (match, spacesBefore, token, spacesAfter, start, value, position) => {
|
||||
if (typeof value === 'undefined') {
|
||||
value = ''
|
||||
}
|
||||
|
||||
return `${o}${spacesBefore}${token}${spacesAfter}<button class="is-primary filter-query__date_value" data-position="${position}">${value}</button><span class="filter-query__date_value_placeholder">${value}</span>`
|
||||
})
|
||||
})
|
||||
ASSIGNEE_FIELDS
|
||||
.forEach(f => {
|
||||
const pattern = new RegExp(f + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*([\'"]?)([^\'"\\s]+\\1?)?', 'ig')
|
||||
highlighted = highlighted.replaceAll(pattern, (match, token, start, value) => {
|
||||
if (typeof value === 'undefined') {
|
||||
value = ''
|
||||
}
|
||||
|
||||
return `${f} ${token} <span class="filter-query__assignee_value">${value}<span>`
|
||||
})
|
||||
})
|
||||
FILTER_JOIN_OPERATOR
|
||||
.map(o => escapeHtml(o))
|
||||
.forEach(o => {
|
||||
highlighted = highlighted.replaceAll(o, `<span class="filter-query__join-operator">${o}</span>`)
|
||||
})
|
||||
LABEL_FIELDS
|
||||
.forEach(f => {
|
||||
const pattern = new RegExp(f + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*([\'"]?)([^\'"\\s]+\\1?)?', 'ig')
|
||||
highlighted = highlighted.replaceAll(pattern, (match, token, start, value) => {
|
||||
if (typeof value === 'undefined') {
|
||||
value = ''
|
||||
}
|
||||
|
||||
const label = labelStore.getLabelsByExactTitles([value])[0] || undefined
|
||||
|
||||
return `${f} ${token} <span class="filter-query__label_value" style="background-color: ${label?.hexColor}; color: ${label?.textColor}">${label?.title ?? value}<span>`
|
||||
})
|
||||
})
|
||||
FILTER_OPERATORS
|
||||
.map(o => ` ${escapeHtml(o)} `)
|
||||
.forEach(o => {
|
||||
highlighted = highlighted.replaceAll(o, `<span class="filter-query__operator">${o}</span>`)
|
||||
})
|
||||
AVAILABLE_FILTER_FIELDS.forEach(f => {
|
||||
highlighted = highlighted.replaceAll(f, `<span class="filter-query__field">${f}</span>`)
|
||||
})
|
||||
return highlighted
|
||||
})
|
||||
|
||||
const currentOldDatepickerValue = ref('')
|
||||
const currentDatepickerValue = ref('')
|
||||
const currentDatepickerPos = ref()
|
||||
const datePickerPopupOpen = ref(false)
|
||||
|
||||
watch(
|
||||
() => highlightedFilterQuery.value,
|
||||
async () => {
|
||||
await nextTick()
|
||||
document.querySelectorAll('button.filter-query__date_value')
|
||||
.forEach(b => {
|
||||
b.addEventListener('click', event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const button = event.target
|
||||
currentOldDatepickerValue.value = button?.innerText
|
||||
currentDatepickerValue.value = button?.innerText
|
||||
currentDatepickerPos.value = parseInt(button?.dataset.position)
|
||||
datePickerPopupOpen.value = true
|
||||
})
|
||||
})
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
function updateDateInQuery(newDate: string) {
|
||||
// Need to escape and unescape the query because the positions are based on the escaped query
|
||||
let escaped = escapeHtml(filterQuery.value)
|
||||
escaped = escaped
|
||||
.substring(0, currentDatepickerPos.value)
|
||||
+ escaped
|
||||
.substring(currentDatepickerPos.value)
|
||||
.replace(currentOldDatepickerValue.value, newDate)
|
||||
currentOldDatepickerValue.value = newDate
|
||||
filterQuery.value = unEscapeHtml(escaped)
|
||||
}
|
||||
|
||||
const autocompleteMatchPosition = ref(0)
|
||||
const autocompleteMatchText = ref('')
|
||||
const autocompleteResultType = ref<'labels' | 'assignees' | 'projects' | null>(null)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const autocompleteResults = ref<any[]>([])
|
||||
const labelStore = useLabelStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
function handleFieldInput() {
|
||||
const cursorPosition = filterInput.value.selectionStart
|
||||
const textUpToCursor = filterQuery.value.substring(0, cursorPosition)
|
||||
|
||||
AUTOCOMPLETE_FIELDS.forEach(field => {
|
||||
const pattern = new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()]+\\1?)?$', 'ig')
|
||||
const match = pattern.exec(textUpToCursor)
|
||||
|
||||
if (match !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [matched, prefix, operator, space, keyword] = match
|
||||
if (keyword) {
|
||||
if (matched.startsWith('label')) {
|
||||
autocompleteResultType.value = 'labels'
|
||||
autocompleteResults.value = labelStore.filterLabelsByQuery([], keyword)
|
||||
}
|
||||
if (matched.startsWith('assignee')) {
|
||||
autocompleteResultType.value = 'assignees'
|
||||
if (projectId) {
|
||||
projectUserService.getAll({projectId}, {s: keyword})
|
||||
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
|
||||
} else {
|
||||
userService.getAll({}, {s: keyword})
|
||||
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
|
||||
}
|
||||
}
|
||||
if (!projectId && matched.startsWith('project')) {
|
||||
autocompleteResultType.value = 'projects'
|
||||
autocompleteResults.value = projectStore.searchProject(keyword)
|
||||
}
|
||||
autocompleteMatchText.value = keyword
|
||||
autocompleteMatchPosition.value = prefix.length - 1
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function autocompleteSelect(value) {
|
||||
filterQuery.value = filterQuery.value.substring(0, autocompleteMatchPosition.value + 1) +
|
||||
(autocompleteResultType.value === 'labels'
|
||||
? value.title
|
||||
: value.username) +
|
||||
filterQuery.value.substring(autocompleteMatchPosition.value + autocompleteMatchText.value.length + 1)
|
||||
|
||||
autocompleteResults.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('filters.query.title') }}</label>
|
||||
<AutocompleteDropdown
|
||||
:options="autocompleteResults"
|
||||
@blur="filterInput?.blur()"
|
||||
@update:modelValue="autocompleteSelect"
|
||||
>
|
||||
<template
|
||||
#input="{ onKeydown, onFocusField }"
|
||||
>
|
||||
<div class="control filter-input">
|
||||
<textarea
|
||||
ref="filterInput"
|
||||
v-model="filterQuery"
|
||||
autocomplete="off"
|
||||
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
class="input"
|
||||
:class="{'has-autocomplete-results': autocompleteResults.length > 0}"
|
||||
:placeholder="$t('filters.query.placeholder')"
|
||||
@input="handleFieldInput"
|
||||
@focus="onFocusField"
|
||||
@keydown="onKeydown"
|
||||
@blur="e => emit('blur', e)"
|
||||
/>
|
||||
<div
|
||||
class="filter-input-highlight"
|
||||
:style="{'height': height}"
|
||||
v-html="highlightedFilterQuery"
|
||||
/>
|
||||
<DatepickerWithValues
|
||||
v-model="currentDatepickerValue"
|
||||
:open="datePickerPopupOpen"
|
||||
@close="() => datePickerPopupOpen = false"
|
||||
@update:modelValue="updateDateInQuery"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
#result="{ item }"
|
||||
>
|
||||
<XLabel
|
||||
v-if="autocompleteResultType === 'labels'"
|
||||
:label="item"
|
||||
/>
|
||||
<User
|
||||
v-else-if="autocompleteResultType === 'assignees'"
|
||||
:user="item"
|
||||
:avatar-size="25"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ item.title }}
|
||||
</template>
|
||||
</template>
|
||||
</AutocompleteDropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.filter-input-highlight {
|
||||
span {
|
||||
&.filter-query__field {
|
||||
color: var(--code-literal);
|
||||
}
|
||||
|
||||
&.filter-query__operator {
|
||||
color: var(--code-keyword);
|
||||
}
|
||||
|
||||
&.filter-query__join-operator {
|
||||
color: var(--code-section);
|
||||
}
|
||||
|
||||
&.filter-query__date_value_placeholder {
|
||||
display: inline-block;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&.filter-query__assignee_value, &.filter-query__label_value {
|
||||
border-radius: $radius;
|
||||
background-color: var(--grey-200);
|
||||
color: var(--grey-700);
|
||||
}
|
||||
}
|
||||
|
||||
button.filter-query__date_value {
|
||||
border-radius: $radius;
|
||||
position: absolute;
|
||||
margin-top: calc((0.25em - 0.125rem) * -1);
|
||||
height: 1.75rem;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter-input {
|
||||
position: relative;
|
||||
|
||||
textarea {
|
||||
position: absolute;
|
||||
background: transparent !important;
|
||||
resize: none;
|
||||
text-fill-color: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
|
||||
&::placeholder {
|
||||
text-fill-color: var(--input-placeholder-color);
|
||||
-webkit-text-fill-color: var(--input-placeholder-color);
|
||||
}
|
||||
|
||||
&.has-autocomplete-results {
|
||||
border-radius: var(--input-radius) var(--input-radius) 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-input-highlight {
|
||||
height: 2.5em;
|
||||
line-height: 1.5;
|
||||
padding: .5em .75em;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,80 @@
|
|||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
const showDocs = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseButton
|
||||
class="has-text-primary"
|
||||
@click="showDocs = !showDocs"
|
||||
>
|
||||
{{ $t('filters.query.help.link') }}
|
||||
</BaseButton>
|
||||
|
||||
<Transition>
|
||||
<div
|
||||
v-if="showDocs"
|
||||
class="content"
|
||||
>
|
||||
<p>{{ $t('filters.query.help.intro') }}</p>
|
||||
<ul>
|
||||
<li><code>done</code>: {{ $t('filters.query.help.fields.done') }}</li>
|
||||
<li><code>priority</code>: {{ $t('filters.query.help.fields.priority') }}</li>
|
||||
<li><code>percentDone</code>: {{ $t('filters.query.help.fields.percentDone') }}</li>
|
||||
<li><code>dueDate</code>: {{ $t('filters.query.help.fields.dueDate') }}</li>
|
||||
<li><code>startDate</code>: {{ $t('filters.query.help.fields.startDate') }}</li>
|
||||
<li><code>endDate</code>: {{ $t('filters.query.help.fields.endDate') }}</li>
|
||||
<li><code>doneAt</code>: {{ $t('filters.query.help.fields.doneAt') }}</li>
|
||||
<li><code>assignees</code>: {{ $t('filters.query.help.fields.assignees') }}</li>
|
||||
<li><code>labels</code>: {{ $t('filters.query.help.fields.labels') }}</li>
|
||||
<li><code>project</code>: {{ $t('filters.query.help.fields.project') }}</li>
|
||||
</ul>
|
||||
<p>{{ $t('filters.query.help.canUseDatemath') }}</p>
|
||||
<p>{{ $t('filters.query.help.operators.intro') }}</p>
|
||||
<ul>
|
||||
<li><code>!=</code>: {{ $t('filters.query.help.operators.notEqual') }}</li>
|
||||
<li><code>=</code>: {{ $t('filters.query.help.operators.equal') }}</li>
|
||||
<li><code>></code>: {{ $t('filters.query.help.operators.greaterThan') }}</li>
|
||||
<li><code>>=</code>: {{ $t('filters.query.help.operators.greaterThanOrEqual') }}</li>
|
||||
<li><code><</code>: {{ $t('filters.query.help.operators.lessThan') }}</li>
|
||||
<li><code><=</code>: {{ $t('filters.query.help.operators.lessThanOrEqual') }}</li>
|
||||
<li><code>like</code>: {{ $t('filters.query.help.operators.like') }}</li>
|
||||
<li><code>in</code>: {{ $t('filters.query.help.operators.in') }}</li>
|
||||
</ul>
|
||||
<p>{{ $t('filters.query.help.logicalOperators.intro') }}</p>
|
||||
<ul>
|
||||
<li><code>&&</code>: {{ $t('filters.query.help.logicalOperators.and') }}</li>
|
||||
<li><code>||</code>: {{ $t('filters.query.help.logicalOperators.or') }}</li>
|
||||
<li><code>(</code> and <code>)</code>: {{ $t('filters.query.help.logicalOperators.parentheses') }}</li>
|
||||
</ul>
|
||||
<p>{{ $t('filters.query.help.examples.intro') }}</p>
|
||||
<ul>
|
||||
<li><code>priority = 4</code>: {{ $t('filters.query.help.examples.priorityEqual') }}</li>
|
||||
<li><code>dueDate < now</code>: {{ $t('filters.query.help.examples.dueDatePast') }}</li>
|
||||
<li>
|
||||
<code>done = false && priority >= 3</code>:
|
||||
{{ $t('filters.query.help.examples.undoneHighPriority') }}
|
||||
</li>
|
||||
<li><code>assignees in [user1, user2]</code>: {{ $t('filters.query.help.examples.assigneesIn') }}</li>
|
||||
<li>
|
||||
<code>(priority = 1 || priority = 2) && dueDate <= now</code>:
|
||||
{{ $t('filters.query.help.examples.priorityOneOrTwoPastDue') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: all $transition-duration ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
</style>
|
|
@ -25,6 +25,8 @@
|
|||
v-model="value"
|
||||
:has-title="true"
|
||||
class="filter-popup"
|
||||
@update:modelValue="emitChanges"
|
||||
@showResultsButtonClicked="() => modalOpen = false"
|
||||
/>
|
||||
</modal>
|
||||
</template>
|
||||
|
@ -34,47 +36,38 @@ import {computed, ref, watch} from 'vue'
|
|||
|
||||
import Filters from '@/components/project/partials/filters.vue'
|
||||
|
||||
import {getDefaultParams} from '@/composables/useTaskList'
|
||||
import {getDefaultTaskFilterParams, type TaskFilterParams} from '@/services/taskCollection'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const modelValue = defineModel<TaskFilterParams>({})
|
||||
|
||||
const value = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
if(props.modelValue === value) {
|
||||
return
|
||||
}
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
const value = ref<TaskFilterParams>({})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(modelValue) => {
|
||||
() => modelValue.value,
|
||||
(modelValue: TaskFilterParams) => {
|
||||
value.value = modelValue
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
function emitChanges(newValue: TaskFilterParams) {
|
||||
if (modelValue.value?.filter === newValue.filter && modelValue.value?.s === newValue.s) {
|
||||
return
|
||||
}
|
||||
|
||||
modelValue.value.filter = newValue.filter
|
||||
modelValue.value.s = newValue.s
|
||||
}
|
||||
|
||||
const hasFilters = computed(() => {
|
||||
// this.value also contains the page parameter which we don't want to include in filters
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {filter_by, filter_value, filter_comparator, filter_concat, s} = value.value
|
||||
const def = {...getDefaultParams()}
|
||||
const {filter, s} = value.value
|
||||
const def = {...getDefaultTaskFilterParams()}
|
||||
|
||||
const params = {filter_by, filter_value, filter_comparator, filter_concat, s}
|
||||
const params = {filter, s}
|
||||
const defaultParams = {
|
||||
filter_by: def.filter_by,
|
||||
filter_value: def.filter_value,
|
||||
filter_comparator: def.filter_comparator,
|
||||
filter_concat: def.filter_concat,
|
||||
filter: def.filter,
|
||||
s: s ? def.s : undefined,
|
||||
}
|
||||
|
||||
|
@ -84,7 +77,7 @@ const hasFilters = computed(() => {
|
|||
const modalOpen = ref(false)
|
||||
|
||||
function clearFilters() {
|
||||
value.value = {...getDefaultParams()}
|
||||
value.value = {...getDefaultTaskFilterParams()}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -2,195 +2,35 @@
|
|||
<card
|
||||
class="filters has-overflow"
|
||||
:title="hasTitle ? $t('filters.title') : ''"
|
||||
role="search"
|
||||
>
|
||||
<FilterInput
|
||||
v-model="params.filter"
|
||||
:project-id="projectId"
|
||||
@blur="change()"
|
||||
/>
|
||||
|
||||
<div class="field is-flex is-flex-direction-column">
|
||||
<Fancycheckbox
|
||||
v-model="params.filter_include_nulls"
|
||||
@update:modelValue="change()"
|
||||
@blur="change()"
|
||||
>
|
||||
{{ $t('filters.attributes.includeNulls') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox
|
||||
v-model="filters.requireAllFilters"
|
||||
@update:modelValue="setFilterConcat()"
|
||||
>
|
||||
{{ $t('filters.attributes.requireAll') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox
|
||||
v-model="filters.done"
|
||||
@update:modelValue="setDoneFilter"
|
||||
>
|
||||
{{ $t('filters.attributes.showDoneTasks') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox
|
||||
v-if="!['project.kanban', 'project.table'].includes($route.name as string)"
|
||||
v-model="sortAlphabetically"
|
||||
@update:modelValue="change()"
|
||||
>
|
||||
{{ $t('filters.attributes.sortAlphabetically') }}
|
||||
</Fancycheckbox>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('misc.search') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
v-model="params.s"
|
||||
class="input"
|
||||
:placeholder="$t('misc.search')"
|
||||
@blur="change()"
|
||||
@keyup.enter="change()"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.priority') }}</label>
|
||||
<div class="control single-value-control">
|
||||
<PrioritySelect
|
||||
v-model.number="filters.priority"
|
||||
:disabled="!filters.usePriority || undefined"
|
||||
@update:modelValue="setPriority"
|
||||
/>
|
||||
<Fancycheckbox
|
||||
v-model="filters.usePriority"
|
||||
@update:modelValue="setPriority"
|
||||
>
|
||||
{{ $t('filters.attributes.enablePriority') }}
|
||||
</Fancycheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.percentDone') }}</label>
|
||||
<div class="control single-value-control">
|
||||
<PercentDoneSelect
|
||||
v-model.number="filters.percentDone"
|
||||
:disabled="!filters.usePercentDone || undefined"
|
||||
@update:modelValue="setPercentDoneFilter"
|
||||
/>
|
||||
<Fancycheckbox
|
||||
v-model="filters.usePercentDone"
|
||||
@update:modelValue="setPercentDoneFilter"
|
||||
>
|
||||
{{ $t('filters.attributes.enablePercentDone') }}
|
||||
</Fancycheckbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.dueDate') }}</label>
|
||||
<div class="control">
|
||||
<DatepickerWithRange
|
||||
v-model="filters.dueDate"
|
||||
@update:modelValue="values => setDateFilter('due_date', values)"
|
||||
>
|
||||
<template #trigger="{toggle, buttonText}">
|
||||
<x-button
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
class="mb-2"
|
||||
@click.prevent.stop="toggle()"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</x-button>
|
||||
</template>
|
||||
</DatepickerWithRange>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.startDate') }}</label>
|
||||
<div class="control">
|
||||
<DatepickerWithRange
|
||||
v-model="filters.startDate"
|
||||
@update:modelValue="values => setDateFilter('start_date', values)"
|
||||
>
|
||||
<template #trigger="{toggle, buttonText}">
|
||||
<x-button
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
class="mb-2"
|
||||
@click.prevent.stop="toggle()"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</x-button>
|
||||
</template>
|
||||
</DatepickerWithRange>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.endDate') }}</label>
|
||||
<div class="control">
|
||||
<DatepickerWithRange
|
||||
v-model="filters.endDate"
|
||||
@update:modelValue="values => setDateFilter('end_date', values)"
|
||||
>
|
||||
<template #trigger="{toggle, buttonText}">
|
||||
<x-button
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
class="mb-2"
|
||||
@click.prevent.stop="toggle()"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</x-button>
|
||||
</template>
|
||||
</DatepickerWithRange>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.reminders') }}</label>
|
||||
<div class="control">
|
||||
<DatepickerWithRange
|
||||
v-model="filters.reminders"
|
||||
@update:modelValue="values => setDateFilter('reminders', values)"
|
||||
>
|
||||
<template #trigger="{toggle, buttonText}">
|
||||
<x-button
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
class="mb-2"
|
||||
@click.prevent.stop="toggle()"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</x-button>
|
||||
</template>
|
||||
</DatepickerWithRange>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.assignees') }}</label>
|
||||
<div class="control">
|
||||
<SelectUser
|
||||
v-model="entities.users"
|
||||
@select="changeMultiselectFilter('users', 'assignees')"
|
||||
@remove="changeMultiselectFilter('users', 'assignees')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('task.attributes.labels') }}</label>
|
||||
<div class="control labels-list">
|
||||
<EditLabels
|
||||
v-model="entities.labels"
|
||||
:creatable="false"
|
||||
@update:modelValue="changeLabelFilter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FilterInputDocs />
|
||||
|
||||
<template
|
||||
v-if="['filters.create', 'project.edit', 'filter.settings.edit'].includes($route.name as string)"
|
||||
v-if="hasFooter"
|
||||
#footer
|
||||
>
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('project.projects') }}</label>
|
||||
<div class="control">
|
||||
<SelectProject
|
||||
v-model="entities.projects"
|
||||
:project-filter="p => p.id > 0"
|
||||
@select="changeMultiselectFilter('projects', 'project_id')"
|
||||
@remove="changeMultiselectFilter('projects', 'project_id')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<x-button
|
||||
variant="primary"
|
||||
@click.prevent.stop="changeAndEmitButton"
|
||||
>
|
||||
{{ $t('filters.showResults') }}
|
||||
</x-button>
|
||||
</template>
|
||||
</card>
|
||||
</template>
|
||||
|
@ -200,419 +40,94 @@ export const ALPHABETICAL_SORT = 'title'
|
|||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, nextTick, onMounted, reactive, ref, shallowReactive, toRefs} from 'vue'
|
||||
import {camelCase} from 'camel-case'
|
||||
import {computed, ref} from 'vue'
|
||||
import {watchDebounced} from '@vueuse/core'
|
||||
|
||||
import type {ILabel} from '@/modelTypes/ILabel'
|
||||
import type {IUser} from '@/modelTypes/IUser'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
|
||||
import DatepickerWithRange from '@/components/date/datepickerWithRange.vue'
|
||||
import PrioritySelect from '@/components/tasks/partials/prioritySelect.vue'
|
||||
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect.vue'
|
||||
import EditLabels from '@/components/tasks/partials/editLabels.vue'
|
||||
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
|
||||
import SelectUser from '@/components/input/SelectUser.vue'
|
||||
import SelectProject from '@/components/input/SelectProject.vue'
|
||||
import FilterInput from '@/components/project/partials/FilterInput.vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import type {TaskFilterParams} from '@/services/taskCollection'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {FILTER_OPERATORS, transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
|
||||
import FilterInputDocs from '@/components/project/partials/FilterInputDocs.vue'
|
||||
|
||||
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||
import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
|
||||
import {objectToSnakeCase} from '@/helpers/case'
|
||||
const {
|
||||
hasTitle= false,
|
||||
hasFooter = true,
|
||||
modelValue,
|
||||
} = defineProps<{
|
||||
hasTitle?: boolean,
|
||||
hasFooter?: boolean,
|
||||
modelValue: TaskFilterParams,
|
||||
}>()
|
||||
|
||||
import UserService from '@/services/user'
|
||||
import ProjectService from '@/services/project'
|
||||
const emit = defineEmits(['update:modelValue', 'showResultsButtonClicked'])
|
||||
|
||||
// FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS
|
||||
import {getDefaultParams} from '@/composables/useTaskList'
|
||||
const route = useRoute()
|
||||
const projectId = computed(() => {
|
||||
if (route.name?.startsWith('project.')) {
|
||||
return Number(route.params.projectId)
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
required: true,
|
||||
},
|
||||
hasTitle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
return undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in taskProject.js
|
||||
const DEFAULT_PARAMS = {
|
||||
const params = ref<TaskFilterParams>({
|
||||
sort_by: [],
|
||||
order_by: [],
|
||||
filter_by: [],
|
||||
filter_value: [],
|
||||
filter_comparator: [],
|
||||
filter_include_nulls: true,
|
||||
filter_concat: 'or',
|
||||
filter: '',
|
||||
filter_include_nulls: false,
|
||||
s: '',
|
||||
} as const
|
||||
|
||||
const DEFAULT_FILTERS = {
|
||||
done: false,
|
||||
dueDate: '',
|
||||
requireAllFilters: false,
|
||||
priority: 0,
|
||||
usePriority: false,
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
percentDone: 0,
|
||||
usePercentDone: false,
|
||||
reminders: '',
|
||||
assignees: '',
|
||||
labels: '',
|
||||
project_id: '',
|
||||
} as const
|
||||
|
||||
const {modelValue} = toRefs(props)
|
||||
|
||||
const labelStore = useLabelStore()
|
||||
|
||||
const params = ref({...DEFAULT_PARAMS})
|
||||
const filters = ref({...DEFAULT_FILTERS})
|
||||
|
||||
const services = {
|
||||
users: shallowReactive(new UserService()),
|
||||
projects: shallowReactive(new ProjectService()),
|
||||
}
|
||||
|
||||
interface Entities {
|
||||
users: IUser[]
|
||||
labels: ILabel[]
|
||||
projects: IProject[]
|
||||
}
|
||||
|
||||
type EntityType = 'users' | 'labels' | 'projects'
|
||||
|
||||
const entities: Entities = reactive({
|
||||
users: [],
|
||||
labels: [],
|
||||
projects: [],
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
filters.value.requireAllFilters = params.value.filter_concat === 'and'
|
||||
})
|
||||
|
||||
// Using watchDebounced to prevent the filter re-triggering itself.
|
||||
// FIXME: Only here until this whole component changes a lot with the new filter syntax.
|
||||
watchDebounced(
|
||||
modelValue,
|
||||
(value) => {
|
||||
// FIXME: filters should only be converted to snake case in the last moment
|
||||
params.value = objectToSnakeCase(value)
|
||||
prepareFilters()
|
||||
() => modelValue,
|
||||
(value: TaskFilterParams) => {
|
||||
const val = {...value}
|
||||
val.filter = transformFilterStringFromApi(
|
||||
val?.filter || '',
|
||||
labelId => labelStore.getLabelById(labelId)?.title,
|
||||
projectId => projectStore.projects.value[projectId]?.title || null,
|
||||
)
|
||||
params.value = val
|
||||
},
|
||||
{immediate: true, debounce: 500, maxWait: 1000},
|
||||
)
|
||||
|
||||
const sortAlphabetically = computed({
|
||||
get() {
|
||||
return params.value?.sort_by?.find(sortBy => sortBy === ALPHABETICAL_SORT) !== undefined
|
||||
},
|
||||
set(sortAlphabetically) {
|
||||
params.value.sort_by = sortAlphabetically
|
||||
? [ALPHABETICAL_SORT]
|
||||
: getDefaultParams().sort_by
|
||||
|
||||
change()
|
||||
},
|
||||
})
|
||||
const labelStore = useLabelStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
function change() {
|
||||
const newParams = {...params.value}
|
||||
newParams.filter_value = newParams.filter_value.map(v => v instanceof Date ? v.toISOString() : v)
|
||||
const filter = transformFilterStringForApi(
|
||||
params.value.filter,
|
||||
labelTitle => labelStore.filterLabelsByQuery([], labelTitle)[0]?.id || null,
|
||||
projectTitle => projectStore.searchProject(projectTitle)[0]?.id || null,
|
||||
)
|
||||
|
||||
let s = ''
|
||||
|
||||
// When the filter does not contain any filter tokens, assume a simple search and redirect the input
|
||||
const hasFilterQueries = FILTER_OPERATORS.find(o => filter.includes(o)) || false
|
||||
if (!hasFilterQueries) {
|
||||
s = filter
|
||||
}
|
||||
|
||||
const newParams = {
|
||||
...params.value,
|
||||
filter: s === '' ? filter : '',
|
||||
s,
|
||||
}
|
||||
|
||||
if (JSON.stringify(modelValue) === JSON.stringify(newParams)) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('update:modelValue', newParams)
|
||||
}
|
||||
|
||||
function prepareFilters() {
|
||||
prepareDone()
|
||||
prepareDate('due_date', 'dueDate')
|
||||
prepareDate('start_date', 'startDate')
|
||||
prepareDate('end_date', 'endDate')
|
||||
prepareSingleValue('priority', 'priority', 'usePriority', true)
|
||||
prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
|
||||
prepareDate('reminders', 'reminders')
|
||||
prepareRelatedObjectFilter('users', 'assignees')
|
||||
prepareProjectsFilter()
|
||||
|
||||
prepareSingleValue('labels')
|
||||
|
||||
const newLabels = typeof filters.value.labels === 'string'
|
||||
? filters.value.labels
|
||||
: ''
|
||||
const labelIds = newLabels.split(',').map(i => parseInt(i))
|
||||
|
||||
entities.labels = labelStore.getLabelsByIds(labelIds)
|
||||
}
|
||||
|
||||
function removePropertyFromFilter(filterName) {
|
||||
// Because of the way arrays work, we can only ever remove one element at once.
|
||||
// To remove multiple filter elements of the same name this function has to be called multiple times.
|
||||
for (const i in params.value.filter_by) {
|
||||
if (params.value.filter_by[i] === filterName) {
|
||||
params.value.filter_by.splice(i, 1)
|
||||
params.value.filter_comparator.splice(i, 1)
|
||||
params.value.filter_value.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setDateFilter(filterName, {dateFrom, dateTo}) {
|
||||
dateFrom = parseDateOrString(dateFrom, null)
|
||||
dateTo = parseDateOrString(dateTo, null)
|
||||
|
||||
// Only filter if we have a date
|
||||
if (dateFrom !== null && dateTo !== null) {
|
||||
|
||||
// Check if we already have values in params and only update them if we do
|
||||
let foundStart = false
|
||||
let foundEnd = false
|
||||
params.value.filter_by.forEach((f, i) => {
|
||||
if (f === filterName && params.value.filter_comparator[i] === 'greater_equals') {
|
||||
foundStart = true
|
||||
params.value.filter_value[i] = dateFrom
|
||||
}
|
||||
if (f === filterName && params.value.filter_comparator[i] === 'less_equals') {
|
||||
foundEnd = true
|
||||
params.value.filter_value[i] = dateTo
|
||||
}
|
||||
})
|
||||
|
||||
if (!foundStart) {
|
||||
params.value.filter_by.push(filterName)
|
||||
params.value.filter_comparator.push('greater_equals')
|
||||
params.value.filter_value.push(dateFrom)
|
||||
}
|
||||
if (!foundEnd) {
|
||||
params.value.filter_by.push(filterName)
|
||||
params.value.filter_comparator.push('less_equals')
|
||||
params.value.filter_value.push(dateTo)
|
||||
}
|
||||
|
||||
filters.value[camelCase(filterName)] = {
|
||||
// Passing the dates as string values avoids an endless loop between values changing
|
||||
// in the datepicker (bubbling up to here) and changing here and bubbling down to the
|
||||
// datepicker (because there's a new date instance every time this function gets called).
|
||||
// See https://kolaente.dev/vikunja/frontend/issues/2384
|
||||
dateFrom: dateIsValid(dateFrom) ? formatISO(dateFrom) : dateFrom,
|
||||
dateTo: dateIsValid(dateTo) ? formatISO(dateTo) : dateTo,
|
||||
}
|
||||
change()
|
||||
return
|
||||
}
|
||||
|
||||
removePropertyFromFilter(filterName)
|
||||
removePropertyFromFilter(filterName)
|
||||
function changeAndEmitButton() {
|
||||
change()
|
||||
}
|
||||
|
||||
function prepareDate(filterName: string, variableName: 'dueDate' | 'startDate' | 'endDate' | 'reminders') {
|
||||
if (typeof params.value.filter_by === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
let foundDateStart: boolean | string = false
|
||||
let foundDateEnd: boolean | string = false
|
||||
for (const i in params.value.filter_by) {
|
||||
if (params.value.filter_by[i] === filterName && params.value.filter_comparator[i] === 'greater_equals') {
|
||||
foundDateStart = i
|
||||
}
|
||||
if (params.value.filter_by[i] === filterName && params.value.filter_comparator[i] === 'less_equals') {
|
||||
foundDateEnd = i
|
||||
}
|
||||
|
||||
if (foundDateStart !== false && foundDateEnd !== false) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (foundDateStart !== false && foundDateEnd !== false) {
|
||||
const startDate = new Date(params.value.filter_value[foundDateStart])
|
||||
const endDate = new Date(params.value.filter_value[foundDateEnd])
|
||||
filters.value[variableName] = {
|
||||
dateFrom: !isNaN(startDate)
|
||||
? `${startDate.getUTCFullYear()}-${startDate.getUTCMonth() + 1}-${startDate.getUTCDate()}`
|
||||
: params.value.filter_value[foundDateStart],
|
||||
dateTo: !isNaN(endDate)
|
||||
? `${endDate.getUTCFullYear()}-${endDate.getUTCMonth() + 1}-${endDate.getUTCDate()}`
|
||||
: params.value.filter_value[foundDateEnd],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSingleValueFilter(filterName, variableName, useVariableName = '', comparator = 'equals') {
|
||||
if (useVariableName !== '' && !filters.value[useVariableName]) {
|
||||
removePropertyFromFilter(filterName)
|
||||
return
|
||||
}
|
||||
|
||||
let found = false
|
||||
params.value.filter_by.forEach((f, i) => {
|
||||
if (f === filterName) {
|
||||
found = true
|
||||
params.value.filter_value[i] = filters.value[variableName]
|
||||
}
|
||||
})
|
||||
|
||||
if (!found) {
|
||||
params.value.filter_by.push(filterName)
|
||||
params.value.filter_comparator.push(comparator)
|
||||
params.value.filter_value.push(filters.value[variableName])
|
||||
}
|
||||
|
||||
change()
|
||||
}
|
||||
|
||||
function prepareSingleValue(
|
||||
/** The filter name in the api. */
|
||||
filterName,
|
||||
/** The name of the variable in filters ref. */
|
||||
variableName = null,
|
||||
/** The name of the variable of the "Use this filter" variable. Will only be set if the parameter is not null. */
|
||||
useVariableName = null,
|
||||
/** Toggles if the value should be parsed as a number. */
|
||||
isNumber = false,
|
||||
) {
|
||||
if (variableName === null) {
|
||||
variableName = filterName
|
||||
}
|
||||
|
||||
let found = false
|
||||
for (const i in params.value.filter_by) {
|
||||
if (params.value.filter_by[i] === filterName) {
|
||||
found = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (found === false && useVariableName !== null) {
|
||||
filters.value[useVariableName] = false
|
||||
return
|
||||
}
|
||||
|
||||
if (isNumber) {
|
||||
filters.value[variableName] = Number(params.value.filter_value[found])
|
||||
} else {
|
||||
filters.value[variableName] = params.value.filter_value[found]
|
||||
}
|
||||
|
||||
if (useVariableName !== null) {
|
||||
filters.value[useVariableName] = true
|
||||
}
|
||||
}
|
||||
|
||||
function prepareDone() {
|
||||
// Set filters.done based on params
|
||||
if (typeof params.value.filter_by === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
filters.value.done = params.value.filter_by.some((f) => f === 'done') === false
|
||||
}
|
||||
|
||||
async function prepareRelatedObjectFilter(kind: EntityType, filterName = null, servicePrefix: Omit<EntityType, 'labels'> | null = null) {
|
||||
if (filterName === null) {
|
||||
filterName = kind
|
||||
}
|
||||
|
||||
if (servicePrefix === null) {
|
||||
servicePrefix = kind
|
||||
}
|
||||
|
||||
prepareSingleValue(filterName)
|
||||
if (typeof filters.value[filterName] === 'undefined' || filters.value[filterName] === '') {
|
||||
return
|
||||
}
|
||||
|
||||
// Don't load things if we already have something loaded.
|
||||
// This is not the most ideal solution because it prevents a re-population when filters are changed
|
||||
// from the outside. It is still fine because we're not changing them from the outside, other than
|
||||
// loading them initially.
|
||||
if (entities[kind].length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
entities[kind] = await services[servicePrefix].getAll({}, {s: filters.value[filterName]})
|
||||
}
|
||||
|
||||
async function prepareProjectsFilter() {
|
||||
await prepareRelatedObjectFilter('projects', 'project_id')
|
||||
entities.projects = entities.projects.filter(p => p.id > 0)
|
||||
}
|
||||
|
||||
function setDoneFilter() {
|
||||
if (filters.value.done) {
|
||||
removePropertyFromFilter('done')
|
||||
} else {
|
||||
params.value.filter_by.push('done')
|
||||
params.value.filter_comparator.push('equals')
|
||||
params.value.filter_value.push('false')
|
||||
}
|
||||
change()
|
||||
}
|
||||
|
||||
function setFilterConcat() {
|
||||
if (filters.value.requireAllFilters) {
|
||||
params.value.filter_concat = 'and'
|
||||
} else {
|
||||
params.value.filter_concat = 'or'
|
||||
}
|
||||
change()
|
||||
}
|
||||
|
||||
function setPriority() {
|
||||
setSingleValueFilter('priority', 'priority', 'usePriority')
|
||||
}
|
||||
|
||||
function setPercentDoneFilter() {
|
||||
setSingleValueFilter('percent_done', 'percentDone', 'usePercentDone')
|
||||
}
|
||||
|
||||
async function changeMultiselectFilter(kind: EntityType, filterName) {
|
||||
await nextTick()
|
||||
|
||||
if (entities[kind].length === 0) {
|
||||
removePropertyFromFilter(filterName)
|
||||
change()
|
||||
return
|
||||
}
|
||||
|
||||
const ids = entities[kind].map(u => kind === 'users' ? u.username : u.id)
|
||||
|
||||
filters.value[filterName] = ids.join(',')
|
||||
setSingleValueFilter(filterName, filterName, '', 'in')
|
||||
}
|
||||
|
||||
function changeLabelFilter() {
|
||||
if (entities.labels.length === 0) {
|
||||
removePropertyFromFilter('labels')
|
||||
change()
|
||||
return
|
||||
}
|
||||
|
||||
const labelIDs = entities.labels.map(u => u.id)
|
||||
filters.value.labels = labelIDs.join(',')
|
||||
setSingleValueFilter('labels', 'labels', '', 'in')
|
||||
emit('showResultsButtonClicked')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.single-value-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.fancycheckbox {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.datepicker-with-range-container .popup) {
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -67,8 +67,10 @@
|
|||
{{ $t('menu.duplicate') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-tooltip="isDefaultProject ? $t('menu.cantArchiveIsDefault') : ''"
|
||||
:to="{ name: 'project.settings.archive', params: { projectId: project.id } }"
|
||||
icon="archive"
|
||||
:disabled="isDefaultProject"
|
||||
>
|
||||
{{ $t('menu.archive') }}
|
||||
</DropdownItem>
|
||||
|
@ -95,9 +97,11 @@
|
|||
{{ $t('menu.createProject') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-tooltip="isDefaultProject ? $t('menu.cantDeleteIsDefault') : ''"
|
||||
:to="{ name: 'project.settings.delete', params: { projectId: project.id } }"
|
||||
icon="trash-alt"
|
||||
class="has-text-danger"
|
||||
:disabled="isDefaultProject"
|
||||
>
|
||||
{{ $t('menu.delete') }}
|
||||
</DropdownItem>
|
||||
|
@ -106,7 +110,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, watchEffect, type PropType} from 'vue'
|
||||
import {computed, type PropType, ref, watchEffect} from 'vue'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
|
@ -118,6 +122,7 @@ import type {ISubscription} from '@/modelTypes/ISubscription'
|
|||
import {isSavedFilter} from '@/services/savedFilter'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
|
@ -146,4 +151,7 @@ function setSubscriptionInStore(sub: ISubscription) {
|
|||
}
|
||||
projectStore.setProject(updatedProject)
|
||||
}
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const isDefaultProject = computed(() => props.project?.id === authStore.settings.defaultProjectId)
|
||||
</script>
|
|
@ -17,6 +17,7 @@
|
|||
bar-end="endDate"
|
||||
:grid="true"
|
||||
:width="ganttChartWidth"
|
||||
:color-scheme="GANTT_COLOR_SCHEME"
|
||||
@dragendBar="updateGanttTask"
|
||||
@dblclickBar="openTask"
|
||||
>
|
||||
|
@ -59,7 +60,7 @@ import {
|
|||
extendDayjs,
|
||||
GGanttChart,
|
||||
GGanttRow,
|
||||
type GanttBarObject,
|
||||
type GanttBarObject, type ColorScheme,
|
||||
} from '@infectoone/vue-ganttastic'
|
||||
|
||||
import Loading from '@/components/misc/loading.vue'
|
||||
|
@ -113,6 +114,16 @@ const ganttChartWidth = computed(() => {
|
|||
|
||||
const ganttBars = ref<GanttBarObject[][]>([])
|
||||
|
||||
const GANTT_COLOR_SCHEME: ColorScheme = {
|
||||
primary: 'var(--grey-100)',
|
||||
secondary: 'var(--grey-300)',
|
||||
ternary: 'var(--grey-500)',
|
||||
quartenary: 'var(--grey-600)',
|
||||
hoverHighlight: 'var(--grey-700)',
|
||||
text: 'var(--grey-800)',
|
||||
background: 'var(--white)',
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ganttBars when tasks change
|
||||
*/
|
||||
|
|
|
@ -77,7 +77,7 @@ const props = defineProps({
|
|||
const emit = defineEmits(['taskAdded'])
|
||||
|
||||
const newTaskTitle = ref('')
|
||||
const newTaskInput = useAutoHeightTextarea(newTaskTitle)
|
||||
const {textarea: newTaskInput} = useAutoHeightTextarea(newTaskTitle)
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const authStore = useAuthStore()
|
||||
|
|
|
@ -59,7 +59,7 @@ const hasDelete = computed(() => typeof remove !== 'undefined' && !disabled)
|
|||
}
|
||||
|
||||
&:hover .assignee:not(:first-child) {
|
||||
margin-left: -1rem;
|
||||
margin-left: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@ const hasDelete = computed(() => typeof remove !== 'undefined' && !disabled)
|
|||
transition: all $transition;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: -1.5rem;
|
||||
margin-left: -1rem;
|
||||
}
|
||||
|
||||
:deep(.user img) {
|
||||
|
|
|
@ -27,7 +27,8 @@ defineProps({
|
|||
display: inline;
|
||||
|
||||
:deep(.tag) {
|
||||
margin-bottom: .25rem;
|
||||
margin-top: .125rem;
|
||||
margin-bottom: .125rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -103,7 +103,7 @@ 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 Labels from '@/components/tasks/partials/labels.vue'
|
||||
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
|
||||
|
||||
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||
|
@ -197,6 +197,7 @@ const project = computed(() => projectStore.projects[task.projectId])
|
|||
span.parent-tasks {
|
||||
color: var(--grey-500);
|
||||
width: auto;
|
||||
margin-left: .25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,6 +6,7 @@ import {debouncedWatch, tryOnMounted, useWindowSize, type MaybeRef} from '@vueus
|
|||
export function useAutoHeightTextarea(value: MaybeRef<string>) {
|
||||
const textarea = ref<HTMLTextAreaElement | null>(null)
|
||||
const minHeight = ref(0)
|
||||
const height = ref('')
|
||||
|
||||
// adapted from https://github.com/LeaVerou/stretchy/blob/47f5f065c733029acccb755cae793009645809e2/src/stretchy.js#L34
|
||||
function resize(textareaEl: HTMLTextAreaElement | null) {
|
||||
|
@ -19,18 +20,17 @@ export function useAutoHeightTextarea(value: MaybeRef<string>) {
|
|||
textareaEl.value = textareaEl.placeholder
|
||||
}
|
||||
|
||||
const cs = getComputedStyle(textareaEl)
|
||||
// const cs = getComputedStyle(textareaEl)
|
||||
|
||||
textareaEl.style.minHeight = ''
|
||||
textareaEl.style.height = '0'
|
||||
const offset = textareaEl.offsetHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom)
|
||||
const height = textareaEl.scrollHeight + offset + 'px'
|
||||
height.value = textareaEl.scrollHeight + 'px'
|
||||
|
||||
textareaEl.style.height = height
|
||||
textareaEl.style.height = height.value
|
||||
|
||||
// calculate min-height for the first time
|
||||
if (!minHeight.value) {
|
||||
minHeight.value = parseFloat(height)
|
||||
minHeight.value = parseFloat(height.value)
|
||||
}
|
||||
|
||||
textareaEl.style.minHeight = minHeight.value.toString()
|
||||
|
@ -68,5 +68,8 @@ export function useAutoHeightTextarea(value: MaybeRef<string>) {
|
|||
},
|
||||
)
|
||||
|
||||
return textarea
|
||||
return {
|
||||
textarea,
|
||||
height,
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ import {ref, shallowReactive, watch, computed, type ComputedGetter} from 'vue'
|
|||
import {useRoute} from 'vue-router'
|
||||
import {useRouteQuery} from '@vueuse/router'
|
||||
|
||||
import TaskCollectionService from '@/services/taskCollection'
|
||||
import TaskCollectionService, {getDefaultTaskFilterParams} from '@/services/taskCollection'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import {error} from '@/message'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
@ -24,16 +24,6 @@ export interface SortBy {
|
|||
done_at?: Order,
|
||||
}
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in filters.vue
|
||||
export const getDefaultParams = () => ({
|
||||
sort_by: ['position', 'id'],
|
||||
order_by: ['asc', 'desc'],
|
||||
filter_by: ['done'],
|
||||
filter_value: ['false'],
|
||||
filter_comparator: ['equals'],
|
||||
filter_concat: 'and',
|
||||
})
|
||||
|
||||
const SORT_BY_DEFAULT: SortBy = {
|
||||
id: 'desc',
|
||||
}
|
||||
|
@ -67,7 +57,7 @@ export function useTaskList(projectIdGetter: ComputedGetter<IProject['id']>, sor
|
|||
|
||||
const projectId = computed(() => projectIdGetter())
|
||||
|
||||
const params = ref({...getDefaultParams()})
|
||||
const params = ref({...getDefaultTaskFilterParams()})
|
||||
|
||||
const search = ref('')
|
||||
const page = useRouteQuery('page', '1', { transform: Number })
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
import {describe, expect, it} from 'vitest'
|
||||
import {transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
|
||||
|
||||
const nullTitleToIdResolver = (title: string) => null
|
||||
const nullIdToTitleResolver = (id: number) => null
|
||||
describe('Filter Transformation', () => {
|
||||
|
||||
const fieldCases = {
|
||||
'done': 'done',
|
||||
'priority': 'priority',
|
||||
'percentDone': 'percent_done',
|
||||
'dueDate': 'due_date',
|
||||
'startDate': 'start_date',
|
||||
'endDate': 'end_date',
|
||||
'doneAt': 'done_at',
|
||||
'reminders': 'reminders',
|
||||
'assignees': 'assignees',
|
||||
'labels': 'labels',
|
||||
}
|
||||
|
||||
describe('For api', () => {
|
||||
for (const c in fieldCases) {
|
||||
it('should transform all filter params for ' + c + ' to snake_case', () => {
|
||||
const transformed = transformFilterStringForApi(c + ' = ipsum', nullTitleToIdResolver, nullTitleToIdResolver)
|
||||
|
||||
expect(transformed).toBe(fieldCases[c] + ' = ipsum')
|
||||
})
|
||||
}
|
||||
|
||||
it('should correctly resolve labels', () => {
|
||||
const transformed = transformFilterStringForApi(
|
||||
'labels = lorem',
|
||||
(title: string) => 1,
|
||||
nullTitleToIdResolver,
|
||||
)
|
||||
|
||||
expect(transformed).toBe('labels = 1')
|
||||
})
|
||||
|
||||
it('should correctly resolve multiple labels', () => {
|
||||
const transformed = transformFilterStringForApi(
|
||||
'labels = lorem && dueDate = now && labels = ipsum',
|
||||
(title: string) => {
|
||||
switch (title) {
|
||||
case 'lorem':
|
||||
return 1
|
||||
case 'ipsum':
|
||||
return 2
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
nullTitleToIdResolver,
|
||||
)
|
||||
|
||||
expect(transformed).toBe('labels = 1&& due_date = now && labels = 2')
|
||||
})
|
||||
|
||||
it('should correctly resolve projects', () => {
|
||||
const transformed = transformFilterStringForApi(
|
||||
'project = lorem',
|
||||
nullTitleToIdResolver,
|
||||
(title: string) => 1,
|
||||
)
|
||||
|
||||
expect(transformed).toBe('project = 1')
|
||||
})
|
||||
|
||||
it('should correctly resolve multiple projects', () => {
|
||||
const transformed = transformFilterStringForApi(
|
||||
'project = lorem && dueDate = now || project = ipsum',
|
||||
nullTitleToIdResolver,
|
||||
(title: string) => {
|
||||
switch (title) {
|
||||
case 'lorem':
|
||||
return 1
|
||||
case 'ipsum':
|
||||
return 2
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(transformed).toBe('project = 1&& due_date = now || project = 2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('To API', () => {
|
||||
for (const c in fieldCases) {
|
||||
it('should transform all filter params for ' + c + ' to snake_case', () => {
|
||||
const transformed = transformFilterStringFromApi(fieldCases[c] + ' = ipsum', nullTitleToIdResolver, nullTitleToIdResolver)
|
||||
|
||||
expect(transformed).toBe(c + ' = ipsum')
|
||||
})
|
||||
}
|
||||
|
||||
it('should correctly resolve labels', () => {
|
||||
const transformed = transformFilterStringFromApi(
|
||||
'labels = 1',
|
||||
(id: number) => 'lorem',
|
||||
nullIdToTitleResolver,
|
||||
)
|
||||
|
||||
expect(transformed).toBe('labels = lorem')
|
||||
})
|
||||
|
||||
it('should correctly resolve multiple labels', () => {
|
||||
const transformed = transformFilterStringFromApi(
|
||||
'labels = 1 && due_date = now && labels = 2',
|
||||
(id: number) => {
|
||||
switch (id) {
|
||||
case 1:
|
||||
return 'lorem'
|
||||
case 2:
|
||||
return 'ipsum'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
nullIdToTitleResolver,
|
||||
)
|
||||
|
||||
expect(transformed).toBe('labels = lorem&& dueDate = now && labels = ipsum')
|
||||
})
|
||||
|
||||
it('should correctly resolve projects', () => {
|
||||
const transformed = transformFilterStringFromApi(
|
||||
'project = 1',
|
||||
nullIdToTitleResolver,
|
||||
(id: number) => 'lorem',
|
||||
)
|
||||
|
||||
expect(transformed).toBe('project = lorem')
|
||||
})
|
||||
|
||||
it('should correctly resolve multiple projects', () => {
|
||||
const transformed = transformFilterStringFromApi(
|
||||
'project = lorem && due_date = now || project = ipsum',
|
||||
nullIdToTitleResolver,
|
||||
(id: number) => {
|
||||
switch (id) {
|
||||
case 1:
|
||||
return 'lorem'
|
||||
case 2:
|
||||
return 'ipsum'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
expect(transformed).toBe('project = lorem && dueDate = now || project = ipsum')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,164 @@
|
|||
import {snakeCase} from 'snake-case'
|
||||
|
||||
export const DATE_FIELDS = [
|
||||
'dueDate',
|
||||
'startDate',
|
||||
'endDate',
|
||||
'doneAt',
|
||||
'reminders',
|
||||
]
|
||||
|
||||
export const ASSIGNEE_FIELDS = [
|
||||
'assignees',
|
||||
]
|
||||
|
||||
export const LABEL_FIELDS = [
|
||||
'labels',
|
||||
]
|
||||
|
||||
export const PROJECT_FIELDS = [
|
||||
'project',
|
||||
]
|
||||
|
||||
export const AUTOCOMPLETE_FIELDS = [
|
||||
...LABEL_FIELDS,
|
||||
...ASSIGNEE_FIELDS,
|
||||
...PROJECT_FIELDS,
|
||||
]
|
||||
|
||||
export const AVAILABLE_FILTER_FIELDS = [
|
||||
'done',
|
||||
'priority',
|
||||
'percentDone',
|
||||
...DATE_FIELDS,
|
||||
...ASSIGNEE_FIELDS,
|
||||
...LABEL_FIELDS,
|
||||
]
|
||||
|
||||
export const FILTER_OPERATORS = [
|
||||
'!=',
|
||||
'=',
|
||||
'>',
|
||||
'>=',
|
||||
'<',
|
||||
'<=',
|
||||
'like',
|
||||
'in',
|
||||
'?=',
|
||||
]
|
||||
|
||||
export const FILTER_JOIN_OPERATOR = [
|
||||
'&&',
|
||||
'||',
|
||||
'(',
|
||||
')',
|
||||
]
|
||||
|
||||
export const FILTER_OPERATORS_REGEX = '(<|>|<=|>=|=|!=)'
|
||||
|
||||
function getFieldPattern(field: string): RegExp {
|
||||
return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()]+\\1?)?', 'ig')
|
||||
}
|
||||
|
||||
export function transformFilterStringForApi(
|
||||
filter: string,
|
||||
labelResolver: (title: string) => number | null,
|
||||
projectResolver: (title: string) => number | null,
|
||||
): string {
|
||||
|
||||
if (filter.trim() === '') {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Transform labels to ids
|
||||
LABEL_FIELDS.forEach(field => {
|
||||
const pattern = getFieldPattern(field)
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = pattern.exec(filter)) !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [matched, prefix, operator, space, keyword] = match
|
||||
if (keyword) {
|
||||
const labelId = labelResolver(keyword.trim())
|
||||
if (labelId !== null) {
|
||||
filter = filter.replace(keyword, String(labelId))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// Transform projects to ids
|
||||
PROJECT_FIELDS.forEach(field => {
|
||||
const pattern = getFieldPattern(field)
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = pattern.exec(filter)) !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [matched, prefix, operator, space, keyword] = match
|
||||
if (keyword) {
|
||||
const projectId = projectResolver(keyword.trim())
|
||||
if (projectId !== null) {
|
||||
filter = filter.replace(keyword, String(projectId))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Transform all attributes to snake case
|
||||
AVAILABLE_FILTER_FIELDS.forEach(f => {
|
||||
filter = filter.replace(f, snakeCase(f))
|
||||
})
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
export function transformFilterStringFromApi(
|
||||
filter: string,
|
||||
labelResolver: (id: number) => string | null,
|
||||
projectResolver: (id: number) => string | null,
|
||||
): string {
|
||||
|
||||
if (filter.trim() === '') {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Transform all attributes from snake case
|
||||
AVAILABLE_FILTER_FIELDS.forEach(f => {
|
||||
filter = filter.replace(snakeCase(f), f)
|
||||
})
|
||||
|
||||
// Transform labels to their titles
|
||||
LABEL_FIELDS.forEach(field => {
|
||||
const pattern = getFieldPattern(field)
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = pattern.exec(filter)) !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [matched, prefix, operator, space, keyword] = match
|
||||
if (keyword) {
|
||||
const labelTitle = labelResolver(Number(keyword.trim()))
|
||||
if (labelTitle !== null) {
|
||||
filter = filter.replace(keyword, labelTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Transform projects to ids
|
||||
PROJECT_FIELDS.forEach(field => {
|
||||
const pattern = getFieldPattern(field)
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = pattern.exec(filter)) !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [matched, prefix, operator, space, keyword] = match
|
||||
if (keyword) {
|
||||
const project = projectResolver(Number(keyword.trim()))
|
||||
if (project !== null) {
|
||||
filter = filter.replace(keyword, project)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return filter
|
||||
}
|
|
@ -11,14 +11,17 @@ export function getRedirectUrlFromCurrentFrontendPath(provider: IProvider): stri
|
|||
|
||||
export const redirectToProvider = (provider: IProvider) => {
|
||||
|
||||
console.log({provider})
|
||||
|
||||
const redirectUrl = getRedirectUrlFromCurrentFrontendPath(provider)
|
||||
const state = createRandomID(24)
|
||||
localStorage.setItem('state', state)
|
||||
|
||||
window.location.href = `${provider.authUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=openid email profile&state=${state}`
|
||||
let scope = 'openid email profile'
|
||||
if (provider.scope !== null){
|
||||
scope = provider.scope
|
||||
}
|
||||
window.location.href = `${provider.authUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope}&state=${state}`
|
||||
}
|
||||
|
||||
export const redirectToProviderOnLogout = (provider: IProvider) => {
|
||||
if (provider.logoutUrl.length > 0) {
|
||||
window.location.href = `${provider.logoutUrl}`
|
||||
|
|
|
@ -21,6 +21,7 @@ export const SUPPORTED_LOCALES = {
|
|||
'hu-HU': 'Magyar',
|
||||
'ar-SA': 'اَلْعَرَبِيَّةُ',
|
||||
'sl-SI': 'Slovenščina',
|
||||
'pt-BR': 'Português Brasileiro',
|
||||
// IMPORTANT: Also add new languages to useDayjsLanguageSync
|
||||
} as const
|
||||
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "تسجيل الخروج",
|
||||
"emailInvalid": "الرجاء إدخال عنوان بريد إلكتروني صحيح.",
|
||||
"usernameRequired": "الرجاء إدخال اسم المستخدم.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "الرجاء إدخال كلمة المرور.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "إظهار كلمة المرور",
|
||||
"hidePassword": "إخفاء كلمة المرور",
|
||||
"noAccountYet": "ليس لديك حساب بعد؟",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "تكرار",
|
||||
"startDate": "تاريخ البدء",
|
||||
"title": "العنوان",
|
||||
"updated": "تاريخ التحديث"
|
||||
"updated": "تاريخ التحديث",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "لا يمكنك إلغاء الاشتراك هنا لأنك مشترك في هذه المهمة من خلال المشروع.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "تعيين خلفية",
|
||||
"share": "مشاركة",
|
||||
"newProject": "مشروع جديد",
|
||||
"createProject": "إنشاء مشروع"
|
||||
"createProject": "إنشاء مشروع",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "رابط Vikunja",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "عن البرنامج",
|
||||
"frontendVersion": "إصدار الواجهة: {version}",
|
||||
"apiVersion": "إصدار API: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Logout",
|
||||
"emailInvalid": "Please enter a valid email address.",
|
||||
"usernameRequired": "Please provide a username.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Please provide a password.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Repeat",
|
||||
"startDate": "Start Date",
|
||||
"title": "Title",
|
||||
"updated": "Updated"
|
||||
"updated": "Updated",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Set background",
|
||||
"share": "Share",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"frontendVersion": "Frontend Version: {version}",
|
||||
"apiVersion": "API Version: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Odhlásit se",
|
||||
"emailInvalid": "Prosím zadejte platnou emailovou adresu.",
|
||||
"usernameRequired": "Zadejte prosím uživatelské jméno.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Zadejte prosím heslo.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Ukázat heslo",
|
||||
"hidePassword": "Skrýt heslo",
|
||||
"noAccountYet": "Ještě nemáte účet?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Opakovat",
|
||||
"startDate": "Počáteční datum",
|
||||
"title": "Název",
|
||||
"updated": "Aktualizováno"
|
||||
"updated": "Aktualizováno",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "Zde se nemůžete odhlásit, protože jste přihlášeni k odběru tohoto úkolu prostřednictvím jeho projektu.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Nastavit pozadí",
|
||||
"share": "Sdílet",
|
||||
"newProject": "Nový projekt",
|
||||
"createProject": "Vytvořit projekt"
|
||||
"createProject": "Vytvořit projekt",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "O aplikaci",
|
||||
"frontendVersion": "Verze frontendu: {version}",
|
||||
"apiVersion": "Verze API: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Log ud",
|
||||
"emailInvalid": "Indtast venligst en gyldig e-mailadresse.",
|
||||
"usernameRequired": "Angiv venligst et brugernavn.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Angiv venligst en adgangskode.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Vis adgangskoden",
|
||||
"hidePassword": "Skjul adgangskoden",
|
||||
"noAccountYet": "Har du ikke en konto endnu?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Gentag",
|
||||
"startDate": "Startdato",
|
||||
"title": "Titel",
|
||||
"updated": "Opdateret"
|
||||
"updated": "Opdateret",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Indstil baggrund",
|
||||
"share": "Del",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "Om",
|
||||
"frontendVersion": "Frontend Version: {version}",
|
||||
"apiVersion": "API Version: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Abmelden",
|
||||
"emailInvalid": "Bitte gib eine gültige E-Mail-Adresse ein.",
|
||||
"usernameRequired": "Bitte gib einen Anmeldenamen ein.",
|
||||
"usernameMustNotContainSpace": "Der Anmeldename darf keine Leerzeichen enthalten.",
|
||||
"usernameMustNotLookLikeUrl": "Der Anmeldename darf nicht wie eine URL aussehen.",
|
||||
"passwordRequired": "Bitte gib ein Passwort ein.",
|
||||
"passwordNotMin": "Das Passwort muss aus mindestens 8 Zeichen bestehen.",
|
||||
"passwordNotMax": "Das Passwort darf höchstens 250 Zeichen lang sein.",
|
||||
"showPassword": "Passwort anzeigen",
|
||||
"hidePassword": "Passwort verbergen",
|
||||
"noAccountYet": "Noch kein Account?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Wiederholen",
|
||||
"startDate": "Anfangsdatum",
|
||||
"title": "Titel",
|
||||
"updated": "Aktualisiert"
|
||||
"updated": "Aktualisiert",
|
||||
"doneAt": "Erledigt am"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihr Projekt abonniert hast.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Hintergrund einstellen",
|
||||
"share": "Teilen",
|
||||
"newProject": "Neues Projekt",
|
||||
"createProject": "Projekt erstellen"
|
||||
"createProject": "Projekt erstellen",
|
||||
"cantArchiveIsDefault": "Du kannst dieses Projekt nicht archivieren, da es dein Standardprojekt ist.",
|
||||
"cantDeleteIsDefault": "Du kannst dieses Projekt nicht löschen, da es dein Standardprojekt ist."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja-URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "Über",
|
||||
"frontendVersion": "Frontend-Version: {version}",
|
||||
"apiVersion": "API-Version: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Uuslogge",
|
||||
"emailInvalid": "Bitte gib eine gültige E-Mail-Adresse ein.",
|
||||
"usernameRequired": "Bitte gib einen Anmeldenamen ein.",
|
||||
"usernameMustNotContainSpace": "Der Anmeldename darf keine Leerzeichen enthalten.",
|
||||
"usernameMustNotLookLikeUrl": "Der Anmeldename darf nicht wie eine URL aussehen.",
|
||||
"passwordRequired": "Bitte gib ein Passwort ein.",
|
||||
"passwordNotMin": "Das Passwort muss aus mindestens 8 Zeichen bestehen.",
|
||||
"passwordNotMax": "Das Passwort darf höchstens 250 Zeichen lang sein.",
|
||||
"showPassword": "Passwort anzeigen",
|
||||
"hidePassword": "Passwort verbergen",
|
||||
"noAccountYet": "Noch kein Account?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Widerhole",
|
||||
"startDate": "Aahfangs Datum",
|
||||
"title": "Titl",
|
||||
"updated": "Aktualisiert"
|
||||
"updated": "Aktualisiert",
|
||||
"doneAt": "Erledigt am"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihr Projekt abonniert hast.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Hintergrund iihstelle",
|
||||
"share": "Teilä",
|
||||
"newProject": "Neues Projekt",
|
||||
"createProject": "Projekt erstellen"
|
||||
"createProject": "Projekt erstellen",
|
||||
"cantArchiveIsDefault": "Du kannst dieses Projekt nicht archivieren, da es dein Standardprojekt ist.",
|
||||
"cantDeleteIsDefault": "Du kannst dieses Projekt nicht löschen, da es dein Standardprojekt ist."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "Über",
|
||||
"frontendVersion": "Frontend Version: {version}",
|
||||
"apiVersion": "API Version: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -385,6 +385,7 @@
|
|||
"filters": {
|
||||
"title": "Filters",
|
||||
"clear": "Clear Filters",
|
||||
"showResults": "Show results",
|
||||
"attributes": {
|
||||
"title": "Title",
|
||||
"titlePlaceholder": "The saved filter title goes here…",
|
||||
|
@ -415,6 +416,52 @@
|
|||
"edit": {
|
||||
"title": "Edit This Saved Filter",
|
||||
"success": "The filter was saved successfully."
|
||||
},
|
||||
"query": {
|
||||
"title": "Query",
|
||||
"placeholder": "Type a search or filter query…",
|
||||
"help": {
|
||||
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
|
||||
"link": "How does this work?",
|
||||
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
|
||||
"fields": {
|
||||
"done": "Whether the task is completed or not",
|
||||
"priority": "The priority level of the task (1-5)",
|
||||
"percentDone": "The percentage of completion for the task (0-100)",
|
||||
"dueDate": "The due date of the task",
|
||||
"startDate": "The start date of the task",
|
||||
"endDate": "The end date of the task",
|
||||
"doneAt": "The date and time when the task was completed",
|
||||
"assignees": "The assignees of the task",
|
||||
"labels": "The labels associated with the task",
|
||||
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
|
||||
},
|
||||
"operators": {
|
||||
"intro": "The available operators for filtering include:",
|
||||
"notEqual": "Not equal to",
|
||||
"equal": "Equal to",
|
||||
"greaterThan": "Greater than",
|
||||
"greaterThanOrEqual": "Greater than or equal to",
|
||||
"lessThan": "Less than",
|
||||
"lessThanOrEqual": "Less than or equal to",
|
||||
"like": "Matches a pattern (using wildcard %)",
|
||||
"in": "Matches any value in a list"
|
||||
},
|
||||
"logicalOperators": {
|
||||
"intro": "To combine multiple conditions, you can use the following logical operators:",
|
||||
"and": "AND operator, matches if all conditions are true",
|
||||
"or": "OR operator, matches if any of the conditions are true",
|
||||
"parentheses": "Parentheses for grouping conditions"
|
||||
},
|
||||
"examples": {
|
||||
"intro": "Here are some examples of filter queries:",
|
||||
"priorityEqual": "Matches tasks with priority level 4",
|
||||
"dueDatePast": "Matches tasks with a due date in the past",
|
||||
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
|
||||
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
|
||||
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"migrate": {
|
||||
|
@ -585,23 +632,42 @@
|
|||
"to": "To",
|
||||
"from": "From",
|
||||
"fromto": "{from} to {to}",
|
||||
"date": "Date",
|
||||
"ranges": {
|
||||
"today": "Today",
|
||||
|
||||
"thisWeek": "This Week",
|
||||
"restOfThisWeek": "The Rest of This Week",
|
||||
"nextWeek": "Next Week",
|
||||
"next7Days": "Next 7 Days",
|
||||
"lastWeek": "Last Week",
|
||||
|
||||
"thisMonth": "This Month",
|
||||
"restOfThisMonth": "The Rest of This Month",
|
||||
"nextMonth": "Next Month",
|
||||
"next30Days": "Next 30 Days",
|
||||
"lastMonth": "Last Month",
|
||||
|
||||
"thisYear": "This Year",
|
||||
"restOfThisYear": "The Rest of This Year"
|
||||
},
|
||||
"values": {
|
||||
"now": "Now",
|
||||
"startOfToday": "Start of today",
|
||||
"endOfToday": "End of today",
|
||||
"beginningOflastWeek": "Beginning of last week",
|
||||
"endOfLastWeek": "End of last week",
|
||||
"beginningOfThisWeek": "Beginning of this week",
|
||||
"endOfThisWeek": "End of this week",
|
||||
"startOfNextWeek": "Start of next week",
|
||||
"endOfNextWeek": "End of next week",
|
||||
"in7Days": "In 7 days",
|
||||
"beginningOfLastMonth": "Beginning of last month",
|
||||
"endOfLastMonth": "End of last month",
|
||||
"startOfThisMonth": "Start of this month",
|
||||
"endOfThisMonth": "End of this month",
|
||||
"startOfNextMonth": "Start of next month",
|
||||
"endOfNextMonth": "End of next month",
|
||||
"in30Days": "In 30 days",
|
||||
"startOfThisYear": "Beginning of this year",
|
||||
"endOfThisYear": "End of this year"
|
||||
}
|
||||
},
|
||||
"datemathHelp": {
|
||||
|
@ -978,7 +1044,9 @@
|
|||
"setBackground": "Set background",
|
||||
"share": "Share",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Logout",
|
||||
"emailInvalid": "Please enter a valid email address.",
|
||||
"usernameRequired": "Please provide a username.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Please provide a password.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Repeat",
|
||||
"startDate": "Start Date",
|
||||
"title": "Title",
|
||||
"updated": "Updated"
|
||||
"updated": "Updated",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Set background",
|
||||
"share": "Share",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"frontendVersion": "Frontend Version: {version}",
|
||||
"apiVersion": "API Version: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Cerrar sesión",
|
||||
"emailInvalid": "Por favor, introduce una dirección de correo electrónico válida.",
|
||||
"usernameRequired": "Por favor, proporciona un nombre de usuario.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Por favor, proporciona una contraseña.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Mostrar la contraseña",
|
||||
"hidePassword": "Ocultar la contraseña",
|
||||
"noAccountYet": "¿Aún no tienes una cuenta?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Repetir",
|
||||
"startDate": "Fecha de Inicio",
|
||||
"title": "Título",
|
||||
"updated": "Actualizado"
|
||||
"updated": "Actualizado",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "No puedes darte de baja aquí porque estás suscrito a esta tarea a través de su proyecto.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Establecer fondo",
|
||||
"share": "Compartir",
|
||||
"newProject": "Nuevo proyecto",
|
||||
"createProject": "Crear proyecto"
|
||||
"createProject": "Crear proyecto",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "URL de Vikunja",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "Acerca de",
|
||||
"frontendVersion": "Versión del Frontend: {version}",
|
||||
"apiVersion": "Versión de la API: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Se déconnecter",
|
||||
"emailInvalid": "Veuillez saisir une adresse courriel valide.",
|
||||
"usernameRequired": "Veuillez saisir un nom d'utilisateur.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Veuillez fournir un mot de passe.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Afficher le mot de passe",
|
||||
"hidePassword": "Masquer le mot de passe",
|
||||
"noAccountYet": "Vous n'avez pas encore de compte ?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Répéter",
|
||||
"startDate": "Date de début",
|
||||
"title": "Nom",
|
||||
"updated": "Mis à jour"
|
||||
"updated": "Mis à jour",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "Impossible de se désabonner ici, car vous êtes abonné·e à cette tâche depuis son projet.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Définir l’arrière-plan",
|
||||
"share": "Partager",
|
||||
"newProject": "Nouveau projet",
|
||||
"createProject": "Créer un projet"
|
||||
"createProject": "Créer un projet",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "URL Vikunja",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "À propos",
|
||||
"frontendVersion": "Version : {version}",
|
||||
"apiVersion": "Version de l’API : {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Kijelentkezés",
|
||||
"emailInvalid": "Kérjük, adjon meg egy valós email címet!",
|
||||
"usernameRequired": "Kérjük adjon meg egy felhasználónevet.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Kérjük, adjon meg új jelszót.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Jelszó megjelenítése",
|
||||
"hidePassword": "A jelszó elrejtése",
|
||||
"noAccountYet": "Még nincs fiókja?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Ismétlés",
|
||||
"startDate": "Kezdő dátum",
|
||||
"title": "Cím",
|
||||
"updated": "Frissítve"
|
||||
"updated": "Frissítve",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "Itt nem iratkozhat le, mert a projektjén keresztül feliratkozott erre a feladatra.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Háttérkép beállítása",
|
||||
"share": "Megosztás",
|
||||
"newProject": "Új projekt",
|
||||
"createProject": "Projekt létrehozása"
|
||||
"createProject": "Projekt létrehozása",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "Névjegy",
|
||||
"frontendVersion": "Frontend verzió: {version}",
|
||||
"apiVersion": "API verzió: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Esci",
|
||||
"emailInvalid": "Inserisci un indirizzo e-mail valido.",
|
||||
"usernameRequired": "Inserisci un nome utente.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Inserisci una password.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Mostra la password",
|
||||
"hidePassword": "Nascondi la password",
|
||||
"noAccountYet": "Non hai un account?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Ripeti",
|
||||
"startDate": "Data Inizio",
|
||||
"title": "Titolo",
|
||||
"updated": "Aggiornato"
|
||||
"updated": "Aggiornato",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Imposta sfondo",
|
||||
"share": "Condividi",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "URL Vikunja",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "Informazioni",
|
||||
"frontendVersion": "Versione Frontend: {version}",
|
||||
"apiVersion": "Versione API: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "ログアウト",
|
||||
"emailInvalid": "有効なメールアドレスを入力してください。",
|
||||
"usernameRequired": "ユーザー名を入力してください。",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "パスワードを入力してください。",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "パスワードの表示",
|
||||
"hidePassword": "パスワードの非表示",
|
||||
"noAccountYet": "まだアカウントをお持ちでないですか?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "繰り返し間隔",
|
||||
"startDate": "開始日",
|
||||
"title": "タスク名",
|
||||
"updated": "Updated"
|
||||
"updated": "Updated",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "現在プロジェクトを通して購読しているため、ここでこのタスクの購読を解除することはできません。",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "背景画像の設定",
|
||||
"share": "共有",
|
||||
"newProject": "新しいプロジェクトの作成",
|
||||
"createProject": "プロジェクトの作成"
|
||||
"createProject": "プロジェクトの作成",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "Vikunjaについて",
|
||||
"frontendVersion": "フロントエンドのバージョン: {version}",
|
||||
"apiVersion": "APIのバージョン: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "로그아웃",
|
||||
"emailInvalid": "유효한 이메일 주소를 입력하여 주십시오.",
|
||||
"usernameRequired": "사용자 이름을 입력하세요.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "비밀번호를 입력하세요.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "비밀번호 표시",
|
||||
"hidePassword": "비밀번호 숨김",
|
||||
"noAccountYet": "아직 계정이 없으신가요?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Repeat",
|
||||
"startDate": "Start Date",
|
||||
"title": "Title",
|
||||
"updated": "Updated"
|
||||
"updated": "Updated",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Set background",
|
||||
"share": "Share",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"frontendVersion": "Frontend Version: {version}",
|
||||
"apiVersion": "API Version: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Uitloggen",
|
||||
"emailInvalid": "Please enter a valid email address.",
|
||||
"usernameRequired": "Please provide a username.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Please provide a password.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Herhalen",
|
||||
"startDate": "Begindatum",
|
||||
"title": "Titel",
|
||||
"updated": "Bijgewerkt"
|
||||
"updated": "Bijgewerkt",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Achtergrond instellen",
|
||||
"share": "Delen",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "Over",
|
||||
"frontendVersion": "Frontend versie: {version}",
|
||||
"apiVersion": "API Versie: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Logg ut",
|
||||
"emailInvalid": "Vennligst oppgi en gyldig e-postadresse.",
|
||||
"usernameRequired": "Angi et brukernavn.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Angi et passord.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Vis passord",
|
||||
"hidePassword": "Skjul passord",
|
||||
"noAccountYet": "Har du ikke konto ennå?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Gjenta",
|
||||
"startDate": "Start Dato",
|
||||
"title": "Tittel",
|
||||
"updated": "Oppdatert"
|
||||
"updated": "Oppdatert",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "Du kan ikke melde deg ut her fordi du abonnerer på denne oppgaven gjennom prosjektet.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Bruk som bakgrunn",
|
||||
"share": "Del",
|
||||
"newProject": "Nytt prosjekt",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "Om",
|
||||
"frontendVersion": "Frontend versjon: {version}",
|
||||
"apiVersion": "API versjon: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Wyloguj",
|
||||
"emailInvalid": "Proszę podać poprawny adres e-mail.",
|
||||
"usernameRequired": "Proszę podać nazwę użytkownika.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Proszę podać hasło.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Pokaż hasło",
|
||||
"hidePassword": "Ukryj hasło",
|
||||
"noAccountYet": "Nie masz jeszcze konta?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Powtarzanie",
|
||||
"startDate": "Data rozpoczęcia",
|
||||
"title": "Tytuł",
|
||||
"updated": "Zaktualizowano"
|
||||
"updated": "Zaktualizowano",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "Nie możesz zrezygnować z subskrypcji, ponieważ subskrybujesz to zadanie poprzez jego projekt.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Ustaw tło",
|
||||
"share": "Udostępnij",
|
||||
"newProject": "Nowy projekt",
|
||||
"createProject": "Utwórz projekt"
|
||||
"createProject": "Utwórz projekt",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "URL Vikunji",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "O aplikacji",
|
||||
"frontendVersion": "Wersja frontendu: {version}",
|
||||
"apiVersion": "Wersja API: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -57,7 +57,11 @@
|
|||
"logout": "Terminar Sessão",
|
||||
"emailInvalid": "Por favor, insire um endereço de e-mail válido.",
|
||||
"usernameRequired": "Por favor, fornece um nome de utilizador.",
|
||||
"usernameMustNotContainSpace": "O nome de utilizador não deve conter espaços.",
|
||||
"usernameMustNotLookLikeUrl": "O nome de utilizador não se deve assemelhar a um URL.",
|
||||
"passwordRequired": "Por favor, fornece uma palavra-passe.",
|
||||
"passwordNotMin": "A palavra-passe deve ter no mínimo 8 caracteres.",
|
||||
"passwordNotMax": "A palavra-passe deve ter no máximo 250 caracteres.",
|
||||
"showPassword": "Mostrar a palavra-passe",
|
||||
"hidePassword": "Esconder a palavra-passe",
|
||||
"noAccountYet": "Ainda não tens uma conta?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Repetir",
|
||||
"startDate": "Data de Início",
|
||||
"title": "Título",
|
||||
"updated": "Atualizado"
|
||||
"updated": "Atualizado",
|
||||
"doneAt": "Concluído Em"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "Não podes cancelar a tua subscrição aqui porque estás subscrito nesta tarefa através do seu projeto.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Definir Fundo",
|
||||
"share": "Partilhar",
|
||||
"newProject": "Novo projeto",
|
||||
"createProject": "Criar projeto"
|
||||
"createProject": "Criar projeto",
|
||||
"cantArchiveIsDefault": "Não podes arquivar isto porque é o teu projeto padrão.",
|
||||
"cantDeleteIsDefault": "Não podes eliminar isto porque é o teu projeto padrão."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "URL do Vikunja",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "Sobre",
|
||||
"frontendVersion": "Versão Atual: {version}",
|
||||
"apiVersion": "Versão da API: {version}"
|
||||
"version": "Versão: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Logout",
|
||||
"emailInvalid": "Please enter a valid email address.",
|
||||
"usernameRequired": "Please provide a username.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Please provide a password.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Repeat",
|
||||
"startDate": "Start Date",
|
||||
"title": "Title",
|
||||
"updated": "Updated"
|
||||
"updated": "Updated",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Set background",
|
||||
"share": "Share",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"frontendVersion": "Frontend Version: {version}",
|
||||
"apiVersion": "API Version: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Выйти",
|
||||
"emailInvalid": "Введите корректный email адрес.",
|
||||
"usernameRequired": "Введите имя пользователя.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Введите пароль.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Показать пароль",
|
||||
"hidePassword": "Скрыть пароль",
|
||||
"noAccountYet": "Ещё нет аккаунта?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Повтор",
|
||||
"startDate": "Дата начала",
|
||||
"title": "Название",
|
||||
"updated": "Дата изменения"
|
||||
"updated": "Дата изменения",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "Вы не можете отписаться здесь, потому что вы подписаны эту задачу через её проект.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Задать фон",
|
||||
"share": "Поделиться",
|
||||
"newProject": "Создать проект",
|
||||
"createProject": "Создать проект"
|
||||
"createProject": "Создать проект",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "О Vikunja",
|
||||
"frontendVersion": "Версия фронтенда: {version}",
|
||||
"apiVersion": "Версия API: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Logout",
|
||||
"emailInvalid": "Please enter a valid email address.",
|
||||
"usernameRequired": "Please provide a username.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Please provide a password.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Repeat",
|
||||
"startDate": "Start Date",
|
||||
"title": "Title",
|
||||
"updated": "Updated"
|
||||
"updated": "Updated",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Set background",
|
||||
"share": "Share",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"frontendVersion": "Frontend Version: {version}",
|
||||
"apiVersion": "API Version: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Odjava",
|
||||
"emailInvalid": "Prosim vnesite veljaven e-poštni naslov.",
|
||||
"usernameRequired": "Prosim vnesite uporabniško ime.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Prosim vnesite geslo.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Prikažite geslo",
|
||||
"hidePassword": "Skrijte geslo",
|
||||
"noAccountYet": "Še nimate računa?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Ponovi",
|
||||
"startDate": "Začetni datum",
|
||||
"title": "Naslov",
|
||||
"updated": "Posodobljeno"
|
||||
"updated": "Posodobljeno",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "Ker ste na to nalogo naročeni prek njenega projekta, se tu ne morete odjaviti.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Nastavi ozadje",
|
||||
"share": "Skupna raba",
|
||||
"newProject": "Nov projekt",
|
||||
"createProject": "Ustvari projekt"
|
||||
"createProject": "Ustvari projekt",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "O programu",
|
||||
"frontendVersion": "Frontend verzija: {version}",
|
||||
"apiVersion": "API verzija: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Logout",
|
||||
"emailInvalid": "Please enter a valid email address.",
|
||||
"usernameRequired": "Please provide a username.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Please provide a password.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Repeat",
|
||||
"startDate": "Start Date",
|
||||
"title": "Title",
|
||||
"updated": "Updated"
|
||||
"updated": "Updated",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Set background",
|
||||
"share": "Share",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"frontendVersion": "Frontend Version: {version}",
|
||||
"apiVersion": "API Version: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Logga ut",
|
||||
"emailInvalid": "Vänligen ange en giltig e-postadress.",
|
||||
"usernameRequired": "Vänligen ange ett användarnamn.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Vänligen ange ett lösenord.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Har du inget konto än?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Upprepa",
|
||||
"startDate": "Startdatum",
|
||||
"title": "Titel",
|
||||
"updated": "Updated"
|
||||
"updated": "Updated",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Set background",
|
||||
"share": "Dela",
|
||||
"newProject": "Nytt projekt",
|
||||
"createProject": "Skapa projekt"
|
||||
"createProject": "Skapa projekt",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "Om",
|
||||
"frontendVersion": "Frontend Version: {version}",
|
||||
"apiVersion": "API Version: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Logout",
|
||||
"emailInvalid": "Please enter a valid email address.",
|
||||
"usernameRequired": "Please provide a username.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Please provide a password.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Repeat",
|
||||
"startDate": "Start Date",
|
||||
"title": "Title",
|
||||
"updated": "Updated"
|
||||
"updated": "Updated",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Set background",
|
||||
"share": "Share",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"frontendVersion": "Frontend Version: {version}",
|
||||
"apiVersion": "API Version: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Đăng xuất",
|
||||
"emailInvalid": "Vui lòng nhập một địa chỉ email hợp lệ.",
|
||||
"usernameRequired": "Vui lòng cung cấp tên người dùng.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Vui lòng cung cấp một mật khẩu.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Hiển thị mật khẩu",
|
||||
"hidePassword": "Ẩn mật khẩu",
|
||||
"noAccountYet": "Bạn chưa có tài khoản?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Lặp lại",
|
||||
"startDate": "Ngày bắt đầu",
|
||||
"title": "Tiêu đề",
|
||||
"updated": "Đã cập nhật"
|
||||
"updated": "Đã cập nhật",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Cài hình nền",
|
||||
"share": "Chia sẻ",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "URL Vikunja",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "Về ứng dụng",
|
||||
"frontendVersion": "Phiên bản giao diện người dùng: {version}",
|
||||
"apiVersion": "Phiên bản API: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "注销",
|
||||
"emailInvalid": "请输入有效的电子邮件地址。",
|
||||
"usernameRequired": "请输入用户名",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "请提供密码",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "显示密码",
|
||||
"hidePassword": "隐藏密码",
|
||||
"noAccountYet": "还没有账号?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "重复",
|
||||
"startDate": "开始日期",
|
||||
"title": "标题",
|
||||
"updated": "已更新"
|
||||
"updated": "已更新",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "你无法在此处取消订阅,因为你已通过其项目订阅了此任务。",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "设置背景",
|
||||
"share": "共享",
|
||||
"newProject": "新项目",
|
||||
"createProject": "创建项目"
|
||||
"createProject": "创建项目",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "关于",
|
||||
"frontendVersion": "前端版本:{version}",
|
||||
"apiVersion": "API 版本:{version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
"logout": "Logout",
|
||||
"emailInvalid": "Please enter a valid email address.",
|
||||
"usernameRequired": "Please provide a username.",
|
||||
"usernameMustNotContainSpace": "The username must not contain spaces.",
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Please provide a password.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
|
@ -708,7 +712,8 @@
|
|||
"repeat": "Repeat",
|
||||
"startDate": "Start Date",
|
||||
"title": "Title",
|
||||
"updated": "Updated"
|
||||
"updated": "Updated",
|
||||
"doneAt": "Done At"
|
||||
},
|
||||
"subscription": {
|
||||
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
|
||||
|
@ -969,7 +974,9 @@
|
|||
"setBackground": "Set background",
|
||||
"share": "Share",
|
||||
"newProject": "New project",
|
||||
"createProject": "Create project"
|
||||
"createProject": "Create project",
|
||||
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
|
||||
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
|
||||
},
|
||||
"apiConfig": {
|
||||
"url": "Vikunja URL",
|
||||
|
@ -1089,8 +1096,7 @@
|
|||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"frontendVersion": "Frontend Version: {version}",
|
||||
"apiVersion": "API Version: {version}"
|
||||
"version": "Version: {version}"
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {computed, ref, watch} from 'vue'
|
||||
import type dayjs from 'dayjs'
|
||||
|
||||
import {i18n, type SupportedLocale, type ISOLanguage} from '@/i18n'
|
||||
import {i18n, type ISOLanguage, type SupportedLocale} from '@/i18n'
|
||||
|
||||
export const DAYJS_LOCALE_MAPPING = {
|
||||
'de-de': 'de',
|
||||
|
@ -22,6 +22,7 @@ export const DAYJS_LOCALE_MAPPING = {
|
|||
'hu-HU': 'hu',
|
||||
'ar-SA': 'ar-sa',
|
||||
'sl-SI': 'sl',
|
||||
'pt-BR': 'pt',
|
||||
} as Record<SupportedLocale, ISOLanguage>
|
||||
|
||||
export const DAYJS_LANGUAGE_IMPORTS = {
|
||||
|
@ -36,13 +37,14 @@ export const DAYJS_LANGUAGE_IMPORTS = {
|
|||
'nl-nl': () => import('dayjs/locale/nl'),
|
||||
'pt-pt': () => import('dayjs/locale/pt'),
|
||||
'zh-cn': () => import('dayjs/locale/zh-cn'),
|
||||
'no-no': () => import('dayjs/locale/nn'),
|
||||
'es-es': () => import('dayjs/locale/es'),
|
||||
'da-dk': () => import('dayjs/locale/da'),
|
||||
'ja-jp': () => import('dayjs/locale/ja'),
|
||||
'hu-hu': () => import('dayjs/locale/hu'),
|
||||
'ar-sa': () => import('dayjs/locale/ar-sa'),
|
||||
'sl-si': () => import('dayjs/locale/sl'),
|
||||
'no-no': () => import('dayjs/locale/nn'),
|
||||
'es-es': () => import('dayjs/locale/es'),
|
||||
'da-dk': () => import('dayjs/locale/da'),
|
||||
'ja-jp': () => import('dayjs/locale/ja'),
|
||||
'hu-hu': () => import('dayjs/locale/hu'),
|
||||
'ar-sa': () => import('dayjs/locale/ar-sa'),
|
||||
'sl-si': () => import('dayjs/locale/sl'),
|
||||
'pt-br': () => import('dayjs/locale/pt-br'),
|
||||
} as Record<SupportedLocale, () => Promise<ILocale>>
|
||||
|
||||
export function useDayjsLanguageSync(dayjsGlobal: typeof dayjs) {
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
import type {IAbstract} from './IAbstract'
|
||||
import type {IUser} from './IUser'
|
||||
import type {IFilter} from '@/types/IFilter'
|
||||
|
||||
interface Filters {
|
||||
sortBy: ('start_date' | 'done' | 'id' | 'position')[],
|
||||
orderBy: ('asc' | 'desc')[],
|
||||
filter: string,
|
||||
filterIncludeNulls: boolean,
|
||||
s: string,
|
||||
}
|
||||
|
||||
export interface ISavedFilter extends IAbstract {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
filters: IFilter
|
||||
filters: Filters
|
||||
|
||||
owner: IUser
|
||||
created: Date
|
||||
|
|
|
@ -9,6 +9,7 @@ export interface ITeam extends IAbstract {
|
|||
description: string
|
||||
members: ITeamMember[]
|
||||
right: Right
|
||||
oidcId: string
|
||||
|
||||
createdBy: IUser
|
||||
created: Date
|
||||
|
|
|
@ -11,11 +11,9 @@ export default class SavedFilterModel extends AbstractModel<ISavedFilter> implem
|
|||
filters: ISavedFilter['filters'] = {
|
||||
sortBy: ['done', 'id'],
|
||||
orderBy: ['asc', 'desc'],
|
||||
filterBy: ['done'],
|
||||
filterValue: ['false'],
|
||||
filterComparator: ['equals'],
|
||||
filterConcat: 'and',
|
||||
filter: 'done = false',
|
||||
filterIncludeNulls: true,
|
||||
s: '',
|
||||
}
|
||||
|
||||
owner: IUser = {}
|
||||
|
|
|
@ -13,6 +13,7 @@ export default class TeamModel extends AbstractModel<ITeam> implements ITeam {
|
|||
description = ''
|
||||
members: ITeamMember[] = []
|
||||
right: Right = RIGHTS.READ
|
||||
oidcId = ''
|
||||
|
||||
createdBy: IUser = {} // FIXME: seems wrong
|
||||
created: Date = null
|
||||
|
|
|
@ -63,9 +63,6 @@ export default class SavedFilterService extends AbstractService<ISavedFilter> {
|
|||
// the filter values in snake_sćase for url parameters.
|
||||
model.filters = objectToCamelCase(model.filters)
|
||||
|
||||
// Make sure all filterValues are passes as strings. This is a requirement of the api.
|
||||
model.filters.filterValue = model.filters.filterValue.map(v => String(v))
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
|
@ -111,13 +108,13 @@ export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) {
|
|||
|
||||
async function createFilter() {
|
||||
filter.value = await filterService.create(filter.value)
|
||||
await projectStore.loadProjects()
|
||||
await projectStore.loadAllProjects()
|
||||
router.push({name: 'project.index', params: {projectId: getProjectId(filter.value)}})
|
||||
}
|
||||
|
||||
async function saveFilter() {
|
||||
const response = await filterService.update(filter.value)
|
||||
await projectStore.loadProjects()
|
||||
await projectStore.loadAllProjects()
|
||||
success({message: t('filters.edit.success')})
|
||||
response.filters = objectToSnakeCase(response.filters)
|
||||
filter.value = response
|
||||
|
@ -130,7 +127,7 @@ export function useSavedFilter(projectId?: MaybeRef<IProject['id']>) {
|
|||
|
||||
async function deleteFilter() {
|
||||
await filterService.delete(filter.value)
|
||||
await projectStore.loadProjects()
|
||||
await projectStore.loadAllProjects()
|
||||
success({message: t('filters.delete.success')})
|
||||
router.push({name: 'projects.index'})
|
||||
}
|
||||
|
|
|
@ -3,15 +3,22 @@ import TaskModel from '@/models/task'
|
|||
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
|
||||
// FIXME: unite with other filter params types
|
||||
export interface GetAllTasksParams {
|
||||
sort_by: ('start_date' | 'done' | 'id')[],
|
||||
order_by: ('asc' | 'asc' | 'desc')[],
|
||||
filter_by: 'start_date'[],
|
||||
filter_comparator: ('greater_equals' | 'less_equals')[],
|
||||
filter_value: [string, string] // [dateFrom, dateTo],
|
||||
filter_concat: 'and',
|
||||
export interface TaskFilterParams {
|
||||
sort_by: ('start_date' | 'done' | 'id' | 'position')[],
|
||||
order_by: ('asc' | 'desc')[],
|
||||
filter: string,
|
||||
filter_include_nulls: boolean,
|
||||
s: string,
|
||||
}
|
||||
|
||||
export function getDefaultTaskFilterParams(): TaskFilterParams {
|
||||
return {
|
||||
sort_by: ['position', 'id'],
|
||||
order_by: ['asc', 'desc'],
|
||||
filter: '',
|
||||
filter_include_nulls: false,
|
||||
s: '',
|
||||
}
|
||||
}
|
||||
|
||||
export default class TaskCollectionService extends AbstractService<ITask> {
|
||||
|
|
|
@ -246,8 +246,9 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||
}
|
||||
|
||||
async function loadNextTasksForBucket(
|
||||
{projectId, ps = {}, bucketId} :
|
||||
{projectId: IProject['id'], ps, bucketId: IBucket['id']},
|
||||
projectId: IProject['id'],
|
||||
ps,
|
||||
bucketId: IBucket['id'],
|
||||
) {
|
||||
const isLoading = bucketLoading.value[bucketId] ?? false
|
||||
if (isLoading) {
|
||||
|
|
|
@ -33,6 +33,10 @@ export const useLabelStore = defineStore('label', () => {
|
|||
const getLabelsByIds = computed(() => {
|
||||
return (ids: ILabel['id'][]) => Object.values(labels.value).filter(({id}) => ids.includes(id))
|
||||
})
|
||||
|
||||
const getLabelById = computed(() => {
|
||||
return (labelId: ILabel['id']) => Object.values(labels.value).find(({id}) => id === labelId)
|
||||
})
|
||||
|
||||
// **
|
||||
// * Checks if a project of labels is available in the store and filters them then query
|
||||
|
@ -138,6 +142,7 @@ export const useLabelStore = defineStore('label', () => {
|
|||
isLoading,
|
||||
|
||||
getLabelsByIds,
|
||||
getLabelById,
|
||||
filterLabelsByQuery,
|
||||
getLabelsByExactTitles,
|
||||
|
||||
|
|
|
@ -175,20 +175,28 @@ export const useProjectStore = defineStore('project', () => {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
async function loadAllProjects() {
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
|
||||
const projectService = new ProjectService()
|
||||
const loadedProjects: IProject[] = []
|
||||
let page = 1
|
||||
try {
|
||||
const loadedProjects = await projectService.getAll({}, {is_archived: true}) as IProject[]
|
||||
projects.value = {}
|
||||
setProjects(loadedProjects)
|
||||
loadedProjects.forEach(p => add(p))
|
||||
|
||||
return loadedProjects
|
||||
do {
|
||||
const newProjects = await projectService.getAll({}, {is_archived: true}, page) as IProject[]
|
||||
loadedProjects.push(...newProjects)
|
||||
page++
|
||||
} while (page <= projectService.totalPages)
|
||||
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
|
||||
projects.value = {}
|
||||
setProjects(loadedProjects)
|
||||
loadedProjects.forEach(p => add(p))
|
||||
|
||||
return loadedProjects
|
||||
}
|
||||
|
||||
function getAncestors(project: IProject): IProject[] {
|
||||
|
@ -222,7 +230,7 @@ export const useProjectStore = defineStore('project', () => {
|
|||
setProjects,
|
||||
removeProjectById,
|
||||
toggleProjectFavorite,
|
||||
loadProjects,
|
||||
loadAllProjects,
|
||||
createProject,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
|
|
|
@ -473,7 +473,7 @@ export const useTaskStore = defineStore('task', () => {
|
|||
task = await taskService.update(task)
|
||||
|
||||
// reloading the projects list so that the Favorites project shows up or is hidden when there are (or are not) favorite tasks
|
||||
await projectStore.loadProjects()
|
||||
await projectStore.loadAllProjects()
|
||||
|
||||
return task
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
export interface IFilter {
|
||||
sortBy: ('done' | 'id')[]
|
||||
orderBy: ('asc' | 'desc')[]
|
||||
filterBy: 'done'[]
|
||||
filterValue: 'false'[]
|
||||
filterComparator: 'equals'[]
|
||||
filterConcat: 'and'
|
||||
filterIncludeNulls: boolean
|
||||
}
|
|
@ -4,4 +4,5 @@ export interface IProvider {
|
|||
authUrl: string;
|
||||
clientId: string;
|
||||
logoutUrl: string;
|
||||
scope: string;
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
:class="{ 'disabled': filterService.loading}"
|
||||
:disabled="filterService.loading"
|
||||
class="has-no-shadow has-no-border"
|
||||
:has-footer="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
:loading="migrationService.loading"
|
||||
:disabled="migrationService.loading || undefined"
|
||||
:href="authUrl"
|
||||
:open-external-in-new-tab="false"
|
||||
>
|
||||
{{ $t('migrate.getStarted') }}
|
||||
</x-button>
|
||||
|
@ -53,7 +54,7 @@
|
|||
<p>{{ $t('migrate.inProgress') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="lastMigrationStartedAt && lastMigrationFinishedAt === null">
|
||||
<div v-else-if="!migrationJustStarted && lastMigrationStartedAt && lastMigrationFinishedAt === null">
|
||||
<Message class="mb-4">
|
||||
{{ $t('migrate.migrationInProgress') }}
|
||||
</Message>
|
||||
|
@ -145,6 +146,7 @@ const lastMigrationFinishedAt = ref<Date | null>(null)
|
|||
const lastMigrationStartedAt = ref<Date | null>(null)
|
||||
const message = ref('')
|
||||
const migratorAuthCode = ref('')
|
||||
const migrationJustStarted = ref(false)
|
||||
|
||||
const migrator = computed<Migrator>(() => MIGRATORS[props.service])
|
||||
|
||||
|
@ -207,12 +209,15 @@ async function migrate() {
|
|||
}
|
||||
|
||||
try {
|
||||
const result = migrator.value.isFileMigrator
|
||||
? await migrationFileService.migrate(migrationConfig as File)
|
||||
: await migrationService.migrate(migrationConfig as MigrationConfig)
|
||||
message.value = result.message
|
||||
const projectStore = useProjectStore()
|
||||
return projectStore.loadProjects()
|
||||
if (migrator.value.isFileMigrator) {
|
||||
const result = await migrationFileService.migrate(migrationConfig as File)
|
||||
message.value = result.message
|
||||
const projectStore = useProjectStore()
|
||||
return projectStore.loadAllProjects()
|
||||
}
|
||||
|
||||
await migrationService.migrate(migrationConfig as MigrationConfig)
|
||||
migrationJustStarted.value = true
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
|
|
|
@ -296,6 +296,7 @@ import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
|||
import {isSavedFilter} from '@/services/savedFilter'
|
||||
import {success} from '@/message'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import type {TaskFilterParams} from '@/services/taskCollection'
|
||||
|
||||
const {
|
||||
projectId = undefined,
|
||||
|
@ -347,11 +348,12 @@ const collapsedBuckets = ref<CollapsedBuckets>({})
|
|||
const taskUpdating = ref<{[id: ITask['id']]: boolean}>({})
|
||||
const oneTaskUpdating = ref(false)
|
||||
|
||||
const params = ref({
|
||||
filter_by: [],
|
||||
filter_value: [],
|
||||
filter_comparator: [],
|
||||
filter_concat: 'and',
|
||||
const params = ref<TaskFilterParams>({
|
||||
sort_by: [],
|
||||
order_by: [],
|
||||
filter: '',
|
||||
filter_include_nulls: false,
|
||||
s: '',
|
||||
})
|
||||
|
||||
const getTaskDraggableTaskComponentData = computed(() => (bucket: IBucket) => {
|
||||
|
@ -416,11 +418,11 @@ function handleTaskContainerScroll(id: IBucket['id'], projectId: IProject['id'],
|
|||
return
|
||||
}
|
||||
|
||||
kanbanStore.loadNextTasksForBucket({
|
||||
projectId: projectId,
|
||||
params: params.value,
|
||||
bucketId: id,
|
||||
})
|
||||
kanbanStore.loadNextTasksForBucket(
|
||||
projectId,
|
||||
params.value,
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
function updateTasks(bucketId: IBucket['id'], tasks: IBucket['tasks']) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import {useRouteFilters} from '@/composables/useRouteFilters'
|
|||
import {useGanttTaskList} from './useGanttTaskList'
|
||||
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {GetAllTasksParams} from '@/services/taskCollection'
|
||||
import type {TaskFilterParams} from '@/services/taskCollection'
|
||||
|
||||
import type {DateISO} from '@/types/DateISO'
|
||||
import type {DateKebab} from '@/types/DateKebab'
|
||||
|
@ -75,14 +75,11 @@ function ganttFiltersToRoute(filters: GanttFilters): RouteLocationRaw {
|
|||
}
|
||||
}
|
||||
|
||||
function ganttFiltersToApiParams(filters: GanttFilters): GetAllTasksParams {
|
||||
function ganttFiltersToApiParams(filters: GanttFilters): TaskFilterParams {
|
||||
return {
|
||||
sort_by: ['start_date', 'done', 'id'],
|
||||
order_by: ['asc', 'asc', 'desc'],
|
||||
filter_by: ['start_date', 'start_date'],
|
||||
filter_comparator: ['greater_equals', 'less_equals'],
|
||||
filter_value: [isoToKebabDate(filters.dateFrom), isoToKebabDate(filters.dateTo)],
|
||||
filter_concat: 'and',
|
||||
filter: 'start_date >= "' + isoToKebabDate(filters.dateFrom) + '" && start_date <= "' + isoToKebabDate(filters.dateTo) + '"',
|
||||
filter_include_nulls: filters.showTasksWithoutDates,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import {klona} from 'klona/lite'
|
|||
import type {Filters} from '@/composables/useRouteFilters'
|
||||
import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask'
|
||||
|
||||
import TaskCollectionService, {type GetAllTasksParams} from '@/services/taskCollection'
|
||||
import TaskCollectionService, {type TaskFilterParams} from '@/services/taskCollection'
|
||||
import TaskService from '@/services/task'
|
||||
|
||||
import TaskModel from '@/models/task'
|
||||
|
@ -13,7 +13,7 @@ import {error, success} from '@/message'
|
|||
// FIXME: unify with general `useTaskList`
|
||||
export function useGanttTaskList<F extends Filters>(
|
||||
filters: Ref<F>,
|
||||
filterToApiParams: (filters: F) => GetAllTasksParams,
|
||||
filterToApiParams: (filters: F) => TaskFilterParams,
|
||||
options: {
|
||||
loadAll?: boolean,
|
||||
} = {
|
||||
|
@ -26,7 +26,7 @@ export function useGanttTaskList<F extends Filters>(
|
|||
|
||||
const tasks = ref<Map<ITask['id'], ITask>>(new Map())
|
||||
|
||||
async function fetchTasks(params: GetAllTasksParams, page = 1): Promise<ITask[]> {
|
||||
async function fetchTasks(params: TaskFilterParams, page = 1): Promise<ITask[]> {
|
||||
const tasks = await taskCollectionService.getAll({projectId: filters.value.projectId}, params, page) as ITask[]
|
||||
if (options.loadAll && page < taskCollectionService.totalPages) {
|
||||
const nextTasks = await fetchTasks(params, page + 1)
|
||||
|
@ -40,7 +40,7 @@ export function useGanttTaskList<F extends Filters>(
|
|||
* Normally there is no need to trigger this manually
|
||||
*/
|
||||
async function loadTasks() {
|
||||
const params: GetAllTasksParams = filterToApiParams(filters.value)
|
||||
const params: TaskFilterParams = filterToApiParams(filters.value)
|
||||
|
||||
const loadedTasks = await fetchTasks(params)
|
||||
tasks.value = new Map()
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
:class="{ 'is-loading': teamService.loading }"
|
||||
>
|
||||
<card
|
||||
v-if="userIsAdmin"
|
||||
v-if="userIsAdmin && !team.oidcId"
|
||||
class="is-fullwidth"
|
||||
:title="title"
|
||||
>
|
||||
|
@ -77,7 +77,7 @@
|
|||
:padding="false"
|
||||
>
|
||||
<div
|
||||
v-if="userIsAdmin"
|
||||
v-if="userIsAdmin && !team.oidcId"
|
||||
class="p-4"
|
||||
>
|
||||
<div class="field has-addons">
|
||||
|
@ -173,6 +173,7 @@
|
|||
</card>
|
||||
|
||||
<x-button
|
||||
v-if="team && !team.oidcId"
|
||||
class="is-fullwidth is-danger"
|
||||
@click="showLeaveModal = true"
|
||||
>
|
||||
|
|
|
@ -17,11 +17,13 @@
|
|||
class="teams box"
|
||||
>
|
||||
<li
|
||||
v-for="team in teams"
|
||||
:key="team.id"
|
||||
v-for="t in teams"
|
||||
:key="t.id"
|
||||
>
|
||||
<router-link :to="{name: 'teams.edit', params: {id: team.id}}">
|
||||
{{ team.name }}
|
||||
<router-link :to="{name: 'teams.edit', params: {id: t.id}}">
|
||||
<p>
|
||||
{{ t.name }}
|
||||
</p>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -63,7 +65,7 @@ ul.teams {
|
|||
li {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid $border;
|
||||
border-bottom: 1px solid var(--grey-200);
|
||||
|
||||
a {
|
||||
color: var(--text);
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
v-model="password"
|
||||
tabindex="2"
|
||||
:validate-initially="validatePasswordInitially"
|
||||
:validate-min-length="false"
|
||||
@submit="submit"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -286,16 +286,15 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
|
|||
:key="group"
|
||||
class="mb-2"
|
||||
>
|
||||
<strong class="is-capitalized">{{ formatPermissionTitle(group) }}</strong><br>
|
||||
<template
|
||||
v-if="Object.keys(routes).length > 1"
|
||||
v-if="Object.keys(routes).length >= 1"
|
||||
>
|
||||
<Fancycheckbox
|
||||
v-model="newTokenPermissionsGroup[group]"
|
||||
class="mr-2 is-italic"
|
||||
class="mr-2 is-capitalized has-text-weight-bold"
|
||||
@update:modelValue="checked => selectPermissionGroup(group, checked)"
|
||||
>
|
||||
{{ $t('user.settings.apiTokens.selectAll') }}
|
||||
{{ formatPermissionTitle(group) }}
|
||||
</Fancycheckbox>
|
||||
<br>
|
||||
</template>
|
||||
|
@ -305,7 +304,7 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
|
|||
>
|
||||
<Fancycheckbox
|
||||
v-model="newTokenPermissions[group][route]"
|
||||
class="mr-2 is-capitalized"
|
||||
class="ml-4 mr-2 is-capitalized"
|
||||
@update:modelValue="checked => toggleGroupPermissionsFromChild(group, checked)"
|
||||
>
|
||||
{{ formatPermissionTitle(route) }}
|
||||
|
|
51
go.mod
51
go.mod
|
@ -21,7 +21,7 @@ require (
|
|||
dario.cat/mergo v1.0.0
|
||||
github.com/ThreeDotsLabs/watermill v1.3.5
|
||||
github.com/adlio/trello v1.10.0
|
||||
github.com/arran4/golang-ical v0.2.5
|
||||
github.com/arran4/golang-ical v0.2.7
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||
github.com/bbrks/go-blurhash v1.1.1
|
||||
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
|
||||
|
@ -31,11 +31,12 @@ require (
|
|||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2
|
||||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/ganigeorgiev/fexpr v0.4.0
|
||||
github.com/getsentry/sentry-go v0.27.0
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.9.0
|
||||
github.com/go-sql-driver/mysql v1.8.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.10.0
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-version v1.6.0
|
||||
|
@ -52,41 +53,42 @@ require (
|
|||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/redis/go-redis/v9 v9.4.0
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/redis/go-redis/v9 v9.5.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/swaggo/swag v1.16.3
|
||||
github.com/tkuchiki/go-timezone v0.2.2
|
||||
github.com/typesense/typesense-go v1.0.0
|
||||
github.com/ulule/limiter/v3 v3.11.2
|
||||
github.com/wneessen/go-mail v0.4.1
|
||||
github.com/yuin/goldmark v1.7.0
|
||||
golang.org/x/crypto v0.19.0
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/image v0.15.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
golang.org/x/oauth2 v0.18.0
|
||||
golang.org/x/sync v0.6.0
|
||||
golang.org/x/sys v0.17.0
|
||||
golang.org/x/term v0.17.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/term v0.18.0
|
||||
golang.org/x/text v0.14.0
|
||||
gopkg.in/d4l3k/messagediff.v1 v1.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
mvdan.cc/xurls/v2 v2.5.0
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240206231429-45b9ea635e03
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240305180710-770b8eae9cec
|
||||
src.techknowlogick.com/xormigrate v1.7.1
|
||||
xorm.io/builder v0.3.13
|
||||
xorm.io/xorm v1.3.8
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ClickHouse/ch-go v0.55.0 // indirect
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.9.1 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/ClickHouse/ch-go v0.58.2 // indirect
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.18.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
|
@ -106,7 +108,7 @@ require (
|
|||
github.com/go-chi/chi/v5 v5.0.10 // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.6.1 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.20.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.3 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
|
@ -134,21 +136,19 @@ require (
|
|||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/onsi/ginkgo v1.16.4 // indirect
|
||||
github.com/onsi/gomega v1.16.0 // indirect
|
||||
github.com/paulmach/orb v0.9.0 // indirect
|
||||
github.com/paulmach/orb v0.11.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
|
@ -162,23 +162,24 @@ require (
|
|||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/tj/assert v0.0.3 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/urfave/cli/v2 v2.3.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 // indirect
|
||||
go.opentelemetry.io/otel v1.15.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.15.0 // indirect
|
||||
go.opentelemetry.io/otel v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue