mirror of https://kolaente.dev/vikunja/api.git
Compare commits
130 Commits
5943d999f4
...
450cc7df77
Author | SHA1 | Date |
---|---|---|
renovate | 450cc7df77 | |
renovate | 9260b3f1d3 | |
renovate | ab74b08314 | |
renovate | 9637db5a6b | |
renovate | 08645d38a0 | |
renovate | f155d6bb60 | |
renovate | 9a3d63a713 | |
renovate | 050f4313c8 | |
renovate | e917323d91 | |
renovate | 8ad7d00559 | |
renovate | fd126fa234 | |
renovate | 0c39a3dd38 | |
kolaente | 66e96322ea | |
kolaente | 00a96663ba | |
kolaente | 741370b613 | |
renovate | 70183dd7c6 | |
renovate | 760bec5e76 | |
renovate | 78f03373b8 | |
renovate | 09c6d095df | |
renovate | b102fe8188 | |
renovate | f7c367b5bb | |
renovate | b94053e42e | |
renovate | 6e98a6d7ff | |
renovate | 42bfe107ae | |
Frederick [Bot] | a1892ea10b | |
renovate | 899f8f9bc1 | |
renovate | 40f0ca6670 | |
renovate | f8d35396dc | |
kolaente | 409822442b | |
kolaente | aec60f3591 | |
renovate | 9b5ae38784 | |
renovate | 3e40a43d56 | |
kolaente | 15e0c716ad | |
kolaente | 26ada628a2 | |
kolaente | d86fdcb756 | |
kolaente | 84197dd9c1 | |
kolaente | 324df991ce | |
kolaente | 1f6a1f8ad4 | |
kolaente | ea7527a3cf | |
kolaente | 574c7f218e | |
kolaente | 1074a8d916 | |
kolaente | e88f95e501 | |
kolaente | 0962aa4262 | |
renovate | a48ad6c9e1 | |
renovate | bc8fe05e9e | |
renovate | b4c12273af | |
renovate | be004793aa | |
renovate | a080400d3e | |
renovate | d4ec0978ee | |
renovate | c37e08635a | |
renovate | 31f448e50f | |
renovate | 00c323891a | |
renovate | ff06bb202b | |
renovate | e806cbaf22 | |
Frederick [Bot] | d35ff0b380 | |
renovate | 982884ee05 | |
renovate | e43d9e9bbd | |
renovate | da8cee0ba5 | |
renovate | 352381f377 | |
renovate | 61455b8795 | |
treysullivent | aceaccbf11 | |
kolaente | 392ce66edb | |
kolaente | ecbefdb921 | |
kolaente | d8ca1a2de1 | |
kolaente | 2d084c091e | |
kolaente | 5a84d37fca | |
kolaente | fd520dab0a | |
kolaente | 144a6e4140 | |
kolaente | a7aa74227a | |
kolaente | d2adbc53c6 | |
kolaente | 422e4371f8 | |
kolaente | 6e5b31f1e0 | |
kolaente | 5756da412b | |
kolaente | 4e05b8e97c | |
kolaente | 5177f516c4 | |
kolaente | 637c8f6ba5 | |
kolaente | 1460d212ee | |
kolaente | e9de7d8a24 | |
kolaente | ce1d7778c7 | |
kolaente | 9a16f6f817 | |
kolaente | 7d755fcb89 | |
kolaente | 77e95642a9 | |
kolaente | a5d02380a3 | |
kolaente | 3519b8b2fe | |
kolaente | cb648e5ad8 | |
kolaente | 75f830457b | |
kolaente | 6e2b540394 | |
kolaente | bf3c8ac9da | |
kolaente | 3e7225ebee | |
kolaente | 9eb19e0362 | |
kolaente | 73bf119409 | |
kolaente | 500b761fe6 | |
kolaente | 0bc9a670d7 | |
renovate | a3e5e98c64 | |
Elscrux | a3a4d05e89 | |
renovate | 72c3e1a03f | |
Elscrux | 61ee0bd5e2 | |
kolaente | 423558f58a | |
kolaente | 75fd17c750 | |
kolaente | 4e49ec9e16 | |
renovate | 58e0ec3d35 | |
kolaente | ed4be389ab | |
renovate | cb2c2eeae8 | |
renovate | e19ac57130 | |
kolaente | 0557d4b5bb | |
kolaente | bc19a2fb78 | |
kolaente | 994aaeb920 | |
kolaente | ee3d20e1d2 | |
Elscrux | 8458e77341 | |
kolaente | af3b0bbea1 | |
renovate | d2317b9531 | |
renovate | 552f8580a4 | |
renovate | c842cb27b2 | |
kolaente | e10cd368bf | |
renovate | 61322d2e2e | |
renovate | a41e248e5f | |
kolaente | 6e37934b61 | |
renovate | d64322bb7a | |
renovate | fa3b657e7e | |
Raymi306 | 1adaa73141 | |
renovate | 3e77e3043e | |
kolaente | d082c0399d | |
kolaente | 0b9ef27d04 | |
kolaente | 7acd1a7e51 | |
kolaente | 8bee5aa806 | |
kolaente | 6641cbebc2 | |
kolaente | 5892622676 | |
kolaente | 191a476823 | |
renovate | c146b72d64 | |
kolaente | ca33c0b2bc |
26
.drone.yml
26
.drone.yml
|
@ -139,7 +139,7 @@ steps:
|
|||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: api-lint
|
||||
image: golangci/golangci-lint:v1.56.2
|
||||
image: golangci/golangci-lint:v1.58.0
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
|
@ -364,7 +364,7 @@ steps:
|
|||
- api-build
|
||||
|
||||
- name: frontend-dependencies
|
||||
image: node:20.11.1-alpine
|
||||
image: node:20.12.2-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -378,7 +378,7 @@ steps:
|
|||
# - restore-cache
|
||||
|
||||
- name: frontend-lint
|
||||
image: node:20.11.1-alpine
|
||||
image: node:20.12.2-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -390,7 +390,7 @@ steps:
|
|||
- frontend-dependencies
|
||||
|
||||
- name: frontend-build-prod
|
||||
image: node:20.11.1-alpine
|
||||
image: node:20.12.2-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -402,7 +402,7 @@ steps:
|
|||
- frontend-dependencies
|
||||
|
||||
- name: frontend-test-unit
|
||||
image: node:20.11.1-alpine
|
||||
image: node:20.12.2-alpine
|
||||
pull: always
|
||||
commands:
|
||||
- cd frontend
|
||||
|
@ -413,7 +413,7 @@ steps:
|
|||
|
||||
- name: frontend-typecheck
|
||||
failure: ignore
|
||||
image: node:20.11.1-alpine
|
||||
image: node:20.12.2-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -544,7 +544,7 @@ steps:
|
|||
- git fetch --tags
|
||||
|
||||
- name: frontend-dependencies
|
||||
image: node:20.11.1-alpine
|
||||
image: node:20.12.2-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -556,7 +556,7 @@ steps:
|
|||
- pnpm install --fetch-timeout 100000
|
||||
|
||||
- name: frontend-build
|
||||
image: node:20.11.1-alpine
|
||||
image: node:20.12.2-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -716,7 +716,7 @@ steps:
|
|||
|
||||
# Build os packages and push it to our bucket
|
||||
- name: build-os-packages-unstable
|
||||
image: goreleaser/nfpm:v2.35.3
|
||||
image: goreleaser/nfpm:v2.36.1
|
||||
pull: always
|
||||
commands:
|
||||
- apk add git go
|
||||
|
@ -732,7 +732,7 @@ steps:
|
|||
depends_on: [ after-build-compress ]
|
||||
|
||||
- name: build-os-packages-version
|
||||
image: goreleaser/nfpm:v2.35.3
|
||||
image: goreleaser/nfpm:v2.36.1
|
||||
pull: always
|
||||
commands:
|
||||
- apk add git go
|
||||
|
@ -901,7 +901,7 @@ steps:
|
|||
- git fetch --tags
|
||||
|
||||
- name: build
|
||||
image: node:20.11.1-alpine
|
||||
image: node:20.12.2-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -962,7 +962,7 @@ steps:
|
|||
- git fetch --tags
|
||||
|
||||
- name: build
|
||||
image: node:20.11.1-alpine
|
||||
image: node:20.12.2-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
|
@ -1400,6 +1400,6 @@ steps:
|
|||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: c312afe632177a2d45f47c429bf6c7528af3c51a097430956558532ccdcc42b9
|
||||
hmac: f2753482faf9e2a3d34a9111587a75dfb4519cb77002cc64a51266540fd2478e
|
||||
|
||||
...
|
||||
|
|
|
@ -18,6 +18,7 @@ linters:
|
|||
- scopelint # Obsolete, using exportloopref instead
|
||||
- durationcheck
|
||||
- goconst
|
||||
- musttag
|
||||
presets:
|
||||
- bugs
|
||||
- unused
|
||||
|
@ -103,3 +104,7 @@ issues:
|
|||
text: "parameter 'tx' seems to be unused, consider removing or renaming it as"
|
||||
linters:
|
||||
- revive
|
||||
- path: pkg/models/typesense.go
|
||||
text: 'structtag: struct field Position repeats json tag "position" also at'
|
||||
linters:
|
||||
- govet
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"eslint.packageManager": "pnpm",
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
"eslint.format.enable": true,
|
||||
"[javascript]": {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
FROM --platform=$BUILDPLATFORM node:20.11.1-alpine AS frontendbuilder
|
||||
FROM --platform=$BUILDPLATFORM node:20.12.2-alpine AS frontendbuilder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ If you find any security-related issues you don't want to disclose publicly, ple
|
|||
|
||||
## Features
|
||||
|
||||
See [the features page](https://vikunja.io/features/) on our website for a more exaustive list or
|
||||
See [the features page](https://vikunja.io/features/) on our website for a more exhaustive list or
|
||||
try it on [try.vikunja.io](https://try.vikunja.io)!
|
||||
|
||||
## Docs
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
{ pkgs ? import <nixpkgs> {}
|
||||
}:
|
||||
pkgs.mkShell {
|
||||
name="electron-dev";
|
||||
buildInputs = [
|
||||
pkgs.electron
|
||||
];
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "29.2.0",
|
||||
"electron": "29.3.1",
|
||||
"electron-builder": "24.13.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
2028
desktop/yarn.lock
2028
desktop/yarn.lock
File diff suppressed because it is too large
Load Diff
|
@ -18,6 +18,7 @@ To fully build Vikunja from source files, you need to build the api and frontend
|
|||
|
||||
1. Make sure you have git installed
|
||||
2. Clone the repo with `git clone https://code.vikunja.io/vikunja` and switch into the directory.
|
||||
3. Check out the version you want to build with `git checkout VERSION` - replace `VERSION` with the version want to use. If you don't do this, you'll build the [latest unstable build]({{< ref "versions.md">}}), which might contain bugs.
|
||||
|
||||
## Frontend
|
||||
|
||||
|
@ -35,7 +36,7 @@ That means compiling it boils down to these steps:
|
|||
|
||||
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.21`.
|
||||
2. Make sure [Mage](https://magefile.org) is properly installed on your system.
|
||||
3. If you did not build the frontend in the steps before, you need to either do that or create a dummy index file with `mkdir -p frontend/dist && touch index.html`.
|
||||
3. If you did not build the frontend in the steps before, you need to either do that or create a dummy index file with `mkdir -p frontend/dist && touch frontend/dist/index.html`.
|
||||
4. Run `mage build` in the source of the main repo. This will build a binary in the root of the repo which will be able to run on your system.
|
||||
|
||||
### Build for different architectures
|
||||
|
|
|
@ -225,10 +225,13 @@ go install github.com/magefile/mage
|
|||
```
|
||||
mkdir /mnt/GO/code.vikunja.io
|
||||
cd /mnt/GO/code.vikunja.io
|
||||
git clone https://code.vikunja.io/api
|
||||
cd /mnt/GO/code.vikunja.io/api
|
||||
git clone https://code.vikunja.io/vikunja
|
||||
cd vikunja
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
```
|
||||
|
|
|
@ -103,7 +103,7 @@ auth:
|
|||
|
||||
## Automatically assign users to teams
|
||||
|
||||
Vikunja is capable of automatically adding users to a team based on OIDC claims added by the identity provider.
|
||||
Starting with version 0.24.0, Vikunja is capable of automatically adding users to a team based on OIDC claims added by the identity provider.
|
||||
If configured, Vikunja will sync teams, automatically create new ones and make sure the members are part of the configured teams.
|
||||
Teams which exist only because they were created from oidc attributes are not editable in Vikunja.
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ menu:
|
|||
|
||||
# Vikunja Versions
|
||||
|
||||
The Vikunja api and frontend are available in two different release flavors.
|
||||
Vikunja api is available in two different release flavors.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
|
@ -30,6 +30,9 @@ There might be multiple new such builds a day.
|
|||
Versions contain the last stable version, the number of commits since then and the commit the currently running binary was built from.
|
||||
They look like this: `v0.18.1+269-5cc4927b9e`
|
||||
|
||||
Since a release is also cut from the main branch at some point, features from unstable will eventually become available in stable releases.
|
||||
At the point in time of a new version release, the unstable build is the same exact thing.
|
||||
|
||||
The demo instance at [try.vikunja.io](https://try.vikunja.io) automatically updates and always runs the last unstable build.
|
||||
|
||||
## Switching between versions
|
||||
|
|
|
@ -28,13 +28,21 @@ All commands use the same standard [config file]({{< ref "../setup/config.md">}}
|
|||
|
||||
## Using the cli in docker
|
||||
|
||||
When running Vikunja in docker, you'll need to execute all commands in the `api` container.
|
||||
When running Vikunja in docker, you'll need to execute all commands in the `vikunja` container.
|
||||
Instead of running the `vikunja` binary directly, run it like this:
|
||||
|
||||
```sh
|
||||
docker exec <name of the vikunja api container> /app/vikunja/vikunja <subcommand>
|
||||
docker exec <name of the vikunja container> /app/vikunja/vikunja <subcommand>
|
||||
```
|
||||
|
||||
If you need to run a bunch of Vikunja commands, you can also create a shell alias for it:
|
||||
|
||||
```sh
|
||||
alias vikunja-docker='docker exec <name of the vikunja container> /app/vikunja/vikunja'
|
||||
```
|
||||
|
||||
Then use it as `vikunja-docker <subcommand>`.
|
||||
|
||||
### `dump`
|
||||
|
||||
Creates a zip file with all vikunja-related files.
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1701336116,
|
||||
"narHash": "sha256-kEmpezCR/FpITc6yMbAh4WrOCiT2zg5pSjnKrq51h5Y=",
|
||||
"lastModified": 1712449641,
|
||||
"narHash": "sha256-U9DDWMexN6o5Td2DznEgguh8TRIUnIl9levmit43GcI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f5c27c6136db4d76c30e533c20517df6864c46ee",
|
||||
"rev": "600b15aea1b36eeb43833a50b0e96579147099ff",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
nodePackages.pnpm cypress
|
||||
# API tools
|
||||
go golangci-lint mage
|
||||
# Desktop
|
||||
electron
|
||||
];
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1 +1 @@
|
|||
20.11.1
|
||||
20.12.2
|
|
@ -12,7 +12,7 @@ describe('Project History', () => {
|
|||
cy.intercept(Cypress.env('API_URL') + '/projects*').as('loadProjectArray')
|
||||
cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
|
||||
|
||||
const projects = ProjectFactory.create(6)
|
||||
const projects = ProjectFactory.create(7)
|
||||
ProjectViewFactory.truncate()
|
||||
projects.forEach(p => ProjectViewFactory.create(1, {
|
||||
id: p.id,
|
||||
|
@ -36,6 +36,8 @@ describe('Project History', () => {
|
|||
cy.wait('@loadProject')
|
||||
cy.visit(`/projects/${projects[5].id}/${projects[5].id}`)
|
||||
cy.wait('@loadProject')
|
||||
cy.visit(`/projects/${projects[6].id}/${projects[6].id}`)
|
||||
cy.wait('@loadProject')
|
||||
|
||||
// cy.visit('/')
|
||||
// Not using cy.visit here to work around the redirect issue fixed in #1337
|
||||
|
@ -52,5 +54,6 @@ describe('Project History', () => {
|
|||
.should('contain', projects[3].title)
|
||||
.should('contain', projects[4].title)
|
||||
.should('contain', projects[5].title)
|
||||
.should('contain', projects[6].title)
|
||||
})
|
||||
})
|
|
@ -13,6 +13,7 @@ import {BucketFactory} from '../../factories/bucket'
|
|||
import {TaskAttachmentFactory} from '../../factories/task_attachments'
|
||||
import {TaskReminderFactory} from '../../factories/task_reminders'
|
||||
import {createDefaultViews} from "../project/prepareProjects";
|
||||
import { TaskBucketFactory } from '../../factories/task_buckets'
|
||||
|
||||
function addLabelToTaskAndVerify(labelTitle: string) {
|
||||
cy.get('.task-view .action-buttons .button')
|
||||
|
@ -48,7 +49,7 @@ function uploadAttachmentAndVerify(taskId: number) {
|
|||
describe('Task', () => {
|
||||
createFakeUserAndLogin()
|
||||
|
||||
let projects
|
||||
let projects: {}[]
|
||||
let buckets
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -470,6 +471,10 @@ describe('Task', () => {
|
|||
})
|
||||
const labels = LabelFactory.create(1)
|
||||
LabelTaskFactory.truncate()
|
||||
TaskBucketFactory.create(1, {
|
||||
task_id: tasks[0].id,
|
||||
bucket_id: buckets[0].id,
|
||||
})
|
||||
|
||||
cy.visit(`/projects/${projects[0].id}/4`)
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@8.15.5",
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
|
@ -50,49 +50,49 @@
|
|||
"story:preview": "histoire preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.5.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.5.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.5.2",
|
||||
"@fortawesome/vue-fontawesome": "3.0.6",
|
||||
"@github/hotkey": "3.1.0",
|
||||
"@infectoone/vue-ganttastic": "2.3.1",
|
||||
"@intlify/unplugin-vue-i18n": "3.0.1",
|
||||
"@infectoone/vue-ganttastic": "2.3.2",
|
||||
"@intlify/unplugin-vue-i18n": "4.0.0",
|
||||
"@kyvg/vue3-notification": "3.2.1",
|
||||
"@sentry/tracing": "7.109.0",
|
||||
"@sentry/vue": "7.109.0",
|
||||
"@tiptap/core": "2.2.4",
|
||||
"@tiptap/extension-blockquote": "2.2.4",
|
||||
"@tiptap/extension-bold": "2.2.4",
|
||||
"@tiptap/extension-bullet-list": "2.2.4",
|
||||
"@tiptap/extension-code": "2.2.4",
|
||||
"@tiptap/extension-code-block-lowlight": "2.2.4",
|
||||
"@tiptap/extension-document": "2.2.4",
|
||||
"@tiptap/extension-dropcursor": "2.2.4",
|
||||
"@tiptap/extension-gapcursor": "2.2.4",
|
||||
"@tiptap/extension-hard-break": "2.2.4",
|
||||
"@tiptap/extension-heading": "2.2.4",
|
||||
"@tiptap/extension-history": "2.2.4",
|
||||
"@tiptap/extension-horizontal-rule": "2.2.4",
|
||||
"@tiptap/extension-image": "2.2.4",
|
||||
"@tiptap/extension-italic": "2.2.4",
|
||||
"@tiptap/extension-link": "2.2.4",
|
||||
"@tiptap/extension-list-item": "2.2.4",
|
||||
"@tiptap/extension-ordered-list": "2.2.4",
|
||||
"@tiptap/extension-paragraph": "2.2.4",
|
||||
"@tiptap/extension-placeholder": "2.2.4",
|
||||
"@tiptap/extension-strike": "2.2.4",
|
||||
"@tiptap/extension-table": "2.2.4",
|
||||
"@tiptap/extension-table-cell": "2.2.4",
|
||||
"@tiptap/extension-table-header": "2.2.4",
|
||||
"@tiptap/extension-table-row": "2.2.4",
|
||||
"@tiptap/extension-task-item": "2.2.4",
|
||||
"@tiptap/extension-task-list": "2.2.4",
|
||||
"@tiptap/extension-text": "2.2.4",
|
||||
"@tiptap/extension-typography": "2.2.4",
|
||||
"@tiptap/extension-underline": "2.2.4",
|
||||
"@tiptap/pm": "2.2.4",
|
||||
"@tiptap/suggestion": "2.2.4",
|
||||
"@tiptap/vue-3": "2.2.4",
|
||||
"@sentry/tracing": "7.113.0",
|
||||
"@sentry/vue": "7.113.0",
|
||||
"@tiptap/core": "2.3.1",
|
||||
"@tiptap/extension-blockquote": "2.3.1",
|
||||
"@tiptap/extension-bold": "2.3.1",
|
||||
"@tiptap/extension-bullet-list": "2.3.1",
|
||||
"@tiptap/extension-code": "2.3.1",
|
||||
"@tiptap/extension-code-block-lowlight": "2.3.1",
|
||||
"@tiptap/extension-document": "2.3.1",
|
||||
"@tiptap/extension-dropcursor": "2.3.1",
|
||||
"@tiptap/extension-gapcursor": "2.3.1",
|
||||
"@tiptap/extension-hard-break": "2.3.1",
|
||||
"@tiptap/extension-heading": "2.3.1",
|
||||
"@tiptap/extension-history": "2.3.1",
|
||||
"@tiptap/extension-horizontal-rule": "2.3.1",
|
||||
"@tiptap/extension-image": "2.3.1",
|
||||
"@tiptap/extension-italic": "2.3.1",
|
||||
"@tiptap/extension-link": "2.3.1",
|
||||
"@tiptap/extension-list-item": "2.3.1",
|
||||
"@tiptap/extension-ordered-list": "2.3.1",
|
||||
"@tiptap/extension-paragraph": "2.3.1",
|
||||
"@tiptap/extension-placeholder": "2.3.1",
|
||||
"@tiptap/extension-strike": "2.3.1",
|
||||
"@tiptap/extension-table": "2.3.1",
|
||||
"@tiptap/extension-table-cell": "2.3.1",
|
||||
"@tiptap/extension-table-header": "2.3.1",
|
||||
"@tiptap/extension-table-row": "2.3.1",
|
||||
"@tiptap/extension-task-item": "2.3.1",
|
||||
"@tiptap/extension-task-list": "2.3.1",
|
||||
"@tiptap/extension-text": "2.3.1",
|
||||
"@tiptap/extension-typography": "2.3.1",
|
||||
"@tiptap/extension-underline": "2.3.1",
|
||||
"@tiptap/pm": "2.3.1",
|
||||
"@tiptap/suggestion": "2.3.1",
|
||||
"@tiptap/vue-3": "2.3.1",
|
||||
"@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.10",
|
||||
"dompurify": "3.0.11",
|
||||
"dayjs": "1.11.11",
|
||||
"dompurify": "3.1.2",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.31",
|
||||
|
@ -118,13 +118,13 @@
|
|||
"sortablejs": "1.15.2",
|
||||
"tippy.js": "6.3.7",
|
||||
"ufo": "1.5.3",
|
||||
"vue": "3.4.21",
|
||||
"vue": "3.4.26",
|
||||
"vue-advanced-cropper": "2.8.8",
|
||||
"vue-flatpickr-component": "11.0.5",
|
||||
"vue-i18n": "9.10.2",
|
||||
"vue-router": "4.3.0",
|
||||
"vue-i18n": "9.13.1",
|
||||
"vue-router": "4.3.2",
|
||||
"vuemoji-picker": "0.2.1",
|
||||
"workbox-precaching": "7.0.0",
|
||||
"workbox-precaching": "7.1.0",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -132,9 +132,9 @@
|
|||
"@cypress/vite-dev-server": "5.0.7",
|
||||
"@cypress/vue": "6.0.0",
|
||||
"@faker-js/faker": "8.4.1",
|
||||
"@histoire/plugin-screenshot": "0.17.15",
|
||||
"@histoire/plugin-vue": "0.17.15",
|
||||
"@rushstack/eslint-patch": "1.10.1",
|
||||
"@histoire/plugin-screenshot": "0.17.17",
|
||||
"@histoire/plugin-vue": "0.17.17",
|
||||
"@rushstack/eslint-patch": "1.10.2",
|
||||
"@tsconfig/node18": "18.2.4",
|
||||
"@types/codemirror": "5.60.15",
|
||||
"@types/dompurify": "3.0.5",
|
||||
|
@ -142,11 +142,11 @@
|
|||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.debounce": "4.0.9",
|
||||
"@types/marked": "5.0.2",
|
||||
"@types/node": "20.12.5",
|
||||
"@types/node": "20.12.8",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "7.5.0",
|
||||
"@typescript-eslint/parser": "7.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.8.0",
|
||||
"@typescript-eslint/parser": "7.8.0",
|
||||
"@vitejs/plugin-legacy": "5.3.2",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vue/eslint-config-typescript": "13.0.0",
|
||||
|
@ -154,34 +154,34 @@
|
|||
"@vue/tsconfig": "0.5.1",
|
||||
"autoprefixer": "10.4.19",
|
||||
"browserslist": "4.23.0",
|
||||
"caniuse-lite": "1.0.30001606",
|
||||
"caniuse-lite": "1.0.30001615",
|
||||
"css-has-pseudo": "6.0.3",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "13.7.2",
|
||||
"cypress": "13.8.1",
|
||||
"esbuild": "0.20.2",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-vue": "9.24.0",
|
||||
"happy-dom": "14.6.1",
|
||||
"histoire": "0.17.15",
|
||||
"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.4",
|
||||
"rollup": "4.14.0",
|
||||
"postcss-preset-env": "9.5.9",
|
||||
"rollup": "4.17.2",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"sass": "1.74.1",
|
||||
"sass": "1.76.0",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"typescript": "5.4.4",
|
||||
"vite": "5.2.8",
|
||||
"typescript": "5.4.5",
|
||||
"vite": "5.2.11",
|
||||
"vite-plugin-inject-preload": "1.3.3",
|
||||
"vite-plugin-pwa": "0.19.8",
|
||||
"vite-plugin-pwa": "0.20.0",
|
||||
"vite-plugin-sentry": "1.4.0",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "1.4.0",
|
||||
"vue-tsc": "2.0.10",
|
||||
"vitest": "1.5.3",
|
||||
"vue-tsc": "2.0.16",
|
||||
"wait-on": "7.2.0",
|
||||
"workbox-cli": "7.0.0"
|
||||
"workbox-cli": "7.1.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
|
|
14610
frontend/pnpm-lock.yaml
14610
frontend/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 218 KiB |
|
@ -162,7 +162,7 @@ projectStore.loadAllProjects()
|
|||
.app-content {
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
padding: 1.5rem 0.5rem 1rem;
|
||||
padding: 1.5rem 0.5rem 0;
|
||||
// TODO refactor: DRY `transition-timing-function` with `./navigation.vue`.
|
||||
transition: margin-left $transition-duration;
|
||||
|
||||
|
@ -172,7 +172,7 @@ projectStore.loadAllProjects()
|
|||
}
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
padding: $navbar-height + 1.5rem 1.5rem 1rem 1.5rem;
|
||||
padding: $navbar-height + 1.5rem 1.5rem 0 1.5rem;
|
||||
}
|
||||
|
||||
&.is-menu-enabled {
|
||||
|
|
|
@ -34,6 +34,7 @@ import {useBaseStore} from '@/stores/base'
|
|||
import Logo from '@/components/home/Logo.vue'
|
||||
import PoweredByLink from './PoweredByLink.vue'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const currentProject = computed(() => baseStore.currentProject)
|
||||
|
@ -42,6 +43,9 @@ const logoVisible = computed(() => baseStore.logoVisible)
|
|||
|
||||
const projectStore = useProjectStore()
|
||||
projectStore.loadAllProjects()
|
||||
|
||||
const labelStore = useLabelStore()
|
||||
labelStore.loadAllLabels()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -155,8 +155,8 @@ const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
|
|||
bottom: 0;
|
||||
left: 0;
|
||||
transform: translateX(-100%);
|
||||
overflow-x: auto;
|
||||
width: $navbar-width;
|
||||
overflow-y: auto;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
top: 0;
|
||||
|
|
|
@ -338,10 +338,12 @@ const editor = useEditor({
|
|||
HardBreak.extend({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
'Shift-Enter': () => this.editor.commands.setHardBreak(),
|
||||
'Mod-Enter': () => {
|
||||
if (contentHasChanged.value) {
|
||||
bubbleSave()
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -816,7 +818,7 @@ watch(
|
|||
td,
|
||||
th {
|
||||
min-width: 1em;
|
||||
border: 2px solid #ced4da;
|
||||
border: 2px solid var(--grey-300) !important;
|
||||
padding: 3px 5px;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
|
@ -830,7 +832,7 @@ watch(
|
|||
th {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
background-color: #f1f3f5;
|
||||
background-color: var(--grey-200);
|
||||
}
|
||||
|
||||
.selectedCell:after {
|
||||
|
@ -891,8 +893,14 @@ ul[data-type='taskList'] {
|
|||
padding: 0;
|
||||
margin-left: 0;
|
||||
|
||||
li[data-checked='true'] {
|
||||
color: var(--grey-500);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
> label {
|
||||
flex: 0 0 auto;
|
||||
|
|
|
@ -88,7 +88,7 @@ const currentProject = computed<IProject>(() => {
|
|||
})
|
||||
useTitle(() => currentProject.value?.id ? getProjectTitle(currentProject.value) : '')
|
||||
|
||||
const views = computed(() => currentProject.value?.views)
|
||||
const views = computed(() => projectStore.projects[projectId]?.views)
|
||||
|
||||
// watchEffect would be called every time the prop would get a value assigned, even if that value was the same as before.
|
||||
// This resulted in loading and setting the project multiple times, even when navigating away from it.
|
||||
|
@ -161,6 +161,7 @@ function getViewTitle(view: IProjectView) {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
justify-content: center;
|
||||
|
|
|
@ -63,6 +63,10 @@ const filteredProjects = computed(() => {
|
|||
|
||||
@media screen and (min-width: $widescreen) {
|
||||
--project-grid-columns: 5;
|
||||
|
||||
.project-grid-item:nth-child(6) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,45 +30,27 @@ import {computed, ref, watch} from 'vue'
|
|||
|
||||
import Filters from '@/components/project/partials/filters.vue'
|
||||
|
||||
import {getDefaultTaskFilterParams, type TaskFilterParams} from '@/services/taskCollection'
|
||||
import {useRouteQuery} from '@vueuse/router'
|
||||
import {type TaskFilterParams} from '@/services/taskCollection'
|
||||
|
||||
const modelValue = defineModel<TaskFilterParams>({})
|
||||
const props = defineProps(['modelValue'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const value = ref<TaskFilterParams>({})
|
||||
const filter = useRouteQuery('filter')
|
||||
|
||||
watch(
|
||||
() => modelValue.value,
|
||||
() => props.modelValue,
|
||||
(modelValue: TaskFilterParams) => {
|
||||
value.value = modelValue
|
||||
if (value.value.filter !== '' && value.value.filter !== getDefaultTaskFilterParams().filter) {
|
||||
filter.value = value.value.filter
|
||||
}
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => filter.value,
|
||||
val => {
|
||||
if (modelValue.value?.filter === val || typeof val === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
modelValue.value.filter = val
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
function emitChanges(newValue: TaskFilterParams) {
|
||||
filter.value = newValue.filter
|
||||
if (modelValue.value?.filter === newValue.filter && modelValue.value?.s === newValue.s) {
|
||||
return
|
||||
}
|
||||
|
||||
modelValue.value.filter = newValue.filter
|
||||
modelValue.value.s = newValue.s
|
||||
emit('update:modelValue', {
|
||||
...value.value,
|
||||
filter: newValue.filter,
|
||||
s: newValue.s,
|
||||
})
|
||||
}
|
||||
|
||||
const hasFilters = computed(() => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
role="search"
|
||||
>
|
||||
<FilterInput
|
||||
v-model="params.filter"
|
||||
v-model="filterQuery"
|
||||
:project-id="projectId"
|
||||
@blur="change()"
|
||||
/>
|
||||
|
@ -28,7 +28,7 @@
|
|||
<x-button
|
||||
variant="secondary"
|
||||
class="mr-2"
|
||||
:disabled="params.filter === ''"
|
||||
:disabled="filterQuery === ''"
|
||||
@click.prevent.stop="clearFiltersAndEmit"
|
||||
>
|
||||
{{ $t('filters.clear') }}
|
||||
|
@ -87,6 +87,16 @@ const params = ref<TaskFilterParams>({
|
|||
s: '',
|
||||
})
|
||||
|
||||
const filterQuery = ref('')
|
||||
watch(
|
||||
() => [params.value.filter, params.value.s],
|
||||
() => {
|
||||
const filter = params.value.filter || ''
|
||||
const s = params.value.s || ''
|
||||
filterQuery.value = filter || s
|
||||
},
|
||||
)
|
||||
|
||||
// Using watchDebounced to prevent the filter re-triggering itself.
|
||||
watch(
|
||||
() => modelValue,
|
||||
|
@ -107,7 +117,7 @@ const projectStore = useProjectStore()
|
|||
|
||||
function change() {
|
||||
const filter = transformFilterStringForApi(
|
||||
params.value.filter,
|
||||
filterQuery.value,
|
||||
labelTitle => labelStore.filterLabelsByQuery([], labelTitle)[0]?.id || null,
|
||||
projectTitle => {
|
||||
const found = projectStore.findProjectByExactname(projectTitle)
|
||||
|
@ -142,7 +152,7 @@ function changeAndEmitButton() {
|
|||
}
|
||||
|
||||
function clearFiltersAndEmit() {
|
||||
params.value.filter = ''
|
||||
filterQuery.value = ''
|
||||
changeAndEmitButton()
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -773,11 +773,6 @@ $crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 1rem - 1.5rem - 11px';
|
|||
$crazy-height-calculation-tasks: '#{$crazy-height-calculation} - 1rem - 2.5rem - 2rem - #{$button-height} - 1rem';
|
||||
$filter-container-height: '1rem - #{$switch-view-height}';
|
||||
|
||||
// FIXME:
|
||||
.app-content.project\.kanban, .app-content.task\.detail {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.kanban {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
@ -785,6 +780,10 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
|||
margin: 0 -1.5rem;
|
||||
padding: 0 1.5rem;
|
||||
|
||||
&:focus, .bucket .tasks:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
height: calc(#{$crazy-height-calculation} - #{$filter-container-height});
|
||||
scroll-snap-type: x mandatory;
|
||||
|
|
|
@ -267,7 +267,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, type Ref} from 'vue'
|
||||
import {computed, type Ref, watch} from 'vue'
|
||||
|
||||
import {useStorage} from '@vueuse/core'
|
||||
|
||||
|
@ -337,6 +337,12 @@ 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]
|
||||
|
@ -347,7 +353,16 @@ function sort(property: keyof SortBy) {
|
|||
} else {
|
||||
delete sortBy.value[property]
|
||||
}
|
||||
sortByParam.value = sortBy.value
|
||||
setActiveColumnsSortParam()
|
||||
}
|
||||
|
||||
function setActiveColumnsSortParam() {
|
||||
sortByParam.value = Object.keys(sortBy.value)
|
||||
.filter(prop => activeColumns.value[prop])
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = sortBy.value[key]
|
||||
return obj
|
||||
}, {})
|
||||
}
|
||||
|
||||
// TODO: re-enable opening task detail in modal
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {ref, shallowReactive, watch, computed, type ComputedGetter} from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {useRouteQuery} from '@vueuse/router'
|
||||
|
||||
import TaskCollectionService, {getDefaultTaskFilterParams, type TaskFilterParams} from '@/services/taskCollection'
|
||||
|
@ -66,7 +66,6 @@ export function useTaskList(
|
|||
|
||||
const params = ref<TaskFilterParams>({...getDefaultTaskFilterParams()})
|
||||
|
||||
const search = ref('')
|
||||
const page = useRouteQuery('page', '1', { transform: Number })
|
||||
|
||||
const sortBy = ref({ ...sortByDefault })
|
||||
|
@ -74,10 +73,6 @@ export function useTaskList(
|
|||
const allParams = computed(() => {
|
||||
const loadParams = {...params.value}
|
||||
|
||||
if (search.value !== '') {
|
||||
loadParams.s = search.value
|
||||
}
|
||||
|
||||
return formatSortOrder(sortBy.value, loadParams)
|
||||
})
|
||||
|
||||
|
@ -122,16 +117,38 @@ export function useTaskList(
|
|||
|
||||
const route = useRoute()
|
||||
watch(() => route.query, (query) => {
|
||||
const { page: pageQueryValue, search: searchQuery } = query
|
||||
if (searchQuery !== undefined) {
|
||||
search.value = searchQuery as string
|
||||
const {
|
||||
page: pageQueryValue,
|
||||
s,
|
||||
filter,
|
||||
} = query
|
||||
if (s !== undefined) {
|
||||
params.value.s = s as string
|
||||
}
|
||||
if (pageQueryValue !== undefined) {
|
||||
page.value = Number(pageQueryValue)
|
||||
}
|
||||
|
||||
if (filter !== undefined) {
|
||||
params.value.filter = filter
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const router = useRouter()
|
||||
watch(
|
||||
() => [page.value, params.value.filter, params.value.s],
|
||||
() => {
|
||||
router.replace({
|
||||
name: route.name,
|
||||
params: route.params,
|
||||
query: {
|
||||
page: page.value,
|
||||
filter: params.value.filter || undefined,
|
||||
s: params.value.s || undefined,
|
||||
},
|
||||
})
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// Only listen for query path changes
|
||||
watch(() => JSON.stringify(getAllTasksParams.value), (newParams, oldParams) => {
|
||||
|
@ -148,7 +165,6 @@ export function useTaskList(
|
|||
totalPages,
|
||||
currentPage: page,
|
||||
loadTasks,
|
||||
searchTerm: search,
|
||||
params,
|
||||
sortByParam: sortBy,
|
||||
}
|
||||
|
|
|
@ -19,9 +19,16 @@ export function secondsToPeriod(seconds: number): { unit: PeriodUnit, amount: nu
|
|||
}
|
||||
}
|
||||
|
||||
if (seconds % SECONDS_A_HOUR === 0) {
|
||||
return {
|
||||
unit: 'hours',
|
||||
amount: seconds / SECONDS_A_HOUR,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
unit: 'hours',
|
||||
amount: seconds / SECONDS_A_HOUR,
|
||||
unit: 'minutes',
|
||||
amount: seconds / SECONDS_A_MINUTE,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Nur Projektadministrator:innen können Ansichten bearbeiten."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Nur Projektadministrator:innen können Ansichten bearbeiten."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "A nézet sikeresen törölve",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "Widok został pomyślnie usunięty",
|
||||
"onlyAdminsCanEdit": "Tylko administratorzy projektu mogą edytować widoki."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "Pogled je bil uspešno izbrisan",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -385,7 +385,7 @@
|
|||
"views": {
|
||||
"header": "Edit views",
|
||||
"title": "Title",
|
||||
"actions": "Actions",
|
||||
"actions": "Åtgärder",
|
||||
"kind": "Kind",
|
||||
"bucketConfigMode": "Bucket configuration mode",
|
||||
"bucketConfig": "Bucket configuration",
|
||||
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
@ -455,7 +456,7 @@
|
|||
},
|
||||
"operators": {
|
||||
"intro": "The available operators for filtering include:",
|
||||
"notEqual": "Not equal to",
|
||||
"notEqual": "Inte lika med",
|
||||
"equal": "Lika med",
|
||||
"greaterThan": "Större än",
|
||||
"greaterThanOrEqual": "Större än eller lika med",
|
||||
|
@ -777,7 +778,7 @@
|
|||
"moveProject": "Flytta",
|
||||
"color": "Set Color",
|
||||
"delete": "Radera",
|
||||
"favorite": "Add to Favorites",
|
||||
"favorite": "Lägg till i favoriter",
|
||||
"unfavorite": "Remove from Favorites"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"titleRequired": "请提供标题。",
|
||||
"delete": "删除此视图",
|
||||
"deleteText": "您确定要删除此视图吗?它将不再可能使用它来查看此项目中的任务。 此操作不会删除任何任务。此操作不能撤销!",
|
||||
"deleteSuccess": "视图已成功删除"
|
||||
"deleteSuccess": "视图已成功删除",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"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"
|
||||
"deleteSuccess": "The view was successfully deleted",
|
||||
"onlyAdminsCanEdit": "Only project admins can edit views."
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
|
|
|
@ -26,7 +26,7 @@ test('store project in history', () => {
|
|||
expect(saved).toBe('[{"id":1}]')
|
||||
})
|
||||
|
||||
test('store only the last 5 projects in history', () => {
|
||||
test('store only the last 6 projects in history', () => {
|
||||
let saved: string | null = null
|
||||
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
|
||||
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
|
||||
|
@ -39,7 +39,8 @@ test('store only the last 5 projects in history', () => {
|
|||
saveProjectToHistory({id: 4})
|
||||
saveProjectToHistory({id: 5})
|
||||
saveProjectToHistory({id: 6})
|
||||
expect(saved).toBe('[{"id":6},{"id":5},{"id":4},{"id":3},{"id":2}]')
|
||||
saveProjectToHistory({id: 7})
|
||||
expect(saved).toBe('[{"id":7},{"id":6},{"id":5},{"id":4},{"id":3},{"id":2}]')
|
||||
})
|
||||
|
||||
test('don\'t store the same project twice', () => {
|
||||
|
|
|
@ -20,6 +20,8 @@ function saveHistory(history: ProjectHistory[]) {
|
|||
localStorage.setItem('projectHistory', JSON.stringify(history))
|
||||
}
|
||||
|
||||
const MAX_SAVED_PROJECTS = 6
|
||||
|
||||
export function saveProjectToHistory(project: ProjectHistory) {
|
||||
const history: ProjectHistory[] = getHistory()
|
||||
|
||||
|
@ -33,7 +35,7 @@ export function saveProjectToHistory(project: ProjectHistory) {
|
|||
// Add the new project to the beginning of the project
|
||||
history.unshift(project)
|
||||
|
||||
if (history.length > 5) {
|
||||
if (history.length > MAX_SAVED_PROJECTS) {
|
||||
history.pop()
|
||||
}
|
||||
saveHistory(history)
|
||||
|
|
|
@ -2,7 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||
import type { RouteLocation } from 'vue-router'
|
||||
import {saveLastVisited} from '@/helpers/saveLastVisited'
|
||||
|
||||
import {saveProjectView, getProjectViewId} from '@/helpers/projectView'
|
||||
import {getProjectViewId} from '@/helpers/projectView'
|
||||
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
|
||||
import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash'
|
||||
|
@ -366,7 +366,6 @@ const router = createRouter({
|
|||
path: '/projects/:projectId/:viewId',
|
||||
name: 'project.view',
|
||||
component: ProjectView,
|
||||
beforeEnter: (to) => saveProjectView(parseInt(to.params.projectId as string), parseInt(to.params.viewId as string)),
|
||||
props: route => ({
|
||||
projectId: parseInt(route.params.projectId as string),
|
||||
viewId: route.params.viewId ? parseInt(route.params.viewId as string): undefined,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {computed, reactive, toRefs} from 'vue'
|
||||
import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||
import {acceptHMRUpdate, defineStore} from 'pinia'
|
||||
import {parseURL} from 'ufo'
|
||||
|
||||
import {HTTPFactory} from '@/helpers/fetcher'
|
||||
|
@ -7,6 +7,7 @@ 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,
|
||||
|
@ -83,15 +84,17 @@ 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') {
|
||||
return false
|
||||
throw new InvalidApiUrlProvidedError()
|
||||
}
|
||||
|
||||
setConfig(objectToCamelCase(config))
|
||||
const success = !!config
|
||||
return success
|
||||
return !!config
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -160,7 +160,6 @@ export const useLabelStore = defineStore('label', () => {
|
|||
deleteLabel,
|
||||
updateLabel,
|
||||
createLabel,
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -106,6 +106,12 @@ export const useProjectStore = defineStore('project', () => {
|
|||
}
|
||||
|
||||
function removeProjectById(project: IProject) {
|
||||
|
||||
// Remove child projects from state as well
|
||||
projectsArray.value
|
||||
.filter(p => p.parentProjectId === project.id)
|
||||
.forEach(p => removeProjectById(p))
|
||||
|
||||
remove(project)
|
||||
delete projects.value[project.id]
|
||||
}
|
||||
|
@ -201,13 +207,17 @@ export const useProjectStore = defineStore('project', () => {
|
|||
}
|
||||
|
||||
function getAncestors(project: IProject): IProject[] {
|
||||
if (typeof project === 'undefined') {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!project?.parentProjectId) {
|
||||
return [project]
|
||||
}
|
||||
|
||||
const parentProject = projects.value[project.parentProjectId]
|
||||
return [
|
||||
...getAncestors(parentProject),
|
||||
...(parentProject ? getAncestors(parentProject) : []),
|
||||
project,
|
||||
]
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<h1>{{ $t('migrate.titleService', {name: migrator.name}) }}</h1>
|
||||
<p>{{ $t('migrate.descriptionDo') }}</p>
|
||||
|
||||
<template v-if="message === '' && lastMigrationStartedAt === null">
|
||||
<template v-if="message === '' && lastMigrationStartedAt === null && !migrationJustStarted">
|
||||
<template v-if="isMigrating === false">
|
||||
<template v-if="migrator.isFileMigrator">
|
||||
<p>{{ $t('migrate.importUpload', {name: migrator.name}) }}</p>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import {computed, watch} from 'vue'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {saveProjectView} from '@/helpers/projectView'
|
||||
|
||||
import ProjectList from '@/components/project/views/ProjectList.vue'
|
||||
import ProjectGantt from '@/components/project/views/ProjectGantt.vue'
|
||||
|
@ -53,6 +54,13 @@ watch(
|
|||
redirectToFirstViewIfNecessary,
|
||||
)
|
||||
|
||||
// using a watcher instead of beforeEnter because beforeEnter is not called when only the viewId changes
|
||||
watch(
|
||||
() => [projectId, viewId],
|
||||
() => saveProjectView(projectId, viewId),
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const route = useRoute()
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import CreateEdit from '@/components/misc/create-edit.vue'
|
||||
import {computed, ref} from 'vue'
|
||||
import {watch, ref, computed} from 'vue'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import ProjectViewModel from '@/models/projectView'
|
||||
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||
|
@ -9,6 +9,10 @@ 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,
|
||||
|
@ -28,6 +32,17 @@ 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
|
||||
|
@ -83,13 +98,17 @@ 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 class="is-flex is-justify-content-end mb-4">
|
||||
<div
|
||||
v-if="isAdmin"
|
||||
class="is-flex is-justify-content-end mb-4"
|
||||
>
|
||||
<XButton
|
||||
:loading="projectViewService.loading"
|
||||
@click="createView"
|
||||
|
@ -97,6 +116,10 @@ async function saveView() {
|
|||
{{ $t('project.views.create') }}
|
||||
</XButton>
|
||||
</div>
|
||||
|
||||
<Message v-if="!isAdmin">
|
||||
{{ $t('project.views.onlyAdminsCanEdit') }}
|
||||
</Message>
|
||||
|
||||
<table
|
||||
v-if="views?.length > 0"
|
||||
|
@ -144,6 +167,7 @@ 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="() => {
|
||||
|
@ -152,6 +176,7 @@ async function saveView() {
|
|||
}"
|
||||
/>
|
||||
<XButton
|
||||
v-if="isAdmin"
|
||||
icon="pen"
|
||||
@click="viewToEdit = {...v}"
|
||||
/>
|
||||
|
|
|
@ -309,6 +309,7 @@
|
|||
v-model="task.labels"
|
||||
:disabled="!canWrite"
|
||||
:task-id="taskId"
|
||||
:creatable="!authStore.isLinkShareAuth"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -653,6 +654,7 @@ 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)
|
||||
|
@ -877,7 +879,7 @@ function toggleTaskDone() {
|
|||
done: !task.value.done,
|
||||
}
|
||||
|
||||
if (newTask.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
|
||||
if (newTask.done && authStore.settings.frontendSettings.playSoundWhenDone) {
|
||||
playPopSound()
|
||||
}
|
||||
|
||||
|
|
|
@ -17,11 +17,11 @@ import postcssEasings from 'postcss-easings'
|
|||
import postcssEasingGradients from 'postcss-easing-gradients'
|
||||
|
||||
|
||||
const pathSrc = fileURLToPath(new URL('./src', import.meta.url))
|
||||
const pathSrc = fileURLToPath(new URL('./src', import.meta.url)).replaceAll('\\', '/')
|
||||
|
||||
// the @use rules have to be the first in the compiled stylesheets
|
||||
const PREFIXED_SCSS_STYLES = `@use "sass:math";
|
||||
@import "${pathSrc}/styles/common-imports";`
|
||||
@import "${pathSrc}/styles/common-imports.scss";`
|
||||
|
||||
const isModernBuild = Boolean(process.env.BUILD_MODERN_ONLY)
|
||||
const legacy = isModernBuild
|
||||
|
|
30
go.mod
30
go.mod
|
@ -20,8 +20,8 @@ require (
|
|||
code.vikunja.io/web v0.0.0-20210706160506-d85def955bd3
|
||||
dario.cat/mergo v1.0.0
|
||||
github.com/ThreeDotsLabs/watermill v1.3.5
|
||||
github.com/adlio/trello v1.11.0
|
||||
github.com/arran4/golang-ical v0.2.7
|
||||
github.com/adlio/trello v1.12.0
|
||||
github.com/arran4/golang-ical v0.2.8
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||
github.com/bbrks/go-blurhash v1.1.1
|
||||
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
|
||||
|
@ -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-20231002161417-6a283f1aaaf2
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b
|
||||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/ganigeorgiev/fexpr v0.4.0
|
||||
github.com/getsentry/sentry-go v0.27.0
|
||||
|
@ -45,11 +45,12 @@ require (
|
|||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6
|
||||
github.com/labstack/echo-jwt/v4 v4.2.0
|
||||
github.com/labstack/echo/v4 v4.11.4
|
||||
github.com/labstack/echo/v4 v4.12.0
|
||||
github.com/labstack/gommon v0.4.2
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/magefile/mage v1.15.0
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pquerna/otp v1.4.0
|
||||
|
@ -62,17 +63,17 @@ require (
|
|||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/swaggo/swag v1.16.3
|
||||
github.com/tkuchiki/go-timezone v0.2.2
|
||||
github.com/tkuchiki/go-timezone v0.2.3
|
||||
github.com/typesense/typesense-go v1.0.0
|
||||
github.com/ulule/limiter/v3 v3.11.2
|
||||
github.com/wneessen/go-mail v0.4.0
|
||||
github.com/yuin/goldmark v1.7.0
|
||||
golang.org/x/crypto v0.21.0
|
||||
github.com/yuin/goldmark v1.7.1
|
||||
golang.org/x/crypto v0.22.0
|
||||
golang.org/x/image v0.15.0
|
||||
golang.org/x/oauth2 v0.18.0
|
||||
golang.org/x/sync v0.6.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/term v0.18.0
|
||||
golang.org/x/oauth2 v0.19.0
|
||||
golang.org/x/sync v0.7.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
|
||||
|
@ -90,6 +91,7 @@ require (
|
|||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
|
@ -108,7 +110,6 @@ require (
|
|||
github.com/go-chi/chi/v5 v5.0.10 // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.6.1 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.20.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.3 // indirect
|
||||
|
@ -119,8 +120,8 @@ 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
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
|
@ -176,10 +177,9 @@ require (
|
|||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
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
|
||||
|
|
77
go.sum
77
go.sum
|
@ -24,17 +24,19 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
|
|||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/ThreeDotsLabs/watermill v1.3.5 h1:50JEPEhMGZQMh08ct0tfO1PsgMOAOhV3zxK2WofkbXg=
|
||||
github.com/ThreeDotsLabs/watermill v1.3.5/go.mod h1:O/u/Ptyrk5MPTxSeWM5vzTtZcZfxXfO9PK9eXTYiFZY=
|
||||
github.com/adlio/trello v1.11.0 h1:PGpwpRZcRiVhsG7VEHb2GWKw4R2ZxB9nc6cMI/7mLD8=
|
||||
github.com/adlio/trello v1.11.0/go.mod h1:I4Lti4jf2KxjTNgTqs5W3lLuE78QZZdYbbPnQQGwjOo=
|
||||
github.com/adlio/trello v1.12.0 h1:JqOE2GFHQ9YtEviRRRSnicSxPbt4WFOxhqXzjMOw8lw=
|
||||
github.com/adlio/trello v1.12.0/go.mod h1:I4Lti4jf2KxjTNgTqs5W3lLuE78QZZdYbbPnQQGwjOo=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/arran4/golang-ical v0.2.7 h1:VO7YlVaGupZE15aj6NhUhte/MIfZuoIzkoI71VsG6Gg=
|
||||
github.com/arran4/golang-ical v0.2.7/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||
github.com/arran4/golang-ical v0.2.8 h1:8lsFcfQqzg0gBpIxq7fWr4RV+8SVENLMXpSic5xsFUs=
|
||||
github.com/arran4/golang-ical v0.2.8/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/bbrks/go-blurhash v1.1.1 h1:uoXOxRPDca9zHYabUTwvS4KnY++KKUbwFo+Yxb8ME4M=
|
||||
github.com/bbrks/go-blurhash v1.1.1/go.mod h1:lkAsdyXp+EhARcUo85yS2G1o+Sh43I2ebF5togC4bAY=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
|
@ -68,8 +70,6 @@ github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4M
|
|||
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
||||
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
|
||||
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
|
||||
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
|
@ -103,6 +103,8 @@ 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=
|
||||
|
@ -127,8 +129,6 @@ github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
|||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
|
||||
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
|
||||
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
|
@ -156,8 +156,6 @@ github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2r
|
|||
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
|
||||
github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
|
@ -193,8 +191,6 @@ 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=
|
||||
|
@ -215,6 +211,8 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
|||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
|
@ -320,8 +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=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
|
@ -372,6 +370,8 @@ github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S
|
|||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
@ -501,8 +501,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=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/typesense/typesense-go v1.0.0 h1:/8Lr1yf9YjmUKdn/xbTNy+OhwOvBd0noBTRkcB22Uhw=
|
||||
|
@ -531,8 +531,8 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7Jul
|
|||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
|
||||
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
||||
|
@ -572,9 +572,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
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/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=
|
||||
|
@ -586,7 +585,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -608,20 +606,18 @@ golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
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.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
|
||||
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
|
||||
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=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -650,27 +646,21 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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/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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
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/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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
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/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -691,7 +681,6 @@ golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4f
|
|||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -700,8 +689,6 @@ 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=
|
||||
|
@ -786,8 +773,6 @@ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYm
|
|||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240305180710-770b8eae9cec h1:ICDp83UjJvLcOFWHAxr7vmziKIHJkE4jsIF1mbT9Bwk=
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240305180710-770b8eae9cec/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240403232151-e01c4fbef884 h1:Ttvt8FCpUXfC8r3+LgSPrBUIr/JkHmYQtvmOwEET8qE=
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240403232151-e01c4fbef884/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
|
||||
src.techknowlogick.com/xormigrate v1.7.1 h1:RKGLLUAqJ+zO8iZ7eOc7oLH7f0cs2gfXSZSvBRBHnlY=
|
||||
|
@ -798,7 +783,5 @@ xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
|||
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/xorm v1.0.5/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
|
||||
xorm.io/xorm v1.3.3/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo=
|
||||
xorm.io/xorm v1.3.8 h1:CJmplmWqfSRpLWSPMmqz+so8toBp3m7ehuRehIWedZo=
|
||||
xorm.io/xorm v1.3.8/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
|
||||
xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU=
|
||||
xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
|
||||
|
|
|
@ -95,6 +95,9 @@ 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")
|
||||
|
|
|
@ -32,9 +32,9 @@ The to-do app to organize your life.
|
|||
Also one of the two wild South American camelids which live in the high
|
||||
alpine areas of the Andes and a relative of the llama.
|
||||
|
||||
Vikunja is a self-hosted To-Do list application with a web app and mobile apps for all platforms. It is licensed under the GPLv3.
|
||||
Vikunja is a self-hosted To-Do list application with a web app and mobile apps for all platforms. It is licensed under the AGPLv3.
|
||||
|
||||
Find more info at vikunja.io.`,
|
||||
Find out more at vikunja.io.`,
|
||||
PreRun: webCmd.PreRun,
|
||||
Run: webCmd.Run,
|
||||
}
|
||||
|
|
|
@ -349,82 +349,135 @@ 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"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "1",
|
||||
"view": "3",
|
||||
}, `{"title":"Lorem Ipsum"}`)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Nonexisting", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9999"}, `{"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"}`)
|
||||
require.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
|
||||
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)
|
||||
})
|
||||
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"}, `{"title":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "20",
|
||||
"view": "80",
|
||||
}, `{"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"}, `{"title":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "6",
|
||||
"view": "24",
|
||||
}, `{"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"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "7",
|
||||
"view": "28",
|
||||
}, `{"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"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "8",
|
||||
"view": "32",
|
||||
}, `{"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"}, `{"title":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "9",
|
||||
"view": "36",
|
||||
}, `{"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"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "10",
|
||||
"view": "40",
|
||||
}, `{"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"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "11",
|
||||
"view": "44",
|
||||
}, `{"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"}, `{"title":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "12",
|
||||
"view": "48",
|
||||
}, `{"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"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "13",
|
||||
"view": "52",
|
||||
}, `{"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"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "14",
|
||||
"view": "56",
|
||||
}, `{"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"}, `{"title":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "15",
|
||||
"view": "60",
|
||||
}, `{"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"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "16",
|
||||
"view": "64",
|
||||
}, `{"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"}, `{"title":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
|
||||
"project": "17",
|
||||
"view": "68",
|
||||
}, `{"title":"Lorem Ipsum"}`)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
|
|
|
@ -99,12 +99,14 @@ 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 table buckets;
|
||||
|
||||
alter table buckets_dg_tmp
|
||||
rename to buckets;
|
||||
|
||||
create unique index UQE_buckets_id
|
||||
create unique index if not exists UQE_buckets_id
|
||||
on buckets (id);
|
||||
`)
|
||||
return err
|
||||
|
|
|
@ -155,6 +155,21 @@ func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (task
|
|||
projectIDs = append(projectIDs, p.ID)
|
||||
}
|
||||
|
||||
views := map[int64]*ProjectView{}
|
||||
err = s.In("project_id", projectIDs).Find(&views)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
viewIDs := []int64{}
|
||||
for _, v := range views {
|
||||
if projectsMap[v.ProjectID].Views == nil {
|
||||
projectsMap[v.ProjectID].Views = []*ProjectView{}
|
||||
}
|
||||
projectsMap[v.ProjectID].Views = append(projectsMap[v.ProjectID].Views, v)
|
||||
viewIDs = append(viewIDs, v.ID)
|
||||
}
|
||||
|
||||
tasks, _, _, err := getTasksForProjects(s, rawProjects, u, &taskSearchOptions{
|
||||
page: 0,
|
||||
perPage: -1,
|
||||
|
@ -194,17 +209,75 @@ func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (task
|
|||
}
|
||||
|
||||
buckets := []*Bucket{}
|
||||
err = s.In("project_id", projectIDs).Find(&buckets)
|
||||
err = s.In("project_view_id", viewIDs).Find(&buckets)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bucketIDs := []int64{}
|
||||
for _, b := range buckets {
|
||||
if _, exists := projectsMap[b.ProjectID]; !exists {
|
||||
log.Debugf("[User Data Export] Project %d does not exist for bucket %d, omitting", b.ProjectID, b.ID)
|
||||
view, exists := views[b.ProjectViewID]
|
||||
if !exists {
|
||||
log.Debugf("[User Data Export] Project view %d does not exist for bucket %d, omitting", b.ProjectViewID, b.ID)
|
||||
continue
|
||||
}
|
||||
projectsMap[b.ProjectID].Buckets = append(projectsMap[b.ProjectID].Buckets, b)
|
||||
_, exists = projectsMap[view.ProjectID]
|
||||
if !exists {
|
||||
log.Debugf("[User Data Export] Project %d does not exist for bucket %d, omitting", view.ProjectID, b.ID)
|
||||
continue
|
||||
}
|
||||
projectsMap[view.ProjectID].Buckets = append(projectsMap[view.ProjectID].Buckets, b)
|
||||
bucketIDs = append(bucketIDs, b.ID)
|
||||
}
|
||||
|
||||
taskBuckets := []*TaskBucket{}
|
||||
err = s.In("bucket_id", bucketIDs).Find(&taskBuckets)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, tb := range taskBuckets {
|
||||
view, exists := views[tb.ProjectViewID]
|
||||
if !exists {
|
||||
log.Debugf("[User Data Export] Project view %d does not exist, omitting", tb.ProjectViewID)
|
||||
continue
|
||||
}
|
||||
_, exists = projectsMap[view.ProjectID]
|
||||
if !exists {
|
||||
log.Debugf("[User Data Export] Project %d does not exist, omitting", view.ProjectID)
|
||||
continue
|
||||
}
|
||||
|
||||
if projectsMap[view.ProjectID].TaskBuckets == nil {
|
||||
projectsMap[view.ProjectID].TaskBuckets = []*TaskBucket{}
|
||||
}
|
||||
|
||||
projectsMap[view.ProjectID].TaskBuckets = append(projectsMap[view.ProjectID].TaskBuckets, tb)
|
||||
}
|
||||
|
||||
taskPositions := []*TaskPosition{}
|
||||
err = s.In("project_view_id", viewIDs).Find(&taskPositions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range taskPositions {
|
||||
view, exists := views[p.ProjectViewID]
|
||||
if !exists {
|
||||
log.Debugf("[User Data Export] Project view %d does not exist, omitting", p.ProjectViewID)
|
||||
continue
|
||||
}
|
||||
_, exists = projectsMap[view.ProjectID]
|
||||
if !exists {
|
||||
log.Debugf("[User Data Export] Project %d does not exist, omitting", view.ProjectID)
|
||||
continue
|
||||
}
|
||||
|
||||
if projectsMap[view.ProjectID].Positions == nil {
|
||||
projectsMap[view.ProjectID].Positions = []*TaskPosition{}
|
||||
}
|
||||
|
||||
projectsMap[view.ProjectID].Positions = append(projectsMap[view.ProjectID].Positions, p)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(projects)
|
||||
|
|
|
@ -23,11 +23,13 @@ import (
|
|||
|
||||
// CanCreate checks if a user can create a new bucket
|
||||
func (b *Bucket) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
pv := &ProjectView{
|
||||
ID: b.ProjectViewID,
|
||||
ProjectID: b.ProjectID,
|
||||
pv, err := GetProjectViewByIDAndProject(s, b.ProjectViewID, b.ProjectID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return pv.CanUpdate(s, a)
|
||||
|
||||
p := &Project{ID: pv.ProjectID}
|
||||
return p.CanUpdate(s, a)
|
||||
}
|
||||
|
||||
// CanUpdate checks if a user can update an existing bucket
|
||||
|
@ -46,9 +48,11 @@ func (b *Bucket) canDoBucket(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
pv := &ProjectView{
|
||||
ID: bb.ProjectViewID,
|
||||
ProjectID: b.ProjectID,
|
||||
pv, err := GetProjectViewByIDAndProject(s, bb.ProjectViewID, b.ProjectID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return pv.CanUpdate(s, a)
|
||||
|
||||
p := &Project{ID: pv.ProjectID}
|
||||
return p.CanUpdate(s, a)
|
||||
}
|
||||
|
|
|
@ -148,23 +148,14 @@ 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: u,
|
||||
GetForUser: u.ID,
|
||||
User: a,
|
||||
Page: page,
|
||||
PerPage: perPage,
|
||||
GetUnusedLabels: true,
|
||||
GroupByLabelIDsOnly: true,
|
||||
GetForUser: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
|
@ -64,27 +63,29 @@ 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) {
|
||||
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, 0, nil
|
||||
}
|
||||
linkShare, isLinkShare := a.(*LinkSharing)
|
||||
|
||||
u, err := user.GetUserByID(s, a.GetID())
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
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()
|
||||
}
|
||||
|
||||
cond := builder.In("label_tasks.task_id",
|
||||
builder.
|
||||
Select("id").
|
||||
From("tasks").
|
||||
Where(builder.In("project_id", getUserProjectsStatement(u.ID, "", false).Select("l.id"))),
|
||||
Where(where),
|
||||
)
|
||||
|
||||
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 = ?", u.ID).
|
||||
Where("label_tasks.label_id is not null OR labels.created_by_id = ?", createdByID).
|
||||
Or(cond).
|
||||
And("labels.id = ?", l.ID).
|
||||
Exist(ll)
|
||||
|
|
|
@ -144,7 +144,7 @@ func (lt *LabelTask) ReadAll(s *xorm.Session, a web.Auth, search string, page in
|
|||
}
|
||||
|
||||
return GetLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
|
||||
User: &user.User{ID: a.GetID()},
|
||||
User: a,
|
||||
Search: []string{search},
|
||||
Page: page,
|
||||
TaskIDs: []int64{lt.TaskID},
|
||||
|
@ -159,23 +159,26 @@ type LabelWithTaskID struct {
|
|||
|
||||
// LabelByTaskIDsOptions is a struct to not clutter the function with too many optional parameters.
|
||||
type LabelByTaskIDsOptions struct {
|
||||
User *user.User
|
||||
User web.Auth
|
||||
Search []string
|
||||
Page int
|
||||
PerPage int
|
||||
TaskIDs []int64
|
||||
GetUnusedLabels bool
|
||||
GroupByLabelIDsOnly bool
|
||||
GetForUser int64
|
||||
GetForUser bool
|
||||
}
|
||||
|
||||
// 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 ideal solution.
|
||||
// Probably not the most ideataskdetaill solution.
|
||||
var groupBy = "labels.id,label_tasks.task_id"
|
||||
var selectStmt = "labels.*, label_tasks.task_id"
|
||||
if opts.GroupByLabelIDsOnly {
|
||||
|
@ -186,20 +189,25 @@ 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 == 0 {
|
||||
if len(opts.TaskIDs) > 0 && !opts.GetForUser {
|
||||
cond = builder.And(builder.In("label_tasks.task_id", opts.TaskIDs), cond)
|
||||
}
|
||||
if opts.GetForUser != 0 {
|
||||
if opts.GetForUser {
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
cond = builder.And(builder.In("label_tasks.task_id",
|
||||
|
@ -209,8 +217,8 @@ func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*Lab
|
|||
Where(builder.In("project_id", projectIDs)),
|
||||
), cond)
|
||||
}
|
||||
if opts.GetUnusedLabels {
|
||||
cond = builder.Or(cond, builder.Eq{"labels.created_by_id": opts.User.ID})
|
||||
if opts.GetUnusedLabels && !isLinkShareAuth {
|
||||
cond = builder.Or(cond, builder.Eq{"labels.created_by_id": opts.User.GetID()})
|
||||
}
|
||||
|
||||
ids := []int64{}
|
||||
|
|
|
@ -62,7 +62,7 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantLs interface{}
|
||||
wantLs []*LabelWithTaskID
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
|
@ -143,7 +143,10 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||
t.Errorf("Label.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if diff, equal := messagediff.PrettyDiff(gotLs, tt.wantLs); !equal {
|
||||
|
||||
got := gotLs.([]*LabelWithTaskID)
|
||||
|
||||
if diff, equal := messagediff.PrettyDiff(got, tt.wantLs); !equal {
|
||||
t.Errorf("Label.ReadAll() = %v, want %v, diff: %v", gotLs, tt.wantLs, diff)
|
||||
}
|
||||
s.Close()
|
||||
|
|
|
@ -529,7 +529,18 @@ func (l *AddTaskToTypesense) Handle(msg *message.Message) (err error) {
|
|||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
ttask, err := getTypesenseTaskForTask(s, event.Task, nil)
|
||||
|
||||
positionsMap, err := getPositionsForTask(s, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bucketsMap, err := getBucketsForTask(s, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ttask, err := getTypesenseTaskForTask(s, event.Task, nil, positionsMap, bucketsMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -540,6 +551,36 @@ func (l *AddTaskToTypesense) Handle(msg *message.Message) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func getPositionsForTask(s *xorm.Session, event *TaskCreatedEvent) (positionsMap map[int64][]*TaskPositionWithView, err error) {
|
||||
positions := []*TaskPositionWithView{}
|
||||
err = s.
|
||||
Table("project_views").
|
||||
Where("project_views.project_id = ?", event.Task.ProjectID).
|
||||
Join("LEFT", "task_positions", "project_views.id = task_positions.project_view_id AND task_positions.task_id = ?", event.Task.ID).
|
||||
Find(&positions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
positionsMap = make(map[int64][]*TaskPositionWithView, 1)
|
||||
positionsMap[event.Task.ID] = positions
|
||||
return
|
||||
}
|
||||
|
||||
func getBucketsForTask(s *xorm.Session, event *TaskCreatedEvent) (bucketsMap map[int64][]*TaskBucket, err error) {
|
||||
buckets := []*TaskBucket{}
|
||||
err = s.
|
||||
Where("task_id = ?", event.Task.ID).
|
||||
Find(&buckets)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bucketsMap = make(map[int64][]*TaskBucket, 1)
|
||||
bucketsMap[event.Task.ID] = buckets
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateTaskInTypesense represents a listener
|
||||
type UpdateTaskInTypesense struct {
|
||||
}
|
||||
|
|
|
@ -17,10 +17,8 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
|
@ -77,20 +75,16 @@ func (n *TaskCommentNotification) SubjectID() int64 {
|
|||
func (n *TaskCommentNotification) ToMail() *notifications.Mail {
|
||||
|
||||
mail := notifications.NewMail().
|
||||
From(n.Doer.GetNameAndFromEmail())
|
||||
From(n.Doer.GetNameAndFromEmail()).
|
||||
Subject("Re: " + n.Task.Title)
|
||||
|
||||
subject := "Re: " + n.Task.Title
|
||||
if n.Mentioned {
|
||||
subject = n.Doer.GetName() + ` mentioned you in a comment in "` + n.Task.Title + `"`
|
||||
mail.Line("**" + n.Doer.GetName() + "** mentioned you in a comment:")
|
||||
mail.
|
||||
Line("**" + n.Doer.GetName() + "** mentioned you in a comment:").
|
||||
Subject(n.Doer.GetName() + ` mentioned you in a comment in "` + n.Task.Title + `"`)
|
||||
}
|
||||
|
||||
mail.Subject(subject)
|
||||
|
||||
lines := bufio.NewScanner(strings.NewReader(n.Comment.Comment))
|
||||
for lines.Scan() {
|
||||
mail.Line(lines.Text())
|
||||
}
|
||||
mail.HTML(n.Comment.Comment)
|
||||
|
||||
return mail.
|
||||
Action("View Task", n.Task.GetFrontendURL())
|
||||
|
@ -306,12 +300,8 @@ func (n *UserMentionedInTaskNotification) ToMail() *notifications.Mail {
|
|||
mail := notifications.NewMail().
|
||||
From(n.Doer.GetNameAndFromEmail()).
|
||||
Subject(subject).
|
||||
Line("**" + n.Doer.GetName() + "** mentioned you in a task:")
|
||||
|
||||
lines := bufio.NewScanner(strings.NewReader(n.Task.Description))
|
||||
for lines.Scan() {
|
||||
mail.Line(lines.Text())
|
||||
}
|
||||
Line("**" + n.Doer.GetName() + "** mentioned you in a task:").
|
||||
HTML(n.Task.Description)
|
||||
|
||||
return mail.
|
||||
Action("View Task", n.Task.GetFrontendURL())
|
||||
|
|
|
@ -93,8 +93,10 @@ type ProjectWithTasksAndBuckets struct {
|
|||
// An array of tasks which belong to the project.
|
||||
Tasks []*TaskWithComments `xorm:"-" json:"tasks"`
|
||||
// Only used for migration.
|
||||
Buckets []*Bucket `xorm:"-" json:"buckets"`
|
||||
BackgroundFileID int64 `xorm:"null" json:"background_file_id"`
|
||||
Buckets []*Bucket `xorm:"-" json:"buckets"`
|
||||
TaskBuckets []*TaskBucket `xorm:"-" json:"task_buckets"`
|
||||
Positions []*TaskPosition `xorm:"-" json:"positions"`
|
||||
BackgroundFileID int64 `xorm:"null" json:"background_file_id"`
|
||||
}
|
||||
|
||||
// TableName returns a better name for the projects table
|
||||
|
@ -110,15 +112,43 @@ type ProjectBackgroundType struct {
|
|||
// ProjectBackgroundUpload represents the project upload background type
|
||||
const ProjectBackgroundUpload string = "upload"
|
||||
|
||||
const FavoritesPseudoProjectID = -1
|
||||
|
||||
// FavoritesPseudoProject holds all tasks marked as favorites
|
||||
var FavoritesPseudoProject = Project{
|
||||
ID: -1,
|
||||
ID: FavoritesPseudoProjectID,
|
||||
Title: "Favorites",
|
||||
Description: "This project has all tasks marked as favorites.",
|
||||
IsFavorite: true,
|
||||
Position: -1,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
|
||||
Views: []*ProjectView{
|
||||
{
|
||||
ID: -1,
|
||||
ProjectID: FavoritesPseudoProjectID,
|
||||
Title: "List",
|
||||
ViewKind: ProjectViewKindList,
|
||||
Position: 100,
|
||||
Filter: "done = false",
|
||||
},
|
||||
{
|
||||
ID: -2,
|
||||
ProjectID: FavoritesPseudoProjectID,
|
||||
Title: "Gantt",
|
||||
ViewKind: ProjectViewKindGantt,
|
||||
Position: 200,
|
||||
},
|
||||
{
|
||||
ID: -3,
|
||||
ProjectID: FavoritesPseudoProjectID,
|
||||
Title: "Table",
|
||||
ViewKind: ProjectViewKindTable,
|
||||
Position: 300,
|
||||
},
|
||||
},
|
||||
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
// ReadAll gets all projects a user has access to
|
||||
|
@ -146,6 +176,9 @@ func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
|
|||
}
|
||||
projects := []*Project{project}
|
||||
err = addProjectDetails(s, projects, a)
|
||||
if err == nil && len(projects) > 0 {
|
||||
projects[0].ParentProjectID = 0
|
||||
}
|
||||
return projects, 0, 0, err
|
||||
}
|
||||
|
||||
|
@ -207,6 +240,7 @@ func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
|
|||
func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
if p.ID == FavoritesPseudoProject.ID {
|
||||
p.Views = FavoritesPseudoProject.Views
|
||||
// Already "built" the project in CanRead
|
||||
return nil
|
||||
}
|
||||
|
@ -226,6 +260,11 @@ func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
|||
p.OwnerID = sf.OwnerID
|
||||
}
|
||||
|
||||
_, isShareAuth := a.(*LinkSharing)
|
||||
if isShareAuth {
|
||||
p.ParentProjectID = 0
|
||||
}
|
||||
|
||||
// Get project owner
|
||||
p.Owner, err = user.GetUserByID(s, p.OwnerID)
|
||||
if err != nil {
|
||||
|
@ -1073,6 +1112,46 @@ func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Delete related project entities
|
||||
views, err := getViewsForProject(s, p.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
viewIDs := []int64{}
|
||||
for _, v := range views {
|
||||
viewIDs = append(viewIDs, v.ID)
|
||||
}
|
||||
|
||||
_, err = s.In("project_view_id", viewIDs).Delete(&Bucket{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.In("id", viewIDs).Delete(&ProjectView{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = removeFromFavorite(s, p.ID, a, FavoriteKindProject)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.Where("project_id = ?", p.ID).Delete(&LinkSharing{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.Where("project_id = ?", p.ID).Delete(&ProjectUser{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.Where("project_id = ?", p.ID).Delete(&TeamProject{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the project
|
||||
_, err = s.ID(p.ID).Delete(&Project{})
|
||||
if err != nil {
|
||||
|
|
|
@ -159,6 +159,7 @@ func (pd *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||
|
||||
log.Debugf("Duplicated all link shares from project %d into %d", pd.ProjectID, pd.Project.ID)
|
||||
|
||||
err = pd.Project.ReadOne(s, doer)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -226,9 +227,11 @@ func duplicateViews(s *xorm.Session, pd *ProjectDuplicate, doer web.Auth, taskMa
|
|||
})
|
||||
}
|
||||
|
||||
_, err = s.Insert(&taskBuckets)
|
||||
if err != nil {
|
||||
return err
|
||||
if len(taskBuckets) > 0 {
|
||||
_, err = s.Insert(&taskBuckets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
oldTaskPositions := []*TaskPosition{}
|
||||
|
@ -246,7 +249,9 @@ func duplicateViews(s *xorm.Session, pd *ProjectDuplicate, doer web.Auth, taskMa
|
|||
})
|
||||
}
|
||||
|
||||
_, err = s.Insert(&taskPositions)
|
||||
if len(taskPositions) > 0 {
|
||||
_, err = s.Insert(&taskPositions)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -347,10 +347,22 @@ func (p *ProjectView) Update(s *xorm.Session, _ web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func GetProjectViewByIDAndProject(s *xorm.Session, id, projectID int64) (view *ProjectView, err error) {
|
||||
func GetProjectViewByIDAndProject(s *xorm.Session, viewID, projectID int64) (view *ProjectView, err error) {
|
||||
if projectID == FavoritesPseudoProjectID && viewID < 0 {
|
||||
for _, v := range FavoritesPseudoProject.Views {
|
||||
if v.ID == viewID {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, &ErrProjectViewDoesNotExist{
|
||||
ProjectViewID: viewID,
|
||||
}
|
||||
}
|
||||
|
||||
view = &ProjectView{}
|
||||
exists, err := s.
|
||||
Where("id = ? AND project_id = ?", id, projectID).
|
||||
Where("id = ? AND project_id = ?", viewID, projectID).
|
||||
NoAutoCondition().
|
||||
Get(view)
|
||||
if err != nil {
|
||||
|
@ -359,7 +371,7 @@ func GetProjectViewByIDAndProject(s *xorm.Session, id, projectID int64) (view *P
|
|||
|
||||
if !exists {
|
||||
return nil, &ErrProjectViewDoesNotExist{
|
||||
ProjectViewID: id,
|
||||
ProjectViewID: viewID,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.CanUpdate(s, a)
|
||||
return pp.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
func (p *ProjectView) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
pp := p.getProject()
|
||||
return pp.CanUpdate(s, a)
|
||||
return pp.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
func (p *ProjectView) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
pp := p.getProject()
|
||||
return pp.CanUpdate(s, a)
|
||||
return pp.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
func (p *ProjectView) getProject() (pp *Project) {
|
||||
|
|
|
@ -100,6 +100,10 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection, projectView *ProjectVie
|
|||
param.orderBy = getSortOrderFromString(tf.OrderBy[i])
|
||||
}
|
||||
|
||||
if s == taskPropertyPosition && projectView != nil && projectView.ID < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if s == taskPropertyPosition && projectView != nil {
|
||||
param.projectViewID = projectView.ID
|
||||
}
|
||||
|
@ -249,11 +253,20 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
|||
opts.perPage = perPage
|
||||
|
||||
if view != nil {
|
||||
opts.sortby = append(opts.sortby, &sortParam{
|
||||
projectViewID: view.ID,
|
||||
sortBy: taskPropertyPosition,
|
||||
orderBy: orderAscending,
|
||||
})
|
||||
var hasOrderByPosition bool
|
||||
for _, param := range opts.sortby {
|
||||
if param.sortBy == taskPropertyPosition {
|
||||
hasOrderByPosition = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOrderByPosition {
|
||||
opts.sortby = append(opts.sortby, &sortParam{
|
||||
projectViewID: view.ID,
|
||||
sortBy: taskPropertyPosition,
|
||||
orderBy: orderAscending,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
shareAuth, is := a.(*LinkSharing)
|
||||
|
|
|
@ -65,6 +65,14 @@ func (tc *TaskComment) TableName() string {
|
|||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/comments [put]
|
||||
func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
tc.Created = time.Time{}
|
||||
tc.Updated = time.Time{}
|
||||
|
||||
return tc.CreateWithTimestamps(s, a)
|
||||
}
|
||||
|
||||
func (tc *TaskComment) CreateWithTimestamps(s *xorm.Session, a web.Auth) (err error) {
|
||||
// Check if the task exists
|
||||
task, err := GetTaskSimple(s, &Task{ID: tc.TaskID})
|
||||
if err != nil {
|
||||
|
@ -77,9 +85,16 @@ func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
tc.AuthorID = tc.Author.ID
|
||||
|
||||
_, err = s.Insert(tc)
|
||||
if err != nil {
|
||||
return
|
||||
if !tc.Created.IsZero() && !tc.Updated.IsZero() {
|
||||
_, err = s.NoAutoTime().Insert(tc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err = s.Insert(tc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return events.Dispatch(&TaskCommentCreatedEvent{
|
||||
|
@ -255,7 +270,7 @@ func (tc *TaskComment) ReadAll(s *xorm.Session, auth web.Auth, search string, pa
|
|||
query := s.
|
||||
Where(builder.And(where...)).
|
||||
Join("LEFT", "users", "users.id = task_comments.author_id").
|
||||
OrderBy("task_comments.id asc")
|
||||
OrderBy("task_comments.created asc")
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
|
|
|
@ -297,6 +297,7 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
|
|||
queryCount = queryCount.Join("LEFT", "task_buckets", "task_buckets.task_id = tasks.id")
|
||||
}
|
||||
totalCount, err = queryCount.
|
||||
Select("count(DISTINCT tasks.id)").
|
||||
Count(&Task{})
|
||||
return
|
||||
}
|
||||
|
@ -379,6 +380,10 @@ func convertParsedFilterToTypesense(rawFilters []*taskFilter) (filterBy string,
|
|||
f.field = "project_id"
|
||||
}
|
||||
|
||||
if f.field == "bucket_id" {
|
||||
f.field = "buckets"
|
||||
}
|
||||
|
||||
filter := f.field
|
||||
|
||||
switch f.comparator {
|
||||
|
@ -450,6 +455,7 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
|
|||
"(" + filter + ")",
|
||||
}
|
||||
|
||||
var projectViewIDForPosition int64
|
||||
var sortbyFields []string
|
||||
for i, param := range opts.sortby {
|
||||
// Validate the params
|
||||
|
@ -457,17 +463,19 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
|
|||
return nil, totalCount, err
|
||||
}
|
||||
|
||||
sortBy := param.sortBy
|
||||
|
||||
// Typesense does not allow sorting by ID, so we sort by created timestamp instead
|
||||
if param.sortBy == taskPropertyID {
|
||||
param.sortBy = taskPropertyCreated
|
||||
sortBy = taskPropertyCreated
|
||||
}
|
||||
|
||||
if param.sortBy == taskPropertyPosition {
|
||||
param.sortBy = "positions.view_" + strconv.FormatInt(param.projectViewID, 10)
|
||||
continue
|
||||
sortBy = "positions.view_" + strconv.FormatInt(param.projectViewID, 10)
|
||||
projectViewIDForPosition = param.projectViewID
|
||||
}
|
||||
|
||||
sortbyFields = append(sortbyFields, param.sortBy+"(missing_values:last):"+param.orderBy.String())
|
||||
sortbyFields = append(sortbyFields, sortBy+"(missing_values:last):"+param.orderBy.String())
|
||||
|
||||
if i == 2 {
|
||||
// Typesense supports up to 3 sorting parameters
|
||||
|
@ -525,9 +533,14 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
|
|||
return nil, 0, err
|
||||
}
|
||||
|
||||
err = t.s.
|
||||
query := t.s.
|
||||
In("id", taskIDs).
|
||||
OrderBy(orderby).
|
||||
Find(&tasks)
|
||||
OrderBy(orderby)
|
||||
|
||||
if projectViewIDForPosition != 0 {
|
||||
query = query.Join("LEFT", "task_positions", "task_positions.task_id = tasks.id AND task_positions.project_view_id = ?", projectViewIDForPosition)
|
||||
}
|
||||
|
||||
err = query.Find(&tasks)
|
||||
return tasks, int64(*result.Found), err
|
||||
}
|
||||
|
|
|
@ -1486,6 +1486,18 @@ func (t *Task) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// Delete all positions
|
||||
_, err = s.Where("task_id = ?", t.ID).Delete(&TaskPosition{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete all bucket relations
|
||||
_, err = s.Where("task_id = ?", t.ID).Delete(&TaskBucket{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Actually delete the task
|
||||
_, err = s.ID(t.ID).Delete(Task{})
|
||||
if err != nil {
|
||||
|
|
|
@ -146,22 +146,10 @@ func CreateTypesenseCollections() error {
|
|||
Name: "updated",
|
||||
Type: "int64", // unix timestamp
|
||||
},
|
||||
{
|
||||
Name: "bucket_id",
|
||||
Type: "int64",
|
||||
},
|
||||
{
|
||||
Name: "position",
|
||||
Type: "float",
|
||||
},
|
||||
{
|
||||
Name: "created_by_id",
|
||||
Type: "int64",
|
||||
},
|
||||
{
|
||||
Name: "project_view_id",
|
||||
Type: "int64",
|
||||
},
|
||||
{
|
||||
Name: "reminders",
|
||||
Type: "object[]", // TODO
|
||||
|
@ -192,6 +180,14 @@ func CreateTypesenseCollections() error {
|
|||
Type: "object[]", // TODO
|
||||
Optional: pointer.True(),
|
||||
},
|
||||
{
|
||||
Name: "positions",
|
||||
Type: "object",
|
||||
},
|
||||
{
|
||||
Name: "buckets",
|
||||
Type: "int64[]",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -248,14 +244,8 @@ func ReindexAllTasks() (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func getTypesenseTaskForTask(s *xorm.Session, task *Task, projectsCache map[int64]*Project) (ttask *typesenseTask, err error) {
|
||||
positions := []*TaskPosition{}
|
||||
err = s.Where("task_id = ?", task.ID).Find(&positions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ttask = convertTaskToTypesenseTask(task, positions)
|
||||
func getTypesenseTaskForTask(s *xorm.Session, task *Task, projectsCache map[int64]*Project, taskPositionCache map[int64][]*TaskPositionWithView, taskBucketCache map[int64][]*TaskBucket) (ttask *typesenseTask, err error) {
|
||||
ttask = convertTaskToTypesenseTask(task, taskPositionCache[task.ID], taskBucketCache[task.ID])
|
||||
|
||||
var p *Project
|
||||
if projectsCache == nil {
|
||||
|
@ -299,9 +289,19 @@ func reindexTasksInTypesense(s *xorm.Session, tasks map[int64]*Task) (err error)
|
|||
projects := make(map[int64]*Project)
|
||||
typesenseTasks := []interface{}{}
|
||||
|
||||
positionsByTask, err := getPositionsByTask(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bucketsByTask, err := getBucketsByTask(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
|
||||
ttask, err := getTypesenseTaskForTask(s, task, projects)
|
||||
ttask, err := getTypesenseTaskForTask(s, task, projects, positionsByTask, bucketsByTask)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -320,11 +320,55 @@ func reindexTasksInTypesense(s *xorm.Session, tasks map[int64]*Task) (err error)
|
|||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Indexed tasks %v into Typesense", tasks)
|
||||
log.Debugf("Indexed %d tasks into Typesense", len(tasks))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type TaskPositionWithView struct {
|
||||
ProjectView `xorm:"extends"`
|
||||
TaskPosition `xorm:"extends"`
|
||||
}
|
||||
|
||||
func getPositionsByTask(s *xorm.Session) (positionsByTask map[int64][]*TaskPositionWithView, err error) {
|
||||
rawPositions := []*TaskPositionWithView{}
|
||||
err = s.
|
||||
Table("project_views").
|
||||
Join("LEFT", "task_positions", "project_views.id = task_positions.project_view_id").
|
||||
Find(&rawPositions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
positionsByTask = make(map[int64][]*TaskPositionWithView, len(rawPositions))
|
||||
for _, p := range rawPositions {
|
||||
_, has := positionsByTask[p.TaskID]
|
||||
if !has {
|
||||
positionsByTask[p.TaskID] = []*TaskPositionWithView{}
|
||||
}
|
||||
positionsByTask[p.TaskID] = append(positionsByTask[p.TaskID], p)
|
||||
}
|
||||
return positionsByTask, nil
|
||||
}
|
||||
|
||||
func getBucketsByTask(s *xorm.Session) (positionsByTask map[int64][]*TaskBucket, err error) {
|
||||
rawBuckets := []*TaskBucket{}
|
||||
err = s.Find(&rawBuckets)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
positionsByTask = make(map[int64][]*TaskBucket, len(rawBuckets))
|
||||
for _, p := range rawBuckets {
|
||||
_, has := positionsByTask[p.TaskID]
|
||||
if !has {
|
||||
positionsByTask[p.TaskID] = []*TaskBucket{}
|
||||
}
|
||||
positionsByTask[p.TaskID] = append(positionsByTask[p.TaskID], p)
|
||||
}
|
||||
return positionsByTask, nil
|
||||
}
|
||||
|
||||
func indexDummyTask() (err error) {
|
||||
// The initial sync should contain one dummy task with all related fields populated so that typesense
|
||||
// creates the indexes properly. A little hacky, but gets the job done.
|
||||
|
@ -386,6 +430,13 @@ func indexDummyTask() (err error) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Positions: map[string]float64{
|
||||
"view_1": 10,
|
||||
"view_2": 30,
|
||||
"view_3": 5450,
|
||||
"view_4": 42,
|
||||
},
|
||||
Buckets: []int64{42},
|
||||
}
|
||||
|
||||
_, err = typesenseClient.Collection("tasks").
|
||||
|
@ -430,9 +481,10 @@ type typesenseTask struct {
|
|||
Attachments interface{} `json:"attachments"`
|
||||
Comments interface{} `json:"comments"`
|
||||
Positions map[string]float64 `json:"positions"`
|
||||
Buckets []int64 `json:"buckets"`
|
||||
}
|
||||
|
||||
func convertTaskToTypesenseTask(task *Task, positions []*TaskPosition) *typesenseTask {
|
||||
func convertTaskToTypesenseTask(task *Task, positions []*TaskPositionWithView, buckets []*TaskBucket) *typesenseTask {
|
||||
|
||||
tt := &typesenseTask{
|
||||
ID: fmt.Sprintf("%d", task.ID),
|
||||
|
@ -461,6 +513,8 @@ func convertTaskToTypesenseTask(task *Task, positions []*TaskPosition) *typesens
|
|||
Labels: task.Labels,
|
||||
//RelatedTasks: task.RelatedTasks,
|
||||
Attachments: task.Attachments,
|
||||
Positions: make(map[string]float64, len(positions)),
|
||||
Buckets: make([]int64, 0, len(buckets)),
|
||||
}
|
||||
|
||||
if task.DoneAt.IsZero() {
|
||||
|
@ -477,7 +531,15 @@ func convertTaskToTypesenseTask(task *Task, positions []*TaskPosition) *typesens
|
|||
}
|
||||
|
||||
for _, position := range positions {
|
||||
tt.Positions["view_"+strconv.FormatInt(position.ProjectViewID, 10)] = position.Position
|
||||
pos := position.TaskPosition.Position
|
||||
if pos == 0 {
|
||||
pos = float64(task.ID)
|
||||
}
|
||||
tt.Positions["view_"+strconv.FormatInt(position.ProjectView.ID, 10)] = pos
|
||||
}
|
||||
|
||||
for _, bucket := range buckets {
|
||||
tt.Buckets = append(tt.Buckets, bucket.BucketID)
|
||||
}
|
||||
|
||||
return tt
|
||||
|
|
|
@ -38,6 +38,10 @@ 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)
|
||||
|
@ -63,6 +67,6 @@ func RegisterEmptyOpenIDTeamCleanupCron() {
|
|||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not empty openid teams cleanup cron: %s", err)
|
||||
log.Fatalf("Could not register empty openid teams cleanup cron: %s", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@ 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
|
||||
|
|
|
@ -17,10 +17,13 @@
|
|||
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
|
||||
|
@ -143,6 +146,13 @@ func (bp *BackgroundProvider) SetBackground(c echo.Context) error {
|
|||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
err = project.ReadOne(s, auth)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, project)
|
||||
}
|
||||
|
||||
|
@ -203,6 +213,12 @@ 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)
|
||||
|
@ -213,7 +229,30 @@ 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)
|
||||
f, err := files.Create(srcf, filename, filesize, auth)
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue