mirror of https://kolaente.dev/vikunja/api.git
Compare commits
89 Commits
0c08db9195
...
0e6fea95cb
Author | SHA1 | Date |
---|---|---|
Elscrux | 0e6fea95cb | |
Elscrux | 0b7a9c09c5 | |
Elscrux | 117a6c9142 | |
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 |
20
.drone.yml
20
.drone.yml
|
@ -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
|
||||
|
@ -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: 2c9cb0483fb346988188515f6423929f46eefb9e14eb26b0f312a0b694d5fe8c
|
||||
hmac: f2753482faf9e2a3d34a9111587a75dfb4519cb77002cc64a51266540fd2478e
|
||||
|
||||
...
|
||||
|
|
|
@ -104,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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
```
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
nodePackages.pnpm cypress
|
||||
# API tools
|
||||
go golangci-lint mage
|
||||
# Desktop
|
||||
electron
|
||||
];
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1 +1 @@
|
|||
20.11.1
|
||||
20.12.2
|
|
@ -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.6",
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
|
@ -56,10 +56,10 @@
|
|||
"@fortawesome/vue-fontawesome": "3.0.6",
|
||||
"@github/hotkey": "3.1.0",
|
||||
"@infectoone/vue-ganttastic": "2.3.2",
|
||||
"@intlify/unplugin-vue-i18n": "3.0.1",
|
||||
"@intlify/unplugin-vue-i18n": "4.0.0",
|
||||
"@kyvg/vue3-notification": "3.2.1",
|
||||
"@sentry/tracing": "7.109.0",
|
||||
"@sentry/vue": "7.109.0",
|
||||
"@sentry/tracing": "7.112.2",
|
||||
"@sentry/vue": "7.112.2",
|
||||
"@tiptap/core": "2.3.0",
|
||||
"@tiptap/extension-blockquote": "2.3.0",
|
||||
"@tiptap/extension-bold": "2.3.0",
|
||||
|
@ -103,7 +103,7 @@
|
|||
"camel-case": "4.1.2",
|
||||
"date-fns": "3.6.0",
|
||||
"dayjs": "1.11.10",
|
||||
"dompurify": "3.1.0",
|
||||
"dompurify": "3.1.1",
|
||||
"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.25",
|
||||
"vue-advanced-cropper": "2.8.8",
|
||||
"vue-flatpickr-component": "11.0.5",
|
||||
"vue-i18n": "9.11.1",
|
||||
"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": {
|
||||
|
@ -134,7 +134,7 @@
|
|||
"@faker-js/faker": "8.4.1",
|
||||
"@histoire/plugin-screenshot": "0.17.17",
|
||||
"@histoire/plugin-vue": "0.17.17",
|
||||
"@rushstack/eslint-patch": "1.10.1",
|
||||
"@rushstack/eslint-patch": "1.10.2",
|
||||
"@tsconfig/node18": "18.2.4",
|
||||
"@types/codemirror": "5.60.15",
|
||||
"@types/dompurify": "3.0.5",
|
||||
|
@ -145,8 +145,8 @@
|
|||
"@types/node": "20.12.7",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "7.6.0",
|
||||
"@typescript-eslint/parser": "7.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.7.1",
|
||||
"@typescript-eslint/parser": "7.7.1",
|
||||
"@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.30001608",
|
||||
"caniuse-lite": "1.0.30001612",
|
||||
"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.1",
|
||||
"eslint-plugin-vue": "9.25.0",
|
||||
"happy-dom": "14.7.1",
|
||||
"histoire": "0.17.17",
|
||||
"postcss": "8.4.38",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-easings": "4.0.0",
|
||||
"postcss-focus-within": "8.0.1",
|
||||
"postcss-preset-env": "9.5.4",
|
||||
"rollup": "4.14.1",
|
||||
"postcss-preset-env": "9.5.9",
|
||||
"rollup": "4.16.4",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"sass": "1.74.1",
|
||||
"sass": "1.75.0",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"typescript": "5.4.5",
|
||||
"vite": "5.2.8",
|
||||
"vite": "5.2.10",
|
||||
"vite-plugin-inject-preload": "1.3.3",
|
||||
"vite-plugin-pwa": "0.19.8",
|
||||
"vite-plugin-sentry": "1.4.0",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "1.4.0",
|
||||
"vue-tsc": "2.0.12",
|
||||
"vitest": "1.5.2",
|
||||
"vue-tsc": "2.0.14",
|
||||
"wait-on": "7.2.0",
|
||||
"workbox-cli": "7.0.0"
|
||||
"workbox-cli": "7.1.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
|
|
14496
frontend/pnpm-lock.yaml
14496
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>
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
class="tiptap__editor"
|
||||
:class="{'tiptap__editor-is-edit-enabled': isEditing}"
|
||||
:editor="editor"
|
||||
@dblclick="setEditIfApplicable()"
|
||||
@click="focusIfEditing()"
|
||||
/>
|
||||
|
||||
|
@ -189,7 +190,7 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
|||
import XButton from '@/components/input/button.vue'
|
||||
import {Placeholder} from '@tiptap/extension-placeholder'
|
||||
import {eventToHotkeyString} from '@github/hotkey'
|
||||
import {mergeAttributes} from '@tiptap/core'
|
||||
import {Extension, mergeAttributes} from '@tiptap/core'
|
||||
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
|
||||
import inputPrompt from '@/helpers/inputPrompt'
|
||||
import {setLinkInEditor} from '@/components/input/editor/setLinkInEditor'
|
||||
|
@ -202,6 +203,7 @@ const {
|
|||
showSave = false,
|
||||
placeholder = '',
|
||||
editShortcut = '',
|
||||
discardShortcutEnabled = false,
|
||||
} = defineProps<{
|
||||
modelValue: string,
|
||||
uploadCallback?: UploadCallback,
|
||||
|
@ -210,6 +212,7 @@ const {
|
|||
showSave?: boolean,
|
||||
placeholder?: string,
|
||||
editShortcut?: string,
|
||||
discardShortcutEnabled?: boolean,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
@ -311,6 +314,8 @@ const internalMode = ref<Mode>('preview')
|
|||
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
|
||||
const contentHasChanged = ref<boolean>(false)
|
||||
|
||||
let lastSavedState = modelValue
|
||||
|
||||
watch(
|
||||
() => internalMode.value,
|
||||
mode => {
|
||||
|
@ -338,10 +343,12 @@ const editor = useEditor({
|
|||
HardBreak.extend({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
'Shift-Enter': () => this.editor.commands.setHardBreak(),
|
||||
'Mod-Enter': () => {
|
||||
if (contentHasChanged.value) {
|
||||
bubbleSave()
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -420,6 +427,24 @@ const editor = useEditor({
|
|||
suggestion: suggestionSetup(t),
|
||||
}),
|
||||
BubbleMenu,
|
||||
|
||||
// Add a custom extension for the Escape key
|
||||
...(
|
||||
discardShortcutEnabled
|
||||
?[discardShortcutEnabled && Extension.create({
|
||||
name: 'escapeKey',
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
'Escape': () => {
|
||||
exitEditMode()
|
||||
return true
|
||||
},
|
||||
}
|
||||
},
|
||||
})]
|
||||
: []
|
||||
),
|
||||
],
|
||||
onUpdate: () => {
|
||||
bubbleNow()
|
||||
|
@ -459,12 +484,27 @@ function bubbleNow() {
|
|||
|
||||
function bubbleSave() {
|
||||
bubbleNow()
|
||||
emit('save', editor.value?.getHTML())
|
||||
lastSavedState = editor.value?.getHTML() ?? ''
|
||||
emit('save', lastSavedState)
|
||||
if (isEditing.value) {
|
||||
internalMode.value = 'preview'
|
||||
}
|
||||
}
|
||||
|
||||
function exitEditMode() {
|
||||
editor.value?.commands.setContent(lastSavedState, false)
|
||||
if (isEditing.value) {
|
||||
internalMode.value = 'preview'
|
||||
}
|
||||
}
|
||||
|
||||
function setEditIfApplicable() {
|
||||
if (!isEditEnabled) return
|
||||
if (isEditing.value) return
|
||||
|
||||
setEdit()
|
||||
}
|
||||
|
||||
function setEdit(focus: boolean = true) {
|
||||
internalMode.value = 'edit'
|
||||
if (focus) {
|
||||
|
@ -816,7 +856,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 +870,7 @@ watch(
|
|||
th {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
background-color: #f1f3f5;
|
||||
background-color: var(--grey-200);
|
||||
}
|
||||
|
||||
.selectedCell:after {
|
||||
|
@ -891,8 +931,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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -85,6 +85,7 @@
|
|||
:upload-enabled="true"
|
||||
:bottom-actions="actions[c.id]"
|
||||
:show-save="true"
|
||||
:discard-shortcut-enabled="true"
|
||||
initial-mode="preview"
|
||||
@update:modelValue="
|
||||
() => {
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
:placeholder="$t('task.description.placeholder')"
|
||||
:show-save="true"
|
||||
edit-shortcut="e"
|
||||
:discard-shortcut-enabled="true"
|
||||
@update:modelValue="saveWithDelay"
|
||||
@save="save"
|
||||
/>
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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,
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -207,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,
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
18
go.mod
18
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,7 +45,7 @@ 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
|
||||
|
@ -63,15 +63,15 @@ 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.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/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
|
||||
|
@ -120,7 +120,6 @@ 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
|
||||
|
@ -181,7 +180,6 @@ require (
|
|||
golang.org/x/net v0.24.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
@ -190,8 +188,6 @@ require (
|
|||
|
||||
replace github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6 and https://github.com/samedi/caldav-go/pull/7
|
||||
|
||||
replace github.com/adlio/trello v1.11.0 => github.com/kolaente/trello v1.8.1-0.20240410214605-9314fa638eab // https://github.com/adlio/trello/pull/95
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.2
|
||||
|
|
31
go.sum
31
go.sum
|
@ -24,13 +24,15 @@ 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.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=
|
||||
|
@ -101,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=
|
||||
|
@ -187,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=
|
||||
|
@ -303,8 +305,6 @@ github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZY
|
|||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible h1:q7DbyV+sFjEoTuuUdRDNl2nlyfztkZgxVVCV7JhzIkY=
|
||||
github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible/go.mod h1:y1UhTNI4g0hVymJrI6yJ5/ohy09hNBeU8iJEZjgdDOw=
|
||||
github.com/kolaente/trello v1.8.1-0.20240410214605-9314fa638eab h1:KAEuOtvjNohQzONw0NpOlR4TlWgsPawY/PLyOiMHlFw=
|
||||
github.com/kolaente/trello v1.8.1-0.20240410214605-9314fa638eab/go.mod h1:I4Lti4jf2KxjTNgTqs5W3lLuE78QZZdYbbPnQQGwjOo=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
|
@ -318,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=
|
||||
|
@ -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=
|
||||
|
@ -608,16 +608,16 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
|
||||
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
|
||||
golang.org/x/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.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=
|
||||
|
@ -660,7 +660,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
|
@ -690,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=
|
||||
|
|
|
@ -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"`)
|
||||
})
|
||||
|
|
|
@ -82,7 +82,6 @@ func init() {
|
|||
|
||||
if config.DatabaseType.GetString() == "sqlite" {
|
||||
_, err = tx.Exec(`
|
||||
|
||||
create table buckets_dg_tmp
|
||||
(
|
||||
id INTEGER not null
|
||||
|
@ -100,14 +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 buckets.UQE_buckets_id;
|
||||
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 {
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -210,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)
|
||||
|
@ -220,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
|
||||
}
|
||||
|
|
|
@ -256,7 +256,7 @@ func (p *Provider) Set(s *xorm.Session, image *background.Image, project *models
|
|||
|
||||
// Download the photo from unsplash
|
||||
// The parameters crop the image to a max width of 2560 and a max height of 2048 to save bandwidth and storage.
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, photo.Urls.Raw+"&w=2560&h=2048&q=90", nil)
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, photo.Urls.Raw+"&fm=jpg&h="+strconv.FormatInt(background.MaxBackgroundImageHeight, 10)+"&q=80", nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -150,36 +150,10 @@ func Restore(filename string) error {
|
|||
|
||||
delete(dbfiles, "migration")
|
||||
|
||||
// Restore all db data
|
||||
for table, d := range dbfiles {
|
||||
content, err := unmarshalFileToJSON(d)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read table %s: %w", table, err)
|
||||
}
|
||||
|
||||
// FIXME: There has to be a general way to do this but this works for now.
|
||||
if table == "notifications" {
|
||||
for i := range content {
|
||||
var decoded []byte
|
||||
decoded, err = base64.StdEncoding.DecodeString(content[i]["notification"].(string))
|
||||
if err != nil && !errors.Is(err, base64.CorruptInputError(0)) {
|
||||
return fmt.Errorf("could not decode notification %s: %w", content[i]["notification"], err)
|
||||
}
|
||||
|
||||
if err != nil && errors.Is(err, base64.CorruptInputError(0)) {
|
||||
decoded = []byte(content[i]["notification"].(string))
|
||||
}
|
||||
|
||||
content[i]["notification"] = string(decoded)
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Restore(table, content); err != nil {
|
||||
return fmt.Errorf("could not restore table data for table %s: %w", table, err)
|
||||
}
|
||||
log.Infof("Restored table %s", table)
|
||||
err = restoreTableData(dbfiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Restored %d tables", len(dbfiles))
|
||||
|
||||
// Run migrations again to migrate a potentially outdated dump
|
||||
migration.Migrate(nil)
|
||||
|
@ -216,6 +190,53 @@ func Restore(filename string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func restoreTableData(tables map[string]*zip.File) error {
|
||||
jsonFields := map[string][]string{
|
||||
"notifications": {"notification"},
|
||||
"users": {"frontend_settings"},
|
||||
}
|
||||
|
||||
// Restore all db data
|
||||
for table, d := range tables {
|
||||
content, err := unmarshalFileToJSON(d)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not read table %s: %w", table, err)
|
||||
}
|
||||
|
||||
fields, hasJSONFields := jsonFields[table]
|
||||
if hasJSONFields {
|
||||
for i := range content {
|
||||
for _, f := range fields {
|
||||
|
||||
if _, hasField := content[i][f]; !hasField {
|
||||
continue
|
||||
}
|
||||
|
||||
var decoded []byte
|
||||
decoded, err = base64.StdEncoding.DecodeString(content[i][f].(string))
|
||||
if err != nil && !errors.Is(err, base64.CorruptInputError(0)) {
|
||||
return fmt.Errorf("could not decode field '%s' %s: %w", f, content[i][f], err)
|
||||
}
|
||||
|
||||
if err != nil && errors.Is(err, base64.CorruptInputError(0)) {
|
||||
decoded = []byte(content[i][f].(string))
|
||||
}
|
||||
|
||||
content[i][f] = string(decoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Restore(table, content); err != nil {
|
||||
return fmt.Errorf("could not restore table data for table %s: %w", table, err)
|
||||
}
|
||||
log.Infof("Restored table %s", table)
|
||||
}
|
||||
log.Infof("Restored %d tables", len(tables))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalFileToJSON(file *zip.File) (contents []map[string]interface{}, err error) {
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
|
|
|
@ -64,6 +64,11 @@ func insertFromStructure(s *xorm.Session, str []*models.ProjectWithTasksAndBucke
|
|||
}
|
||||
|
||||
p.ID = 0
|
||||
|
||||
for _, view := range p.Views {
|
||||
view.ProjectID = 0
|
||||
}
|
||||
|
||||
err = createProject(s, p, &archivedProjects, labels, user)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -167,7 +172,7 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||
}
|
||||
|
||||
// Create all buckets
|
||||
buckets := make(map[int64]*models.Bucket) // old bucket id is the key
|
||||
bucketsByOldID := make(map[int64]*models.Bucket) // old bucket id is the key
|
||||
if len(project.Buckets) > 0 {
|
||||
log.Debugf("[creating structure] Creating %d buckets", len(project.Buckets))
|
||||
}
|
||||
|
@ -179,40 +184,45 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
buckets[oldID] = bucket
|
||||
bucketsByOldID[oldID] = bucket
|
||||
log.Debugf("[creating structure] Created bucket %d, old ID was %d", bucket.ID, oldID)
|
||||
}
|
||||
|
||||
// Create all views, create default views if we don't have any
|
||||
viewsByOldIDs := make(map[int64]*models.ProjectView, len(oldViews))
|
||||
if len(oldViews) > 0 {
|
||||
for _, view := range oldViews {
|
||||
oldID := view.ID
|
||||
view.ID = 0
|
||||
|
||||
if view.DefaultBucketID != 0 {
|
||||
bucket, has := buckets[view.DefaultBucketID]
|
||||
bucket, has := bucketsByOldID[view.DefaultBucketID]
|
||||
if has {
|
||||
view.DefaultBucketID = bucket.ID
|
||||
}
|
||||
}
|
||||
|
||||
if view.DoneBucketID != 0 {
|
||||
bucket, has := buckets[view.DoneBucketID]
|
||||
bucket, has := bucketsByOldID[view.DoneBucketID]
|
||||
if has {
|
||||
view.DoneBucketID = bucket.ID
|
||||
}
|
||||
}
|
||||
|
||||
view.ProjectID = project.ID
|
||||
|
||||
err = view.Create(s, user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
viewsByOldIDs[oldID] = view
|
||||
}
|
||||
} else {
|
||||
// Only using the default views
|
||||
// Add all buckets to the default kanban view
|
||||
for _, view := range project.Views {
|
||||
if view.ViewKind == models.ProjectViewKindKanban {
|
||||
for _, b := range buckets {
|
||||
for _, b := range bucketsByOldID {
|
||||
b.ProjectViewID = view.ID
|
||||
err = b.Update(s, user)
|
||||
if err != nil {
|
||||
|
@ -227,7 +237,7 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||
log.Debugf("[creating structure] Creating %d tasks", len(tasks))
|
||||
|
||||
setBucketOrDefault := func(task *models.Task) {
|
||||
bucket, exists := buckets[task.BucketID]
|
||||
bucket, exists := bucketsByOldID[task.BucketID]
|
||||
if exists {
|
||||
task.BucketID = bucket.ID
|
||||
} else if task.BucketID > 0 {
|
||||
|
@ -240,6 +250,7 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||
}
|
||||
|
||||
tasksByOldID := make(map[int64]*models.TaskWithComments, len(tasks))
|
||||
newTaskIDs := []int64{}
|
||||
// Create all tasks
|
||||
for i, t := range tasks {
|
||||
setBucketOrDefault(&tasks[i].Task)
|
||||
|
@ -251,6 +262,8 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||
continue
|
||||
}
|
||||
|
||||
newTaskIDs = append(newTaskIDs, t.ID)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -359,10 +372,11 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||
log.Debugf("[creating structure] Associated task %d with label %d", t.ID, lb.ID)
|
||||
}
|
||||
|
||||
// Comments
|
||||
for _, comment := range t.Comments {
|
||||
comment.TaskID = t.ID
|
||||
comment.ID = 0
|
||||
err = comment.Create(s, user)
|
||||
err = comment.CreateWithTimestamps(s, user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -400,6 +414,58 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||
}
|
||||
}
|
||||
|
||||
if len(viewsByOldIDs) > 0 {
|
||||
newPositions := []*models.TaskPosition{}
|
||||
for _, pos := range project.Positions {
|
||||
_, hasTask := tasksByOldID[pos.TaskID]
|
||||
_, hasView := viewsByOldIDs[pos.ProjectViewID]
|
||||
if !hasTask || !hasView {
|
||||
continue
|
||||
}
|
||||
newPositions = append(newPositions, &models.TaskPosition{
|
||||
TaskID: tasksByOldID[pos.TaskID].ID,
|
||||
ProjectViewID: viewsByOldIDs[pos.ProjectViewID].ID,
|
||||
Position: pos.Position,
|
||||
})
|
||||
}
|
||||
|
||||
if len(newPositions) > 0 {
|
||||
_, err = s.In("task_id", newTaskIDs).Delete(&models.TaskPosition{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = s.Insert(newPositions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
newTaskBuckets := make([]*models.TaskBucket, 0, len(project.TaskBuckets))
|
||||
for _, tb := range project.TaskBuckets {
|
||||
_, hasTask := tasksByOldID[tb.TaskID]
|
||||
_, hasBucket := bucketsByOldID[tb.BucketID]
|
||||
if !hasTask || !hasBucket {
|
||||
continue
|
||||
}
|
||||
newTaskBuckets = append(newTaskBuckets, &models.TaskBucket{
|
||||
TaskID: tasksByOldID[tb.TaskID].ID,
|
||||
BucketID: bucketsByOldID[tb.BucketID].ID,
|
||||
ProjectViewID: bucketsByOldID[tb.BucketID].ProjectViewID,
|
||||
})
|
||||
}
|
||||
|
||||
if len(newTaskBuckets) > 0 {
|
||||
_, err = s.In("task_id", newTaskIDs).Delete(&models.TaskBucket{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = s.Insert(newTaskBuckets)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project.Tasks = tasks
|
||||
project.Buckets = originalBuckets
|
||||
|
||||
|
|
|
@ -419,7 +419,7 @@ func persistLabels(s *xorm.Session, a web.Auth, task *models.Task, labels []*mod
|
|||
existingLabels, _, _, err := models.GetLabelsByTaskIDs(s, &models.LabelByTaskIDsOptions{
|
||||
Search: labelTitles,
|
||||
User: u,
|
||||
GetForUser: u.ID,
|
||||
GetForUser: true,
|
||||
GetUnusedLabels: true,
|
||||
GroupByLabelIDsOnly: true,
|
||||
})
|
||||
|
@ -636,7 +636,7 @@ func (vcls *VikunjaCaldavProjectStorage) getProjectRessource(isCollection bool)
|
|||
tk := models.TaskCollection{
|
||||
ProjectID: vcls.project.ID,
|
||||
}
|
||||
iface, _, _, err := tk.ReadAll(s, vcls.user, "", 1, 1000)
|
||||
iface, _, _, err := tk.ReadAll(s, vcls.user, "", 0, -1)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return rr, err
|
||||
|
|
|
@ -156,10 +156,18 @@ func setupSentry(e *echo.Echo) {
|
|||
if hub != nil {
|
||||
hub.WithScope(func(scope *sentry.Scope) {
|
||||
scope.SetExtra("url", c.Request().URL)
|
||||
hub.CaptureException(herr.Internal)
|
||||
if herr.Internal == nil {
|
||||
hub.CaptureException(err)
|
||||
} else {
|
||||
hub.CaptureException(herr.Internal)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
sentry.CaptureException(herr.Internal)
|
||||
if herr.Internal == nil {
|
||||
sentry.CaptureException(err)
|
||||
} else {
|
||||
sentry.CaptureException(herr.Internal)
|
||||
}
|
||||
log.Debugf("Could not add context for sending error '%s' to sentry", err.Error())
|
||||
}
|
||||
log.Debugf("Error '%s' sent to sentry", err.Error())
|
||||
|
|
Loading…
Reference in New Issue