Compare commits

..
Author SHA1 Message Date
kolaente c82e5fb235 fix: lint 2024-02-17 23:23:39 +01:00
kolaente e5e8e8d134 fix(editor): add unique ids to task list items 2024-02-17 23:15:27 +01:00
kolaente f120d72211 fix(editor): revert task list dependence on ids
This partially reverts 3969f6ae66. Adding ids to task list items is not as simple and actually made it worse in some cases. Hence we stick to comparing the content of nodes for now, until this is properly fixed in tiptap.

Related https://kolaente.dev/vikunja/vikunja/issues/2091#issuecomment-60063
2024-02-17 23:13:51 +01:00
renovateandkonrad a2951570a7 fix(deps): update module github.com/arran4/golang-ical to v0.2.6 2024-02-17 20:08:08 +00:00
kolaente f4efdaa5de fix(password): don't validate password min length on login page
This would cause the login to fail when the actual password was shorter than 8 characters.
2024-02-17 21:07:35 +01:00
kolaente 32edef2d38 fix(editor): set default id of tasklist items
This fixes a bug where all tasklist items would have the same id which would check all of them when checking one of them.
2024-02-17 11:38:20 +01:00
renovate 44c1f0d281 chore(deps): update pnpm to v8.15.3 2024-02-15 11:07:31 +00:00
kolaente 2dab2ccedd feat: allow using sqlite in memory database
This allows running vikunja for testing purposes. You almost never want to run this in production.
2024-02-15 10:48:48 +01:00
kolaente 827c43fe12 chore(magefile): add aliases for lint 2024-02-14 15:05:11 +01:00
kolaente 415c6380a5 feat(api tokens): add task attachment to api scopes
This explicitly adds download and upload of task attachments. Because these are not handled with the usual CRUDables, they were not picked up automatically.

Resolves https://github.com/go-vikunja/vikunja/issues/112
2024-02-14 15:00:16 +01:00
renovateandkonrad ebe25ee2d7 fix(deps): update module github.com/arran4/golang-ical to v0.2.5 2024-02-14 13:35:06 +00:00
kolaente cc5f48eb74 fix: lint 2024-02-14 14:13:17 +01:00
kolaente 3969f6ae66 fix(editor): ensure task list clicks are only fired once
Before this fix, clicking on a task list item with the same name as another one, both would get marked as done. This was due to the mechanism which walks the dom tree to look for the node to update used its content for comparison. To prevent this, this fix first added unique ids to all task list items and then compared the nodes based on their id instead of the content.

Resolves https://kolaente.dev/vikunja/vikunja/issues/2091
2024-02-14 14:13:03 +01:00
kolaente 5ab720d709 docs: remove outdated information 2024-02-14 10:21:51 +01:00
kolaente 7ae38c5ac1 docs: fix postgres example healthcheck 2024-02-14 10:21:27 +01:00
kolaente 162741e940 fix: lint 2024-02-13 22:24:46 +01:00
kolaente 205f330f8a fix(migration): make sure to correctly check if a migration was already running
This change fixes a bug where Vikunja would not correctly check if a migration was already running. That meant it was not possible for users who had never before migrated anything to start a migration, because Vikunja assumed they already had a migration running for them.
This state was neither properly reflected in the frontend, which is now fixed as well.
2024-02-13 22:21:59 +01:00
kolaente a12c169ce8 fix: do not send etag when serving the frontend index file
Without this change, the browser may serve an outdated index.html file which usually does not work, showing the user only a blank page.
2024-02-13 21:32:41 +01:00
kolaente 2facbae0d7 fix(dump): only allow imports from the same version they were dumped on
Previously, Vikunja would allow imports from any version which then caused problems since the table structure might have changed between releases. This change now checks if the current version is the same as the one the dump was created on.
2024-02-13 21:25:31 +01:00
kolaente 77a779acea fix(dump): do not export files which do not exist in storage 2024-02-13 21:14:31 +01:00
kolaente 89e349f2fd docs: fix database healthcheck command 2024-02-13 20:38:53 +01:00
renovate 77feae47d6 fix(deps): update sentry-javascript monorepo to v7.101.0 2024-02-13 15:05:51 +00:00
renovate 2fb263a109 fix(deps): update dependency vue to v3.4.19 2024-02-13 11:06:25 +00:00
kolaente 641fec1215 fix: never return frontend on routes starting with /api
This fixes a problem where Vikunja would sometimes return the html for the frontend when accessing an api route for a nonexistent ressource, because the static handler was the next best.

Resolves https://kolaente.dev/vikunja/vikunja/issues/2110
2024-02-13 10:05:15 +01:00
kolaente 18374c2e52 docs: fix healthcheck and mariadb password 2024-02-13 09:55:12 +01:00
renovateandkonrad 9921551f96 chore(deps): update pnpm to v8.15.2 2024-02-13 08:24:39 +00:00
renovate b26c359ce6 chore(deps): update dev-dependencies to v7 2024-02-13 00:06:22 +00:00
kolaente d7d3ffeebd feat(navigation): persist project open state in navigation
With this change, the project's collapsed open/closed state in the navigation survives a browser reload. Previously, all state would be lost after reloading.

Resolves https://kolaente.dev/vikunja/vikunja/issues/2067
2024-02-12 22:22:55 +01:00
kolaente 0b61885e89 fix(tasks): correctly show different project in related tasks 2024-02-12 22:05:33 +01:00
kolaente 8d5cb335bd fix(tasks): sort done tasks last in relations
When adding a new task relation, the task search input would previously show all tasks in a seemingly random order, including done tasks. Usually, you don't care about these done tasks when adding relations. This change modifies the sort order so that done tasks show up last in the search results.
2024-02-12 22:00:33 +01:00
kolaente 71e62541a1 fix(ci): escape bash script for drone variable substitution 2024-02-12 21:56:25 +01:00
kolaente ee065d9238 feat(tasks): make done at column available for selection in table view
This change adds the done at column of tasks to the table view. It also ensures it is possible to sort the tasks by that column.

https://community.vikunja.io/t/is-it-possible-to-list-done-tasks-with-their-completion-date/1922
2024-02-12 17:56:24 +01:00
kolaente 28ed754c66 fix(ci): correctly set shell for rename command
It looks like Drone executes all commands with sh, even if the default shell of the container is something else. Because sh does not have support for whitespaces in string extrapolation, the rename command would fail. This change always specifies bash as the shell.
2024-02-12 17:37:20 +01:00
kolaente 390f71b0c6 feat(registration): improve username and password validation
Username and password are validated in the api for length and whitespaces. Previously, the api would tell the user "hey you got it wrong" but the error was not reflected properly in the UI. This change implements a client-side validation which mirrors the one from the api, allowing instant validation and better error UX.
2024-02-12 17:32:24 +01:00
kolaente 8c6d98bb02 feat(kanban): debounce bucket limit setting 2024-02-12 17:05:48 +01:00
kolaente ef30868514 fix(ci): use correct docker image for desktop rename 2024-02-12 16:39:05 +01:00
kolaente 75eb878e33 fix(ci): use correct docker image 2024-02-12 14:24:51 +01:00
kolaente 1ab6fef70a fix(metrics): typo 2024-02-12 14:23:16 +01:00
kolaente 6defc4cba2 fix(ci): use correct docker image for frontend testing 2024-02-11 18:46:46 +01:00
kolaente 3abdf2ccdd feat(ci): automatically create a gitea release when tagging 2024-02-11 18:45:15 +01:00
kolaente 001268a33e docs: add healthcheck to docker compose examples 2024-02-11 18:39:53 +01:00
kolaente 3129051a9c fix(ci): enable cors for api testing 2024-02-11 18:19:57 +01:00
kolaente 45c97a456d feat(ci): rename unstable desktop packages 2024-02-11 17:02:37 +01:00
kolaente 1255bdc4ab docs: add desktop packages 2024-02-11 16:54:53 +01:00
kolaente 99856b2031 docs: add missing traefik label 2024-02-11 15:57:22 +01:00
kolaente eec53e8a54 docs: mention installation of rpm packages 2024-02-11 15:56:34 +01:00
kolaente d4a389279c chore: don't send http errors with a status < 500 to sentry 2024-02-11 15:29:27 +01:00
renovateandkonrad 3a65324a7d fix(deps): update tiptap to v2.2.2 2024-02-11 09:37:38 +00:00
renovateandkonrad bec11d42fc fix(deps): update module golang.org/x/oauth2 to v0.17.0 2024-02-11 09:36:08 +00:00
renovate d2eeac2dbe fix(deps): pin dependencies 2024-02-11 09:05:38 +00:00
renovateandkonrad 05e7468135 fix(deps): update module xorm.io/xorm to v1.3.8 2024-02-11 08:34:26 +00:00
renovateandkonrad 4babe72f24 fix(deps): update module github.com/getsentry/sentry-go to v0.27.0 2024-02-11 08:34:04 +00:00
renovateandkonrad e9fe927644 fix(deps): update module golang.org/x/sys to v0.17.0 2024-02-11 08:33:24 +00:00
renovate 7cefb86a78 fix(deps): update dependency vue to v3.4.18 2024-02-11 08:05:32 +00:00
32 changed files with 1003 additions and 582 deletions
+34 -5
View File
@@ -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
...
+2 -2
View File
@@ -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
View File
@@ -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==
+4 -22
View File
@@ -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
+30
View File
@@ -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:
+32 -17
View File
@@ -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
+16 -3
View File
@@ -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
View File
@@ -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",
+529 -407
View File
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)
+44 -21
View File
@@ -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;
+28 -7
View File
@@ -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,
+1
View File
@@ -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
+6 -1
View File
@@ -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.",
+11 -10
View File
@@ -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
}
+22 -3
View File
@@ -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 = {
+1
View File
@@ -66,6 +66,7 @@
v-model="password"
tabindex="2"
:validate-initially="validatePasswordInitially"
:validate-min-length="false"
@submit="submit"
/>
</div>
+33 -10
View File
@@ -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(() => {
+9 -9
View File
@@ -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
+18
View File
@@ -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
View File
@@ -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,
}
)
+4
View File
@@ -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
View File
@@ -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
}
+1 -1
View 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)
+16 -1
View File
@@ -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.
+50
View File
@@ -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
}
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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 {