Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

306 changed files with 12275 additions and 17326 deletions

View File

@ -139,7 +139,7 @@ steps:
event: [ push, tag, pull_request ]
- name: api-lint
image: golangci/golangci-lint:v1.57.2
image: golangci/golangci-lint:v1.56.2
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
@ -364,7 +364,7 @@ steps:
- api-build
- name: frontend-dependencies
image: node:20.14.0-alpine
image: node:20.11.1-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -378,7 +378,7 @@ steps:
# - restore-cache
- name: frontend-lint
image: node:20.14.0-alpine
image: node:20.11.1-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -390,7 +390,7 @@ steps:
- frontend-dependencies
- name: frontend-build-prod
image: node:20.14.0-alpine
image: node:20.11.1-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -402,7 +402,7 @@ steps:
- frontend-dependencies
- name: frontend-test-unit
image: node:20.14.0-alpine
image: node:20.11.1-alpine
pull: always
commands:
- cd frontend
@ -413,7 +413,7 @@ steps:
- name: frontend-typecheck
failure: ignore
image: node:20.14.0-alpine
image: node:20.11.1-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -544,7 +544,7 @@ steps:
- git fetch --tags
- name: frontend-dependencies
image: node:20.14.0-alpine
image: node:20.11.1-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -556,7 +556,7 @@ steps:
- pnpm install --fetch-timeout 100000
- name: frontend-build
image: node:20.14.0-alpine
image: node:20.11.1-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -716,7 +716,7 @@ steps:
# Build os packages and push it to our bucket
- name: build-os-packages-unstable
image: goreleaser/nfpm:v2.36.1
image: goreleaser/nfpm:v2.35.3
pull: always
commands:
- apk add git go
@ -732,7 +732,7 @@ steps:
depends_on: [ after-build-compress ]
- name: build-os-packages-version
image: goreleaser/nfpm:v2.36.1
image: goreleaser/nfpm:v2.35.3
pull: always
commands:
- apk add git go
@ -901,7 +901,7 @@ steps:
- git fetch --tags
- name: build
image: node:20.14.0-alpine
image: node:20.11.1-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -962,7 +962,7 @@ steps:
- git fetch --tags
- name: build
image: node:20.14.0-alpine
image: node:20.11.1-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -1120,7 +1120,7 @@ steps:
- sed -i 's/\\/api\\/v1//g' frontend/index.html
- ./bumpp.sh
- yarn install
- yarn dist --linux zip
- yarn dist --linux --windows
# - name: rebuild-cache
# image: meltwater/drone-cache:dev
@ -1400,6 +1400,6 @@ steps:
- failure
---
kind: signature
hmac: da7295a4503b9ce865c2cb2ffd1f38fec53cc999f11c842172c475b93bd89798
hmac: c312afe632177a2d45f47c429bf6c7528af3c51a097430956558532ccdcc42b9
...

1
.gitignore vendored
View File

@ -28,4 +28,3 @@ vendor/
os-packages/
mage_output_file.go
mage-static
.direnv/

View File

@ -18,7 +18,6 @@ linters:
- scopelint # Obsolete, using exportloopref instead
- durationcheck
- goconst
- musttag
presets:
- bugs
- unused
@ -104,7 +103,3 @@ issues:
text: "parameter 'tx' seems to be unused, consider removing or renaming it as"
linters:
- revive
- path: pkg/models/typesense.go
text: 'structtag: struct field Position repeats json tag "position" also at'
linters:
- govet

View File

@ -4,10 +4,11 @@
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"vue.volar",
"vue.vscode-typescript-vue-plugin",
"lokalise.i18n-ally",
"mgmcdermott.vscode-language-babel",
"mikestead.dotenv",
"Syler.sass-indented",
"vitest.explorer"
"zixuanchen.vitest-explorer"
]
}

View File

@ -2,9 +2,10 @@
"go.testEnvVars": {
"VIKUNJA_SERVICE_ROOTPATH": "${workspaceRoot}"
},
"eslint.packageManager": "pnpm",
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
"source.fixAll": true
},
"eslint.format.enable": true,
"[javascript]": {
@ -21,6 +22,11 @@
"vue"
],
"volar.completion.preferredTagNameCase": "pascal",
// disable vetur in case it is installed
"vetur.validation.template": false,
// i18n ally
"i18n-ally.localesPaths": [
"src/i18n/lang"

View File

@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM node:20.14.0-alpine AS frontendbuilder
FROM --platform=$BUILDPLATFORM node:20.11.1-alpine AS frontendbuilder
WORKDIR /build

View File

@ -29,7 +29,7 @@ If you find any security-related issues you don't want to disclose publicly, ple
## Features
See [the features page](https://vikunja.io/features/) on our website for a more exhaustive list or
See [the features page](https://vikunja.io/features/) on our website for a more exaustive list or
try it on [try.vikunja.io](https://try.vikunja.io)!
## Docs

View File

@ -156,7 +156,7 @@ mailer:
username: "user"
# SMTP password
password: ""
# Whether to skip verification of the tls certificate on the server
# Wether to skip verification of the tls certificate on the server
skiptlsverify: false
# The default from address when sending emails
fromemail: "mail@vikunja"
@ -306,7 +306,7 @@ auth:
# The provider needs to support the `openid`, `profile` and `email` scopes.<br/>
# **Note:** Some openid providers (like Gitlab) only make the email of the user available through OpenID if they have set it to be publicly visible.
# If the email is not public in those cases, authenticating will fail.
# +**Note 2:** The frontend expects the third party to redirect the user <frontend-url>/auth/openid/<auth key> after authentication. Please make sure to configure the redirect url in your third party auth service accordingly if you're using the default vikunja frontend.
# **Note 2:** The frontend expects the third party to rediect the user <frontend-url>/auth/openid/<auth key> after authentication. Please make sure to configure the redirect url in your third party auth service accordingly if you're using the default vikunja frontend.
# The frontend will automatically provide the API with the redirect url, composed from the current url where it's hosted.
# If you want to use the desktop client with OpenID, make sure to allow redirects to `127.0.0.1`.
# Take a look at the [default config file](https://kolaente.dev/vikunja/vikunja/src/branch/main/config.yml.sample) for more information about how to configure openid authentication.
@ -368,8 +368,8 @@ defaultsettings:
webhooks:
# Whether to enable support for webhooks
enabled: true
# The timeout in seconds until a webhook request fails when no response has been received.
timeoutseconds: 30
# The timout in seconds until a webhook request fails when no response has been received.
timoutseconds: 30
# The URL of [a mole instance](https://github.com/frain-dev/mole) to use to proxy outgoing webhook requests. You should use this and configure appropriately if you're not the only one using your Vikunja instance. More info about why: https://webhooks.fyi/best-practices/webhook-providers#implement-security-on-egress-communication. Must be used in combination with `webhooks.password` (see below).
proxyurl:
# The proxy password to use when authenticating against the proxy.

9
desktop/default.nix Normal file
View File

@ -0,0 +1,9 @@
{ pkgs ? import <nixpkgs> {}
}:
pkgs.mkShell {
name="electron-dev";
buildInputs = [
pkgs.electron
];
}

View File

@ -51,7 +51,7 @@
}
},
"devDependencies": {
"electron": "29.4.2",
"electron": "29.1.4",
"electron-builder": "24.13.3"
},
"dependencies": {

File diff suppressed because it is too large Load Diff

2028
desktop/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,7 @@ In other packages, use the `db.NewSession()` method to get a new database sessio
To add a new table to the database, create the struct and [add a migration for it]({{< ref "db-migrations.md" >}}).
To learn more about how to configure your struct to create "good" tables, refer to [the xorm documentation](https://xorm.io/docs/).
To learn more about how to configure your struct to create "good" tables, refer to [the xorm documentaion](https://xorm.io/docs/).
In most cases you will also need to implement the `TableName() string` method on the new struct to make sure the table name matches the rest of the tables - plural.

View File

@ -26,15 +26,6 @@ If you plan to do a bigger change, it is better to open an issue for discussion
The main repo is [`vikunja/vikunja`](https://kolaente.dev/vikunja/vikunja), it contains all code for the api, frontend and desktop applications.
## Where to file issues
You can file issues on [the Gitea repo](https://kolaente.dev/vikunja/vikunja) or [on the GitHub mirror](https://github.com/go-vikunja/vikunja), when you don't want to create an account on the Gitea instance.
Please note that due to a spam problem, we need to manually enable accounts on Gitea after you've registered there.
To get that started, please reach out on another channel with your username.
Another option is [the community forum](https://community.vikunja.io), especially if you want to discuss a feature in more detail.
## API
You'll need at least Go 1.21 to build Vikunja's api.

View File

@ -101,7 +101,7 @@ You should also document the routes with [swagger annotations]({{< ref "swagger-
There is a method available in the `migration` package which takes a fully nested Vikunja structure and creates it with all relations.
This means you start by adding a project, then add projects inside that project, then tasks in the lists and so on.
In general, it is recommended to have one root project with all projects of the other service as child projects.
In general, it is reccommended to have one root project with all projects of the other service as child projects.
The root structure must be present as `[]*models.ProjectWithTasksAndBuckets`. It allows to represent all of Vikunja's hierarchy as a single data structure.

View File

@ -16,7 +16,7 @@ menu:
The following parts are about the kinds of tests in the API package and how to run them.
### Prerequisites
### Prerequesites
To run any kind of test, you need to specify Vikunja's [root path](https://vikunja.io/docs/config-options/#rootpath).
This is required to make sure all test fixtures are correctly loaded.
@ -39,7 +39,7 @@ This definition is a bit blurry, but we haven't found a better one yet.
All integration tests live in `pkg/integrations`.
You can run them by executing `mage test:integration`.
The integration tests use the same config and fixtures as the unit tests and therefore have the same options available,
The integration tests use the same config and fixtures as the unit tests and therefor have the same options available,
see at the beginning of this document.
To run integration tests, use `mage test:integration`.

View File

@ -18,7 +18,6 @@ To fully build Vikunja from source files, you need to build the api and frontend
1. Make sure you have git installed
2. Clone the repo with `git clone https://code.vikunja.io/vikunja` and switch into the directory.
3. Check out the version you want to build with `git checkout VERSION` - replace `VERSION` with the version want to use. If you don't do this, you'll build the [latest unstable build]({{< ref "versions.md">}}), which might contain bugs.
## Frontend
@ -36,7 +35,7 @@ That means compiling it boils down to these steps:
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.21`.
2. Make sure [Mage](https://magefile.org) is properly installed on your system.
3. If you did not build the frontend in the steps before, you need to either do that or create a dummy index file with `mkdir -p frontend/dist && touch frontend/dist/index.html`.
3. If you did not build the frontend in the steps before, you need to either do that or create a dummy index file with `mkdir -p frontend/dist && touch index.html`.
4. Run `mage build` in the source of the main repo. This will build a binary in the root of the repo which will be able to run on your system.
### Build for different architectures

View File

@ -22,6 +22,7 @@ export VIKUNJA_FIRST_CHILD=true
is the same as defining it in a `config.yml` like so:
```yaml
```yaml
first:
child: true
@ -347,7 +348,6 @@ Environment path: `VIKUNJA_SERVICE_CUSTOMLOGOURL`
### enablepublicteams
Enables the public team feature. If enabled, it is possible to configure teams to be public, which makes them
discoverable when sharing a project, therefore not only showing teams the user is member of.
Default: `false`
@ -779,7 +779,7 @@ Environment path: `VIKUNJA_MAILER_PASSWORD`
### skiptlsverify
Whether to skip verification of the tls certificate on the server
Wether to skip verification of the tls certificate on the server
Default: `false`
@ -1222,7 +1222,7 @@ OpenID configuration will allow users to authenticate through a third-party Open
The provider needs to support the `openid`, `profile` and `email` scopes.<br/>
**Note:** Some openid providers (like Gitlab) only make the email of the user available through OpenID if they have set it to be publicly visible.
If the email is not public in those cases, authenticating will fail.
+**Note 2:** The frontend expects the third party to redirect the user <frontend-url>/auth/openid/<auth key> after authentication. Please make sure to configure the redirect url in your third party auth service accordingly if you're using the default vikunja frontend.
**Note 2:** The frontend expects the third party to rediect the user <frontend-url>/auth/openid/<auth key> after authentication. Please make sure to configure the redirect url in your third party auth service accordingly if you're using the default vikunja frontend.
The frontend will automatically provide the API with the redirect url, composed from the current url where it's hosted.
If you want to use the desktop client with OpenID, make sure to allow redirects to `127.0.0.1`.
Take a look at the [default config file](https://kolaente.dev/vikunja/vikunja/src/branch/main/config.yml.sample) for more information about how to configure openid authentication.
@ -1421,15 +1421,15 @@ Full path: `webhooks.enabled`
Environment path: `VIKUNJA_WEBHOOKS_ENABLED`
### timeoutseconds
### timoutseconds
The timeout in seconds until a webhook request fails when no response has been received.
The timout in seconds until a webhook request fails when no response has been received.
Default: `30`
Full path: `webhooks.timeoutseconds`
Full path: `webhooks.timoutseconds`
Environment path: `VIKUNJA_WEBHOOKS_TIMEOUTSECONDS`
Environment path: `VIKUNJA_WEBHOOKS_TIMOUTSECONDS`
### proxyurl

View File

@ -40,7 +40,7 @@ chown 1000 $PWD/files
You'll need to do this before running any of the examples on this page.
Vikunja will not try to acquire ownership of the files folder, as that would mean it had to run as root.
Vikunja will not try to aquire ownership of the files folder, as that would mean it had to run as root.
## PostgreSQL
@ -312,7 +312,7 @@ To do that, you can
* Either activate SSH and paste the adapted compose file in a terminal (using Putty or similar)
* Without activating SSH as a "custom script" (go to Control Panel / Task Scheduler / Create / Scheduled Task / User-defined script)
* Without activating SSH, by using Portainer (you have to install first, check out [this tutorial](https://www.portainer.io/blog/how-to-install-portainer-on-a-synology-nas) for example):
* Without activating SSH, by using Portainer (you have to install first, check out [this tutorial](https://www.portainer.io/blog/how-to-install-portainer-on-a-synology-nas) for exmple):
1. Go to **Dashboard / Stacks** click the button **"Add Stack"**
2. Give it the name Vikunja and paste the adapted docker compose file
3. Deploy the Stack with the "Deploy Stack" button:

View File

@ -37,7 +37,7 @@ This document provides an overview and instructions for the different methods:
* [FreeBSD](#freebsd--freenas)
* [Kubernetes]({{< ref "k8s.md" >}})
And after you installed Vikunja, you may want to check out these other resources:
And after you installed Vikunja, you may want to check out these other ressources:
* [Configuration]({{< ref "config.md">}})
* [UTF-8 Settings]({{< ref "utf-8.md">}})
@ -225,13 +225,10 @@ go install github.com/magefile/mage
```
mkdir /mnt/GO/code.vikunja.io
cd /mnt/GO/code.vikunja.io
git clone https://code.vikunja.io/vikunja
cd vikunja
git clone https://code.vikunja.io/api
cd /mnt/GO/code.vikunja.io/api
```
**Note:** Check out the version you want to build with `git checkout VERSION` - replace `VERSION` with the version want to use.
If you don't do this, you'll build the [latest unstable build]({{< ref "versions.md">}}), which might contain bugs.
### Compile binaries
```

View File

@ -17,7 +17,7 @@ Vikunja allows for authentication with an external identity source such as Authe
## OpenID Connect Overview
OpenID Connect is a standardized identity layer built on top of the more generic OAuth 2.0 specification, simplifying interaction between the involved parties significantly.
OpenID Connect is a standardized identity layer built on top of the more generic OAuth 2.0 specification, simplying interaction between the involved parties significantly.
While the [OpenID specification](https://openid.net/specs/openid-connect-core-1_0.html#Overview) is worth a read, we summarize the most important basics here.
The involved parties are:
@ -103,7 +103,7 @@ auth:
## Automatically assign users to teams
Starting with version 0.24.0, Vikunja is capable of automatically adding users to a team based on OIDC claims added by the identity provider.
Vikunja is capable of automatically adding users to a team based on OIDC claims added by the identity provider.
If configured, Vikunja will sync teams, automatically create new ones and make sure the members are part of the configured teams.
Teams which exist only because they were created from oidc attributes are not editable in Vikunja.

View File

@ -17,7 +17,7 @@ However, you can still run it in a subdirectory but need to build the frontend y
First, make sure you're able to build the frontend from source.
Check [the guide about building from source]({{< ref "build-from-source.md">}}#frontend) about that.
### Dynamically set with build command
### Dynamicly set with build command
Run the build with the `VIKUNJA_FRONTEND_BASE` variable specified.

View File

@ -10,13 +10,13 @@ menu:
# Vikunja Versions
Vikunja api is available in two different release flavors.
The Vikunja api and frontend are available in two different release flavors.
{{< table_of_contents >}}
## Stable
Stable releases have a fixed version number like `0.20.2` and are published at irregular intervals whenever a new version is ready.
Stable releases have a fixed version number like `0.18.2` and are published at irregular intervals whenever a new version is ready.
They receive few bugfixes and security patches.
We use [Semantic Versioning](https://semver.org) for these releases.
@ -28,10 +28,7 @@ As such, they contain the current development code and are more likely to have b
There might be multiple new such builds a day.
Versions contain the last stable version, the number of commits since then and the commit the currently running binary was built from.
They look like this: `v0.20.2-75-6049427322`
Since a release is also cut from the main branch at some point, features from unstable will eventually become available in stable releases.
At the point in time of a new version release, the unstable build is the same exact thing.
They look like this: `v0.18.1+269-5cc4927b9e`
The demo instance at [try.vikunja.io](https://try.vikunja.io) automatically updates and always runs the last unstable build.

View File

@ -72,7 +72,6 @@ Vikunja **currently does not** support these properties:
* [Evolution](https://wiki.gnome.org/Apps/Evolution/)
* [OpenTasks](https://opentasks.app/) & [DAVx⁵](https://www.davx5.com/)
* [Tasks (Android)](https://tasks.org/)
* [Korganizer](https://apps.kde.org/korganizer/)
### Not working

View File

@ -10,9 +10,17 @@ menu:
# Command line interface
You can interact with Vikunja using its `cli` interface.
You can interact with Vikunja using its `cli` interface.<br />
The following commands are available:
{{< table_of_contents >}}
* [dump](#dump)
* [help](#help)
* [migrate](#migrate)
* [restore](#restore)
* [testmail](#testmail)
* [user](#user)
* [version](#version)
* [web](#web)
If you don't specify a command, the [`web`](#web) command will be executed.
@ -20,22 +28,14 @@ All commands use the same standard [config file]({{< ref "../setup/config.md">}}
## Using the cli in docker
When running Vikunja in docker, you'll need to execute all commands in the `vikunja` container.
When running Vikunja in docker, you'll need to execute all commands in the `api` container.
Instead of running the `vikunja` binary directly, run it like this:
```sh
docker exec <name of the vikunja container> /app/vikunja/vikunja <subcommand>
docker exec <name of the vikunja api container> /app/vikunja/vikunja <subcommand>
```
If you need to run a bunch of Vikunja commands, you can also create a shell alias for it:
```sh
alias vikunja-docker='docker exec <name of the vikunja container> /app/vikunja/vikunja'
```
Then use it as `vikunja-docker <subcommand>`.
## `dump`
### `dump`
Creates a zip file with all vikunja-related files.
This includes config, version, all files and the full database.
@ -45,21 +45,7 @@ Usage:
$ vikunja dump
```
## `index`
Perform a full reindex of all tasks into Typesense. This will clear all tasks already present in the index unless the `--partial` flag is provided, see below.
The command will only work if Typesense is enabled.
Flags:
* `-p`, `--partial`: If provided, Vikunja will only index tasks which are not present in the index yet.
Usage:
```
$ vikunja index [flags]
```
## `help`
### `help`
Shows more detailed help about any command.
@ -69,7 +55,7 @@ Usage:
$ vikunja help [command]
```
## `migrate`
### `migrate`
Run all database migrations which didn't already run.
@ -79,7 +65,7 @@ $ vikunja migrate [flags]
$ vikunja migrate [command]
```
### `migrate list`
#### `migrate list`
Shows a list with all database migrations.
@ -88,7 +74,7 @@ Usage:
$ vikunja migrate list
```
### `migrate rollback`
#### `migrate rollback`
Roll migrations back until a certain point.
@ -100,42 +86,42 @@ $ vikunja migrate rollback [flags]
Flags:
* `-n`, `--name` string: The id of the migration you want to roll back until.
## `restore`
### `restore`
Restores a previously created dump from a zip file, see `dump`.
Usage:
```
$ vikunja restore [path to dump zip file]
$ vikunja restore <path to dump zip file>
```
## `testmail`
### `testmail`
Sends a test mail using the configured smtp connection.
Usage:
```
$ vikunja testmail [email to send the test mail to]
$ vikunja testmail <email to send the test mail to>
```
## `user`
### `user`
Bundles a few commands to manage users.
### `user change-status`
#### `user change-status`
Enable or disable a user. Will toggle the current status if no flag (`--enable` or `--disable`) is provided.
Usage:
```
$ vikunja user change-status [user id] [flags]
$ vikunja user change-status <user id> <flags>
```
Flags:
* `-d`, `--disable`: Disable the user.
* `-e`, `--enable`: Enable the user.
### `user create`
#### `user create`
Create a new user.
@ -150,22 +136,22 @@ Flags:
* `-p`, `--password`: The password of the new user. You will be asked to enter it if not provided through the flag.
* `-u`, `--username`: The username of the new user.
### `user delete`
#### `user delete`
Start the user deletion process.
If called without the `--now` flag, this command will only trigger an email to the user in order for them to confirm and start the deletion process (this is the same behavior as if the user requested their deletion via the web interface).
If called without the `--now` flag, this command will only trigger an email to the user in order for them to confirm and start the deletion process (this is the same behavoir as if the user requested their deletion via the web interface).
With the flag the user is deleted **immediately**.
**USE WITH CAUTION.**
```
$ vikunja user delete [id] [flags]
$ vikunja user delete <id> <flags>
```
Flags:
* `-n`, `--now` If provided, deletes the user immediately instead of emailing them first.
### `user list`
#### `user list`
Shows a list of all users.
@ -174,26 +160,26 @@ Usage:
$ vikunja user list
```
### `user reset-password`
#### `user reset-password`
Reset a users password, either through mailing them a reset link or directly.
Usage:
```
$ vikunja user reset-password [flags]
$ vikunja user reset-password <flags>
```
Flags:
* `-d`, `--direct`: If provided, reset the password directly instead of sending the user a reset mail.
* `-p`, `--password`: The new password of the user. Only used in combination with --direct. You will be asked to enter it if not provided through the flag.
### `user update`
#### `user update`
Update an existing user.
Usage:
```
$ vikunja user update [user id]
$ vikunja user update <user id>
```
Flags:
@ -201,19 +187,19 @@ Flags:
* `-e`, `--email`: The new email address of the user.
* `-u`, `--username`: The new username of the user.
## `version`
### `version`
Prints the version of Vikunja.
This is either the semantic version (something like `0.24.0`) or version + git commit hash.
This is either the semantic version (something like `0.7`) or version + git commit hash.
Usage:
```
$ vikunja version
```
## `web`
### `web`
Starts Vikunja's web server, serving the api and frontend.
Starts Vikunja's REST api server.
Usage:
```

View File

@ -51,7 +51,7 @@ This document describes the different errors Vikunja can return.
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------|
| 2001 | 400 | ID cannot be empty or 0. |
| 2002 | 400 | Some of the request data was invalid. The response contains an additional array with all invalid fields. |
| 2002 | 400 | Some of the request data was invalid. The response contains an aditional array with all invalid fields. |
## Project

View File

@ -61,7 +61,7 @@ Here are some examples of filter queries:
* `priority = 4`: Matches tasks with priority level 4
* `dueDate < now`: Matches tasks with a due date in the past
* `done = false && priority >= 3`: Matches undone tasks with priority level 3 or higher
* `assignees in user1, user2`: Matches tasks assigned to either "user1" or "user2
* `assignees in [user1, user2]`: Matches tasks assigned to either "user1" or "user2
* `(priority = 1 || priority = 2) && dueDate <= now`: Matches tasks with priority level 1 or 2 and a due date in the past

View File

@ -18,7 +18,7 @@ Starting with version 0.22.0, Vikunja allows you to define webhooks to notify ot
To create a webhook, in the project options select "Webhooks". The form will allow you to create and modify webhooks.
Check out [the api docs](https://try.vikunja.io/api/v1/docs#tag/webhooks) for information about how to create webhooks programmatically.
Check out [the api docs](https://try.vikunja.io/api/v1/docs#tag/webhooks) for information about how to create webhooks programatically.
## Available events and their payload

View File

@ -1,20 +0,0 @@
{
description = "Vikunja dev environment";
outputs = { self, nixpkgs }:
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
defaultPackage.x86_64-linux =
pkgs.mkShell { buildInputs = with pkgs; [
# General tools
git-cliff
# Frontend tools
pnpm cypress
# API tools
go golangci-lint mage
# Desktop
electron
];
};
};
}

View File

@ -46,8 +46,7 @@ module.exports = {
'vue/html-indent': ['error', 'tab'],
// vue3
'vue/no-ref-object-reactivity-loss': 'error',
'vue/no-setup-props-reactivity-loss': 'warn', // TODO: switch to error after vite `propsDestructure` is removed
'vue/no-ref-object-destructure': 'error',
},
'parser': 'vue-eslint-parser',
'parserOptions': {

1
frontend/.gitignore vendored
View File

@ -13,6 +13,7 @@ node_modules
/dist*
coverage
*.zip
.direnv/
# Test files
cypress/screenshots

14
frontend/.npmrc Normal file
View File

@ -0,0 +1,14 @@
fetch-timeout=100000
# pnpm settings
# The following settings prepare for the new default value of pnpm 8
# they can be removed directly after having moved to pnpm 8
auto-install-peers=true
dedupe-peer-dependents=true
resolve-peers-from-workspace-root=true
save-workspace-protocol=rolling
resolution-mode=lowest-direct
publishConfig.linkDirectory=true
# remove some time after having moved to pnpm 8
use-lockfile-v6=true

View File

@ -1 +1 @@
20.14.0
20.11.1

View File

@ -12,7 +12,7 @@ describe('Project History', () => {
cy.intercept(Cypress.env('API_URL') + '/projects*').as('loadProjectArray')
cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
const projects = ProjectFactory.create(7)
const projects = ProjectFactory.create(6)
ProjectViewFactory.truncate()
projects.forEach(p => ProjectViewFactory.create(1, {
id: p.id,
@ -36,8 +36,6 @@ describe('Project History', () => {
cy.wait('@loadProject')
cy.visit(`/projects/${projects[5].id}/${projects[5].id}`)
cy.wait('@loadProject')
cy.visit(`/projects/${projects[6].id}/${projects[6].id}`)
cy.wait('@loadProject')
// cy.visit('/')
// Not using cy.visit here to work around the redirect issue fixed in #1337
@ -54,6 +52,5 @@ describe('Project History', () => {
.should('contain', projects[3].title)
.should('contain', projects[4].title)
.should('contain', projects[5].title)
.should('contain', projects[6].title)
})
})

View File

@ -52,7 +52,7 @@ describe('Project View Gantt', () => {
})
cy.visit('/projects/1/2')
cy.get('.gantt-options .fancy-checkbox')
cy.get('.gantt-options .fancycheckbox')
.contains('Show tasks which don\'t have dates set')
.click()

View File

@ -68,7 +68,7 @@ describe('Project View List', () => {
cy.get('.project-title-wrapper .icon')
.should('not.exist')
cy.get('input.input[placeholder="Add a task..."')
cy.get('input.input[placeholder="Add a new task..."')
.should('not.exist')
})

View File

@ -24,10 +24,10 @@ describe('Project View Table', () => {
cy.get('.project-table .filter-container .button')
.contains('Columns')
.click()
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancy-checkbox')
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox')
.contains('Priority')
.click()
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancy-checkbox')
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox')
.contains('Done')
.click()

View File

@ -27,7 +27,7 @@ describe('Link shares', () => {
cy.get('h1.title')
.should('contain', project.title)
cy.get('input.input[placeholder="Add a task..."')
cy.get('input.input[placeholder="Add a new task..."')
.should('not.exist')
cy.get('.tasks')
.should('contain', tasks[0].title)
@ -42,7 +42,7 @@ describe('Link shares', () => {
cy.get('h1.title')
.should('contain', project.title)
cy.get('input.input[placeholder="Add a task..."')
cy.get('input.input[placeholder="Add a new task..."')
.should('not.exist')
cy.get('.tasks')
.should('contain', tasks[0].title)

View File

@ -14,12 +14,12 @@ describe('Team', () => {
const newTeamName = 'New Team'
cy.get('a.button')
.contains('Create a team')
.contains('Create a new team')
.click()
cy.url()
.should('contain', '/teams/new')
cy.get('.card-header-title')
.contains('Create a team')
.contains('Create a new team')
cy.get('input.input')
.type(newTeamName)
cy.get('.button')

View File

@ -13,7 +13,6 @@ import {BucketFactory} from '../../factories/bucket'
import {TaskAttachmentFactory} from '../../factories/task_attachments'
import {TaskReminderFactory} from '../../factories/task_reminders'
import {createDefaultViews} from "../project/prepareProjects";
import { TaskBucketFactory } from '../../factories/task_buckets'
function addLabelToTaskAndVerify(labelTitle: string) {
cy.get('.task-view .action-buttons .button')
@ -42,14 +41,14 @@ function uploadAttachmentAndVerify(taskId: number) {
.selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose
cy.wait('@uploadAttachment')
cy.get('.attachments .attachments .files button.attachment')
cy.get('.attachments .attachments .files a.attachment')
.should('exist')
}
describe('Task', () => {
createFakeUserAndLogin()
let projects: {}[]
let projects
let buckets
beforeEach(() => {
@ -65,7 +64,7 @@ describe('Task', () => {
it('Should be created new', () => {
cy.visit('/projects/1/1')
cy.get('.input[placeholder="Add a task…"')
cy.get('.input[placeholder="Add a new task…"')
.type('New Task')
cy.get('.button')
.contains('Add')
@ -81,7 +80,7 @@ describe('Task', () => {
cy.visit('/projects/1/1')
cy.get('.project-is-empty-notice')
.should('not.exist')
cy.get('.input[placeholder="Add a task…"')
cy.get('.input[placeholder="Add a new task…"')
.type('New Task')
cy.get('.button')
.contains('Add')
@ -97,7 +96,7 @@ describe('Task', () => {
TaskFactory.create(1)
cy.visit('/projects/1/1')
cy.get('.tasks .task .fancy-checkbox')
cy.get('.tasks .task .fancycheckbox')
.first()
.click()
cy.get('.global-notification')
@ -471,10 +470,6 @@ describe('Task', () => {
})
const labels = LabelFactory.create(1)
LabelTaskFactory.truncate()
TaskBucketFactory.create(1, {
task_id: tasks[0].id,
bucket_id: buckets[0].id,
})
cy.visit(`/projects/${projects[0].id}/4`)
@ -640,7 +635,7 @@ describe('Task', () => {
.contains('Set Reminders')
.click()
cy.get('.task-view .columns.details .column button')
.contains('Add a reminder')
.contains('Add a new reminder')
.click()
cy.get('.datepicker__quick-select-date')
.contains('Tomorrow')
@ -665,7 +660,7 @@ describe('Task', () => {
.contains('Set Reminders')
.click()
cy.get('.task-view .columns.details .column button')
.contains('Add a reminder')
.contains('Add a new reminder')
.click()
cy.get('.datepicker__quick-select-date')
.should('not.exist')
@ -694,7 +689,7 @@ describe('Task', () => {
.contains('Set Reminders')
.click()
cy.get('.task-view .columns.details .column button')
.contains('Add a reminder')
.contains('Add a new reminder')
.click()
cy.get('.datepicker__quick-select-date')
.should('not.exist')
@ -723,7 +718,7 @@ describe('Task', () => {
.contains('Set Reminders')
.click()
cy.get('.task-view .columns.details .column button')
.contains('Add a reminder')
.contains('Add a new reminder')
.click()
cy.get('.datepicker__quick-select-date')
.should('not.exist')
@ -759,7 +754,7 @@ describe('Task', () => {
.contains('Set Reminders')
.click()
cy.get('.task-view .columns.details .column button')
.contains('Add a reminder')
.contains('Add a new reminder')
.click()
cy.get('.datepicker__quick-select-date')
.should('not.exist')

10
frontend/env.d.ts vendored
View File

@ -3,6 +3,16 @@
/// <reference types="cypress" />
/// <reference types="@histoire/plugin-vue/components" />
declare module 'postcss-focus-within/browser' {
import focusWithinInit from 'postcss-focus-within/browser'
export default focusWithinInit
}
declare module 'css-has-pseudo/browser' {
import cssHasPseudo from 'css-has-pseudo/browser'
export default cssHasPseudo
}
interface ImportMetaEnv {
readonly VIKUNJA_API_URL?: string
readonly VIKUNJA_HTTP_PORT?: number

View File

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1718149104,
"narHash": "sha256-Ds1QpobBX2yoUDx9ZruqVGJ/uQPgcXoYuobBguyKEh8=",
"lastModified": 1701336116,
"narHash": "sha256-kEmpezCR/FpITc6yMbAh4WrOCiT2zg5pSjnKrq51h5Y=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e913ae340076bbb73d9f4d3d065c2bca7caafb16",
"rev": "f5c27c6136db4d76c30e533c20517df6864c46ee",
"type": "github"
},
"original": {

10
frontend/flake.nix Normal file
View File

@ -0,0 +1,10 @@
{
description = "Vikunja frontend dev environment";
outputs = { self, nixpkgs }:
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
defaultPackage.x86_64-linux =
pkgs.mkShell { buildInputs = [ pkgs.nodePackages.pnpm pkgs.cypress pkgs.git-cliff ]; };
};
}

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@9.3.0",
"packageManager": "pnpm@8.15.5",
"keywords": [
"todo",
"productivity",
@ -50,60 +50,60 @@
"story:preview": "histoire preview"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.5.2",
"@fortawesome/free-regular-svg-icons": "6.5.2",
"@fortawesome/free-solid-svg-icons": "6.5.2",
"@fortawesome/vue-fontawesome": "3.0.8",
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-regular-svg-icons": "6.5.1",
"@fortawesome/free-solid-svg-icons": "6.5.1",
"@fortawesome/vue-fontawesome": "3.0.6",
"@github/hotkey": "3.1.0",
"@infectoone/vue-ganttastic": "2.3.2",
"@intlify/unplugin-vue-i18n": "4.0.0",
"@infectoone/vue-ganttastic": "2.3.1",
"@intlify/unplugin-vue-i18n": "3.0.1",
"@kyvg/vue3-notification": "3.2.1",
"@sentry/tracing": "7.114.0",
"@sentry/vue": "8.9.2",
"@tiptap/core": "2.4.0",
"@tiptap/extension-blockquote": "2.4.0",
"@tiptap/extension-bold": "2.4.0",
"@tiptap/extension-bullet-list": "2.4.0",
"@tiptap/extension-code": "2.4.0",
"@tiptap/extension-code-block-lowlight": "2.4.0",
"@tiptap/extension-document": "2.4.0",
"@tiptap/extension-dropcursor": "2.4.0",
"@tiptap/extension-gapcursor": "2.4.0",
"@tiptap/extension-hard-break": "2.4.0",
"@tiptap/extension-heading": "2.4.0",
"@tiptap/extension-history": "2.4.0",
"@tiptap/extension-horizontal-rule": "2.4.0",
"@tiptap/extension-image": "2.4.0",
"@tiptap/extension-italic": "2.4.0",
"@tiptap/extension-link": "2.4.0",
"@tiptap/extension-list-item": "2.4.0",
"@tiptap/extension-ordered-list": "2.4.0",
"@tiptap/extension-paragraph": "2.4.0",
"@tiptap/extension-placeholder": "2.4.0",
"@tiptap/extension-strike": "2.4.0",
"@tiptap/extension-table": "2.4.0",
"@tiptap/extension-table-cell": "2.4.0",
"@tiptap/extension-table-header": "2.4.0",
"@tiptap/extension-table-row": "2.4.0",
"@tiptap/extension-task-item": "2.4.0",
"@tiptap/extension-task-list": "2.4.0",
"@tiptap/extension-text": "2.4.0",
"@tiptap/extension-typography": "2.4.0",
"@tiptap/extension-underline": "2.4.0",
"@tiptap/pm": "2.4.0",
"@tiptap/suggestion": "2.4.0",
"@tiptap/vue-3": "2.4.0",
"@sentry/tracing": "7.109.0",
"@sentry/vue": "7.109.0",
"@tiptap/core": "2.2.4",
"@tiptap/extension-blockquote": "2.2.4",
"@tiptap/extension-bold": "2.2.4",
"@tiptap/extension-bullet-list": "2.2.4",
"@tiptap/extension-code": "2.2.4",
"@tiptap/extension-code-block-lowlight": "2.2.4",
"@tiptap/extension-document": "2.2.4",
"@tiptap/extension-dropcursor": "2.2.4",
"@tiptap/extension-gapcursor": "2.2.4",
"@tiptap/extension-hard-break": "2.2.4",
"@tiptap/extension-heading": "2.2.4",
"@tiptap/extension-history": "2.2.4",
"@tiptap/extension-horizontal-rule": "2.2.4",
"@tiptap/extension-image": "2.2.4",
"@tiptap/extension-italic": "2.2.4",
"@tiptap/extension-link": "2.2.4",
"@tiptap/extension-list-item": "2.2.4",
"@tiptap/extension-ordered-list": "2.2.4",
"@tiptap/extension-paragraph": "2.2.4",
"@tiptap/extension-placeholder": "2.2.4",
"@tiptap/extension-strike": "2.2.4",
"@tiptap/extension-table": "2.2.4",
"@tiptap/extension-table-cell": "2.2.4",
"@tiptap/extension-table-header": "2.2.4",
"@tiptap/extension-table-row": "2.2.4",
"@tiptap/extension-task-item": "2.2.4",
"@tiptap/extension-task-list": "2.2.4",
"@tiptap/extension-text": "2.2.4",
"@tiptap/extension-typography": "2.2.4",
"@tiptap/extension-underline": "2.2.4",
"@tiptap/pm": "2.2.4",
"@tiptap/suggestion": "2.2.4",
"@tiptap/vue-3": "2.2.4",
"@types/is-touch-device": "1.0.2",
"@types/lodash.clonedeep": "4.5.9",
"@vueuse/core": "10.11.0",
"@vueuse/router": "10.11.0",
"axios": "1.7.2",
"@vueuse/core": "10.9.0",
"@vueuse/router": "10.9.0",
"axios": "1.6.8",
"blurhash": "2.0.5",
"bulma-css-variables": "0.9.33",
"camel-case": "5.0.0",
"camel-case": "4.1.2",
"date-fns": "3.6.0",
"dayjs": "1.11.11",
"dompurify": "3.1.5",
"dayjs": "1.11.10",
"dompurify": "3.0.11",
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.31",
@ -114,70 +114,74 @@
"lowlight": "2.9.0",
"pinia": "2.1.7",
"register-service-worker": "1.7.2",
"snake-case": "4.0.0",
"snake-case": "3.0.4",
"sortablejs": "1.15.2",
"tippy.js": "6.3.7",
"ufo": "1.5.3",
"vue": "3.4.29",
"vue-advanced-cropper": "2.8.9",
"vue": "3.4.21",
"vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.5",
"vue-i18n": "9.13.1",
"vue-router": "4.3.3",
"vue-i18n": "9.10.2",
"vue-router": "4.3.0",
"vuemoji-picker": "0.2.1",
"workbox-precaching": "7.1.0",
"workbox-precaching": "7.0.0",
"zhyswan-vuedraggable": "4.1.3"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.2.5",
"@cypress/vite-dev-server": "5.1.1",
"@cypress/vue": "6.0.1",
"@cypress/vite-dev-server": "5.0.7",
"@cypress/vue": "6.0.0",
"@faker-js/faker": "8.4.1",
"@histoire/plugin-screenshot": "0.17.17",
"@histoire/plugin-vue": "0.17.17",
"@rushstack/eslint-patch": "1.10.3",
"@tsconfig/node20": "20.1.4",
"@histoire/plugin-screenshot": "0.17.14",
"@histoire/plugin-vue": "0.17.14",
"@rushstack/eslint-patch": "1.8.0",
"@tsconfig/node18": "18.2.2",
"@types/codemirror": "5.60.15",
"@types/dompurify": "3.0.5",
"@types/flexsearch": "0.7.6",
"@types/is-touch-device": "1.0.2",
"@types/lodash.debounce": "4.0.9",
"@types/node": "20.14.2",
"@types/marked": "5.0.2",
"@types/node": "20.11.30",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/eslint-plugin": "7.13.0",
"@typescript-eslint/parser": "7.13.0",
"@vitejs/plugin-legacy": "5.4.1",
"@vitejs/plugin-vue": "5.0.5",
"@typescript-eslint/eslint-plugin": "7.3.1",
"@typescript-eslint/parser": "7.3.1",
"@vitejs/plugin-legacy": "5.3.2",
"@vitejs/plugin-vue": "5.0.4",
"@vue/eslint-config-typescript": "13.0.0",
"@vue/test-utils": "2.4.6",
"@vue/test-utils": "2.4.5",
"@vue/tsconfig": "0.5.1",
"autoprefixer": "10.4.19",
"browserslist": "4.23.1",
"caniuse-lite": "1.0.30001634",
"autoprefixer": "10.4.18",
"browserslist": "4.23.0",
"caniuse-lite": "1.0.30001599",
"css-has-pseudo": "6.0.2",
"csstype": "3.1.3",
"cypress": "13.11.0",
"esbuild": "0.21.5",
"cypress": "13.7.0",
"esbuild": "0.20.2",
"eslint": "8.57.0",
"eslint-plugin-vue": "9.26.0",
"happy-dom": "14.12.0",
"histoire": "0.17.17",
"postcss": "8.4.38",
"eslint-plugin-vue": "9.23.0",
"happy-dom": "14.0.0",
"histoire": "0.17.14",
"postcss": "8.4.37",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-preset-env": "9.5.14",
"rollup": "4.18.0",
"postcss-focus-within": "8.0.1",
"postcss-preset-env": "9.5.2",
"rollup": "4.13.0",
"rollup-plugin-visualizer": "5.12.0",
"sass": "1.77.5",
"start-server-and-test": "2.0.4",
"typescript": "5.4.5",
"unplugin-inject-preload": "2.0.4",
"vite": "5.3.0",
"vite-plugin-pwa": "0.20.0",
"sass": "1.72.0",
"start-server-and-test": "2.0.3",
"typescript": "5.4.2",
"vite": "5.1.6",
"vite-plugin-inject-preload": "1.3.3",
"vite-plugin-pwa": "0.19.5",
"vite-plugin-sentry": "1.4.0",
"vite-svg-loader": "5.1.0",
"vitest": "1.6.0",
"vue-tsc": "2.0.21",
"vitest": "1.4.0",
"vue-tsc": "2.0.6",
"wait-on": "7.2.0",
"workbox-cli": "7.1.0"
"workbox-cli": "7.0.0"
},
"pnpm": {
"patchedDependencies": {

File diff suppressed because it is too large Load Diff

View File

@ -26,15 +26,15 @@ import {useRoute, useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n'
import isTouchDevice from 'is-touch-device'
import Notification from '@/components/misc/Notification.vue'
import Notification from '@/components/misc/notification.vue'
import UpdateNotification from '@/components/home/UpdateNotification.vue'
import KeyboardShortcuts from '@/components/misc/keyboard-shortcuts/index.vue'
import TheNavigation from '@/components/home/TheNavigation.vue'
import ContentAuth from '@/components/home/ContentAuth.vue'
import ContentLinkShare from '@/components/home/ContentLinkShare.vue'
import NoAuthWrapper from '@/components/misc/NoAuthWrapper.vue'
import Ready from '@/components/misc/Ready.vue'
import ContentAuth from '@/components/home/contentAuth.vue'
import ContentLinkShare from '@/components/home/contentLinkShare.vue'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
import Ready from '@/components/misc/ready.vue'
import {setLanguage} from '@/i18n'

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 313 KiB

View File

@ -73,16 +73,14 @@ export interface BaseButtonProps extends /* @vue-ignore */ HTMLAttributes {
}
export interface BaseButtonEmits {
click: [payload: MouseEvent]
(e: 'click', payload: MouseEvent): void
}
withDefaults(defineProps<BaseButtonProps>(), {
type: BASE_BUTTON_TYPES_MAP.BUTTON,
disabled: false,
to: undefined,
href: undefined,
openExternalInNewTab: true,
})
const {
type = BASE_BUTTON_TYPES_MAP.BUTTON,
disabled = false,
openExternalInNewTab = true,
} = defineProps<BaseButtonProps>()
const emit = defineEmits<BaseButtonEmits>()
@ -98,7 +96,7 @@ defineExpose({
</script>
<style lang="scss">
// NOTE: we do not use scoped styles to reduce specificity and make it easy to overwrite
// NOTE: we do not use scoped styles to reduce specifity and make it easy to overwrite
// We reset the default styles of a button element to enable easier styling
:where(.base-button--type-button) {

View File

@ -45,7 +45,7 @@ const emit = defineEmits<{
(event: 'update:modelValue', value: boolean): void
}>()
const checkboxId = ref(`checkbox_${createRandomID()}`)
const checkboxId = ref(`fancycheckbox_${createRandomID()}`)
</script>
<style lang="scss" scoped>

View File

@ -28,24 +28,28 @@
</template>
<script setup lang="ts">
// the logic of this component is loosely based on this article
// the logic of this component is loosly based on this article
// https://gomakethings.com/how-to-add-transition-animations-to-vanilla-javascript-show-and-hide-methods/#putting-it-all-together
import {computed, ref} from 'vue'
import {getInheritedBackgroundColor} from '@/helpers/getInheritedBackgroundColor'
const props = withDefaults(defineProps<{
const props = defineProps({
/** Whether the Expandable is open or not */
open?: boolean,
open: {
type: Boolean,
default: false,
},
/** If there is too much content, content will be cut of here. */
initialHeight?: number
initialHeight: {
type: Number,
default: undefined,
},
/** The hidden content is indicated by a gradient. This is the color that the gradient fades to.
* Makes only sense if `initialHeight` is set. */
backgroundColor?: string
}>(), {
open: false,
initialHeight: undefined,
backgroundColor: undefined,
* Makes only sense if `initialHeight` is set. */
backgroundColor: {
type: String,
},
})
const wrapper = ref<HTMLElement | null>(null)
@ -78,7 +82,7 @@ function forceLayout(el: HTMLElement) {
/* ######################################################################
# The following functions are called by the js hooks of the transitions.
# They follow the original hook order of the vue transition component
# They follow the orignal hook order of the vue transition component
# see: https://vuejs.org/guide/built-ins/transition.html#javascript-hooks
###################################################################### */
@ -113,8 +117,8 @@ function beforeLeave(el: HTMLElement) {
function leave(el: HTMLElement) {
// Set the height back to 0
el.style.height = '0'
el.style.willChange = ''
el.style.backfaceVisibility = ''
el.style.willChange = ''
el.style.backfaceVisibility = ''
}
function afterLeave(el: HTMLElement) {

View File

@ -18,7 +18,7 @@ export const DATE_RANGES = {
'thisYear': ['now/y', 'now/y+1y'],
'restOfThisYear': ['now', 'now/y+1y'],
} as const
}
export const DATE_VALUES = {
'now': 'now',
@ -43,4 +43,4 @@ export const DATE_VALUES = {
'startOfThisYear': 'now/y',
'endOfThisYear': 'now/y+1y',
} as const
}

View File

@ -1,11 +1,11 @@
<script setup lang="ts">
import DatemathHelp from './DatemathHelp.vue'
import datemathHelp from './datemathHelp.vue'
</script>
<template>
<Story>
<Variant title="Default">
<DatemathHelp />
<datemathHelp />
</Variant>
</Story>
</template>

View File

@ -108,10 +108,10 @@ import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import Popup from '@/components/misc/Popup.vue'
import Popup from '@/components/misc/popup.vue'
import {DATE_RANGES} from '@/components/date/dateRanges'
import BaseButton from '@/components/base/BaseButton.vue'
import DatemathHelp from '@/components/date/DatemathHelp.vue'
import DatemathHelp from '@/components/date/datemathHelp.vue'
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
const props = defineProps({

View File

@ -85,10 +85,10 @@ import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import Popup from '@/components/misc/Popup.vue'
import Popup from '@/components/misc/popup.vue'
import {DATE_VALUES} from '@/components/date/dateRanges'
import BaseButton from '@/components/base/BaseButton.vue'
import DatemathHelp from '@/components/date/DatemathHelp.vue'
import DatemathHelp from '@/components/date/datemathHelp.vue'
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
const props = defineProps({

View File

@ -1,107 +0,0 @@
<template>
<div
:class="{
'has-background': background,
'link-share-is-fullwidth': isFullWidth,
}"
:style="{'background-image': `url(${background})`}"
class="link-share-container"
>
<div class="has-text-centered link-share-view">
<Logo
v-if="logoVisible"
class="logo"
/>
<h1
:class="{'m-0': !logoVisible}"
:style="{ 'opacity': currentProject?.title === '' ? '0': '1' }"
class="title"
>
{{ currentProject?.title === '' ? $t('misc.loading') : currentProject?.title }}
</h1>
<div class="box has-text-left view">
<router-view />
<PoweredByLink />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {computed} from 'vue'
import {useBaseStore} from '@/stores/base'
import {useRoute} from 'vue-router'
import Logo from '@/components/home/Logo.vue'
import PoweredByLink from './PoweredByLink.vue'
import {useProjectStore} from '@/stores/projects'
import {useLabelStore} from '@/stores/labels'
import {PROJECT_VIEW_KINDS} from '@/modelTypes/IProjectView'
const baseStore = useBaseStore()
const currentProject = computed(() => baseStore.currentProject)
const background = computed(() => baseStore.background)
const logoVisible = computed(() => baseStore.logoVisible)
const projectStore = useProjectStore()
projectStore.loadAllProjects()
const labelStore = useLabelStore()
labelStore.loadAllLabels()
const route = useRoute()
const isFullWidth = computed(() => {
const viewId = route.params?.viewId ?? null
const projectId = route.params?.projectId ?? null
if (!viewId || !projectId) {
return false
}
const view = projectStore.projects[Number(projectId)]?.views.find(v => v.id === Number(viewId))
return view?.viewKind === PROJECT_VIEW_KINDS.KANBAN ||
view?.viewKind === PROJECT_VIEW_KINDS.GANTT
})
</script>
<style lang="scss" scoped>
.link-share-container.has-background .view {
background-color: transparent;
border: none;
}
.logo {
max-width: 300px;
width: 90%;
margin: 1rem auto 2rem;
height: 100px;
}
.title {
text-shadow: 0 0 1rem var(--white);
}
.link-share-view {
width: 100%;
max-width: $desktop;
margin: 0 auto;
}
.link-share-container.link-share-is-fullwidth {
.link-share-view {
max-width: 100vw;
}
}
.link-share-container:not(.has-background) {
:deep(.loader-container, .gantt-chart-container > .card) {
box-shadow: none !important;
border: none;
.task-add {
padding: 1rem 0 0;
}
}
}
</style>

View File

@ -9,7 +9,7 @@ import {MILLISECONDS_A_HOUR} from '@/constants/date'
const now = useNow({
interval: MILLISECONDS_A_HOUR,
})
const Logo = computed(() => window.ALLOW_ICON_CHANGES && now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
const Logo = computed(() => window.ALLOW_ICON_CHANGES && now.value.getMonth() === 6 ? LogoFullPride : LogoFull)
const CustomLogo = computed(() => window.CUSTOM_LOGO_URL)
</script>

View File

@ -26,6 +26,7 @@
:project="project"
:is-loading="projectUpdating[project.id]"
:can-collapse="canCollapse"
:level="level"
:data-project-id="project.id"
/>
</template>
@ -48,6 +49,7 @@ const props = defineProps<{
modelValue?: IProject[],
canEditOrder: boolean,
canCollapse?: boolean,
level?: number,
}>()
const emit = defineEmits<{
(e: 'update:modelValue', projects: IProject[]): void

View File

@ -58,6 +58,7 @@
<ProjectSettingsDropdown
class="menu-list-dropdown"
:project="project"
:level="level"
>
<template #trigger="{toggleOpen}">
<BaseButton
@ -77,6 +78,7 @@
:model-value="childProjects"
:can-edit-order="true"
:can-collapse="canCollapse"
:level="level + 1"
/>
</li>
</template>
@ -90,19 +92,21 @@ import {useStorage} from '@vueuse/core'
import type {IProject} from '@/modelTypes/IProject'
import BaseButton from '@/components/base/BaseButton.vue'
import ProjectSettingsDropdown from '@/components/project/ProjectSettingsDropdown.vue'
import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue'
import {getProjectTitle} from '@/helpers/getProjectTitle'
import ColorBubble from '@/components/misc/ColorBubble.vue'
import ColorBubble from '@/components/misc/colorBubble.vue'
import ProjectsNavigation from '@/components/home/ProjectsNavigation.vue'
const {
project,
isLoading,
canCollapse,
level = 0,
} = defineProps<{
project: IProject,
isLoading?: boolean,
canCollapse?: boolean,
level?: number,
}>()
const projectStore = useProjectStore()

View File

@ -114,10 +114,10 @@ import { computed } from 'vue'
import { RIGHTS as Rights } from '@/constants/rights'
import ProjectSettingsDropdown from '@/components/project/ProjectSettingsDropdown.vue'
import Dropdown from '@/components/misc/Dropdown.vue'
import DropdownItem from '@/components/misc/DropdownItem.vue'
import Notifications from '@/components/notifications/Notifications.vue'
import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import Notifications from '@/components/notifications/notifications.vue'
import Logo from '@/components/home/Logo.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import MenuButton from '@/components/home/MenuButton.vue'

View File

@ -67,8 +67,8 @@
import {watch, computed} from 'vue'
import {useRoute} from 'vue-router'
import Navigation from '@/components/home/Navigation.vue'
import QuickActions from '@/components/quick-actions/QuickActions.vue'
import Navigation from '@/components/home/navigation.vue'
import QuickActions from '@/components/quick-actions/quick-actions.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import {useBaseStore} from '@/stores/base'
@ -162,8 +162,8 @@ projectStore.loadAllProjects()
.app-content {
z-index: 10;
position: relative;
padding: 1.5rem 0.5rem 0;
// TODO refactor: DRY `transition-timing-function` with `./Navigation.vue`.
padding: 1.5rem 0.5rem 1rem;
// TODO refactor: DRY `transition-timing-function` with `./navigation.vue`.
transition: margin-left $transition-duration;
@media screen and (max-width: $tablet) {
@ -172,7 +172,7 @@ projectStore.loadAllProjects()
}
@media screen and (min-width: $tablet) {
padding: $navbar-height + 1.5rem 1.5rem 0 1.5rem;
padding: $navbar-height + 1.5rem 1.5rem 1rem 1.5rem;
}
&.is-menu-enabled {

View File

@ -0,0 +1,72 @@
<template>
<div
:class="[background ? 'has-background' : '', $route.name as string +'-view']"
:style="{'background-image': `url(${background})`}"
class="link-share-container"
>
<div class="container has-text-centered link-share-view">
<div class="column is-10 is-offset-1">
<Logo
v-if="logoVisible"
class="logo"
/>
<h1
:class="{'m-0': !logoVisible}"
:style="{ 'opacity': currentProject?.title === '' ? '0': '1' }"
class="title"
>
{{ currentProject?.title === '' ? $t('misc.loading') : currentProject?.title }}
</h1>
<div class="box has-text-left view">
<router-view />
<PoweredByLink />
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {computed} from 'vue'
import {useBaseStore} from '@/stores/base'
import Logo from '@/components/home/Logo.vue'
import PoweredByLink from './PoweredByLink.vue'
import {useProjectStore} from '@/stores/projects'
const baseStore = useBaseStore()
const currentProject = computed(() => baseStore.currentProject)
const background = computed(() => baseStore.background)
const logoVisible = computed(() => baseStore.logoVisible)
const projectStore = useProjectStore()
projectStore.loadAllProjects()
</script>
<style lang="scss" scoped>
.link-share-container.has-background .view {
background-color: transparent;
border: none;
}
.logo {
max-width: 300px;
width: 90%;
margin: 2rem 0 1.5rem;
height: 100px;
}
.column {
max-width: 100%;
}
.title {
text-shadow: 0 0 1rem var(--white);
}
// FIXME: this should be defined somewhere deep
.link-share-view .card {
background-color: var(--white);
}
</style>

View File

@ -118,7 +118,7 @@ import {computed} from 'vue'
import PoweredByLink from '@/components/home/PoweredByLink.vue'
import Logo from '@/components/home/Logo.vue'
import Loading from '@/components/misc/Loading.vue'
import Loading from '@/components/misc/loading.vue'
import {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects'
@ -155,8 +155,8 @@ const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
bottom: 0;
left: 0;
transform: translateX(-100%);
overflow-x: auto;
width: $navbar-width;
overflow-y: auto;
@media screen and (max-width: $tablet) {
top: 0;

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import {logEvent} from 'histoire/client'
import XButton from './Button.vue'
import XButton from './button.vue'
</script>
<template>

View File

@ -68,7 +68,7 @@
<script setup lang="ts">
import {computed, ref, watch} from 'vue'
import {createRandomID} from '@/helpers/randomId'
import XButton from '@/components/input/Button.vue'
import XButton from '@/components/input/button.vue'
const {
modelValue,

View File

@ -13,7 +13,7 @@
<script setup lang="ts">
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
import Multiselect from '@/components/input/Multiselect.vue'
import Multiselect from '@/components/input/multiselect.vue'
import type {IProject} from '@/modelTypes/IProject'

View File

@ -13,7 +13,7 @@
<script setup lang="ts">
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
import Multiselect from '@/components/input/Multiselect.vue'
import Multiselect from '@/components/input/multiselect.vue'
import type {IUser} from '@/modelTypes/IUser'

View File

@ -36,7 +36,7 @@
import {ref, onMounted, onBeforeUnmount, toRef, watch, type PropType} from 'vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import DatepickerInline from '@/components/input/DatepickerInline.vue'
import DatepickerInline from '@/components/input/datepickerInline.vue'
import SimpleButton from '@/components/input/SimpleButton.vue'
import {formatDateShort} from '@/helpers/time/formatDate'

View File

@ -67,7 +67,6 @@
class="tiptap__editor"
:class="{'tiptap__editor-is-edit-enabled': isEditing}"
:editor="editor"
@dblclick="setEditIfApplicable()"
@click="focusIfEditing()"
/>
@ -172,7 +171,7 @@ import {OrderedList} from '@tiptap/extension-ordered-list'
import {Paragraph} from '@tiptap/extension-paragraph'
import {Strike} from '@tiptap/extension-strike'
import {Text} from '@tiptap/extension-text'
import {BubbleMenu, EditorContent, type Extensions, useEditor} from '@tiptap/vue-3'
import {BubbleMenu, EditorContent, useEditor} from '@tiptap/vue-3'
import {Node} from '@tiptap/pm/model'
import Commands from './commands'
@ -187,10 +186,10 @@ import AttachmentModel from '@/models/attachment'
import AttachmentService from '@/services/attachment'
import {useI18n} from 'vue-i18n'
import BaseButton from '@/components/base/BaseButton.vue'
import XButton from '@/components/input/Button.vue'
import XButton from '@/components/input/button.vue'
import {Placeholder} from '@tiptap/extension-placeholder'
import {eventToHotkeyString} from '@github/hotkey'
import {Extension, mergeAttributes} from '@tiptap/core'
import {mergeAttributes} from '@tiptap/core'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
import inputPrompt from '@/helpers/inputPrompt'
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
@ -203,7 +202,6 @@ const {
showSave = false,
placeholder = '',
editShortcut = '',
enableDiscardShortcut = false,
} = defineProps<{
modelValue: string,
uploadCallback?: UploadCallback,
@ -212,7 +210,6 @@ const {
showSave?: boolean,
placeholder?: string,
editShortcut?: string,
enableDiscardShortcut?: boolean,
}>()
const emit = defineEmits(['update:modelValue', 'save'])
@ -314,8 +311,6 @@ const internalMode = ref<Mode>('preview')
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
const contentHasChanged = ref<boolean>(false)
let lastSavedState = modelValue
watch(
() => internalMode.value,
mode => {
@ -325,127 +320,107 @@ watch(
},
)
const extensions : Extensions = [
// Starterkit:
Blockquote,
Bold,
BulletList,
Code,
CodeBlockLowlight.configure({
lowlight,
}),
Document,
Dropcursor,
Gapcursor,
HardBreak.extend({
addKeyboardShortcuts() {
return {
'Shift-Enter': () => this.editor.commands.setHardBreak(),
'Mod-Enter': () => {
if (contentHasChanged.value) {
const editor = useEditor({
// eslint-disable-next-line vue/no-ref-object-destructure
editable: isEditing.value,
extensions: [
// Starterkit:
Blockquote,
Bold,
BulletList,
Code,
CodeBlockLowlight.configure({
lowlight,
}),
Document,
Dropcursor,
Gapcursor,
HardBreak.extend({
addKeyboardShortcuts() {
return {
'Mod-Enter': () => {
if (contentHasChanged.value) {
bubbleSave()
}
},
}
},
}),
Heading,
History,
HorizontalRule,
Italic,
ListItem,
OrderedList,
Paragraph,
Strike,
Text,
Placeholder.configure({
placeholder: ({editor}) => {
if (!isEditing.value) {
return ''
}
if (editor.getText() !== '' && !editor.isFocused) {
return ''
}
return placeholder !== ''
? placeholder
: t('input.editor.placeholder')
},
}),
Typography,
Underline,
Link.configure({
openOnClick: false,
validate: (href: string) => /^https?:\/\//.test(href),
}),
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
// Custom TableCell with backgroundColor attribute
CustomTableCell,
CustomImage,
TaskList,
TaskItem.configure({
nested: true,
onReadOnlyChecked: (node: Node, checked: boolean): boolean => {
if (!isEditEnabled) {
return false
}
// The following is a workaround for this bug:
// https://github.com/ueberdosis/tiptap/issues/4521
// https://github.com/ueberdosis/tiptap/issues/3676
editor.value!.state.doc.descendants((subnode, pos) => {
if (node.eq(subnode)) {
const {tr} = editor.value!.state
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
checked,
})
editor.value!.view.dispatch(tr)
bubbleSave()
}
return true
},
}
},
}),
Heading,
History,
HorizontalRule,
Italic,
ListItem,
OrderedList,
Paragraph,
Strike,
Text,
Placeholder.configure({
placeholder: ({editor}) => {
if (!isEditing.value) {
return ''
}
if (editor.getText() !== '' && !editor.isFocused) {
return ''
}
return placeholder !== ''
? placeholder
: t('input.editor.placeholder')
},
}),
Typography,
Underline,
Link.configure({
openOnClick: false,
validate: (href: string) => /^https?:\/\//.test(href),
}),
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
// Custom TableCell with backgroundColor attribute
CustomTableCell,
CustomImage,
TaskList,
TaskItem.configure({
nested: true,
onReadOnlyChecked: (node: Node, checked: boolean): boolean => {
if (!isEditEnabled) {
return false
}
// The following is a workaround for this bug:
// https://github.com/ueberdosis/tiptap/issues/4521
// https://github.com/ueberdosis/tiptap/issues/3676
editor.value!.state.doc.descendants((subnode, pos) => {
if (node.eq(subnode)) {
const {tr} = editor.value!.state
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
checked,
})
editor.value!.view.dispatch(tr)
bubbleSave()
}
})
})
return true
},
}),
return true
},
}),
Commands.configure({
suggestion: suggestionSetup(t),
}),
BubbleMenu,
]
// Add a custom extension for the Escape key
if (enableDiscardShortcut) {
extensions.push(Extension.create({
name: 'escapeKey',
addKeyboardShortcuts() {
return {
'Escape': () => {
exitEditMode()
return true
},
}
},
}))
}
const editor = useEditor({
// eslint-disable-next-line vue/no-ref-object-reactivity-loss
editable: isEditing.value,
extensions: extensions,
Commands.configure({
suggestion: suggestionSetup(t),
}),
BubbleMenu,
],
onUpdate: () => {
bubbleNow()
},
@ -484,27 +459,12 @@ function bubbleNow() {
function bubbleSave() {
bubbleNow()
lastSavedState = editor.value?.getHTML() ?? ''
emit('save', lastSavedState)
emit('save', editor.value?.getHTML())
if (isEditing.value) {
internalMode.value = 'preview'
}
}
function exitEditMode() {
editor.value?.commands.setContent(lastSavedState, false)
if (isEditing.value) {
internalMode.value = 'preview'
}
}
function setEditIfApplicable() {
if (!isEditEnabled) return
if (isEditing.value) return
setEdit()
}
function setEdit(focus: boolean = true) {
internalMode.value = 'edit'
if (focus) {
@ -856,7 +816,7 @@ watch(
td,
th {
min-width: 1em;
border: 2px solid var(--grey-300) !important;
border: 2px solid #ced4da;
padding: 3px 5px;
vertical-align: top;
box-sizing: border-box;
@ -870,7 +830,7 @@ watch(
th {
font-weight: bold;
text-align: left;
background-color: var(--grey-200);
background-color: #f1f3f5;
}
.selectedCell:after {
@ -931,14 +891,8 @@ ul[data-type='taskList'] {
padding: 0;
margin-left: 0;
li[data-checked='true'] {
color: var(--grey-500);
text-decoration: line-through;
}
li {
display: flex;
margin-top: 0.25rem;
> label {
flex: 0 0 auto;

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import {ref} from 'vue'
import {logEvent} from 'histoire/client'
import FancyCheckbox from './FancyCheckbox.vue'
import FancyCheckbox from './fancycheckbox.vue'
const isDisabled = ref<boolean | undefined>()

View File

@ -1,6 +1,6 @@
<template>
<BaseCheckbox
class="fancy-checkbox"
class="fancycheckbox"
:class="{
'is-disabled': disabled,
'is-block': isBlock,
@ -9,10 +9,10 @@
:model-value="modelValue"
@update:modelValue="value => emit('update:modelValue', value)"
>
<CheckboxIcon class="fancy-checkbox__icon" />
<CheckboxIcon class="fancycheckbox__icon" />
<span
v-if="$slots.default"
class="fancy-checkbox__content"
class="fancycheckbox__content"
>
<slot />
</span>
@ -44,7 +44,7 @@ const emit = defineEmits<{
<style lang="scss" scoped>
.fancy-checkbox {
.fancycheckbox {
display: inline-block;
padding-right: 5px;
padding-top: 3px;
@ -55,13 +55,13 @@ const emit = defineEmits<{
}
}
.fancy-checkbox__content {
.fancycheckbox__content {
font-size: 0.8rem;
vertical-align: top;
padding-left: .5rem;
}
.fancy-checkbox__icon:deep() {
.fancycheckbox__icon:deep() {
position: relative;
z-index: 1;
stroke: var(--stroke-color, #c8ccd4);
@ -74,8 +74,8 @@ const emit = defineEmits<{
}
}
.fancy-checkbox:not(:has(input:disabled)):hover .fancy-checkbox__icon,
.fancy-checkbox:has(input:checked) .fancy-checkbox__icon {
.fancycheckbox:not(:has(input:disabled)):hover .fancycheckbox__icon,
.fancycheckbox:has(input:checked) .fancycheckbox__icon {
--stroke-color: var(--primary);
}
</style>
@ -84,13 +84,13 @@ const emit = defineEmits<{
// Since css-has-pseudo doesn't work with deep classes,
// the following rules can't be scoped
.fancy-checkbox:has(:not(input:checked)) .fancy-checkbox__icon {
.fancycheckbox:has(:not(input:checked)) .fancycheckbox__icon {
path {
transition-delay: 0.05s;
}
}
.fancy-checkbox:has(input:checked) .fancy-checkbox__icon {
.fancycheckbox:has(input:checked) .fancycheckbox__icon {
path {
stroke-dashoffset: 60;
}

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import Card from './Card.vue'
import Card from './card.vue'
</script>
<template>

View File

@ -24,15 +24,11 @@ import {
faCocktail,
faCoffee,
faCog,
faCopy,
faDownload,
faEllipsisH,
faEllipsisV,
faExclamation,
faEye,
faEyeSlash,
faFile,
faFileImage,
faFillDrip,
faFilter,
faForward,
@ -85,6 +81,7 @@ import {
faCheckSquare,
faClock,
faComments,
faFileImage,
faSave,
faSquareCheck,
faStar,
@ -105,7 +102,6 @@ library.add(faUnlink)
library.add(faParagraph)
library.add(faSquareCheck)
library.add(faTable)
library.add(faFile)
library.add(faFileImage)
library.add(faCheckSquare)
library.add(faStrikethrough)
@ -134,8 +130,6 @@ library.add(faCocktail)
library.add(faCoffee)
library.add(faCog)
library.add(faComments)
library.add(faCopy)
library.add(faDownload)
library.add(faEllipsisH)
library.add(faEllipsisV)
library.add(faExclamation)

View File

@ -68,7 +68,7 @@ import {parseURL} from 'ufo'
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
import {success} from '@/message'
import Message from '@/components/misc/Message.vue'
import Message from '@/components/misc/message.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue'
const props = defineProps({

View File

@ -14,13 +14,15 @@
</Message>
</template>
<script lang="ts" setup>
import Message from '@/components/misc/Message.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue'
defineOptions({
<script lang="ts">
export default {
inheritAttrs: false,
})
}
</script>
<script lang="ts" setup>
import Message from '@/components/misc/message.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue'
function reload() {
window.location.reload()

View File

@ -50,8 +50,8 @@
<script lang="ts" setup>
import {useBaseStore} from '@/stores/base'
import Shortcut from '@/components/misc/Shortcut.vue'
import Message from '@/components/misc/Message.vue'
import Shortcut from '@/components/misc/shortcut.vue'
import Message from '@/components/misc/message.vue'
import {KEYBOARD_SHORTCUTS as shortcuts} from './shortcuts'

View File

@ -4,13 +4,13 @@ import {isAppleDevice} from '@/helpers/isAppleDevice'
const ctrl = isAppleDevice() ? '⌘' : 'ctrl'
export interface Shortcut {
interface Shortcut {
title: string
keys: string[]
combination?: 'then'
}
export interface ShortcutGroup {
interface ShortcutGroup {
title: string
available?: (route: RouteLocation) => boolean
shortcuts: Shortcut[]
@ -158,4 +158,4 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
},
],
},
] as const
]

View File

@ -66,39 +66,41 @@
</Teleport>
</template>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<script lang="ts" setup>
import CustomTransition from '@/components/misc/CustomTransition.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import {ref, useAttrs, watchEffect} from 'vue'
import {useScrollLock} from '@vueuse/core'
const props = withDefaults(defineProps<{
const {
enabled = true,
overflow,
wide,
transitionName = 'modal',
variant = 'default',
} = defineProps<{
enabled?: boolean,
overflow?: boolean,
wide?: boolean,
transitionName?: 'modal' | 'fade',
variant?: 'default' | 'hint-modal' | 'scrolling',
}>(), {
enabled: true,
overflow: false,
wide: false,
transitionName: 'modal',
variant: 'default',
})
}>()
defineEmits(['close', 'submit'])
defineOptions({
inheritAttrs: false,
})
const attrs = useAttrs()
const modal = ref<HTMLElement | null>(null)
const scrollLock = useScrollLock(modal)
watchEffect(() => {
scrollLock.value = props.enabled
scrollLock.value = enabled
})
</script>
@ -186,6 +188,12 @@ $modal-width: 1024px;
.info {
font-style: italic;
}
p {
display: flex;
justify-content: space-between;
align-items: center;
}
}
}

View File

@ -46,9 +46,9 @@ import {useRoute} from 'vue-router'
import {useI18n} from 'vue-i18n'
import Logo from '@/components/home/Logo.vue'
import Message from '@/components/misc/Message.vue'
import Legal from '@/components/misc/Legal.vue'
import ApiConfig from '@/components/misc/ApiConfig.vue'
import Message from '@/components/misc/message.vue'
import Legal from '@/components/misc/legal.vue'
import ApiConfig from '@/components/misc/api-config.vue'
import {useTitle} from '@/composables/useTitle'
import {useConfigStore} from '@/stores/config'

View File

@ -10,7 +10,7 @@
class="popup"
:class="{
'is-open': openValue,
'has-overflow': hasOverflow && openValue
'has-overflow': props.hasOverflow && openValue
}"
>
<slot
@ -23,23 +23,31 @@
</template>
<script setup lang="ts">
import {ref, watchEffect} from 'vue'
import {ref, watch} from 'vue'
import {onClickOutside} from '@vueuse/core'
const props = withDefaults(defineProps<{
hasOverflow?: boolean
open?: boolean
}>(), {
hasOverflow: false,
open: false,
const props = defineProps({
hasOverflow: {
type: Boolean,
default: false,
},
open: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['close'])
const openValue = ref(props.open)
watchEffect(() => {
openValue.value = props.open
})
watch(
() => props.open,
nowOpen => {
openValue.value = nowOpen
},
)
const openValue = ref(false)
const popup = ref<HTMLElement | null>(null)
function close() {
openValue.value = false
@ -50,8 +58,6 @@ function toggle() {
openValue.value = !openValue.value
}
const popup = ref<HTMLElement | null>(null)
onClickOutside(popup, () => {
if (!openValue.value) {
return

View File

@ -61,10 +61,10 @@ import {ref, computed} from 'vue'
import {useRouter, useRoute} from 'vue-router'
import Logo from '@/assets/logo.svg?component'
import ApiConfig from '@/components/misc/ApiConfig.vue'
import Message from '@/components/misc/Message.vue'
import ApiConfig from '@/components/misc/api-config.vue'
import Message from '@/components/misc/message.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import NoAuthWrapper from '@/components/misc/NoAuthWrapper.vue'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
import {ERROR_NO_API_URL, InvalidApiUrlProvidedError, NoApiUrlProvidedError} from '@/helpers/checkAndSetApiUrl'
import {useOnline} from '@/composables/useOnline'

View File

@ -14,13 +14,19 @@
</template>
<script lang="ts" setup>
withDefaults(defineProps<{
keys: string[],
combination?: string,
is?: string
}>(), {
combination: '+',
is: 'div',
defineProps({
keys: {
type: Array,
required: true,
},
combination: {
type: String,
default: '+',
},
is: {
type: String,
default: 'div',
},
})
</script>

View File

@ -33,11 +33,11 @@
</template>
<script lang="ts" setup>
import {computed, shallowReactive} from 'vue'
import {computed, shallowRef, type PropType} from 'vue'
import {useI18n} from 'vue-i18n'
import BaseButton from '@/components/base/BaseButton.vue'
import DropdownItem from '@/components/misc/DropdownItem.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import SubscriptionService from '@/services/subscription'
import SubscriptionModel from '@/models/subscription'
@ -46,23 +46,28 @@ import type {ISubscription} from '@/modelTypes/ISubscription'
import {success} from '@/message'
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
const props = withDefaults(defineProps<{
entity: ISubscription['entity'],
entityId: number,
isButton?: boolean,
modelValue?: ISubscription,
type?: 'button' | 'dropdown',
}>(), {
isButton: true,
modelValue: null,
type: 'button',
const props = defineProps({
entity: String as ISubscription['entity'],
entityId: Number,
isButton: {
type: Boolean,
default: true,
},
modelValue: {
type: Object as PropType<ISubscription>,
default: null,
},
type: {
type: String as PropType<'button' | 'dropdown' | 'null'>,
default: 'button',
},
})
const emit = defineEmits(['update:modelValue'])
const subscriptionEntity = computed<string | null>(() => props.modelValue?.entity ?? null)
const subscriptionService = shallowReactive(new SubscriptionService())
const subscriptionService = shallowRef(new SubscriptionService())
const {t} = useI18n({useScope: 'global'})
@ -72,8 +77,8 @@ const tooltipText = computed(() => {
return t('task.subscription.subscribedTaskThroughParentProject')
}
return ''
}
return ''
}
switch (props.entity) {
case 'project':
@ -110,7 +115,7 @@ async function subscribe() {
entity: props.entity,
entityId: props.entityId,
})
await subscriptionService.create(subscription)
await subscriptionService.value.create(subscription)
emit('update:modelValue', subscription)
let message = ''
@ -130,7 +135,7 @@ async function unsubscribe() {
entity: props.entity,
entityId: props.entityId,
})
await subscriptionService.delete(subscription)
await subscriptionService.value.delete(subscription)
emit('update:modelValue', null)
let message = ''

Some files were not shown because too many files have changed in this diff Show More