Compare commits

..

1 Commits

Author SHA1 Message Date
renovate 813a1b8eb2 fix(deps): update dependency lowlight to v3
continuous-integration/drone/pr Build is failing Details
2024-04-19 07:11:01 +00:00
96 changed files with 4053 additions and 7185 deletions

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.3.3",
"electron": "29.3.1",
"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

@ -348,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`
@ -780,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`
@ -1223,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.
@ -1422,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">}})
@ -229,7 +229,7 @@ git clone https://code.vikunja.io/vikunja
cd vikunja
```
**Note:** Check out the version you want to build with `git checkout VERSION` - replace `VERSION` with the version want to use.
**Note:** Ceck 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:

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

@ -147,7 +147,7 @@ Flags:
#### `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.**

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

@ -12,8 +12,6 @@
nodePackages.pnpm cypress
# API tools
go golangci-lint mage
# Desktop
electron
];
};
};

View File

@ -1 +1 @@
20.13.0
20.12.2

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

@ -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

@ -65,7 +65,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 +81,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')
@ -640,7 +640,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 +665,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 +694,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 +723,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 +759,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')

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@9.1.0",
"packageManager": "pnpm@9.0.4",
"keywords": [
"todo",
"productivity",
@ -58,41 +58,41 @@
"@infectoone/vue-ganttastic": "2.3.2",
"@intlify/unplugin-vue-i18n": "4.0.0",
"@kyvg/vue3-notification": "3.2.1",
"@sentry/tracing": "7.114.0",
"@sentry/vue": "7.114.0",
"@tiptap/core": "2.3.2",
"@tiptap/extension-blockquote": "2.3.2",
"@tiptap/extension-bold": "2.3.2",
"@tiptap/extension-bullet-list": "2.3.2",
"@tiptap/extension-code": "2.3.2",
"@tiptap/extension-code-block-lowlight": "2.3.2",
"@tiptap/extension-document": "2.3.2",
"@tiptap/extension-dropcursor": "2.3.2",
"@tiptap/extension-gapcursor": "2.3.2",
"@tiptap/extension-hard-break": "2.3.2",
"@tiptap/extension-heading": "2.3.2",
"@tiptap/extension-history": "2.3.2",
"@tiptap/extension-horizontal-rule": "2.3.2",
"@tiptap/extension-image": "2.3.2",
"@tiptap/extension-italic": "2.3.2",
"@tiptap/extension-link": "2.3.2",
"@tiptap/extension-list-item": "2.3.2",
"@tiptap/extension-ordered-list": "2.3.2",
"@tiptap/extension-paragraph": "2.3.2",
"@tiptap/extension-placeholder": "2.3.2",
"@tiptap/extension-strike": "2.3.2",
"@tiptap/extension-table": "2.3.2",
"@tiptap/extension-table-cell": "2.3.2",
"@tiptap/extension-table-header": "2.3.2",
"@tiptap/extension-table-row": "2.3.2",
"@tiptap/extension-task-item": "2.3.2",
"@tiptap/extension-task-list": "2.3.2",
"@tiptap/extension-text": "2.3.2",
"@tiptap/extension-typography": "2.3.2",
"@tiptap/extension-underline": "2.3.2",
"@tiptap/pm": "2.3.2",
"@tiptap/suggestion": "2.3.2",
"@tiptap/vue-3": "2.3.2",
"@sentry/tracing": "7.111.0",
"@sentry/vue": "7.111.0",
"@tiptap/core": "2.3.0",
"@tiptap/extension-blockquote": "2.3.0",
"@tiptap/extension-bold": "2.3.0",
"@tiptap/extension-bullet-list": "2.3.0",
"@tiptap/extension-code": "2.3.0",
"@tiptap/extension-code-block-lowlight": "2.3.0",
"@tiptap/extension-document": "2.3.0",
"@tiptap/extension-dropcursor": "2.3.0",
"@tiptap/extension-gapcursor": "2.3.0",
"@tiptap/extension-hard-break": "2.3.0",
"@tiptap/extension-heading": "2.3.0",
"@tiptap/extension-history": "2.3.0",
"@tiptap/extension-horizontal-rule": "2.3.0",
"@tiptap/extension-image": "2.3.0",
"@tiptap/extension-italic": "2.3.0",
"@tiptap/extension-link": "2.3.0",
"@tiptap/extension-list-item": "2.3.0",
"@tiptap/extension-ordered-list": "2.3.0",
"@tiptap/extension-paragraph": "2.3.0",
"@tiptap/extension-placeholder": "2.3.0",
"@tiptap/extension-strike": "2.3.0",
"@tiptap/extension-table": "2.3.0",
"@tiptap/extension-table-cell": "2.3.0",
"@tiptap/extension-table-header": "2.3.0",
"@tiptap/extension-table-row": "2.3.0",
"@tiptap/extension-task-item": "2.3.0",
"@tiptap/extension-task-list": "2.3.0",
"@tiptap/extension-text": "2.3.0",
"@tiptap/extension-typography": "2.3.0",
"@tiptap/extension-underline": "2.3.0",
"@tiptap/pm": "2.3.0",
"@tiptap/suggestion": "2.3.0",
"@tiptap/vue-3": "2.3.0",
"@types/is-touch-device": "1.0.2",
"@types/lodash.clonedeep": "4.5.9",
"@vueuse/core": "10.9.0",
@ -102,8 +102,8 @@
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"date-fns": "3.6.0",
"dayjs": "1.11.11",
"dompurify": "3.1.2",
"dayjs": "1.11.10",
"dompurify": "3.1.0",
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.31",
@ -111,20 +111,20 @@
"is-touch-device": "1.0.1",
"klona": "2.0.6",
"lodash.debounce": "4.0.8",
"lowlight": "2.9.0",
"lowlight": "3.1.0",
"pinia": "2.1.7",
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"sortablejs": "1.15.2",
"tippy.js": "6.3.7",
"ufo": "1.5.3",
"vue": "3.4.27",
"vue": "3.4.23",
"vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.5",
"vue-i18n": "9.13.1",
"vue-i18n": "9.13.0",
"vue-router": "4.3.2",
"vuemoji-picker": "0.2.1",
"workbox-precaching": "7.1.0",
"workbox-precaching": "7.0.0",
"zhyswan-vuedraggable": "4.1.3"
},
"devDependencies": {
@ -142,46 +142,46 @@
"@types/is-touch-device": "1.0.2",
"@types/lodash.debounce": "4.0.9",
"@types/marked": "5.0.2",
"@types/node": "20.12.11",
"@types/node": "20.12.7",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/eslint-plugin": "7.8.0",
"@typescript-eslint/parser": "7.8.0",
"@vitejs/plugin-legacy": "5.4.0",
"@typescript-eslint/eslint-plugin": "7.7.0",
"@typescript-eslint/parser": "7.7.0",
"@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.0",
"caniuse-lite": "1.0.30001617",
"caniuse-lite": "1.0.30001611",
"css-has-pseudo": "6.0.3",
"csstype": "3.1.3",
"cypress": "13.9.0",
"esbuild": "0.21.1",
"cypress": "13.8.0",
"esbuild": "0.20.2",
"eslint": "8.57.0",
"eslint-plugin-vue": "9.26.0",
"happy-dom": "14.10.1",
"eslint-plugin-vue": "9.25.0",
"happy-dom": "14.7.1",
"histoire": "0.17.17",
"postcss": "8.4.38",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-focus-within": "8.0.1",
"postcss-preset-env": "9.5.11",
"rollup": "4.17.2",
"postcss-preset-env": "9.5.6",
"rollup": "4.14.3",
"rollup-plugin-visualizer": "5.12.0",
"sass": "1.77.0",
"sass": "1.75.0",
"start-server-and-test": "2.0.3",
"typescript": "5.4.5",
"vite": "5.2.11",
"vite": "5.2.9",
"vite-plugin-inject-preload": "1.3.3",
"vite-plugin-pwa": "0.20.0",
"vite-plugin-pwa": "0.19.8",
"vite-plugin-sentry": "1.4.0",
"vite-svg-loader": "5.1.0",
"vitest": "1.6.0",
"vue-tsc": "2.0.16",
"vitest": "1.5.0",
"vue-tsc": "2.0.13",
"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

@ -1,27 +1,26 @@
<template>
<div
:class="{
'has-background': background,
'link-share-is-fullwidth': isFullWidth,
}"
:class="[background ? 'has-background' : '', $route.name as string +'-view']"
: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 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>
@ -31,13 +30,10 @@
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)
@ -46,23 +42,6 @@ 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>
@ -74,34 +53,20 @@ const isFullWidth = computed(() => {
.logo {
max-width: 300px;
width: 90%;
margin: 1rem auto 2rem;
margin: 2rem 0 1.5rem;
height: 100px;
}
.column {
max-width: 100%;
}
.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;
}
}
// FIXME: this should be defined somewhere deep
.link-share-view .card {
background-color: var(--white);
}
</style>

View File

@ -818,7 +818,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;
@ -832,7 +832,7 @@ watch(
th {
font-weight: bold;
text-align: left;
background-color: var(--grey-200);
background-color: #f1f3f5;
}
.selectedCell:after {

View File

@ -57,7 +57,7 @@ const showDocs = ref(false)
<code>done = false &amp;&amp; priority &gt;= 3</code>:
{{ $t('filters.query.help.examples.undoneHighPriority') }}
</li>
<li><code>assignees in user1, user2</code>: {{ $t('filters.query.help.examples.assigneesIn') }}</li>
<li><code>assignees in [user1, user2]</code>: {{ $t('filters.query.help.examples.assigneesIn') }}</li>
<li>
<code>(priority = 1 || priority = 2) &amp;&amp; dueDate &lt;= now</code>:
{{ $t('filters.query.help.examples.priorityOneOrTwoPastDue') }}

View File

@ -30,7 +30,7 @@
>
<icon icon="filter" />
</span>
{{ getProjectTitle(project) }}
{{ project.title }}
</div>
<BaseButton
class="project-button"
@ -59,7 +59,6 @@ import BaseButton from '@/components/base/BaseButton.vue'
import {useProjectBackground} from './useProjectBackground'
import {useProjectStore} from '@/stores/projects'
import {getProjectTitle} from '@/helpers/getProjectTitle'
const {
project,

View File

@ -306,12 +306,4 @@ function prepareFiltersAndLoadTasks() {
}
}
}
.list-view {
padding-bottom: 1rem;
:deep(.card) {
margin-bottom: 0;
}
}
</style>

View File

@ -267,7 +267,7 @@
</template>
<script setup lang="ts">
import {computed, type Ref, watch} from 'vue'
import {computed, type Ref} from 'vue'
import {useStorage} from '@vueuse/core'
@ -337,12 +337,6 @@ Object.assign(params.value, {
filter: '',
})
watch(
() => activeColumns.value,
() => setActiveColumnsSortParam(),
{deep: true},
)
// FIXME: by doing this we can have multiple sort orders
function sort(property: keyof SortBy) {
const order = sortBy.value[property]
@ -353,16 +347,7 @@ function sort(property: keyof SortBy) {
} else {
delete sortBy.value[property]
}
setActiveColumnsSortParam()
}
function setActiveColumnsSortParam() {
sortByParam.value = Object.keys(sortBy.value)
.filter(prop => activeColumns.value[prop])
.reduce((obj, key) => {
obj[key] = sortBy.value[key]
return obj
}, {})
sortByParam.value = sortBy.value
}
// TODO: re-enable opening task detail in modal

View File

@ -323,8 +323,9 @@ async function setCoverImage(attachment: IAttachment | null) {
margin-bottom: 0;
display: flex;
> span,
> span:not(:last-child):after,
> button:not(:last-child):after {
content: '·';
padding: 0 .25rem;
}
}

View File

@ -111,7 +111,7 @@
<icon icon="align-left" />
</span>
<span
v-if="task.repeatAfter.amount > 0 || (task.repeatAfter.amount === 0 && task.repeatMode === TASK_REPEAT_MODES.REPEAT_MODE_MONTH)"
v-if="task.repeatAfter.amount > 0"
class="project-task-icon"
>
<icon icon="history" />
@ -207,7 +207,6 @@ import {useIntervalFn} from '@vueuse/core'
import {playPopSound} from '@/helpers/playPop'
import {useAuthStore} from '@/stores/auth'
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
const {
theTask,

View File

@ -2,7 +2,7 @@ import {calculateDayInterval} from './calculateDayInterval'
import {calculateNearestHours} from './calculateNearestHours'
import {replaceAll} from '../replaceAll'
export interface dateParseResult {
interface dateParseResult {
newText: string,
date: Date | null,
}
@ -12,7 +12,7 @@ interface dateFoundResult {
date: Date | null,
}
const monthsRegexGroup = '(january|february|march|april|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)'
const monthsRegexGroup = '(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)'
function matchesDateExpr(text: string, dateExpr: string): boolean {
return text.match(new RegExp('(^| )' + dateExpr, 'gi')) !== null
@ -60,12 +60,12 @@ export const parseDate = (text: string, now: Date = new Date()): dateParseResult
return addTimeToDate(text, date, 'end of month')
}
let parsed = getDateFromWeekday(text, now)
let parsed = getDateFromWeekday(text)
if (parsed.date !== null) {
return addTimeToDate(text, parsed.date, parsed.foundText)
}
parsed = getDayFromText(text, now)
parsed = getDayFromText(text)
if (parsed.date !== null) {
const month = getMonthFromText(text, parsed.date)
return addTimeToDate(month.newText, month.date, parsed.foundText)
@ -76,7 +76,7 @@ export const parseDate = (text: string, now: Date = new Date()): dateParseResult
return addTimeToDate(text, parsed.date, parsed.foundText)
}
parsed = getDateFromText(text, now)
parsed = getDateFromText(text)
if (parsed.date === null) {
return {
@ -230,7 +230,7 @@ export const getDateFromTextIn = (text: string, now: Date = new Date()) => {
}
}
const getDateFromWeekday = (text: string, date: Date = new Date()): dateFoundResult => {
const getDateFromWeekday = (text: string): dateFoundResult => {
const matcher = /(^| )(next )?(monday|mon|tuesday|tue|wednesday|wed|thursday|thu|friday|fri|saturday|sat|sunday|sun)($| )/g
const results: string[] | null = matcher.exec(text.toLowerCase()) // The i modifier does not seem to work.
if (results === null) {
@ -240,6 +240,7 @@ const getDateFromWeekday = (text: string, date: Date = new Date()): dateFoundRes
}
}
const date: Date = new Date()
const currentDay: number = date.getDay()
let day = 0
@ -295,7 +296,7 @@ const getDateFromWeekday = (text: string, date: Date = new Date()): dateFoundRes
}
}
const getDayFromText = (text: string, now: Date = new Date()) => {
const getDayFromText = (text: string) => {
const matcher = /(^| )(([1-2][0-9])|(3[01])|(0?[1-9]))(st|nd|rd|th|\.)($| )/ig
const results = matcher.exec(text)
if (results === null) {
@ -305,6 +306,7 @@ const getDayFromText = (text: string, now: Date = new Date()) => {
}
}
const now = new Date()
const date = new Date(now)
const day = parseInt(results[0])
date.setDate(day)

View File

@ -19,16 +19,16 @@ export function secondsToPeriod(seconds: number): { unit: PeriodUnit, amount: nu
}
}
if (seconds % SECONDS_A_HOUR === 0) {
if (seconds % SECONDS_A_MINUTE === 0) {
return {
unit: 'hours',
amount: seconds / SECONDS_A_HOUR,
unit: 'minutes',
amount: seconds / SECONDS_A_MINUTE,
}
}
return {
unit: 'minutes',
amount: seconds / SECONDS_A_MINUTE,
unit: 'hours',
amount: seconds / SECONDS_A_HOUR,
}
}

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "كل 30 يوماً",
"mode": "وضع التكرار",
"monthly": "شهرياً",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "من التاريخ الحالي",
"each": "كل",
"specifyAmount": "حدد قيمة…",
"hours": "الساعات",
@ -1060,7 +1059,7 @@
"duplicate": "تكرار",
"delete": "حذف",
"unarchive": "إلغاء الأرشفة",
"setBackground": "Background settings",
"setBackground": "تعيين خلفية",
"share": "مشاركة",
"newProject": "مشروع جديد",
"createProject": "إنشاء مشروع",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "From Current Date",
"each": "Each",
"specifyAmount": "Specify an amount…",
"hours": "Hours",
@ -1060,7 +1059,7 @@
"duplicate": "Duplicate",
"delete": "Delete",
"unarchive": "Un-Archive",
"setBackground": "Background settings",
"setBackground": "Set background",
"share": "Share",
"newProject": "New project",
"createProject": "Create project",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Každých 30 dní",
"mode": "Režim opakování",
"monthly": "Měsíčně",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "Od aktuálního data",
"each": "Každý",
"specifyAmount": "Zadejte množství…",
"hours": "Hodin",
@ -1060,7 +1059,7 @@
"duplicate": "Duplikovat",
"delete": "Smazat",
"unarchive": "Zrušit archivaci",
"setBackground": "Background settings",
"setBackground": "Nastavit pozadí",
"share": "Sdílet",
"newProject": "Nový projekt",
"createProject": "Vytvořit projekt",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Gentagelsestilstand",
"monthly": "Månedligt",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "Fra Nuværende Dato",
"each": "Hver",
"specifyAmount": "Angiv et beløb…",
"hours": "Timer",
@ -1060,7 +1059,7 @@
"duplicate": "Dupliker",
"delete": "Slet",
"unarchive": "Tilbagekald",
"setBackground": "Background settings",
"setBackground": "Indstil baggrund",
"share": "Del",
"newProject": "New project",
"createProject": "Create project",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Nur Projektadministrator:innen können Ansichten bearbeiten."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Alle 30 Tage",
"mode": "Wiederholungsmodus",
"monthly": "Monatlich",
"fromCurrentDate": "Ab Fertigstellungsdatum",
"fromCurrentDate": "Ab dem aktuellen Datum",
"each": "Alle",
"specifyAmount": "Gib einen Anzahl an …",
"hours": "Stunden",
@ -1060,7 +1059,7 @@
"duplicate": "Duplizieren",
"delete": "Löschen",
"unarchive": "Archivierung aufheben",
"setBackground": "Hintergrundeinstellungen",
"setBackground": "Hintergrund einstellen",
"share": "Teilen",
"newProject": "Neues Projekt",
"createProject": "Projekt erstellen",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Nur Projektadministrator:innen können Ansichten bearbeiten."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Alle 30 Tage",
"mode": "Widerholigs Modus",
"monthly": "Monatlich",
"fromCurrentDate": "Ab Fertigstellungsdatum",
"fromCurrentDate": "Vom Hüttige Datum",
"each": "Jedä",
"specifyAmount": "Gib e Ahzahl ah…",
"hours": "Stundä",
@ -1060,7 +1059,7 @@
"duplicate": "Dublizierä",
"delete": "Chüble",
"unarchive": "Ent-archiviere",
"setBackground": "Hintergrundeinstellungen",
"setBackground": "Hintergrund iihstelle",
"share": "Teilä",
"newProject": "Neues Projekt",
"createProject": "Projekt erstellen",

View File

@ -53,7 +53,7 @@
"loginWith": "Log in with {provider}",
"authenticating": "Authenticating…",
"openIdStateError": "State does not match, refusing to continue!",
"openIdGeneralError": "An error occurred while authenticating against the third party.",
"openIdGeneralError": "An error occured while authenticating against the third party.",
"logout": "Logout",
"emailInvalid": "Please enter a valid email address.",
"usernameRequired": "Please provide a username.",
@ -227,8 +227,8 @@
"title": "Archive \"{project}\"",
"archive": "Archive this project",
"unarchive": "Un-Archive this project",
"unarchiveText": "You will be able to create tasks or edit it.",
"archiveText": "You won't be able to edit this project or create tasks until you un-archive it.",
"unarchiveText": "You will be able to create new tasks or edit it.",
"archiveText": "You won't be able to edit this project or create new tasks until you un-archive it.",
"success": "The project was successfully archived."
},
"background": {
@ -277,7 +277,7 @@
"title": "Share Links",
"what": "What is a share link?",
"explanation": "Share Links allow you to easily share a project with other users who don't have an account on Vikunja.",
"create": "Create a link share",
"create": "Create a new link share",
"name": "Name (optional)",
"namePlaceholder": "e.g. Lorem Ipsum",
"nameExplanation": "All actions done by this link share will show up with the name.",
@ -317,9 +317,9 @@
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a task…",
"addPlaceholder": "Add a new task…",
"empty": "This project is currently empty.",
"newTaskCta": "Create a task.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": {
@ -352,7 +352,7 @@
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
"addAnotherTask": "Add another task",
"addBucket": "Create a bucket",
"addBucket": "Create a new bucket",
"addBucketPlaceholder": "Enter the new bucket title…",
"deleteHeaderBucket": "Delete the bucket",
"deleteBucketText1": "Are you sure you want to delete this bucket?",
@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -423,7 +422,7 @@
"create": {
"title": "New Saved Filter",
"description": "A saved filter is a virtual project which is computed from a set of filters each time it is accessed.",
"action": "Create saved filter",
"action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter."
},
"delete": {
@ -507,7 +506,7 @@
"search": "Type to search for a label…",
"create": {
"header": "New label",
"title": "Create a label",
"title": "Create a new label",
"titleRequired": "Please specify a title.",
"success": "The label was successfully created."
},
@ -528,7 +527,7 @@
"sharing": {
"authenticating": "Authenticating…",
"passwordRequired": "This shared project requires a password. Please enter it below:",
"error": "An error occurred.",
"error": "An error occured.",
"invalidPassword": "The password is invalid."
},
"navigation": {
@ -643,7 +642,7 @@
}
},
"multiselect": {
"createPlaceholder": "Create",
"createPlaceholder": "Create new",
"selectPlaceholder": "Click or press enter to select"
},
"datepickerRange": {
@ -722,10 +721,10 @@
},
"task": {
"task": "Task",
"new": "Create a task",
"new": "Create a new task",
"delete": "Delete this task",
"createSuccess": "The task was successfully created.",
"addReminder": "Add a reminder…",
"addReminder": "Add a new reminder…",
"doneSuccess": "The task was successfully marked as done.",
"undoneSuccess": "The task was successfully un-marked as done.",
"undo": "Undo",
@ -861,7 +860,7 @@
"unassignSuccess": "The user has been unassigned successfully."
},
"label": {
"placeholder": "Type to add a label…",
"placeholder": "Type to add a new label…",
"createPlaceholder": "Add this as new label",
"addSuccess": "The label has been added successfully.",
"createSuccess": "The label has been created successfully.",
@ -884,8 +883,8 @@
"relation": {
"add": "Add a New Task Relation",
"new": "New Task Relation",
"searchPlaceholder": "Type search for a task to add as related…",
"createPlaceholder": "Add this as related task",
"searchPlaceholder": "Type search for a new task to add as related…",
"createPlaceholder": "Add this as new related task",
"differentProject": "This task belongs to a different project.",
"noneYet": "No task relations yet.",
"delete": "Delete Task Relation",
@ -923,7 +922,7 @@
"every30d": "Every 30 Days",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "From Current Date",
"each": "Each",
"specifyAmount": "Specify an amount…",
"hours": "Hours",
@ -963,7 +962,7 @@
"title": "Teams",
"noTeams": "You are currently not part of any teams.",
"create": {
"title": "Create a team",
"title": "Create a new team",
"success": "The team was successfully created."
},
"edit": {
@ -1061,7 +1060,7 @@
"duplicate": "Duplicate",
"delete": "Delete",
"unarchive": "Un-Archive",
"setBackground": "Background settings",
"setBackground": "Set background",
"share": "Share",
"newProject": "New project",
"createProject": "Create project",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "From Current Date",
"each": "Each",
"specifyAmount": "Specify an amount…",
"hours": "Hours",
@ -1060,7 +1059,7 @@
"duplicate": "Duplicate",
"delete": "Delete",
"unarchive": "Un-Archive",
"setBackground": "Background settings",
"setBackground": "Set background",
"share": "Share",
"newProject": "New project",
"createProject": "Create project",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Modo de repetición",
"monthly": "Mensualmente",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "Desde la Fecha Actual",
"each": "Cada",
"specifyAmount": "Especifique una cantidad…",
"hours": "Horas",
@ -1060,7 +1059,7 @@
"duplicate": "Duplicar",
"delete": "Eliminar",
"unarchive": "Desarchivar",
"setBackground": "Background settings",
"setBackground": "Establecer fondo",
"share": "Compartir",
"newProject": "Nuevo proyecto",
"createProject": "Crear proyecto",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Mode de répétition",
"monthly": "Mensuel",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "À partir de la date actuelle",
"each": "Tous ou toutes les",
"specifyAmount": "Indiquer un nombre…",
"hours": "Heures",
@ -1060,7 +1059,7 @@
"duplicate": "Dupliquer",
"delete": "Supprimer",
"unarchive": "Désarchiver",
"setBackground": "Background settings",
"setBackground": "Définir larrière-plan",
"share": "Partager",
"newProject": "Nouveau projet",
"createProject": "Créer un projet",

File diff suppressed because it is too large Load Diff

View File

@ -396,8 +396,7 @@
"titleRequired": "Kérjük, adjon meg egy címet.",
"delete": "Törölje ezt a nézetet",
"deleteText": "Biztosan eltávolítja ezt a nézetet? A továbbiakban nem lesz használható a projektben szereplő feladatok megtekintésére. Ez a művelet nem töröl semmilyen feladatot. Ezt nem lehet visszacsinálni!",
"deleteSuccess": "A nézet sikeresen törölve",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "A nézet sikeresen törölve"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "30 naponta",
"mode": "Ismétlés típusa",
"monthly": "Havi",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "Aktuális dátumtól",
"each": "Minden egyes",
"specifyAmount": "Adja meg a mennyiséget…",
"hours": "Órák",
@ -1060,7 +1059,7 @@
"duplicate": "Duplikálás",
"delete": "Törlés",
"unarchive": "Archiválás visszavonása",
"setBackground": "Background settings",
"setBackground": "Háttérkép beállítása",
"share": "Megosztás",
"newProject": "Új projekt",
"createProject": "Projekt létrehozása",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Modalità Ripetizione",
"monthly": "Mensilmente",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "Dalla Data Attuale",
"each": "Ogni",
"specifyAmount": "Specifica una quantità…",
"hours": "Ore",
@ -1060,7 +1059,7 @@
"duplicate": "Duplica",
"delete": "Elimina",
"unarchive": "Disarchivia",
"setBackground": "Background settings",
"setBackground": "Imposta sfondo",
"share": "Condividi",
"newProject": "New project",
"createProject": "Create project",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "30日ごと",
"mode": "繰り返しモード",
"monthly": "毎月",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "現在時刻からの間隔",
"each": "隔",
"specifyAmount": "数字を入力…",
"hours": "時間ごと",
@ -1060,7 +1059,7 @@
"duplicate": "複製",
"delete": "削除",
"unarchive": "アーカイブの取り消し",
"setBackground": "Background settings",
"setBackground": "背景画像の設定",
"share": "共有",
"newProject": "新しいプロジェクトの作成",
"createProject": "プロジェクトの作成",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "From Current Date",
"each": "Each",
"specifyAmount": "Specify an amount…",
"hours": "Hours",
@ -1060,7 +1059,7 @@
"duplicate": "Duplicate",
"delete": "Delete",
"unarchive": "Un-Archive",
"setBackground": "Background settings",
"setBackground": "Set background",
"share": "Share",
"newProject": "New project",
"createProject": "Create project",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Herhaalmodus",
"monthly": "Maandelijks",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "Vanaf huidige datum",
"each": "Each",
"specifyAmount": "Specify an amount…",
"hours": "Uren",
@ -1060,7 +1059,7 @@
"duplicate": "Dupliceer",
"delete": "Verwijderen",
"unarchive": "Archivering opheffen",
"setBackground": "Background settings",
"setBackground": "Achtergrond instellen",
"share": "Delen",
"newProject": "New project",
"createProject": "Create project",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Repeter modus",
"monthly": "Månedlig",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "Fra gjeldende dato",
"each": "Hver",
"specifyAmount": "Angi beløp…",
"hours": "Timer",
@ -1060,7 +1059,7 @@
"duplicate": "Dupliser",
"delete": "Slett",
"unarchive": "Av-arkiver",
"setBackground": "Background settings",
"setBackground": "Bruk som bakgrunn",
"share": "Del",
"newProject": "Nytt prosjekt",
"createProject": "Create project",

View File

@ -396,8 +396,7 @@
"titleRequired": "Proszę podać tytuł.",
"delete": "Usuń ten widok",
"deleteText": "Czy na pewno chcesz usunąć ten widok? Nie będzie już możliwe wyświetlanie zadań w tym projekcie. Ta akcja nie usunie żadnych zadań. Tej operacji nie można cofnąć!",
"deleteSuccess": "Widok został pomyślnie usunięty",
"onlyAdminsCanEdit": "Tylko administratorzy projektu mogą edytować widoki."
"deleteSuccess": "Widok został pomyślnie usunięty"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Co 30 dni",
"mode": "Tryb powtarzania",
"monthly": "Miesięczny",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "Od bieżącej daty",
"each": "Co",
"specifyAmount": "Określ ilość…",
"hours": "Godziny",
@ -1060,7 +1059,7 @@
"duplicate": "Duplikuj",
"delete": "Usuń",
"unarchive": "Cofnij archiwizację",
"setBackground": "Background settings",
"setBackground": "Ustaw tło",
"share": "Udostępnij",
"newProject": "Nowy projekt",
"createProject": "Utwórz projekt",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "A cada 30 dias",
"mode": "Modo repetição",
"monthly": "Mensalmente",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "Data atual",
"each": "Cada",
"specifyAmount": "Especifique uma quantidade…",
"hours": "Horas",
@ -1060,7 +1059,7 @@
"duplicate": "Duplicar",
"delete": "Excluir",
"unarchive": "Desarquivar",
"setBackground": "Background settings",
"setBackground": "Definir plano de fundo",
"share": "Compartilhar",
"newProject": "Novo projeto",
"createProject": "Criar projeto",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "A cada 30 Dias",
"mode": "Modo de repetição",
"monthly": "Mensal",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "Da Data Atual",
"each": "Cada",
"specifyAmount": "Especifica uma quantidade…",
"hours": "Horas",
@ -1060,7 +1059,7 @@
"duplicate": "Duplicar",
"delete": "Eliminar",
"unarchive": "Desarquivar",
"setBackground": "Background settings",
"setBackground": "Definir Fundo",
"share": "Partilhar",
"newProject": "Novo projeto",
"createProject": "Criar projeto",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "From Current Date",
"each": "Each",
"specifyAmount": "Specify an amount…",
"hours": "Hours",
@ -1060,7 +1059,7 @@
"duplicate": "Duplicate",
"delete": "Delete",
"unarchive": "Un-Archive",
"setBackground": "Background settings",
"setBackground": "Set background",
"share": "Share",
"newProject": "New project",
"createProject": "Create project",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Каждые 30 дней",
"mode": "Режим повтора",
"monthly": "Ежемесячно",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "От сегодняшей даты",
"each": "Каждые",
"specifyAmount": "Укажите количество…",
"hours": "Часов",
@ -1060,7 +1059,7 @@
"duplicate": "Создать копию",
"delete": "Удалить",
"unarchive": "Вернуть из архива",
"setBackground": "Background settings",
"setBackground": "Задать фон",
"share": "Поделиться",
"newProject": "Создать проект",
"createProject": "Создать проект",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "From Current Date",
"each": "Each",
"specifyAmount": "Specify an amount…",
"hours": "Hours",
@ -1060,7 +1059,7 @@
"duplicate": "Duplicate",
"delete": "Delete",
"unarchive": "Un-Archive",
"setBackground": "Background settings",
"setBackground": "Set background",
"share": "Share",
"newProject": "New project",
"createProject": "Create project",

View File

@ -396,8 +396,7 @@
"titleRequired": "Prosim navedite naslov.",
"delete": "Izbriši pogled",
"deleteText": "Ali ste prepričani, da želite odstraniti ta pogled? Ne bo ga več mogoče uporabljati za ogled nalog v tem projektu. To dejanje ne bo izbrisalo nobenih opravil. Tega ni mogoče razveljaviti!",
"deleteSuccess": "Pogled je bil uspešno izbrisan",
"onlyAdminsCanEdit": "Samo skrbniki projekta lahko urejajo poglede."
"deleteSuccess": "Pogled je bil uspešno izbrisan"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Vsakih 30 dni",
"mode": "Način ponavljanja",
"monthly": "Mesečno",
"fromCurrentDate": "Od datuma dokončanja",
"fromCurrentDate": "Od trenutnega datuma",
"each": "Vsak",
"specifyAmount": "Določi znesek…",
"hours": "Ur",
@ -1060,7 +1059,7 @@
"duplicate": "Podvoji",
"delete": "Izbriši",
"unarchive": "Odstrani iz arhiva",
"setBackground": "Nastavitve ozadja",
"setBackground": "Nastavi ozadje",
"share": "Skupna raba",
"newProject": "Nov projekt",
"createProject": "Ustvari projekt",

View File

@ -53,7 +53,7 @@
"loginWith": "Log in with {provider}",
"authenticating": "Authenticating…",
"openIdStateError": "State does not match, refusing to continue!",
"openIdGeneralError": "An error occurred while authenticating against the third party.",
"openIdGeneralError": "An error occured while authenticating against the third party.",
"logout": "Logout",
"emailInvalid": "Please enter a valid email address.",
"usernameRequired": "Please provide a username.",
@ -227,8 +227,8 @@
"title": "Archive \"{project}\"",
"archive": "Archive this project",
"unarchive": "Un-Archive this project",
"unarchiveText": "You will be able to create tasks or edit it.",
"archiveText": "You won't be able to edit this project or create tasks until you un-archive it.",
"unarchiveText": "You will be able to create new tasks or edit it.",
"archiveText": "You won't be able to edit this project or create new tasks until you un-archive it.",
"success": "The project was successfully archived."
},
"background": {
@ -277,7 +277,7 @@
"title": "Share Links",
"what": "What is a share link?",
"explanation": "Share Links allow you to easily share a project with other users who don't have an account on Vikunja.",
"create": "Create a link share",
"create": "Create a new link share",
"name": "Name (optional)",
"namePlaceholder": "e.g. Lorem Ipsum",
"nameExplanation": "All actions done by this link share will show up with the name.",
@ -317,9 +317,9 @@
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a task…",
"addPlaceholder": "Add a new task…",
"empty": "This project is currently empty.",
"newTaskCta": "Create a task.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": {
@ -352,7 +352,7 @@
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
"addAnotherTask": "Add another task",
"addBucket": "Create a bucket",
"addBucket": "Create a new bucket",
"addBucketPlaceholder": "Enter the new bucket title…",
"deleteHeaderBucket": "Delete the bucket",
"deleteBucketText1": "Are you sure you want to delete this bucket?",
@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -423,7 +422,7 @@
"create": {
"title": "New Saved Filter",
"description": "A saved filter is a virtual project which is computed from a set of filters each time it is accessed.",
"action": "Create saved filter",
"action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter."
},
"delete": {
@ -507,7 +506,7 @@
"search": "Type to search for a label…",
"create": {
"header": "New label",
"title": "Create a label",
"title": "Create a new label",
"titleRequired": "Please specify a title.",
"success": "The label was successfully created."
},
@ -528,7 +527,7 @@
"sharing": {
"authenticating": "Authenticating…",
"passwordRequired": "This shared project requires a password. Please enter it below:",
"error": "An error occurred.",
"error": "An error occured.",
"invalidPassword": "The password is invalid."
},
"navigation": {
@ -642,7 +641,7 @@
"placeholder": "Type some text or hit '/' to see more options…"
},
"multiselect": {
"createPlaceholder": "Create",
"createPlaceholder": "Create new",
"selectPlaceholder": "Click or press enter to select"
},
"datepickerRange": {
@ -721,10 +720,10 @@
},
"task": {
"task": "Task",
"new": "Create a task",
"new": "Create a new task",
"delete": "Delete this task",
"createSuccess": "The task was successfully created.",
"addReminder": "Add a reminder…",
"addReminder": "Add a new reminder…",
"doneSuccess": "The task was successfully marked as done.",
"undoneSuccess": "The task was successfully un-marked as done.",
"undo": "Undo",
@ -860,7 +859,7 @@
"unassignSuccess": "The user has been unassigned successfully."
},
"label": {
"placeholder": "Type to add a label…",
"placeholder": "Type to add a new label…",
"createPlaceholder": "Add this as new label",
"addSuccess": "The label has been added successfully.",
"createSuccess": "The label has been created successfully.",
@ -883,8 +882,8 @@
"relation": {
"add": "Add a New Task Relation",
"new": "New Task Relation",
"searchPlaceholder": "Type search for a task to add as related…",
"createPlaceholder": "Add this as related task",
"searchPlaceholder": "Type search for a new task to add as related…",
"createPlaceholder": "Add this as new related task",
"differentProject": "This task belongs to a different project.",
"noneYet": "No task relations yet.",
"delete": "Delete Task Relation",
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "From Current Date",
"each": "Each",
"specifyAmount": "Specify an amount…",
"hours": "Hours",
@ -962,7 +961,7 @@
"title": "Teams",
"noTeams": "You are currently not part of any teams.",
"create": {
"title": "Create a team",
"title": "Create a new team",
"success": "The team was successfully created."
},
"edit": {
@ -1060,7 +1059,7 @@
"duplicate": "Duplicate",
"delete": "Delete",
"unarchive": "Un-Archive",
"setBackground": "Background settings",
"setBackground": "Set background",
"share": "Share",
"newProject": "New project",
"createProject": "Create project",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "From Current Date",
"each": "Each",
"specifyAmount": "Specify an amount…",
"hours": "Timmar",
@ -1060,7 +1059,7 @@
"duplicate": "Duplicera",
"delete": "Radera",
"unarchive": "Un-Archive",
"setBackground": "Background settings",
"setBackground": "Set background",
"share": "Dela",
"newProject": "Nytt projekt",
"createProject": "Skapa projekt",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "From Current Date",
"each": "Each",
"specifyAmount": "Specify an amount…",
"hours": "Hours",
@ -1060,7 +1059,7 @@
"duplicate": "Duplicate",
"delete": "Delete",
"unarchive": "Un-Archive",
"setBackground": "Background settings",
"setBackground": "Set background",
"share": "Share",
"newProject": "New project",
"createProject": "Create project",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Chế độ lặp lại",
"monthly": "Hàng tháng",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "Từ ngày hiện tại",
"each": "Mỗi",
"specifyAmount": "Chỉ định một số lượng…",
"hours": "Giờ",
@ -1060,7 +1059,7 @@
"duplicate": "Nhân bản",
"delete": "Xóa",
"unarchive": "Bỏ lưu trữ",
"setBackground": "Background settings",
"setBackground": "Cài hình nền",
"share": "Chia sẻ",
"newProject": "New project",
"createProject": "Create project",

View File

@ -396,8 +396,7 @@
"titleRequired": "请提供标题。",
"delete": "删除此视图",
"deleteText": "您确定要删除此视图吗?它将不再可能使用它来查看此项目中的任务。 此操作不会删除任何任务。此操作不能撤销!",
"deleteSuccess": "视图已成功删除",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "视图已成功删除"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "每 30 天",
"mode": "重复模式",
"monthly": "每月",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "从当前日期",
"each": "每个",
"specifyAmount": "指定数量…",
"hours": "小时",
@ -1060,7 +1059,7 @@
"duplicate": "复制",
"delete": "删除",
"unarchive": "取消存档",
"setBackground": "Background settings",
"setBackground": "设置背景",
"share": "共享",
"newProject": "新项目",
"createProject": "创建项目",

View File

@ -396,8 +396,7 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -922,7 +921,7 @@
"every30d": "Every 30 Days",
"mode": "Repeat mode",
"monthly": "Monthly",
"fromCurrentDate": "From completion date",
"fromCurrentDate": "From Current Date",
"each": "Each",
"specifyAmount": "Specify an amount…",
"hours": "Hours",
@ -1060,7 +1059,7 @@
"duplicate": "Duplicate",
"delete": "Delete",
"unarchive": "Un-Archive",
"setBackground": "Background settings",
"setBackground": "Set background",
"share": "Share",
"newProject": "New project",
"createProject": "Create project",

View File

@ -1,13 +1,8 @@
import type {IAbstract} from './IAbstract'
import type {IProject} from '@/modelTypes/IProject'
export const PROJECT_VIEW_KINDS = {
LIST: 'list',
GANTT: 'gantt',
TABLE: 'table',
KANBAN: 'kanban',
} as const
export type ProjectViewKind = typeof PROJECT_VIEW_KINDS[keyof typeof PROJECT_VIEW_KINDS]
export const PROJECT_VIEW_KINDS = ['list', 'gantt', 'table', 'kanban']
export type ProjectViewKind = typeof PROJECT_VIEW_KINDS[number]
export const PROJECT_VIEW_BUCKET_CONFIGURATION_MODES = ['none', 'manual', 'filter']
export type ProjectViewBucketConfigurationMode = typeof PROJECT_VIEW_BUCKET_CONFIGURATION_MODES[number]

View File

@ -22,6 +22,6 @@ export interface IUserSettings extends IAbstract {
defaultProjectId: undefined | IProject['id']
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
timezone: string
language: SupportedLocale | null
language: SupportedLocale
frontendSettings: IFrontendSettings
}

View File

@ -1,7 +1,7 @@
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
import {beforeEach, afterEach, describe, it, expect, vi} from 'vitest'
import {ParsedTaskText, parseTaskText, PrefixMode} from './parseTaskText'
import {parseDate} from '../helpers/time/parseDate'
import {parseTaskText, PrefixMode} from './parseTaskText'
import {getDateFromText, parseDate} from '../helpers/time/parseDate'
import {calculateDayInterval} from '../helpers/time/calculateDayInterval'
import {PRIORITIES} from '@/constants/priorities'
import {MILLISECONDS_A_DAY} from '@/constants/date'
@ -461,69 +461,50 @@ describe('Parse Task Text', () => {
'1/27': '2022-1-27',
'jan 27': '2022-1-27',
'Jan 27': '2022-1-27',
'january 27': '2022-1-27',
'January 27': '2022-1-27',
'feb 21': '2022-2-21',
'Feb 21': '2022-2-21',
'february 21': '2022-2-21',
'February 21': '2022-2-21',
'mar 21': '2022-3-21',
'Mar 21': '2022-3-21',
'march 21': '2022-3-21',
'March 21': '2022-3-21',
'apr 21': '2022-4-21',
'Apr 21': '2022-4-21',
'april 21': '2022-4-21',
'April 21': '2022-4-21',
'may 21': '2022-5-21',
'May 21': '2022-5-21',
'jun 21': '2022-6-21',
'Jun 21': '2022-6-21',
'june 21': '2022-6-21',
'June 21': '2022-6-21',
'21st June': '2021-6-21',
'jul 21': '2021-7-21',
'Jul 21': '2021-7-21',
'july 21': '2021-7-21',
'July 21': '2021-7-21',
'aug 21': '2021-8-21',
'Aug 21': '2021-8-21',
'august 21': '2021-8-21',
'August 21': '2021-8-21',
'sep 21': '2021-9-21',
'Sep 21': '2021-9-21',
'september 21': '2021-9-21',
'September 21': '2021-9-21',
'oct 21': '2021-10-21',
'Oct 21': '2021-10-21',
'october 21': '2021-10-21',
'October 21': '2021-10-21',
'nov 21': '2021-11-21',
'Nov 21': '2021-11-21',
'november 21': '2021-11-21',
'November 21': '2021-11-21',
'dec 21': '2021-12-21',
'Dec 21': '2021-12-21',
'december 21': '2021-12-21',
'December 21': '2021-12-21',
} as Record<string, string | null>
for (const c in cases) {
const assertResult = ({date, text}: ParsedTaskText) => {
it(`should parse '${c}' as '${cases[c]}' with the date at the end`, () => {
const {date, foundText} = getDateFromText(`Lorem Ipsum ${c}`, now)
if (date === null && cases[c] === null) {
expect(date).toBeNull()
return
}
expect(`${date?.getFullYear()}-${date?.getMonth() + 1}-${date?.getDate()}`).toBe(cases[c])
expect(text.trim()).toBe('Lorem Ipsum')
}
it(`should parse '${c}' as '${cases[c]}' with the date at the end`, () => {
assertResult(parseTaskText(`Lorem Ipsum ${c}`, PrefixMode.Default, now))
expect(foundText.trim()).toBe(c)
})
it(`should parse '${c}' as '${cases[c]}' with the date at the beginning`, () => {
assertResult(parseTaskText(`${c} Lorem Ipsum`, PrefixMode.Default, now))
const {date, foundText} = getDateFromText(`${c} Lorem Ipsum`, now)
if (date === null && cases[c] === null) {
expect(date).toBeNull()
return
}
expect(`${date?.getFullYear()}-${date?.getMonth() + 1}-${date?.getDate()}`).toBe(cases[c])
expect(foundText.trim()).toBe(c)
})
}
})

View File

@ -55,7 +55,7 @@ interface Prefixes {
*
* @param text
*/
export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMode.Default, now: Date = new Date()): ParsedTaskText => {
export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMode.Default): ParsedTaskText => {
const result: ParsedTaskText = {
text: text,
date: null,
@ -86,7 +86,7 @@ export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMod
result.text = textWithoutMatched
result.repeats = repeats
const {newText, date} = parseDate(result.text, now)
const {newText, date} = parseDate(result.text)
result.text = newText
result.date = date

View File

@ -52,16 +52,12 @@ export default class ProjectService extends AbstractService<IProject> {
return window.URL.createObjectURL(new Blob([response.data]))
}
async removeBackground(project: IProject) {
async removeBackground(project: Pick<IProject, 'id'>) {
const cancel = this.setLoading()
try {
await this.http.delete(`/projects/${project.id}/background`)
return {
...project,
backgroundInformation: null,
backgroundBlurHash: '',
}
const response = await this.http.delete(`/projects/${project.id}/background`, project)
return response.data
} finally {
cancel()
}

View File

@ -37,8 +37,6 @@ function redirectToProviderIfNothingElseIsEnabled() {
}
export const useAuthStore = defineStore('auth', () => {
const configStore = useConfigStore()
const authenticated = ref(false)
const isLinkShareAuth = ref(false)
const needsTotpPasscode = ref(false)
@ -187,7 +185,8 @@ export const useAuthStore = defineStore('auth', () => {
const HTTP = HTTPFactory()
setIsLoading(true)
const fullProvider: IProvider = configStore.auth.openidConnect.providers.find((p: IProvider) => p.key === provider)
const {auth} = useConfigStore()
const fullProvider: IProvider = auth.openidConnect.providers.find((p: IProvider) => p.key === provider)
const data = {
code: code,
@ -358,15 +357,8 @@ export const useAuthStore = defineStore('auth', () => {
const cancel = setModuleLoading(setIsLoadingGeneralSettings)
try {
let settingsUpdate = {...settings}
if (configStore.demoModeEnabled) {
settingsUpdate = {
...settingsUpdate,
language: null,
}
}
const updateSettingsPromise = userSettingsService.update(settingsUpdate)
setUserSettings(settingsUpdate)
const updateSettingsPromise = userSettingsService.update(settings)
setUserSettings({...settings})
await setLanguage(settings.language)
await updateSettingsPromise
if (showMessage) {
@ -411,12 +403,13 @@ export const useAuthStore = defineStore('auth', () => {
await checkAuth()
// if configured, redirect to OIDC Provider on logout
const {auth} = useConfigStore()
if (
configStore.auth.local.enabled === false &&
configStore.auth.openidConnect.enabled &&
configStore.auth.openidConnect.providers?.length === 1)
auth.local.enabled === false &&
auth.openidConnect.enabled &&
auth.openidConnect.providers?.length === 1)
{
redirectToProviderOnLogout(configStore.auth.openidConnect.providers[0])
redirectToProviderOnLogout(auth.openidConnect.providers[0])
}
}

View File

@ -1,5 +1,5 @@
import {computed, reactive, toRefs} from 'vue'
import {acceptHMRUpdate, defineStore} from 'pinia'
import {defineStore, acceptHMRUpdate} from 'pinia'
import {parseURL} from 'ufo'
import {HTTPFactory} from '@/helpers/fetcher'
@ -7,7 +7,6 @@ import {objectToCamelCase} from '@/helpers/case'
import type {IProvider} from '@/types/IProvider'
import type {MIGRATORS} from '@/views/migrate/migrators'
import {InvalidApiUrlProvidedError} from '@/helpers/checkAndSetApiUrl'
export interface ConfigState {
version: string,
@ -84,17 +83,15 @@ export const useConfigStore = defineStore('config', () => {
function setConfig(config: ConfigState) {
Object.assign(state, config)
}
async function update(): Promise<boolean> {
const HTTP = HTTPFactory()
const {data: config} = await HTTP.get('info')
if (typeof config.version === 'undefined') {
throw new InvalidApiUrlProvidedError()
return false
}
setConfig(objectToCamelCase(config))
return !!config
const success = !!config
return success
}
return {

View File

@ -160,6 +160,7 @@ export const useLabelStore = defineStore('label', () => {
deleteLabel,
updateLabel,
createLabel,
}
})

View File

@ -14,4 +14,33 @@
border-radius: $radius !important;
}
}
}
.link-share-container {
&.project\.gantt-view,
&.project\.kanban-view {
.container {
max-width: 100vw;
.column {
width: 100%;
margin: 0;
}
}
}
}
.link-share-container:not(.has-background) {
.list-view {
max-width: 100%;
}
.loader-container, .gantt-chart-container > .card {
box-shadow: none !important;
border: none;
.task-add {
padding: 1rem 0 0;
}
}
}

View File

@ -162,7 +162,7 @@ const configStore = useConfigStore()
const unsplashBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('unsplash'))
const uploadBackgroundEnabled = computed(() => configStore.enabledBackgroundProviders.includes('upload'))
const currentProject = computed(() => baseStore.currentProject)
const hasBackground = computed(() => !!currentProject.value.backgroundInformation)
const hasBackground = computed(() => baseStore.background !== null)
// Show the default collection of backgrounds
newBackgroundSearch()

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import CreateEdit from '@/components/misc/create-edit.vue'
import {watch, ref, computed} from 'vue'
import {computed, ref} from 'vue'
import {useProjectStore} from '@/stores/projects'
import ProjectViewModel from '@/models/projectView'
import type {IProjectView} from '@/modelTypes/IProjectView'
@ -9,10 +9,6 @@ import ProjectViewService from '@/services/projectViews'
import XButton from '@/components/input/button.vue'
import {error, success} from '@/message'
import {useI18n} from 'vue-i18n'
import ProjectService from '@/services/project'
import {RIGHTS} from '@/constants/rights'
import ProjectModel from '@/models/project'
import Message from '@/components/misc/message.vue'
const {
projectId,
@ -32,17 +28,6 @@ const viewIdToDelete = ref<number | null>(null)
const showDeleteModal = ref(false)
const viewToEdit = ref<IProjectView | null>(null)
const isAdmin = ref<boolean>(false)
watch(
() => projectId,
async () => {
const projectService = new ProjectService()
const project = await projectService.get(new ProjectModel({id: projectId}))
isAdmin.value = project.maxRight === RIGHTS.ADMIN
},
{immediate: true},
)
async function createView() {
if (!showCreateForm.value) {
showCreateForm.value = true
@ -98,17 +83,13 @@ async function saveView() {
<CreateEdit
:title="$t('project.views.header')"
:primary-label="$t('misc.save')"
:has-primary-action="false"
>
<ViewEditForm
v-if="showCreateForm"
v-model="newView"
class="mb-4"
/>
<div
v-if="isAdmin"
class="is-flex is-justify-content-end mb-4"
>
<div class="is-flex is-justify-content-end mb-4">
<XButton
:loading="projectViewService.loading"
@click="createView"
@ -116,10 +97,6 @@ async function saveView() {
{{ $t('project.views.create') }}
</XButton>
</div>
<Message v-if="!isAdmin">
{{ $t('project.views.onlyAdminsCanEdit') }}
</Message>
<table
v-if="views?.length > 0"
@ -167,7 +144,6 @@ async function saveView() {
<td>{{ v.viewKind }}</td>
<td class="has-text-right">
<XButton
v-if="isAdmin"
class="is-danger mr-2"
icon="trash-alt"
@click="() => {
@ -176,7 +152,6 @@ async function saveView() {
}"
/>
<XButton
v-if="isAdmin"
icon="pen"
@click="viewToEdit = {...v}"
/>

View File

@ -309,7 +309,6 @@
v-model="task.labels"
:disabled="!canWrite"
:task-id="taskId"
:creatable="!authStore.isLinkShareAuth"
/>
</div>
@ -654,7 +653,6 @@ const projectStore = useProjectStore()
const attachmentStore = useAttachmentStore()
const taskStore = useTaskStore()
const kanbanStore = useKanbanStore()
const authStore = useAuthStore()
const task = ref<ITask>(new TaskModel())
const taskTitle = computed(() => task.value.title)
@ -879,7 +877,7 @@ function toggleTaskDone() {
done: !task.value.done,
}
if (newTask.done && authStore.settings.frontendSettings.playSoundWhenDone) {
if (newTask.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
playPopSound()
}

18
go.mod
View File

@ -29,7 +29,7 @@ require (
github.com/cweill/gotests v1.6.0
github.com/d4l3k/messagediff v1.2.1
github.com/disintegration/imaging v1.6.2
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2
github.com/gabriel-vasile/mimetype v1.4.3
github.com/ganigeorgiev/fexpr v0.4.0
github.com/getsentry/sentry-go v0.27.0
@ -68,13 +68,13 @@ require (
github.com/ulule/limiter/v3 v3.11.2
github.com/wneessen/go-mail v0.4.0
github.com/yuin/goldmark v1.7.1
golang.org/x/crypto v0.23.0
golang.org/x/image v0.16.0
golang.org/x/oauth2 v0.20.0
golang.org/x/crypto v0.22.0
golang.org/x/image v0.15.0
golang.org/x/oauth2 v0.19.0
golang.org/x/sync v0.7.0
golang.org/x/sys v0.20.0
golang.org/x/term v0.20.0
golang.org/x/text v0.15.0
golang.org/x/sys v0.19.0
golang.org/x/term v0.19.0
golang.org/x/text v0.14.0
gopkg.in/d4l3k/messagediff.v1 v1.2.1
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/xurls/v2 v2.5.0
@ -120,6 +120,7 @@ require (
github.com/go-playground/validator/v10 v10.15.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@ -180,6 +181,7 @@ require (
golang.org/x/net v0.24.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
@ -190,4 +192,4 @@ replace github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20
go 1.21
toolchain go1.22.3
toolchain go1.21.2

25
go.sum
View File

@ -103,8 +103,6 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 h1:S6Dco8FtAhEI/qkg/00H6RdEGC+MCy5GPiQ+xweNRFE=
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc=
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b h1:+0Xqob+onh+4l9TSWmFyZ4JHqGUiCy5P1muyH8Evfpw=
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@ -191,6 +189,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
@ -318,6 +318,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c=
github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU=
github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
@ -501,6 +503,8 @@ github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpP
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
github.com/tkuchiki/go-timezone v0.2.2 h1:MdHR65KwgVTwWFQrota4SKzc4L5EfuH5SdZZGtk/P2Q=
github.com/tkuchiki/go-timezone v0.2.2/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY=
github.com/tkuchiki/go-timezone v0.2.3 h1:D3TVdIPrFsu9lxGxqNX2wsZwn1MZtTqTW0mdevMozHc=
github.com/tkuchiki/go-timezone v0.2.3/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
@ -574,15 +578,11 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
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=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
@ -612,10 +612,10 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -654,27 +654,22 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.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=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
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=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
@ -701,6 +696,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View File

@ -95,9 +95,6 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
if err != nil {
return nil, err
}
if len(parsed.Components) == 0 {
return nil, errors.New("VTODO element does seem not contain any components")
}
vTodo, ok := parsed.Components[0].(*ics.VTodo)
if !ok {
return nil, errors.New("VTODO element not found")

View File

@ -349,135 +349,82 @@ func TestBucket(t *testing.T) {
})
t.Run("Create", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "1",
"view": "3",
}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Nonexistent project", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "9999",
"view": "1",
}, `{"title":"Lorem Ipsum"}`)
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9999"}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeProjectViewDoesNotExist)
})
t.Run("Nonexistent view", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "1",
"view": "9999",
}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeProjectViewDoesNotExist)
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
// Owned by user13
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "20",
"view": "80",
}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "20"}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "6",
"view": "24",
}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "6"}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "7",
"view": "28",
}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "7"}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "8",
"view": "32",
}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "8"}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "9",
"view": "36",
}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9"}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "10",
"view": "40",
}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "10"}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "11",
"view": "44",
}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "11"}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "12",
"view": "48",
}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "12"}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "13",
"view": "52",
}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "13"}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "14",
"view": "56",
}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "14"}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "15",
"view": "60",
}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "15"}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "16",
"view": "64",
}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "16"}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "17",
"view": "68",
}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "17"}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})

View File

@ -99,7 +99,7 @@ insert into buckets_dg_tmp(id, title, "limit", position, created, updated, creat
select id, title, "limit", position, created, updated, created_by_id, project_view_id
from buckets;
drop index if exists UQE_buckets_id;
drop index if exists buckets.UQE_buckets_id;
drop table buckets;

View File

@ -23,13 +23,11 @@ import (
// CanCreate checks if a user can create a new bucket
func (b *Bucket) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
pv, err := GetProjectViewByIDAndProject(s, b.ProjectViewID, b.ProjectID)
if err != nil {
return false, err
pv := &ProjectView{
ID: b.ProjectViewID,
ProjectID: b.ProjectID,
}
p := &Project{ID: pv.ProjectID}
return p.CanUpdate(s, a)
return pv.CanUpdate(s, a)
}
// CanUpdate checks if a user can update an existing bucket
@ -48,11 +46,9 @@ func (b *Bucket) canDoBucket(s *xorm.Session, a web.Auth) (bool, error) {
if err != nil {
return false, err
}
pv, err := GetProjectViewByIDAndProject(s, bb.ProjectViewID, b.ProjectID)
if err != nil {
return false, err
pv := &ProjectView{
ID: bb.ProjectViewID,
ProjectID: b.ProjectID,
}
p := &Project{ID: pv.ProjectID}
return p.CanUpdate(s, a)
return pv.CanUpdate(s, a)
}

View File

@ -148,14 +148,23 @@ func (l *Label) Delete(s *xorm.Session, _ web.Auth) (err error) {
// @Failure 500 {object} models.Message "Internal error"
// @Router /labels [get]
func (l *Label) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (ls interface{}, resultCount int, numberOfEntries int64, err error) {
if _, is := a.(*LinkSharing); is {
return nil, 0, 0, ErrGenericForbidden{}
}
u, err := user.GetUserByID(s, a.GetID())
if err != nil {
return nil, 0, 0, err
}
return GetLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
Search: []string{search},
User: a,
User: u,
GetForUser: u.ID,
Page: page,
PerPage: perPage,
GetUnusedLabels: true,
GroupByLabelIDsOnly: true,
GetForUser: true,
})
}

View File

@ -17,6 +17,7 @@
package models
import (
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/builder"
"xorm.io/xorm"
@ -63,29 +64,27 @@ func (l *Label) isLabelOwner(s *xorm.Session, a web.Auth) (bool, error) {
// Helper method to check if a user can see a specific label
func (l *Label) hasAccessToLabel(s *xorm.Session, a web.Auth) (has bool, maxRight int, err error) {
linkShare, isLinkShare := a.(*LinkSharing)
if _, is := a.(*LinkSharing); is {
return false, 0, nil
}
var where builder.Cond
var createdByID int64
if isLinkShare {
where = builder.Eq{"project_id": linkShare.ProjectID}
} else {
where = builder.In("project_id", getUserProjectsStatement(a.GetID(), "", false).Select("l.id"))
createdByID = a.GetID()
u, err := user.GetUserByID(s, a.GetID())
if err != nil {
return false, 0, err
}
cond := builder.In("label_tasks.task_id",
builder.
Select("id").
From("tasks").
Where(where),
Where(builder.In("project_id", getUserProjectsStatement(u.ID, "", false).Select("l.id"))),
)
ll := &LabelTask{}
has, err = s.Table("labels").
Select("label_tasks.*").
Join("LEFT", "label_tasks", "label_tasks.label_id = labels.id").
Where("label_tasks.label_id is not null OR labels.created_by_id = ?", createdByID).
Where("label_tasks.label_id is not null OR labels.created_by_id = ?", u.ID).
Or(cond).
And("labels.id = ?", l.ID).
Exist(ll)

View File

@ -144,7 +144,7 @@ func (lt *LabelTask) ReadAll(s *xorm.Session, a web.Auth, search string, page in
}
return GetLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
User: a,
User: &user.User{ID: a.GetID()},
Search: []string{search},
Page: page,
TaskIDs: []int64{lt.TaskID},
@ -159,26 +159,23 @@ type LabelWithTaskID struct {
// LabelByTaskIDsOptions is a struct to not clutter the function with too many optional parameters.
type LabelByTaskIDsOptions struct {
User web.Auth
User *user.User
Search []string
Page int
PerPage int
TaskIDs []int64
GetUnusedLabels bool
GroupByLabelIDsOnly bool
GetForUser bool
GetForUser int64
}
// GetLabelsByTaskIDs is a helper function to get all labels for a set of tasks
// Used when getting all labels for one task as well when getting all lables
func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*LabelWithTaskID, resultCount int, totalEntries int64, err error) {
linkShare, isLinkShareAuth := opts.User.(*LinkSharing)
// We still need the task ID when we want to get all labels for a task, but because of this, we get the same label
// multiple times when it is associated to more than one task.
// Because of this whole thing, we need this extra switch here to only group by Task IDs if needed.
// Probably not the most ideataskdetaill solution.
// Probably not the most ideal solution.
var groupBy = "labels.id,label_tasks.task_id"
var selectStmt = "labels.*, label_tasks.task_id"
if opts.GroupByLabelIDsOnly {
@ -189,25 +186,20 @@ func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*Lab
// Get all labels associated with these tasks
var labels []*LabelWithTaskID
cond := builder.And(builder.NotNull{"label_tasks.label_id"})
if len(opts.TaskIDs) > 0 && !opts.GetForUser {
if len(opts.TaskIDs) > 0 && opts.GetForUser == 0 {
cond = builder.And(builder.In("label_tasks.task_id", opts.TaskIDs), cond)
}
if opts.GetForUser {
if opts.GetForUser != 0 {
var projectIDs []int64
if isLinkShareAuth {
projectIDs = []int64{linkShare.ProjectID}
} else {
projects, _, _, err := getRawProjectsForUser(s, &projectOptions{
user: &user.User{ID: opts.User.GetID()},
})
if err != nil {
return nil, 0, 0, err
}
projectIDs = make([]int64, 0, len(projects))
for _, project := range projects {
projectIDs = append(projectIDs, project.ID)
}
projects, _, _, err := getRawProjectsForUser(s, &projectOptions{
user: &user.User{ID: opts.GetForUser},
})
if err != nil {
return nil, 0, 0, err
}
projectIDs := make([]int64, 0, len(projects))
for _, project := range projects {
projectIDs = append(projectIDs, project.ID)
}
cond = builder.And(builder.In("label_tasks.task_id",
@ -217,8 +209,8 @@ func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*Lab
Where(builder.In("project_id", projectIDs)),
), cond)
}
if opts.GetUnusedLabels && !isLinkShareAuth {
cond = builder.Or(cond, builder.Eq{"labels.created_by_id": opts.User.GetID()})
if opts.GetUnusedLabels {
cond = builder.Or(cond, builder.Eq{"labels.created_by_id": opts.User.ID})
}
ids := []int64{}

View File

@ -62,7 +62,7 @@ func TestLabel_ReadAll(t *testing.T) {
name string
fields fields
args args
wantLs []*LabelWithTaskID
wantLs interface{}
wantErr bool
}{
{
@ -143,10 +143,7 @@ func TestLabel_ReadAll(t *testing.T) {
t.Errorf("Label.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
return
}
got := gotLs.([]*LabelWithTaskID)
if diff, equal := messagediff.PrettyDiff(got, tt.wantLs); !equal {
if diff, equal := messagediff.PrettyDiff(gotLs, tt.wantLs); !equal {
t.Errorf("Label.ReadAll() = %v, want %v, diff: %v", gotLs, tt.wantLs, diff)
}
s.Close()

View File

@ -28,17 +28,17 @@ func (p *ProjectView) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
func (p *ProjectView) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
pp := p.getProject()
return pp.IsAdmin(s, a)
return pp.CanUpdate(s, a)
}
func (p *ProjectView) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
pp := p.getProject()
return pp.IsAdmin(s, a)
return pp.CanUpdate(s, a)
}
func (p *ProjectView) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
pp := p.getProject()
return pp.IsAdmin(s, a)
return pp.CanUpdate(s, a)
}
func (p *ProjectView) getProject() (pp *Project) {

View File

@ -640,7 +640,6 @@ func checkBucketLimit(s *xorm.Session, t *Task, bucket *Bucket) (err error) {
if bucket.Limit > 0 {
taskCount, err := s.
Where("bucket_id = ?", bucket.ID).
GroupBy("task_id").
Count(&TaskBucket{})
if err != nil {
return err

View File

@ -38,10 +38,6 @@ func RemoveEmptySSOTeams(s *xorm.Session) (err error) {
return err
}
if len(teams) == 0 {
return nil
}
teamIDs := make([]int64, 0, len(teams))
for _, team := range teams {
teamIDs = append(teamIDs, team.ID)
@ -67,6 +63,6 @@ func RegisterEmptyOpenIDTeamCleanupCron() {
}
})
if err != nil {
log.Fatalf("Could not register empty openid teams cleanup cron: %s", err)
log.Fatalf("Could not empty openid teams cleanup cron: %s", err)
}
}

View File

@ -32,8 +32,6 @@ type Image struct {
Info interface{} `json:"info,omitempty"`
}
const MaxBackgroundImageHeight = 3840
// Provider represents something that is able to get a project of images and set one of them as background
type Provider interface {
// Search is used to either return a pre-defined project of Image or let the user search for an image

View File

@ -17,13 +17,10 @@
package handler
import (
"bytes"
_ "image/gif" // To make sure the decoder used for generating blurHashes recognizes gifs
_ "image/jpeg" // To make sure the decoder used for generating blurHashes recognizes jpgs
_ "image/png" // To make sure the decoder used for generating blurHashes recognizes pngs
"github.com/disintegration/imaging"
_ "golang.org/x/image/bmp" // To make sure the decoder used for generating blurHashes recognizes bmps
_ "golang.org/x/image/tiff" // To make sure the decoder used for generating blurHashes recognizes tiffs
_ "golang.org/x/image/webp" // To make sure the decoder used for generating blurHashes recognizes tiffs
@ -213,12 +210,6 @@ func (bp *BackgroundProvider) UploadBackground(c echo.Context) error {
return handler.HandleHTTPError(err, c)
}
err = project.ReadOne(s, auth)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
if err := s.Commit(); err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
@ -229,30 +220,7 @@ func (bp *BackgroundProvider) UploadBackground(c echo.Context) error {
func SaveBackgroundFile(s *xorm.Session, auth web.Auth, project *models.Project, srcf io.ReadSeeker, filename string, filesize uint64) (err error) {
_, _ = srcf.Seek(0, io.SeekStart)
src, err := imaging.Decode(srcf)
if err != nil {
return err
}
_, _ = srcf.Seek(0, io.SeekStart)
imgConfig, _, err := image.DecodeConfig(srcf)
if err != nil {
return err
}
height := imgConfig.Height
if imgConfig.Height > background.MaxBackgroundImageHeight {
height = background.MaxBackgroundImageHeight
}
buf := bytes.Buffer{}
dst := imaging.Resize(src, 0, height, imaging.Lanczos)
err = imaging.Encode(&buf, dst, imaging.JPEG, imaging.JPEGQuality(80))
if err != nil {
return err
}
f, err := files.Create(&buf, filename, filesize, auth)
f, err := files.Create(srcf, filename, filesize, auth)
if err != nil {
return err
}

View File

@ -256,7 +256,7 @@ func (p *Provider) Set(s *xorm.Session, image *background.Image, project *models
// Download the photo from unsplash
// The parameters crop the image to a max width of 2560 and a max height of 2048 to save bandwidth and storage.
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, photo.Urls.Raw+"&fm=jpg&h="+strconv.FormatInt(background.MaxBackgroundImageHeight, 10)+"&q=80", nil)
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, photo.Urls.Raw+"&w=2560&h=2048&q=90", nil)
if err != nil {
return
}

View File

@ -150,10 +150,36 @@ func Restore(filename string) error {
delete(dbfiles, "migration")
err = restoreTableData(dbfiles)
if err != nil {
return err
// Restore all db data
for table, d := range dbfiles {
content, err := unmarshalFileToJSON(d)
if err != nil {
return fmt.Errorf("could not read table %s: %w", table, err)
}
// FIXME: There has to be a general way to do this but this works for now.
if table == "notifications" {
for i := range content {
var decoded []byte
decoded, err = base64.StdEncoding.DecodeString(content[i]["notification"].(string))
if err != nil && !errors.Is(err, base64.CorruptInputError(0)) {
return fmt.Errorf("could not decode notification %s: %w", content[i]["notification"], err)
}
if err != nil && errors.Is(err, base64.CorruptInputError(0)) {
decoded = []byte(content[i]["notification"].(string))
}
content[i]["notification"] = string(decoded)
}
}
if err := db.Restore(table, content); err != nil {
return fmt.Errorf("could not restore table data for table %s: %w", table, err)
}
log.Infof("Restored table %s", table)
}
log.Infof("Restored %d tables", len(dbfiles))
// Run migrations again to migrate a potentially outdated dump
migration.Migrate(nil)
@ -190,53 +216,6 @@ func Restore(filename string) error {
return nil
}
func restoreTableData(tables map[string]*zip.File) error {
jsonFields := map[string][]string{
"notifications": {"notification"},
"users": {"frontend_settings"},
}
// Restore all db data
for table, d := range tables {
content, err := unmarshalFileToJSON(d)
if err != nil {
return fmt.Errorf("could not read table %s: %w", table, err)
}
fields, hasJSONFields := jsonFields[table]
if hasJSONFields {
for i := range content {
for _, f := range fields {
if _, hasField := content[i][f]; !hasField {
continue
}
var decoded []byte
decoded, err = base64.StdEncoding.DecodeString(content[i][f].(string))
if err != nil && !errors.Is(err, base64.CorruptInputError(0)) {
return fmt.Errorf("could not decode field '%s' %s: %w", f, content[i][f], err)
}
if err != nil && errors.Is(err, base64.CorruptInputError(0)) {
decoded = []byte(content[i][f].(string))
}
content[i][f] = string(decoded)
}
}
}
if err := db.Restore(table, content); err != nil {
return fmt.Errorf("could not restore table data for table %s: %w", table, err)
}
log.Infof("Restored table %s", table)
}
log.Infof("Restored %d tables", len(tables))
return nil
}
func unmarshalFileToJSON(file *zip.File) (contents []map[string]interface{}, err error) {
rc, err := file.Open()
if err != nil {

View File

@ -419,7 +419,7 @@ func persistLabels(s *xorm.Session, a web.Auth, task *models.Task, labels []*mod
existingLabels, _, _, err := models.GetLabelsByTaskIDs(s, &models.LabelByTaskIDsOptions{
Search: labelTitles,
User: u,
GetForUser: true,
GetForUser: u.ID,
GetUnusedLabels: true,
GroupByLabelIDsOnly: true,
})
@ -636,7 +636,7 @@ func (vcls *VikunjaCaldavProjectStorage) getProjectRessource(isCollection bool)
tk := models.TaskCollection{
ProjectID: vcls.project.ID,
}
iface, _, _, err := tk.ReadAll(s, vcls.user, "", 0, -1)
iface, _, _, err := tk.ReadAll(s, vcls.user, "", 1, 1000)
if err != nil {
_ = s.Rollback()
return rr, err