Compare commits
54
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c82e5fb235 | ||
|
|
e5e8e8d134 | ||
|
|
f120d72211 | ||
|
|
a2951570a7 | ||
|
|
f4efdaa5de | ||
|
|
32edef2d38 | ||
|
|
44c1f0d281 | ||
|
|
2dab2ccedd | ||
|
|
827c43fe12 | ||
|
|
415c6380a5 | ||
|
|
ebe25ee2d7 | ||
|
|
cc5f48eb74 | ||
|
|
3969f6ae66 | ||
|
|
5ab720d709 | ||
|
|
7ae38c5ac1 | ||
|
|
162741e940 | ||
|
|
205f330f8a | ||
|
|
a12c169ce8 | ||
|
|
2facbae0d7 | ||
|
|
77a779acea | ||
|
|
89e349f2fd | ||
|
|
77feae47d6 | ||
|
|
2fb263a109 | ||
|
|
641fec1215 | ||
|
|
18374c2e52 | ||
|
|
9921551f96 | ||
|
|
b26c359ce6 | ||
|
|
d7d3ffeebd | ||
|
|
0b61885e89 | ||
|
|
8d5cb335bd | ||
|
|
71e62541a1 | ||
|
|
ee065d9238 | ||
|
|
28ed754c66 | ||
|
|
390f71b0c6 | ||
|
|
8c6d98bb02 | ||
|
|
ef30868514 | ||
|
|
75eb878e33 | ||
|
|
1ab6fef70a | ||
|
|
6defc4cba2 | ||
|
|
3abdf2ccdd | ||
|
|
001268a33e | ||
|
|
3129051a9c | ||
|
|
45c97a456d | ||
|
|
1255bdc4ab | ||
|
|
99856b2031 | ||
|
|
eec53e8a54 | ||
|
|
d4a389279c | ||
|
|
3a65324a7d | ||
|
|
bec11d42fc | ||
|
|
d2eeac2dbe | ||
|
|
05e7468135 | ||
|
|
4babe72f24 | ||
|
|
e9fe927644 | ||
|
|
7cefb86a78 |
+34
-5
@@ -353,11 +353,12 @@ trigger:
|
||||
|
||||
services:
|
||||
- name: api
|
||||
image: vikunja/api:unstable
|
||||
image: vikunja/vikunja:unstable
|
||||
pull: always
|
||||
environment:
|
||||
VIKUNJA_SERVICE_TESTINGTOKEN: averyLongSecretToSe33dtheDB
|
||||
VIKUNJA_LOG_LEVEL: DEBUG
|
||||
VIKUNJA_CORS_ENABLE: 1
|
||||
|
||||
steps:
|
||||
- name: dependencies
|
||||
@@ -783,6 +784,20 @@ steps:
|
||||
- tag
|
||||
depends_on: [ build-os-packages-version ]
|
||||
|
||||
- name: gitea-release
|
||||
image: plugins/gitea-release
|
||||
pull: true
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: gitea_token
|
||||
base_url: https://kolaente.dev
|
||||
files: dist/zip/*
|
||||
prerelease: true
|
||||
title: ${DRONE_TAG##v}
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
depends_on: [ sign-release ]
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
@@ -1199,8 +1214,21 @@ steps:
|
||||
# - '.cache'
|
||||
# depends_on:
|
||||
# - build
|
||||
|
||||
- name: rename-unstable
|
||||
image: bash
|
||||
pull: true
|
||||
commands:
|
||||
- cd desktop/dist
|
||||
- bash -c 'for file in Vikunja*; do suffix=".$${file##*.}"; if [[ ! -d $file ]]; then mv "$file" "Vikunja-Desktop-unstable$${suffix}"; fi; done'
|
||||
depends_on:
|
||||
- build
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: release-latest
|
||||
- name: release-unstable
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
settings:
|
||||
@@ -1213,13 +1241,14 @@ steps:
|
||||
region: fr-par
|
||||
path_style: true
|
||||
strip_prefix: desktop/dist/
|
||||
source: desktop/dist/*
|
||||
source: desktop/dist/Vikunja-Desktop*
|
||||
target: /desktop/unstable/
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
depends_on: [ build ]
|
||||
depends_on:
|
||||
- rename-unstable
|
||||
|
||||
- name: release-version
|
||||
image: plugins/s3
|
||||
@@ -1360,6 +1389,6 @@ steps:
|
||||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: 128214182f90834702da16ed7b0f1dec09dc61bbbe7f293aea7cb6c46e7edae2
|
||||
hmac: aa9bd51fc7d73686ee169060dcb4d6540214825a0d5134035f477a97f77dd24d
|
||||
|
||||
...
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"electron-builder": "24.9.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"connect-history-api-fallback": "^2.0.0",
|
||||
"express": "^4.17.1"
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"express": "4.18.2"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -558,7 +558,7 @@ config-file-ts@^0.2.4:
|
||||
glob "^7.1.6"
|
||||
typescript "^4.0.2"
|
||||
|
||||
connect-history-api-fallback@^2.0.0:
|
||||
connect-history-api-fallback@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8"
|
||||
integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==
|
||||
@@ -829,7 +829,7 @@ etag@~1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
||||
|
||||
express@^4.17.1:
|
||||
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==
|
||||
|
||||
@@ -41,24 +41,13 @@ There are multiple categories of subcommands in the magefile:
|
||||
|
||||
These tasks are automatically run in our CI every time someone pushes to main or you update a pull request:
|
||||
|
||||
* `mage check:lint`
|
||||
* `mage check:fmt`
|
||||
* `mage check:ineffassign`
|
||||
* `mage check:misspell`
|
||||
* `mage check:goconst`
|
||||
* `mage build:generate`
|
||||
* `mage lint`
|
||||
* `mage build:build`
|
||||
|
||||
## Build
|
||||
|
||||
### Build Vikunja
|
||||
|
||||
```
|
||||
mage build:build
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
mage build
|
||||
```
|
||||
@@ -79,16 +68,9 @@ All check sub-commands exit with a status code of 1 if the check fails.
|
||||
|
||||
Various code-checks are available:
|
||||
|
||||
* `mage check:all`: Runs fmt-check, lint, got-swag, misspell-check, ineffasign-check, gocyclo-check, static-check, gosec-check, goconst-check all in parallel
|
||||
* `mage check:fmt`: Checks if the code is properly formatted with go fmt
|
||||
* `mage check:go-sec`: Checks the source code for potential security issues by scanning the Go AST using the [gosec tool](https://github.com/securego/gosec)
|
||||
* `mage check:goconst`: Checks for repeated strings that could be replaced by a constant using [goconst](https://github.com/jgautheron/goconst/)
|
||||
* `mage check:gocyclo`: Checks for the cyclomatic complexity of the source code using [gocyclo](https://github.com/fzipp/gocyclo)
|
||||
* `mage check:got-swag`: Checks if the swagger docs need to be re-generated from the code annotations
|
||||
* `mage check:ineffassign`: Checks the source code for ineffectual assigns using [ineffassign](https://github.com/gordonklaus/ineffassign)
|
||||
* `mage check:lint`: Runs golint on all packages
|
||||
* `mage check:misspell`: Checks the source code for misspellings
|
||||
* `mage check:static`: Statically analyzes the source code about a range of different problems using [staticcheck](https://staticcheck.io/docs/)
|
||||
* `mage check:all`: Runs golangci and swagger documentation check
|
||||
* `mage lint`: Checks if the code follows the rules as defined in the `.golangci.yml` config file.
|
||||
* `mage lint:fix`: Fixes all code style issues which are easily fixable.
|
||||
|
||||
## Release
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: "Desktop Packages"
|
||||
date: 2024-02-11T15:58:18+01:00
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "setup"
|
||||
---
|
||||
|
||||
# Desktop Packages
|
||||
|
||||
Vikunja is available as an electron-based desktop application for Linux and Windows.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download the latest release for your platform from [the download page](https://dl.vikunja.io/desktop/).
|
||||
* For Windows, choose the file with the `.exe` or `.msi` file ending
|
||||
* For a Linux-based operating system, choose a file with an ending for your operating system - we have builds for Alpine, AppImage, Arch Linux, Debian-based systems, FreeBSD, Fedora and Snap.
|
||||
2. Run the downloaded package in the same way you would normally install a package for your OS.
|
||||
|
||||
## Flatpack
|
||||
|
||||
Vikunja Desktop can be installed via the [Flathub](https://flathub.org/apps/io.vikunja.Vikunja).
|
||||
|
||||
To install it, run the following command:
|
||||
|
||||
```
|
||||
flatpak install flathub io.vikunja.Vikunja
|
||||
```
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
environment:
|
||||
VIKUNJA_SERVICE_PUBLICURL: http://<the public url where vikunja is reachable>
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: secret
|
||||
VIKUNJA_DATABASE_PASSWORD: changeme
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
@@ -47,19 +47,24 @@ services:
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
db:
|
||||
image: mariadb:10
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersupersecret
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
MYSQL_USER: vikunja
|
||||
MYSQL_PASSWORD: supersecret
|
||||
MYSQL_PASSWORD: changeme
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u $$MYSQL_USER --password=$$MYSQL_PASSWORD"]
|
||||
interval: 2s
|
||||
start_period: 30s
|
||||
```
|
||||
|
||||
This defines two services, each with their own container:
|
||||
|
||||
@@ -34,21 +34,20 @@ To use postgres as a database backend, change the `db` section of the examples t
|
||||
|
||||
```yaml
|
||||
db:
|
||||
image: postgres:13
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_PASSWORD: secret
|
||||
POSTGRES_PASSWORD: changeme
|
||||
POSTGRES_USER: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -h localhost -U $$POSTGRES_USER"]
|
||||
interval: 2s
|
||||
```
|
||||
|
||||
You'll also need to change the `VIKUNJA_DATABASE_TYPE` to `postgres` on the api container declaration.
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> The mariadb container can sometimes take a while to initialize, especially on the first run. During this time, the api container will fail to start at all. It will automatically restart every few seconds.
|
||||
</div>
|
||||
|
||||
## Sqlite
|
||||
|
||||
Vikunja supports postgres, mysql and sqlite as a database backend. The examples on this page use mysql with a mariadb container.
|
||||
@@ -101,7 +100,7 @@ services:
|
||||
environment:
|
||||
VIKUNJA_SERVICE_PUBLICURL: http://<the public url where vikunja is reachable>
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: secret
|
||||
VIKUNJA_DATABASE_PASSWORD: changeme
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
@@ -111,19 +110,24 @@ services:
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
db:
|
||||
image: mariadb:10
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersupersecret
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
MYSQL_USER: vikunja
|
||||
MYSQL_PASSWORD: supersecret
|
||||
MYSQL_PASSWORD: changeme
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u $$MYSQL_USER --password=$$MYSQL_PASSWORD"]
|
||||
interval: 2s
|
||||
start_period: 30s
|
||||
```
|
||||
|
||||
## Example with Traefik 2
|
||||
@@ -145,7 +149,7 @@ services:
|
||||
environment:
|
||||
VIKUNJA_SERVICE_PUBLICURL: http://<the public url where vikunja is reachable>
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: supersecret
|
||||
VIKUNJA_DATABASE_PASSWORD: changeme
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
@@ -156,10 +160,12 @@ services:
|
||||
- web
|
||||
- default
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web"
|
||||
- "traefik.http.routers.vikunja.rule=Host(`vikunja.example.com`)"
|
||||
- "traefik.http.routers.vikunja.entrypoints=https"
|
||||
- "traefik.http.routers.vikunja.tls.certResolver=acme"
|
||||
@@ -169,11 +175,15 @@ services:
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersupersecret
|
||||
MYSQL_USER: vikunja
|
||||
MYSQL_PASSWORD: supersecret
|
||||
MYSQL_PASSWORD: changeme
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u $$MYSQL_USER --password=$$MYSQL_PASSWORD"]
|
||||
interval: 2s
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
web:
|
||||
@@ -204,7 +214,7 @@ services:
|
||||
environment:
|
||||
VIKUNJA_SERVICE_PUBLICURL: http://<the public url where vikunja is reachable>
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: secret
|
||||
VIKUNJA_DATABASE_PASSWORD: changeme
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
@@ -214,19 +224,24 @@ services:
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
db:
|
||||
image: mariadb:10
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersupersecret
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
MYSQL_USER: vikunja
|
||||
MYSQL_PASSWORD: supersecret
|
||||
MYSQL_PASSWORD: changeme
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u $$MYSQL_USER --password=$$MYSQL_PASSWORD"]
|
||||
interval: 2s
|
||||
start_period: 30s
|
||||
caddy:
|
||||
image: caddy
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -29,10 +29,11 @@ You can also:
|
||||
Vikunja can be installed in various ways.
|
||||
This document provides an overview and instructions for the different methods:
|
||||
|
||||
* [Installing from binary](#install-from-binary)
|
||||
* [Installing from binary (manual)](#install-from-binary)
|
||||
* [Build from source]({{< ref "build-from-source.md">}})
|
||||
* [Docker](#docker)
|
||||
* [Debian packages](#debian-packages)
|
||||
* [Debian](#debian-packages)
|
||||
* [RPM](#rpm)
|
||||
* [FreeBSD](#freebsd--freenas)
|
||||
* [Kubernetes]({{< ref "k8s.md" >}})
|
||||
|
||||
@@ -175,7 +176,7 @@ Check out the [docker examples]({{<ref "full-docker-example.md">}}) for more adv
|
||||
|
||||
## Debian packages
|
||||
|
||||
Vikunja is available as debian packages.
|
||||
Vikunja is available as deb package for installation on debian-like systems.
|
||||
|
||||
To install these, grab a `.deb` file from [the download page](https://dl.vikunja.io/vikunja) and run
|
||||
|
||||
@@ -186,6 +187,18 @@ dpkg -i vikunja.deb
|
||||
This will install Vikunja to `/opt/vikunja`.
|
||||
To configure it, use the config file in `/etc/vikunja/config.yml`.
|
||||
|
||||
## RPM
|
||||
|
||||
Vikunja is available as rpm package for installation on Fedora, CentOS and others.
|
||||
|
||||
To install these, grab a `.rpm` file from [the download page](https://dl.vikunja.io/vikunja) and run
|
||||
|
||||
```
|
||||
rpm -i vikunja.rpm
|
||||
```
|
||||
|
||||
To configure Vikunja, use the config file in `/etc/vikunja/config.yml`.
|
||||
|
||||
## FreeBSD / FreeNAS
|
||||
|
||||
Unfortunately, we currently can't provide pre-built binaries for FreeBSD.
|
||||
|
||||
+39
-39
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@8.15.1",
|
||||
"packageManager": "pnpm@8.15.3",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
@@ -56,41 +56,41 @@
|
||||
"@infectoone/vue-ganttastic": "2.2.0",
|
||||
"@intlify/unplugin-vue-i18n": "2.0.0",
|
||||
"@kyvg/vue3-notification": "3.1.4",
|
||||
"@sentry/tracing": "7.100.1",
|
||||
"@sentry/vue": "7.100.1",
|
||||
"@tiptap/core": "2.2.1",
|
||||
"@tiptap/extension-blockquote": "2.2.1",
|
||||
"@tiptap/extension-bold": "2.2.1",
|
||||
"@tiptap/extension-bullet-list": "2.2.1",
|
||||
"@tiptap/extension-code": "2.2.1",
|
||||
"@tiptap/extension-code-block-lowlight": "2.2.1",
|
||||
"@tiptap/extension-document": "2.2.1",
|
||||
"@tiptap/extension-dropcursor": "2.2.1",
|
||||
"@tiptap/extension-gapcursor": "2.2.1",
|
||||
"@tiptap/extension-hard-break": "2.2.1",
|
||||
"@tiptap/extension-heading": "2.2.1",
|
||||
"@tiptap/extension-history": "2.2.1",
|
||||
"@tiptap/extension-horizontal-rule": "2.2.1",
|
||||
"@tiptap/extension-image": "2.2.1",
|
||||
"@tiptap/extension-italic": "2.2.1",
|
||||
"@tiptap/extension-link": "2.2.1",
|
||||
"@tiptap/extension-list-item": "2.2.1",
|
||||
"@tiptap/extension-ordered-list": "2.2.1",
|
||||
"@tiptap/extension-paragraph": "2.2.1",
|
||||
"@tiptap/extension-placeholder": "2.2.1",
|
||||
"@tiptap/extension-strike": "2.2.1",
|
||||
"@tiptap/extension-table": "2.2.1",
|
||||
"@tiptap/extension-table-cell": "2.2.1",
|
||||
"@tiptap/extension-table-header": "2.2.1",
|
||||
"@tiptap/extension-table-row": "2.2.1",
|
||||
"@tiptap/extension-task-item": "2.2.1",
|
||||
"@tiptap/extension-task-list": "2.2.1",
|
||||
"@tiptap/extension-text": "2.2.1",
|
||||
"@tiptap/extension-typography": "2.2.1",
|
||||
"@tiptap/extension-underline": "2.2.1",
|
||||
"@tiptap/pm": "2.2.1",
|
||||
"@tiptap/suggestion": "2.2.1",
|
||||
"@tiptap/vue-3": "2.2.1",
|
||||
"@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",
|
||||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.clonedeep": "4.5.9",
|
||||
"@vueuse/core": "10.7.2",
|
||||
@@ -116,7 +116,7 @@
|
||||
"sortablejs": "1.15.2",
|
||||
"tippy.js": "6.3.7",
|
||||
"ufo": "1.4.0",
|
||||
"vue": "3.4.15",
|
||||
"vue": "3.4.19",
|
||||
"vue-advanced-cropper": "2.8.8",
|
||||
"vue-flatpickr-component": "11.0.3",
|
||||
"vue-i18n": "9.9.1",
|
||||
@@ -142,8 +142,8 @@
|
||||
"@types/node": "20.11.10",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@types/sortablejs": "1.15.7",
|
||||
"@typescript-eslint/eslint-plugin": "6.20.0",
|
||||
"@typescript-eslint/parser": "6.20.0",
|
||||
"@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",
|
||||
|
||||
Generated
+529
-407
File diff suppressed because it is too large
Load Diff
@@ -84,9 +84,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref} from 'vue'
|
||||
import {computed} from 'vue'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useStorage} from '@vueuse/core'
|
||||
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
@@ -112,7 +113,18 @@ const projectStore = useProjectStore()
|
||||
const baseStore = useBaseStore()
|
||||
const currentProject = computed(() => baseStore.currentProject)
|
||||
|
||||
const childProjectsOpen = ref(true)
|
||||
// Persist open state across browser reloads. Using a seperate ref for the state
|
||||
// allows us to use only one entry in local storage instead of one for every project id.
|
||||
type openState = { [key: number]: boolean }
|
||||
const childProjectsOpenState = useStorage<openState>('navigation-child-projects-open', {})
|
||||
const childProjectsOpen = computed({
|
||||
get() {
|
||||
return childProjectsOpenState.value[project.id] ?? true
|
||||
},
|
||||
set(open) {
|
||||
childProjectsOpenState.value[project.id] = open
|
||||
},
|
||||
})
|
||||
|
||||
const childProjects = computed(() => {
|
||||
return projectStore.getChildProjects(project.id)
|
||||
|
||||
@@ -193,6 +193,7 @@ 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,
|
||||
@@ -388,7 +389,20 @@ const editor = useEditor({
|
||||
CustomImage,
|
||||
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
TaskItem.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
id: {
|
||||
default: () => createRandomID(),
|
||||
parseHTML: element => element.getAttribute('data-id'),
|
||||
renderHTML: attributes => ({
|
||||
'data-id': attributes.id,
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
}).configure({
|
||||
nested: true,
|
||||
onReadOnlyChecked: (node: Node, checked: boolean): boolean => {
|
||||
if (!isEditEnabled) {
|
||||
@@ -400,7 +414,7 @@ const editor = useEditor({
|
||||
// https://github.com/ueberdosis/tiptap/issues/3676
|
||||
|
||||
editor.value!.state.doc.descendants((subnode, pos) => {
|
||||
if (node.eq(subnode)) {
|
||||
if (node.attrs.id === subnode.attrs.id) {
|
||||
const {tr} = editor.value!.state
|
||||
tr.setNodeMarkup(pos, undefined, {
|
||||
...node.attrs,
|
||||
@@ -408,10 +422,10 @@ const editor = useEditor({
|
||||
})
|
||||
editor.value!.view.dispatch(tr)
|
||||
bubbleSave()
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return true
|
||||
},
|
||||
}),
|
||||
@@ -594,27 +608,21 @@ function clickTasklistCheckbox(event) {
|
||||
|
||||
watch(
|
||||
() => isEditing.value,
|
||||
editing => {
|
||||
nextTick(() => {
|
||||
const checkboxes = tiptapInstanceRef.value?.querySelectorAll('[data-checked]')
|
||||
async editing => {
|
||||
await nextTick()
|
||||
|
||||
let checkboxes = tiptapInstanceRef.value?.querySelectorAll('[data-checked]')
|
||||
if (typeof checkboxes === 'undefined' || checkboxes.length === 0) {
|
||||
// For some reason, this works when we check a second time.
|
||||
await nextTick()
|
||||
|
||||
checkboxes = tiptapInstanceRef.value?.querySelectorAll('[data-checked]')
|
||||
if (typeof checkboxes === 'undefined' || checkboxes.length === 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
checkboxes.forEach(check => {
|
||||
if (check.children.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
// We assume the first child contains the label element with the checkbox and the second child the actual label
|
||||
// When the actual label is clicked, we forward that click to the checkbox.
|
||||
check.children[1].removeEventListener('click', clickTasklistCheckbox)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
checkboxes.forEach(check => {
|
||||
if (check.children.length < 2) {
|
||||
return
|
||||
@@ -622,8 +630,21 @@ watch(
|
||||
|
||||
// We assume the first child contains the label element with the checkbox and the second child the actual label
|
||||
// When the actual label is clicked, we forward that click to the checkbox.
|
||||
check.children[1].addEventListener('click', clickTasklistCheckbox)
|
||||
check.children[1].removeEventListener('click', clickTasklistCheckbox)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
checkboxes.forEach(check => {
|
||||
if (check.children.length < 2) {
|
||||
return
|
||||
}
|
||||
|
||||
// We assume the first child contains the label element with the checkbox and the second child the actual label
|
||||
// When the actual label is clicked, we forward that click to the checkbox.
|
||||
check.children[1].removeEventListener('click', clickTasklistCheckbox)
|
||||
check.children[1].addEventListener('click', clickTasklistCheckbox)
|
||||
})
|
||||
},
|
||||
{immediate: true},
|
||||
@@ -781,6 +802,7 @@ watch(
|
||||
|
||||
.ProseMirror {
|
||||
/* Table-specific styling */
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
@@ -836,6 +858,7 @@ watch(
|
||||
}
|
||||
|
||||
// Lists
|
||||
|
||||
ul {
|
||||
margin-left: .5rem;
|
||||
margin-top: 0 !important;
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
autocomplete="current-password"
|
||||
:tabindex="props.tabindex"
|
||||
@keyup.enter="e => $emit('submit', e)"
|
||||
@focusout="validate"
|
||||
@focusout="() => {validate(); validateAfterFirst = true}"
|
||||
@keyup="() => {validateAfterFirst ? validate() : null}"
|
||||
@input="handleInput"
|
||||
>
|
||||
<BaseButton
|
||||
@@ -23,16 +24,17 @@
|
||||
</BaseButton>
|
||||
</div>
|
||||
<p
|
||||
v-if="!isValid"
|
||||
v-if="isValid !== true"
|
||||
class="help is-danger"
|
||||
>
|
||||
{{ $t('user.auth.passwordRequired') }}
|
||||
{{ isValid }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, watch} from 'vue'
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -40,13 +42,17 @@ 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()
|
||||
const passwordFieldType = ref('password')
|
||||
const password = ref('')
|
||||
const isValid = ref(!props.validateInitially)
|
||||
const isValid = ref<true | string>(props.validateInitially === true ? true : '')
|
||||
const validateAfterFirst = ref(false)
|
||||
|
||||
watch(
|
||||
() => props.validateInitially,
|
||||
@@ -55,7 +61,22 @@ watch(
|
||||
)
|
||||
|
||||
const validate = useDebounceFn(() => {
|
||||
isValid.value = password.value !== ''
|
||||
if (password.value === '') {
|
||||
isValid.value = t('user.auth.passwordRequired')
|
||||
return
|
||||
}
|
||||
|
||||
if (props.validateMinLength && password.value.length < 8) {
|
||||
isValid.value = t('user.auth.passwordNotMin')
|
||||
return
|
||||
}
|
||||
|
||||
if (props.validateMinLength && password.value.length > 250) {
|
||||
isValid.value = t('user.auth.passwordNotMax')
|
||||
return
|
||||
}
|
||||
|
||||
isValid.value = true
|
||||
}, 100)
|
||||
|
||||
function togglePasswordFieldType() {
|
||||
|
||||
@@ -261,13 +261,18 @@ const foundTasks = ref<ITask[]>([])
|
||||
|
||||
async function findTasks(newQuery: string) {
|
||||
query.value = newQuery
|
||||
foundTasks.value = await taskService.getAll({}, {s: newQuery})
|
||||
const result = await taskService.getAll({}, {
|
||||
s: newQuery,
|
||||
sort_by: 'done',
|
||||
})
|
||||
|
||||
foundTasks.value = mapRelatedTasks(result)
|
||||
}
|
||||
|
||||
function mapRelatedTasks(tasks: ITask[]) {
|
||||
return tasks.map(task => {
|
||||
// by doing this here once we can save a lot of duplicate calls in the template
|
||||
const project = projectStore.projects[task.ProjectId]
|
||||
const project = projectStore.projects[task.projectId]
|
||||
|
||||
return {
|
||||
...task,
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface SortBy {
|
||||
percent_done?: Order
|
||||
created?: Order
|
||||
updated?: Order
|
||||
done_at?: Order,
|
||||
}
|
||||
|
||||
// FIXME: merge with DEFAULT_PARAMS in filters.vue
|
||||
|
||||
@@ -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?",
|
||||
@@ -712,7 +716,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.",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h1>{{ $t('migrate.titleService', {name: migrator.name}) }}</h1>
|
||||
<p>{{ $t('migrate.descriptionDo') }}</p>
|
||||
|
||||
<template v-if="message === '' && lastMigrationFinishedAt === null">
|
||||
<template v-if="message === '' && lastMigrationStartedAt === null">
|
||||
<template v-if="isMigrating === false">
|
||||
<template v-if="migrator.isFileMigrator">
|
||||
<p>{{ $t('migrate.importUpload', {name: migrator.name}) }}</p>
|
||||
@@ -54,9 +54,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="lastMigrationStartedAt && lastMigrationFinishedAt === null">
|
||||
<p>
|
||||
<Message class="mb-4">
|
||||
{{ $t('migrate.migrationInProgress') }}
|
||||
</p>
|
||||
</Message>
|
||||
<x-button :to="{name: 'home'}">
|
||||
{{ $t('home.goToOverview') }}
|
||||
</x-button>
|
||||
@@ -170,19 +170,18 @@ async function initMigration() {
|
||||
if (!migratorAuthCode.value) {
|
||||
return
|
||||
}
|
||||
const {startedAt, finishedAt} = await migrationService.getStatus()
|
||||
if (startedAt) {
|
||||
lastMigrationStartedAt.value = parseDateOrNull(startedAt)
|
||||
const {started_at, finished_at} = await migrationService.getStatus()
|
||||
if (started_at) {
|
||||
lastMigrationStartedAt.value = parseDateOrNull(started_at)
|
||||
}
|
||||
if (finishedAt) {
|
||||
lastMigrationFinishedAt.value = parseDateOrNull(finishedAt)
|
||||
if (finished_at) {
|
||||
lastMigrationFinishedAt.value = parseDateOrNull(finished_at)
|
||||
if (lastMigrationFinishedAt.value) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (lastMigrationStartedAt.value && lastMigrationFinishedAt.value === null) {
|
||||
// Migration already in progress
|
||||
return
|
||||
}
|
||||
|
||||
@@ -214,6 +213,8 @@ async function migrate() {
|
||||
message.value = result.message
|
||||
const projectStore = useProjectStore()
|
||||
return projectStore.loadProjects()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
isMigrating.value = false
|
||||
}
|
||||
|
||||
@@ -82,14 +82,15 @@
|
||||
>
|
||||
<div class="control">
|
||||
<input
|
||||
ref="bucketLimitInputRef"
|
||||
v-focus.always
|
||||
:value="bucket.limit"
|
||||
class="input"
|
||||
type="number"
|
||||
min="0"
|
||||
@keyup.esc="() => showSetLimitInput = false"
|
||||
@keyup.enter="() => showSetLimitInput = false"
|
||||
@input="(event) => setBucketLimit(bucket.id, parseInt((event.target as HTMLInputElement).value))"
|
||||
@keyup.enter="() => {setBucketLimit(bucket.id, true); showSetLimitInput = false}"
|
||||
@input="setBucketLimit(bucket.id)"
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
@@ -98,6 +99,7 @@
|
||||
:disabled="bucket.limit < 0"
|
||||
:icon="['far', 'save']"
|
||||
:shadow="false"
|
||||
@click="setBucketLimit(bucket.id, true)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -320,6 +322,7 @@ const taskStore = useTaskStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const taskContainerRefs = ref<{[id: IBucket['id']]: HTMLElement}>({})
|
||||
const bucketLimitInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const drag = ref(false)
|
||||
const dragBucket = ref(false)
|
||||
@@ -615,7 +618,7 @@ function updateBucketPosition(e: {newIndex: number}) {
|
||||
})
|
||||
}
|
||||
|
||||
async function setBucketLimit(bucketId: IBucket['id'], limit: number) {
|
||||
async function saveBucketLimit(bucketId: IBucket['id'], limit: number) {
|
||||
if (limit < 0) {
|
||||
return
|
||||
}
|
||||
@@ -627,6 +630,22 @@ async function setBucketLimit(bucketId: IBucket['id'], limit: number) {
|
||||
success({message: t('project.kanban.bucketLimitSavedSuccess')})
|
||||
}
|
||||
|
||||
const setBucketLimitCancel = ref<number|null>(null)
|
||||
|
||||
async function setBucketLimit(bucketId: IBucket['id'], now: boolean = false) {
|
||||
const limit = parseInt(bucketLimitInputRef.value?.value || '')
|
||||
|
||||
if (setBucketLimitCancel.value !== null) {
|
||||
clearTimeout(setBucketLimitCancel.value)
|
||||
}
|
||||
|
||||
if (now) {
|
||||
return saveBucketLimit(bucketId, limit)
|
||||
}
|
||||
|
||||
setBucketLimitCancel.value = setTimeout(saveBucketLimit, 2500, bucketId, limit)
|
||||
}
|
||||
|
||||
function shouldAcceptDrop(bucket: IBucket) {
|
||||
return (
|
||||
// When dragging from a bucket who has its limit reached, dragging should still be possible
|
||||
|
||||
@@ -52,6 +52,9 @@
|
||||
<Fancycheckbox v-model="activeColumns.percentDone">
|
||||
{{ $t('task.attributes.percentDone') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.doneAt">
|
||||
{{ $t('task.attributes.doneAt') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.created">
|
||||
{{ $t('task.attributes.created') }}
|
||||
</Fancycheckbox>
|
||||
@@ -144,6 +147,13 @@
|
||||
@click="sort('percent_done')"
|
||||
/>
|
||||
</th>
|
||||
<th v-if="activeColumns.doneAt">
|
||||
{{ $t('task.attributes.doneAt') }}
|
||||
<Sort
|
||||
:order="sortBy.done_at"
|
||||
@click="sort('done_at')"
|
||||
/>
|
||||
</th>
|
||||
<th v-if="activeColumns.created">
|
||||
{{ $t('task.attributes.created') }}
|
||||
<Sort
|
||||
@@ -223,6 +233,10 @@
|
||||
<td v-if="activeColumns.percentDone">
|
||||
{{ t.percentDone * 100 }}%
|
||||
</td>
|
||||
<DateTableCell
|
||||
v-if="activeColumns.doneAt"
|
||||
:date="t.doneAt"
|
||||
/>
|
||||
<DateTableCell
|
||||
v-if="activeColumns.created"
|
||||
:date="t.created"
|
||||
@@ -297,6 +311,7 @@ const ACTIVE_COLUMNS_DEFAULT = {
|
||||
created: false,
|
||||
updated: false,
|
||||
createdBy: false,
|
||||
doneAt: false,
|
||||
}
|
||||
|
||||
const SORT_BY_DEFAULT: SortBy = {
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
v-model="password"
|
||||
tabindex="2"
|
||||
:validate-initially="validatePasswordInitially"
|
||||
:validate-min-length="false"
|
||||
@submit="submit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -28,21 +28,24 @@
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
@keyup.enter="submit"
|
||||
@focusout="validateUsername"
|
||||
@focusout="() => {validateUsername(); validateUsernameAfterFirst = true}"
|
||||
@keyup="() => {validateUsernameAfterFirst ? validateUsername() : null}"
|
||||
>
|
||||
</div>
|
||||
<p
|
||||
v-if="!usernameValid"
|
||||
v-if="usernameValid !== true"
|
||||
class="help is-danger"
|
||||
>
|
||||
{{ $t('user.auth.usernameRequired') }}
|
||||
{{ usernameValid }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label
|
||||
class="label"
|
||||
for="email"
|
||||
>{{ $t('user.auth.email') }}</label>
|
||||
>
|
||||
{{ $t('user.auth.email') }}
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
id="email"
|
||||
@@ -53,7 +56,8 @@
|
||||
required
|
||||
type="email"
|
||||
@keyup.enter="submit"
|
||||
@focusout="validateEmail"
|
||||
@focusout="() => {validateEmail(); validateEmailAfterFirst = true}"
|
||||
@keyup="() => {validateEmailAfterFirst ? validateEmail() : null}"
|
||||
>
|
||||
</div>
|
||||
<p
|
||||
@@ -84,7 +88,7 @@
|
||||
>
|
||||
{{ $t('user.auth.createAccount') }}
|
||||
</x-button>
|
||||
|
||||
|
||||
<Message
|
||||
v-if="configStore.demoModeEnabled"
|
||||
variant="warning"
|
||||
@@ -94,7 +98,7 @@
|
||||
{{ $t('demo.accountWillBeDeleted') }}<br>
|
||||
<strong class="is-uppercase">{{ $t('demo.everythingWillBeDeleted') }}</strong>
|
||||
</Message>
|
||||
|
||||
|
||||
<p class="mt-2">
|
||||
{{ $t('user.auth.alreadyHaveAnAccount') }}
|
||||
<router-link :to="{ name: 'user.login' }">
|
||||
@@ -107,7 +111,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue'
|
||||
import {computed, onBeforeMount, reactive, ref, toRaw} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import router from '@/router'
|
||||
import Message from '@/components/misc/message.vue'
|
||||
@@ -117,6 +122,7 @@ import Password from '@/components/input/password.vue'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
const {t} = useI18n()
|
||||
const authStore = useAuthStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
@@ -142,13 +148,30 @@ const DEBOUNCE_TIME = 100
|
||||
|
||||
// debouncing to prevent error messages when clicking on the log in button
|
||||
const emailValid = ref(true)
|
||||
const validateEmailAfterFirst = ref(false)
|
||||
const validateEmail = useDebounceFn(() => {
|
||||
emailValid.value = isEmail(credentials.email)
|
||||
}, DEBOUNCE_TIME)
|
||||
|
||||
const usernameValid = ref(true)
|
||||
const usernameValid = ref<true | string>(true)
|
||||
const validateUsernameAfterFirst = ref(false)
|
||||
const validateUsername = useDebounceFn(() => {
|
||||
usernameValid.value = credentials.username !== ''
|
||||
if (credentials.username === '') {
|
||||
usernameValid.value = t('user.auth.usernameRequired')
|
||||
return
|
||||
}
|
||||
|
||||
if(credentials.username.indexOf(' ') !== -1) {
|
||||
usernameValid.value = t('user.auth.usernameMustNotContainSpace')
|
||||
return
|
||||
}
|
||||
|
||||
if(credentials.username.indexOf('://') !== -1) {
|
||||
usernameValid.value = t('user.auth.usernameMustNotLookLikeUrl')
|
||||
return
|
||||
}
|
||||
|
||||
usernameValid.value = true
|
||||
}, DEBOUNCE_TIME)
|
||||
|
||||
const everythingValid = computed(() => {
|
||||
|
||||
@@ -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.4
|
||||
github.com/arran4/golang-ical v0.2.6
|
||||
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,7 +31,7 @@ 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/getsentry/sentry-go v0.26.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/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
|
||||
@@ -39,6 +39,7 @@ require (
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-version v1.6.0
|
||||
github.com/hhsnopek/etag v0.0.0-20171206181245-aea95f647346
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6
|
||||
@@ -65,12 +66,12 @@ require (
|
||||
github.com/ulule/limiter/v3 v3.11.2
|
||||
github.com/wneessen/go-mail v0.4.0
|
||||
github.com/yuin/goldmark v1.7.0
|
||||
golang.org/x/crypto v0.18.0
|
||||
golang.org/x/crypto v0.19.0
|
||||
golang.org/x/image v0.15.0
|
||||
golang.org/x/oauth2 v0.16.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
golang.org/x/sync v0.6.0
|
||||
golang.org/x/sys v0.16.0
|
||||
golang.org/x/term v0.16.0
|
||||
golang.org/x/sys v0.17.0
|
||||
golang.org/x/term v0.17.0
|
||||
golang.org/x/text v0.14.0
|
||||
gopkg.in/d4l3k/messagediff.v1 v1.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -78,7 +79,7 @@ require (
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240206231429-45b9ea635e03
|
||||
src.techknowlogick.com/xormigrate v1.7.1
|
||||
xorm.io/builder v0.3.13
|
||||
xorm.io/xorm v1.3.7
|
||||
xorm.io/xorm v1.3.8
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -120,7 +121,6 @@ require (
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hhsnopek/etag v0.0.0-20171206181245-aea95f647346 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
@@ -174,7 +174,7 @@ require (
|
||||
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.20.0 // indirect
|
||||
golang.org/x/net v0.21.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
|
||||
|
||||
@@ -33,6 +33,10 @@ github.com/arran4/golang-ical v0.2.3 h1:C4Vj7+BjJBIrAJhHgi6Ku+XUkQVugRq4re5Cqj5Q
|
||||
github.com/arran4/golang-ical v0.2.3/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||
github.com/arran4/golang-ical v0.2.4 h1:0/rTXn2qqEekLKec3SzRRy+z7pCLtniMb0KD/dPogUo=
|
||||
github.com/arran4/golang-ical v0.2.4/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||
github.com/arran4/golang-ical v0.2.5 h1:zaAdee/cOnOCeSuxUSgkWnF9jZl/oYq2ZgDk+LU3wGs=
|
||||
github.com/arran4/golang-ical v0.2.5/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||
github.com/arran4/golang-ical v0.2.6 h1:WRpbLKSIMjujycCNKGAjOALyj6evvklVpWXH+Hp72G4=
|
||||
github.com/arran4/golang-ical v0.2.6/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/bbrks/go-blurhash v1.1.1 h1:uoXOxRPDca9zHYabUTwvS4KnY++KKUbwFo+Yxb8ME4M=
|
||||
@@ -113,6 +117,8 @@ github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX
|
||||
github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/getsentry/sentry-go v0.26.0 h1:IX3++sF6/4B5JcevhdZfdKIHfyvMmAq/UnqcyT2H6mA=
|
||||
github.com/getsentry/sentry-go v0.26.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
|
||||
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
@@ -599,6 +605,8 @@ golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
@@ -637,10 +645,14 @@ golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
|
||||
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
|
||||
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -684,6 +696,8 @@ golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -692,6 +706,8 @@ golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -835,3 +851,5 @@ xorm.io/xorm v1.3.6 h1:hfpWHkDIWWqUi8FRF2H2M9O8lO3Ov47rwFcS9gPzPkU=
|
||||
xorm.io/xorm v1.3.6/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo=
|
||||
xorm.io/xorm v1.3.7 h1:mLceAGu0b87r9pD4qXyxGHxifOXIIrAdVcA6k95/osw=
|
||||
xorm.io/xorm v1.3.7/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
|
||||
xorm.io/xorm v1.3.8 h1:CJmplmWqfSRpLWSPMmqz+so8toBp3m7ehuRehIWedZo=
|
||||
xorm.io/xorm v1.3.8/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
|
||||
|
||||
+2
-1
@@ -71,7 +71,8 @@ var (
|
||||
"dev:make-listener": Dev.MakeListener,
|
||||
"dev:make-notification": Dev.MakeNotification,
|
||||
"generate-docs": GenerateDocs,
|
||||
"check:golangci-fix": Check.GolangciFix,
|
||||
"lint": Check.Golangci,
|
||||
"lint:fix": Check.GolangciFix,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -179,6 +179,10 @@ func initSqliteEngine() (engine *xorm.Engine, err error) {
|
||||
path = "./db.db"
|
||||
}
|
||||
|
||||
if path == "memory" {
|
||||
return xorm.NewEngine("sqlite3", "file::memory:?cache=shared")
|
||||
}
|
||||
|
||||
// Try opening the db file to return a better error message if that does not work
|
||||
var exists = true
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
|
||||
+9
-2
@@ -17,7 +17,9 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
gofs "io/fs"
|
||||
)
|
||||
|
||||
// Dump dumps all saved files
|
||||
@@ -31,8 +33,13 @@ func Dump() (allFiles map[int64]io.ReadCloser, err error) {
|
||||
|
||||
allFiles = make(map[int64]io.ReadCloser, len(files))
|
||||
for _, file := range files {
|
||||
if err := file.LoadFileByID(); err != nil {
|
||||
return nil, err
|
||||
err = file.LoadFileByID()
|
||||
if err != nil {
|
||||
var pathError *gofs.PathError
|
||||
if errors.As(err, &pathError) {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
allFiles[file.ID] = file.File
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func init() {
|
||||
func setupActiveUsersMetric() {
|
||||
err := registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_active_users",
|
||||
Help: "The number of shares active within the last 30 seconds",
|
||||
Help: "The number of users active within the last 30 seconds",
|
||||
}, func() float64 {
|
||||
allActiveUsers := activeUsersMap{}
|
||||
_, err := keyvalue.GetWithValue(activeUsersKey, &allActiveUsers)
|
||||
|
||||
@@ -69,7 +69,7 @@ func getRouteGroupName(path string) string {
|
||||
// CollectRoutesForAPITokenUsage gets called for every added APITokenRoute and builds a list of all routes we can use for the api tokens.
|
||||
func CollectRoutesForAPITokenUsage(route echo.Route) {
|
||||
|
||||
if !strings.Contains(route.Name, "(*WebHandler)") {
|
||||
if !strings.Contains(route.Name, "(*WebHandler)") && !strings.Contains(route.Name, "Attachment") {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -116,6 +116,21 @@ func CollectRoutesForAPITokenUsage(route echo.Route) {
|
||||
Method: route.Method,
|
||||
}
|
||||
}
|
||||
|
||||
if routeGroupName == "tasks_attachments" {
|
||||
if strings.Contains(route.Name, "UploadTaskAttachment") {
|
||||
apiTokenRoutes[routeGroupName].Create = &RouteDetail{
|
||||
Path: route.Path,
|
||||
Method: route.Method,
|
||||
}
|
||||
}
|
||||
if strings.Contains(route.Name, "GetTaskAttachment") {
|
||||
apiTokenRoutes[routeGroupName].ReadOne = &RouteDetail{
|
||||
Path: route.Path,
|
||||
Method: route.Method,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetAvailableAPIRoutesForToken returns a list of all API routes which are available for token usage.
|
||||
|
||||
@@ -30,11 +30,14 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/initialize"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/migration"
|
||||
vversion "code.vikunja.io/api/pkg/version"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
)
|
||||
@@ -63,6 +66,7 @@ func Restore(filename string) error {
|
||||
// Find the configFile, database and files files
|
||||
var configFile *zip.File
|
||||
var dotEnvFile *zip.File
|
||||
var versionFile *zip.File
|
||||
dbfiles := make(map[string]*zip.File)
|
||||
filesFiles := make(map[string]*zip.File)
|
||||
for _, file := range r.File {
|
||||
@@ -81,7 +85,18 @@ func Restore(filename string) error {
|
||||
}
|
||||
if strings.HasPrefix(file.Name, "files/") {
|
||||
filesFiles[strings.ReplaceAll(file.Name, "files/", "")] = file
|
||||
continue
|
||||
}
|
||||
if file.Name == "VERSION" {
|
||||
versionFile = file
|
||||
}
|
||||
}
|
||||
|
||||
///////
|
||||
// Check if we're restoring to the same version as the dump
|
||||
err = checkVikunjaVersion(versionFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
///////
|
||||
@@ -278,3 +293,38 @@ func restoreConfig(configFile, dotEnvFile *zip.File) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkVikunjaVersion(versionFile *zip.File) error {
|
||||
if versionFile == nil {
|
||||
return fmt.Errorf("dump does not contain VERSION file, refusing to continue")
|
||||
}
|
||||
vf, err := versionFile.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open version file: %w", err)
|
||||
}
|
||||
|
||||
var bufVersion bytes.Buffer
|
||||
if _, err := bufVersion.ReadFrom(vf); err != nil {
|
||||
return fmt.Errorf("could not read version file: %w", err)
|
||||
}
|
||||
|
||||
versionString := bufVersion.String()
|
||||
if versionString == "dev" && vversion.Version == "dev" {
|
||||
log.Debugf("Importing from dev version")
|
||||
} else {
|
||||
dumpedVersion, err := version.NewVersion(bufVersion.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentVersion, err := version.NewVersion(vversion.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !dumpedVersion.Equal(currentVersion) {
|
||||
return fmt.Errorf("export was created with version %s but this is %s - please make sure you are running the same Vikunja version before restoring", dumpedVersion, currentVersion)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -73,8 +73,8 @@ func (mw *MigrationWeb) Migrate(c echo.Context) error {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
if stats.FinishedAt.IsZero() {
|
||||
return c.JSON(http.StatusOK, map[string]string{
|
||||
if !stats.StartedAt.IsZero() && stats.FinishedAt.IsZero() {
|
||||
return c.JSON(http.StatusPreconditionFailed, map[string]string{
|
||||
"message": "Migration already running",
|
||||
"running_since": stats.StartedAt.String(),
|
||||
})
|
||||
@@ -95,7 +95,7 @@ func (mw *MigrationWeb) Migrate(c echo.Context) error {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, models.Message{Message: "Everything was migrated successfully."})
|
||||
return c.JSON(http.StatusOK, models.Message{Message: "Migration was started successfully."})
|
||||
}
|
||||
|
||||
// Status returns whether or not a user has already done this migration
|
||||
|
||||
@@ -151,7 +151,7 @@ func setupSentry(e *echo.Echo) {
|
||||
e.HTTPErrorHandler = func(err error, c echo.Context) {
|
||||
// Only capture errors not already handled by echo
|
||||
var herr *echo.HTTPError
|
||||
if errors.As(err, &herr) && herr.Code > 403 {
|
||||
if errors.As(err, &herr) && herr.Code > 499 {
|
||||
hub := sentryecho.GetHubFromContext(c)
|
||||
if hub != nil {
|
||||
hub.WithScope(func(scope *sentry.Scope) {
|
||||
|
||||
+11
-7
@@ -126,12 +126,11 @@ func serveIndexFile(c echo.Context, assetFs http.FileSystem) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
etag, err := generateEtag(index, info.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return serveFile(c, reader, info, etag)
|
||||
//etag, err := generateEtag(index, info.Name())
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
return serveFile(c, reader, info, "")
|
||||
}
|
||||
|
||||
// Copied from echo's middleware.StaticWithConfig simplified and adjusted for caching
|
||||
@@ -141,6 +140,9 @@ func static() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) (err error) {
|
||||
p := c.Request().URL.Path
|
||||
if strings.HasPrefix(p, "/api/") {
|
||||
return next(c)
|
||||
}
|
||||
if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`.
|
||||
p = c.Param("*")
|
||||
}
|
||||
@@ -268,7 +270,9 @@ func serveFile(c echo.Context, file io.ReadSeeker, info os.FileInfo, etag string
|
||||
|
||||
c.Response().Header().Set("Server", "Vikunja")
|
||||
c.Response().Header().Set("Vary", "Accept-Encoding")
|
||||
c.Response().Header().Set("Etag", etag)
|
||||
if etag != "" {
|
||||
c.Response().Header().Set("Etag", etag)
|
||||
}
|
||||
|
||||
cacheControl, err := getCacheControlHeader(info, file)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user