Compare commits

..

1 Commits

Author SHA1 Message Date
renovate 53dd05bf6d chore(deps): update dependency node to v20.12.1
continuous-integration/drone/pr Build is pending Details
2024-04-04 15:09:23 +00:00
47 changed files with 1018 additions and 1055 deletions

View File

@ -139,7 +139,7 @@ steps:
event: [ push, tag, pull_request ]
- name: api-lint
image: golangci/golangci-lint:v1.57.2
image: golangci/golangci-lint:v1.56.2
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
@ -716,7 +716,7 @@ steps:
# Build os packages and push it to our bucket
- name: build-os-packages-unstable
image: goreleaser/nfpm:v2.36.1
image: goreleaser/nfpm:v2.35.3
pull: always
commands:
- apk add git go
@ -732,7 +732,7 @@ steps:
depends_on: [ after-build-compress ]
- name: build-os-packages-version
image: goreleaser/nfpm:v2.36.1
image: goreleaser/nfpm:v2.35.3
pull: always
commands:
- apk add git go
@ -1400,6 +1400,6 @@ steps:
- failure
---
kind: signature
hmac: 2c9cb0483fb346988188515f6423929f46eefb9e14eb26b0f312a0b694d5fe8c
hmac: c312afe632177a2d45f47c429bf6c7528af3c51a097430956558532ccdcc42b9
...

1
.gitignore vendored
View File

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

View File

@ -18,7 +18,6 @@ linters:
- scopelint # Obsolete, using exportloopref instead
- durationcheck
- goconst
- musttag
presets:
- bugs
- unused

View File

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

View File

@ -769,10 +769,10 @@ electron-publish@24.13.1:
lazy-val "^1.0.5"
mime "^2.5.2"
electron@29.2.0:
version "29.2.0"
resolved "https://registry.yarnpkg.com/electron/-/electron-29.2.0.tgz#98e9d45dcebda124fb0bd1ff20fc509ec692101c"
integrity sha512-ALKrCN52RG4g9prx4DriXSPnY5WoiyRUCNp7zEVQuoiNOpHTNqMMpRidQAHzntV4hajF1LMWHVoBkwqIs1jHhg==
electron@29.1.4:
version "29.1.4"
resolved "https://registry.yarnpkg.com/electron/-/electron-29.1.4.tgz#6c47467ba50be5dd60b99b8737f69cd12fc0733f"
integrity sha512-IWXys0SqgmIfrqXusUGQC0gGG7CCqA5vfmNsUMj8dFkAnK3lisKyjSESStWlrsste/OX/AAC5wsVlf23reUNnw==
dependencies:
"@electron/get" "^2.0.0"
"@types/node" "^20.9.0"

View File

@ -35,7 +35,7 @@ That means compiling it boils down to these steps:
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.21`.
2. Make sure [Mage](https://magefile.org) is properly installed on your system.
3. If you did not build the frontend in the steps before, you need to either do that or create a dummy index file with `mkdir -p frontend/dist && touch frontend/dist/index.html`.
3. If you did not build the frontend in the steps before, you need to either do that or create a dummy index file with `mkdir -p frontend/dist && touch index.html`.
4. Run `mage build` in the source of the main repo. This will build a binary in the root of the repo which will be able to run on your system.
### Build for different architectures

View File

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

View File

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

1
frontend/.gitignore vendored
View File

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

View File

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

View File

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1712449641,
"narHash": "sha256-U9DDWMexN6o5Td2DznEgguh8TRIUnIl9levmit43GcI=",
"lastModified": 1701336116,
"narHash": "sha256-kEmpezCR/FpITc6yMbAh4WrOCiT2zg5pSjnKrq51h5Y=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "600b15aea1b36eeb43833a50b0e96579147099ff",
"rev": "f5c27c6136db4d76c30e533c20517df6864c46ee",
"type": "github"
},
"original": {

10
frontend/flake.nix Normal file
View File

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

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@8.15.6",
"packageManager": "pnpm@8.15.5",
"keywords": [
"todo",
"productivity",
@ -50,12 +50,12 @@
"story:preview": "histoire preview"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.5.2",
"@fortawesome/free-regular-svg-icons": "6.5.2",
"@fortawesome/free-solid-svg-icons": "6.5.2",
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-regular-svg-icons": "6.5.1",
"@fortawesome/free-solid-svg-icons": "6.5.1",
"@fortawesome/vue-fontawesome": "3.0.6",
"@github/hotkey": "3.1.0",
"@infectoone/vue-ganttastic": "2.3.2",
"@infectoone/vue-ganttastic": "2.3.1",
"@intlify/unplugin-vue-i18n": "3.0.1",
"@kyvg/vue3-notification": "3.2.1",
"@sentry/tracing": "7.109.0",
@ -132,54 +132,54 @@
"@cypress/vite-dev-server": "5.0.7",
"@cypress/vue": "6.0.0",
"@faker-js/faker": "8.4.1",
"@histoire/plugin-screenshot": "0.17.15",
"@histoire/plugin-vue": "0.17.15",
"@rushstack/eslint-patch": "1.10.1",
"@tsconfig/node18": "18.2.4",
"@histoire/plugin-screenshot": "0.17.14",
"@histoire/plugin-vue": "0.17.14",
"@rushstack/eslint-patch": "1.8.0",
"@tsconfig/node18": "18.2.2",
"@types/codemirror": "5.60.15",
"@types/dompurify": "3.0.5",
"@types/flexsearch": "0.7.6",
"@types/is-touch-device": "1.0.2",
"@types/lodash.debounce": "4.0.9",
"@types/marked": "5.0.2",
"@types/node": "20.12.5",
"@types/node": "20.11.30",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/eslint-plugin": "7.5.0",
"@typescript-eslint/parser": "7.5.0",
"@typescript-eslint/eslint-plugin": "7.3.1",
"@typescript-eslint/parser": "7.3.1",
"@vitejs/plugin-legacy": "5.3.2",
"@vitejs/plugin-vue": "5.0.4",
"@vue/eslint-config-typescript": "13.0.0",
"@vue/test-utils": "2.4.5",
"@vue/tsconfig": "0.5.1",
"autoprefixer": "10.4.19",
"autoprefixer": "10.4.18",
"browserslist": "4.23.0",
"caniuse-lite": "1.0.30001607",
"css-has-pseudo": "6.0.3",
"caniuse-lite": "1.0.30001599",
"css-has-pseudo": "6.0.2",
"csstype": "3.1.3",
"cypress": "13.7.2",
"cypress": "13.7.0",
"esbuild": "0.20.2",
"eslint": "8.57.0",
"eslint-plugin-vue": "9.24.0",
"happy-dom": "14.7.1",
"histoire": "0.17.15",
"postcss": "8.4.38",
"eslint-plugin-vue": "9.23.0",
"happy-dom": "14.0.0",
"histoire": "0.17.14",
"postcss": "8.4.37",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-focus-within": "8.0.1",
"postcss-preset-env": "9.5.4",
"rollup": "4.14.1",
"postcss-preset-env": "9.5.2",
"rollup": "4.13.0",
"rollup-plugin-visualizer": "5.12.0",
"sass": "1.74.1",
"sass": "1.72.0",
"start-server-and-test": "2.0.3",
"typescript": "5.4.4",
"vite": "5.2.8",
"typescript": "5.4.2",
"vite": "5.1.6",
"vite-plugin-inject-preload": "1.3.3",
"vite-plugin-pwa": "0.19.8",
"vite-plugin-pwa": "0.19.5",
"vite-plugin-sentry": "1.4.0",
"vite-svg-loader": "5.1.0",
"vitest": "1.4.0",
"vue-tsc": "2.0.11",
"vue-tsc": "2.0.6",
"wait-on": "7.2.0",
"workbox-cli": "7.0.0"
},

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -58,6 +58,7 @@
<ProjectSettingsDropdown
class="menu-list-dropdown"
:project="project"
:level="level"
>
<template #trigger="{toggleOpen}">
<BaseButton
@ -77,6 +78,7 @@
:model-value="childProjects"
:can-edit-order="true"
:can-collapse="canCollapse"
:level="level + 1"
/>
</li>
</template>
@ -99,10 +101,12 @@ const {
project,
isLoading,
canCollapse,
level = 0,
} = defineProps<{
project: IProject,
isLoading?: boolean,
canCollapse?: boolean,
level?: number,
}>()
const projectStore = useProjectStore()

View File

@ -188,6 +188,12 @@ $modal-width: 1024px;
.info {
font-style: italic;
}
p {
display: flex;
justify-content: space-between;
align-items: center;
}
}
}

View File

@ -91,15 +91,7 @@ const highlightedFilterQuery = computed(() => {
value = ''
}
let endPadding = ''
if(value.endsWith(' ')) {
const fullLength = value.length
value = value.trimEnd()
const numberOfRemovedSpaces = fullLength - value.length
endPadding = endPadding.padEnd(numberOfRemovedSpaces, ' ')
}
return `${o}${spacesBefore}${token}${spacesAfter}<button class="is-primary filter-query__date_value" data-position="${position}">${value}</button><span class="filter-query__date_value_placeholder">${value}</span>${endPadding}`
return `${o}${spacesBefore}${token}${spacesAfter}<button class="is-primary filter-query__date_value" data-position="${position}">${value}</button><span class="filter-query__date_value_placeholder">${value}</span>`
})
})
ASSIGNEE_FIELDS
@ -325,11 +317,6 @@ const blurDebounced = useDebounceFn(() => emit('blur'), 500)
<style lang="scss">
.filter-input-highlight {
&, button.filter-query__date_value {
color: var(--card-color);
}
span {
&.filter-query__field {
color: var(--code-literal);

View File

@ -63,10 +63,6 @@ const filteredProjects = computed(() => {
@media screen and (min-width: $widescreen) {
--project-grid-columns: 5;
.project-grid-item:nth-child(6) {
display: none;
}
}
}

View File

@ -96,6 +96,7 @@
{{ $t('project.webhooks.title') }}
</DropdownItem>
<DropdownItem
v-if="level < 2"
:to="{ name: 'project.createFromParent', params: { parentProjectId: project.id } }"
icon="layer-group"
>
@ -134,6 +135,9 @@ const props = defineProps({
type: Object as PropType<IProject>,
required: true,
},
level: {
type: Number,
},
})
const projectStore = useProjectStore()

View File

@ -41,7 +41,7 @@
@click="() => unCollapseBucket(bucket)"
>
<span
v-if="view?.doneBucketId === bucket.id"
v-if="project?.doneBucketId === bucket.id"
v-tooltip="$t('project.kanban.doneBucketHint')"
class="icon is-small has-text-success mr-2"
>
@ -109,7 +109,7 @@
</DropdownItem>
<DropdownItem
v-tooltip="$t('project.kanban.doneBucketHintExtended')"
:icon-class="{'has-text-success': bucket.id === view?.doneBucketId}"
:icon-class="{'has-text-success': bucket.id === project?.doneBucketId}"
icon="check-double"
@click.stop="toggleDoneBucket(bucket)"
>
@ -117,7 +117,7 @@
</DropdownItem>
<DropdownItem
v-tooltip="$t('project.kanban.defaultBucketHint')"
:icon-class="{'has-text-primary': bucket.id === view?.defaultBucketId}"
:icon-class="{'has-text-primary': bucket.id === project.defaultBucketId}"
icon="th"
@click.stop="toggleDefaultBucket(bucket)"
>
@ -304,8 +304,6 @@ import type {IProjectView} from '@/modelTypes/IProjectView'
import TaskPositionService from '@/services/taskPosition'
import TaskPositionModel from '@/models/taskPosition'
import {i18n} from '@/i18n'
import ProjectViewService from '@/services/projectViews'
import ProjectViewModel from '@/models/projectView'
const {
projectId,
@ -395,8 +393,6 @@ const project = computed(() => projectId ? projectStore.projects[projectId] : nu
const buckets = computed(() => kanbanStore.buckets)
const loading = computed(() => kanbanStore.isLoading)
const view = computed<IProjectView | null>(() => project.value?.views.find(v => v.id === viewId) || null)
const taskLoading = computed(() => taskStore.isLoading || taskPositionService.value.loading)
watch(
@ -705,46 +701,26 @@ function dragstart(bucket: IBucket) {
}
async function toggleDefaultBucket(bucket: IBucket) {
const defaultBucketId = view.value?.defaultBucketId === bucket.id
const defaultBucketId = project.value.defaultBucketId === bucket.id
? 0
: bucket.id
const projectViewService = new ProjectViewService()
const updatedView = await projectViewService.update(new ProjectViewModel({
...view.value,
defaultBucketId,
}))
const views = project.value.views.map(v => v.id === view.value?.id ? updatedView : v)
const updatedProject = {
await projectStore.updateProject({
...project.value,
views,
}
projectStore.setProject(updatedProject)
defaultBucketId,
})
success({message: t('project.kanban.defaultBucketSavedSuccess')})
}
async function toggleDoneBucket(bucket: IBucket) {
const doneBucketId = view.value?.doneBucketId === bucket.id
const doneBucketId = project.value?.doneBucketId === bucket.id
? 0
: bucket.id
const projectViewService = new ProjectViewService()
const updatedView = await projectViewService.update(new ProjectViewModel({
...view.value,
doneBucketId,
}))
const views = project.value.views.map(v => v.id === view.value?.id ? updatedView : v)
const updatedProject = {
await projectStore.updateProject({
...project.value,
views,
}
projectStore.setProject(updatedProject)
doneBucketId,
})
success({message: t('project.kanban.doneBucketSavedSuccess')})
}

View File

@ -107,7 +107,7 @@ describe('Filter Transformation', () => {
expect(transformed).toBe('project = 1')
})
it('should resolve project and labels independently', () => {
const transformed = transformFilterStringForApi(
'project = lorem && labels = ipsum',
@ -117,16 +117,6 @@ describe('Filter Transformation', () => {
expect(transformed).toBe('project = 1 && labels = 2')
})
it('should transform the same attribute multiple times', () => {
const transformed = transformFilterStringForApi(
'dueDate = now/d || dueDate > now/w+1w',
nullTitleToIdResolver,
nullTitleToIdResolver,
)
expect(transformed).toBe('due_date = now/d || due_date > now/w+1w')
})
})
describe('To API', () => {
@ -208,15 +198,5 @@ describe('Filter Transformation', () => {
expect(transformed).toBe('project in lorem, ipsum')
})
it('should transform the same attribute multiple times', () => {
const transformed = transformFilterStringFromApi(
'due_date = now/d || due_date > now/w+1w',
nullIdToTitleResolver,
nullIdToTitleResolver,
)
expect(transformed).toBe('dueDate = now/d || dueDate > now/w+1w')
})
})
})

View File

@ -127,7 +127,7 @@ export function transformFilterStringForApi(
// Transform all attributes to snake case
AVAILABLE_FILTER_FIELDS.forEach(f => {
filter = filter.replaceAll(f, snakeCase(f))
filter = filter.replace(f, snakeCase(f))
})
return filter
@ -145,7 +145,7 @@ export function transformFilterStringFromApi(
// Transform all attributes from snake case
AVAILABLE_FILTER_FIELDS.forEach(f => {
filter = filter.replaceAll(snakeCase(f), f)
filter = filter.replace(snakeCase(f), f)
})
// Transform labels to their titles

View File

@ -20,6 +20,8 @@ export interface IProject extends IAbstract {
position: number
backgroundBlurHash: string
parentProjectId: number
doneBucketId: number
defaultBucketId: number
views: IProjectView[]
created: Date

View File

@ -24,6 +24,8 @@ export default class ProjectModel extends AbstractModel<IProject> implements IPr
position = 0
backgroundBlurHash = ''
parentProjectId = 0
doneBucketId = 0
defaultBucketId = 0
views = []
created: Date = null

View File

@ -2,14 +2,14 @@ import {test, expect, vi} from 'vitest'
import {getHistory, removeProjectFromHistory, saveProjectToHistory} from './projectHistory'
test('return an empty history when none was saved', () => {
vi.spyOn(localStorage, 'getItem').mockImplementation(() => null)
Storage.prototype.getItem = vi.fn(() => null)
const h = getHistory()
expect(h).toStrictEqual([])
})
test('return a saved history', () => {
const saved = [{id: 1}, {id: 2}]
vi.spyOn(localStorage, 'getItem').mockImplementation(() => JSON.stringify(saved))
Storage.prototype.getItem = vi.fn(() => JSON.stringify(saved))
const h = getHistory()
expect(h).toStrictEqual(saved)
@ -17,8 +17,8 @@ test('return a saved history', () => {
test('store project in history', () => {
let saved = {}
vi.spyOn(localStorage, 'getItem').mockImplementation(() => null)
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
Storage.prototype.getItem = vi.fn(() => null)
Storage.prototype.setItem = vi.fn((key, projects) => {
saved = projects
})
@ -26,10 +26,10 @@ test('store project in history', () => {
expect(saved).toBe('[{"id":1}]')
})
test('store only the last 6 projects in history', () => {
test('store only the last 5 projects in history', () => {
let saved: string | null = null
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
Storage.prototype.getItem = vi.fn(() => saved)
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
saved = projects
})
@ -39,14 +39,13 @@ test('store only the last 6 projects in history', () => {
saveProjectToHistory({id: 4})
saveProjectToHistory({id: 5})
saveProjectToHistory({id: 6})
saveProjectToHistory({id: 7})
expect(saved).toBe('[{"id":7},{"id":6},{"id":5},{"id":4},{"id":3},{"id":2}]')
expect(saved).toBe('[{"id":6},{"id":5},{"id":4},{"id":3},{"id":2}]')
})
test('don\'t store the same project twice', () => {
let saved: string | null = null
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
Storage.prototype.getItem = vi.fn(() => saved)
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
saved = projects
})
@ -57,8 +56,8 @@ test('don\'t store the same project twice', () => {
test('move a project to the beginning when storing it multiple times', () => {
let saved: string | null = null
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
Storage.prototype.getItem = vi.fn(() => saved)
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
saved = projects
})
@ -70,11 +69,11 @@ test('move a project to the beginning when storing it multiple times', () => {
test('remove project from history', () => {
let saved: string | null = '[{"id": 1}]'
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
Storage.prototype.getItem = vi.fn(() => null)
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
saved = projects
})
vi.spyOn(localStorage, 'removeItem').mockImplementation((key: string) => {
Storage.prototype.removeItem = vi.fn((key: string) => {
saved = null
})

View File

@ -20,8 +20,6 @@ function saveHistory(history: ProjectHistory[]) {
localStorage.setItem('projectHistory', JSON.stringify(history))
}
const MAX_SAVED_PROJECTS = 6
export function saveProjectToHistory(project: ProjectHistory) {
const history: ProjectHistory[] = getHistory()
@ -35,7 +33,7 @@ export function saveProjectToHistory(project: ProjectHistory) {
// Add the new project to the beginning of the project
history.unshift(project)
if (history.length > MAX_SAVED_PROJECTS) {
if (history.length > 5) {
history.pop()
}
saveHistory(history)

View File

@ -106,12 +106,6 @@ export const useProjectStore = defineStore('project', () => {
}
function removeProjectById(project: IProject) {
// Remove child projects from state as well
projectsArray.value
.filter(p => p.parentProjectId === project.id)
.forEach(p => removeProjectById(p))
remove(project)
delete projects.value[project.id]
}

View File

@ -3,7 +3,7 @@
<h1>{{ $t('migrate.titleService', {name: migrator.name}) }}</h1>
<p>{{ $t('migrate.descriptionDo') }}</p>
<template v-if="message === '' && lastMigrationStartedAt === null && !migrationJustStarted">
<template v-if="message === '' && lastMigrationStartedAt === null">
<template v-if="isMigrating === false">
<template v-if="migrator.isFileMigrator">
<p>{{ $t('migrate.importUpload', {name: migrator.name}) }}</p>

15
go.mod
View File

@ -50,7 +50,6 @@ require (
github.com/lib/pq v1.10.9
github.com/magefile/mage v1.15.0
github.com/mattn/go-sqlite3 v1.14.22
github.com/microcosm-cc/bluemonday v1.0.26
github.com/olekukonko/tablewriter v0.0.5
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pquerna/otp v1.4.0
@ -67,18 +66,18 @@ require (
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
github.com/yuin/goldmark v1.7.0
golang.org/x/crypto v0.21.0
golang.org/x/image v0.15.0
golang.org/x/oauth2 v0.18.0
golang.org/x/sync v0.6.0
golang.org/x/sys v0.19.0
golang.org/x/term v0.19.0
golang.org/x/sys v0.18.0
golang.org/x/term v0.18.0
golang.org/x/text v0.14.0
gopkg.in/d4l3k/messagediff.v1 v1.2.1
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/xurls/v2 v2.5.0
src.techknowlogick.com/xgo v1.7.1-0.20240403232151-e01c4fbef884
src.techknowlogick.com/xgo v1.7.1-0.20240305180710-770b8eae9cec
src.techknowlogick.com/xormigrate v1.7.1
xorm.io/builder v0.3.13
xorm.io/xorm v1.3.9
@ -91,7 +90,6 @@ require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
@ -123,7 +121,6 @@ require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
@ -179,7 +176,7 @@ require (
golang.org/x/arch v0.4.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/appengine v1.6.8 // indirect

18
go.sum
View File

@ -35,8 +35,6 @@ github.com/arran4/golang-ical v0.2.7 h1:VO7YlVaGupZE15aj6NhUhte/MIfZuoIzkoI71VsG
github.com/arran4/golang-ical v0.2.7/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bbrks/go-blurhash v1.1.1 h1:uoXOxRPDca9zHYabUTwvS4KnY++KKUbwFo+Yxb8ME4M=
github.com/bbrks/go-blurhash v1.1.1/go.mod h1:lkAsdyXp+EhARcUo85yS2G1o+Sh43I2ebF5togC4bAY=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
@ -217,8 +215,6 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -376,8 +372,6 @@ github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -539,8 +533,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
@ -583,8 +575,6 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@ -621,8 +611,6 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -666,8 +654,6 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -676,8 +662,6 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -804,8 +788,6 @@ sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
src.techknowlogick.com/xgo v1.7.1-0.20240305180710-770b8eae9cec h1:ICDp83UjJvLcOFWHAxr7vmziKIHJkE4jsIF1mbT9Bwk=
src.techknowlogick.com/xgo v1.7.1-0.20240305180710-770b8eae9cec/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xgo v1.7.1-0.20240403232151-e01c4fbef884 h1:Ttvt8FCpUXfC8r3+LgSPrBUIr/JkHmYQtvmOwEET8qE=
src.techknowlogick.com/xgo v1.7.1-0.20240403232151-e01c4fbef884/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xormigrate v1.7.1 h1:RKGLLUAqJ+zO8iZ7eOc7oLH7f0cs2gfXSZSvBRBHnlY=
src.techknowlogick.com/xormigrate v1.7.1/go.mod h1:YGNBdj8prENlySwIKmfoEXp7ILGjAltyKFXD0qLgD7U=
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=

View File

@ -82,7 +82,6 @@ func init() {
if config.DatabaseType.GetString() == "sqlite" {
_, err = tx.Exec(`
create table buckets_dg_tmp
(
id INTEGER not null
@ -100,8 +99,6 @@ 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 table buckets;
alter table buckets_dg_tmp

View File

@ -78,9 +78,8 @@ func init() {
if view.ViewKind == 3 { // Kanban view
pos := taskBuckets20240315110428{
TaskID: task.ID,
BucketID: task.BucketID,
ProjectViewID: view.ID,
TaskID: task.ID,
BucketID: task.BucketID,
}
_, err = tx.Insert(pos)

View File

@ -1,89 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"code.vikunja.io/api/pkg/log"
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type taskBucket20240406125227 struct {
BucketID int64 `xorm:"bigint not null index"`
TaskID int64 `xorm:"bigint not null index"`
ProjectViewID int64 `xorm:"bigint not null index"`
}
func (taskBucket20240406125227) TableName() string {
return "task_buckets"
}
type bucket20240406125227 struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"bucket"`
ProjectViewID int64 `xorm:"bigint not null" json:"project_view_id" param:"view"`
}
func (bucket20240406125227) TableName() string {
return "buckets"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20240406125227",
Description: "Add correct project_view_id to task_buckets",
Migrate: func(tx *xorm.Engine) error {
buckets := make(map[int64]*bucket20240406125227)
err := tx.Find(&buckets)
if err != nil {
return err
}
tbs := []*taskBucket20240406125227{}
err = tx.Where("project_view_id = 0").Find(&tbs)
if err != nil {
return err
}
if len(tbs) == 0 {
return nil
}
for _, tb := range tbs {
bucket, exists := buckets[tb.BucketID]
if !exists {
log.Debugf("Bucket %d does not exist but has task_buckets relation", tb.BucketID)
continue
}
tb.ProjectViewID = bucket.ProjectViewID
_, err = tx.
Where("task_id = ? AND bucket_id = ?", tb.TaskID, tb.BucketID).
Cols("project_view_id").
Update(tb)
if err != nil {
return err
}
}
return nil
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -77,7 +77,7 @@ func (l *Label) hasAccessToLabel(s *xorm.Session, a web.Auth) (has bool, maxRigh
builder.
Select("id").
From("tasks").
Where(builder.In("project_id", getUserProjectsStatement(u.ID, "", false).Select("l.id"))),
Where(builder.In("project_id", getUserProjectsStatement(nil, u.ID, "", false).Select("l.id"))),
)
ll := &LabelTask{}

View File

@ -22,7 +22,7 @@ import (
"time"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
@ -46,7 +46,7 @@ type LabelTask struct {
}
// TableName makes a pretty table name
func (*LabelTask) TableName() string {
func (LabelTask) TableName() string {
return "label_tasks"
}
@ -84,7 +84,7 @@ func (lt *LabelTask) Delete(s *xorm.Session, _ web.Auth) (err error) {
// @Failure 404 {object} web.HTTPError "The label does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{task}/labels [put]
func (lt *LabelTask) Create(s *xorm.Session, auth web.Auth) (err error) {
func (lt *LabelTask) Create(s *xorm.Session, _ web.Auth) (err error) {
// Check if the label is already added
exists, err := s.Exist(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
if err != nil {
@ -100,20 +100,6 @@ func (lt *LabelTask) Create(s *xorm.Session, auth web.Auth) (err error) {
return err
}
t, err := GetTaskByIDSimple(s, lt.TaskID)
if err != nil {
return err
}
doer, _ := user.GetFromAuth(auth)
err = events.Dispatch(&TaskUpdatedEvent{
Task: &t,
Doer: doer,
})
if err != nil {
return err
}
err = updateProjectByTaskID(s, lt.TaskID)
return
}

View File

@ -17,8 +17,10 @@
package models
import (
"bufio"
"sort"
"strconv"
"strings"
"time"
"code.vikunja.io/api/pkg/utils"
@ -75,16 +77,20 @@ func (n *TaskCommentNotification) SubjectID() int64 {
func (n *TaskCommentNotification) ToMail() *notifications.Mail {
mail := notifications.NewMail().
From(n.Doer.GetNameAndFromEmail()).
Subject("Re: " + n.Task.Title)
From(n.Doer.GetNameAndFromEmail())
subject := "Re: " + n.Task.Title
if n.Mentioned {
mail.
Line("**" + n.Doer.GetName() + "** mentioned you in a comment:").
Subject(n.Doer.GetName() + ` mentioned you in a comment in "` + n.Task.Title + `"`)
subject = n.Doer.GetName() + ` mentioned you in a comment in "` + n.Task.Title + `"`
mail.Line("**" + n.Doer.GetName() + "** mentioned you in a comment:")
}
mail.HTML(n.Comment.Comment)
mail.Subject(subject)
lines := bufio.NewScanner(strings.NewReader(n.Comment.Comment))
for lines.Scan() {
mail.Line(lines.Text())
}
return mail.
Action("View Task", n.Task.GetFrontendURL())
@ -300,8 +306,12 @@ func (n *UserMentionedInTaskNotification) ToMail() *notifications.Mail {
mail := notifications.NewMail().
From(n.Doer.GetNameAndFromEmail()).
Subject(subject).
Line("**" + n.Doer.GetName() + "** mentioned you in a task:").
HTML(n.Task.Description)
Line("**" + n.Doer.GetName() + "** mentioned you in a task:")
lines := bufio.NewScanner(strings.NewReader(n.Task.Description))
for lines.Scan() {
mail.Line(lines.Text())
}
return mail.
Action("View Task", n.Task.GetFrontendURL())

View File

@ -368,7 +368,7 @@ type projectOptions struct {
getArchived bool
}
func getUserProjectsStatement(userID int64, search string, getArchived bool) *builder.Builder {
func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search string, getArchived bool) *builder.Builder {
dialect := db.GetDialect()
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
@ -413,13 +413,18 @@ func getUserProjectsStatement(userID int64, search string, getArchived bool) *bu
),
)
}
projectCol := "id"
if len(parentProjectIDs) > 0 {
parentCondition = builder.In("l.parent_project_id", parentProjectIDs)
projectCol = "parent_project_id"
}
return builder.Dialect(dialect).
Select("l.*").
From("projects", "l").
Join("LEFT", "team_projects tl", "tl.project_id = l.id").
Join("LEFT", "team_projects tl", "tl.project_id = l."+projectCol).
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
Join("LEFT", "users_projects ul", "ul.project_id = l.id").
Join("LEFT", "users_projects ul", "ul.project_id = l."+projectCol).
Where(builder.And(
builder.Or(
builder.Eq{"tm2.user_id": userID},
@ -429,6 +434,7 @@ func getUserProjectsStatement(userID int64, search string, getArchived bool) *bu
filterCond,
getArchivedCond,
parentCondition,
builder.NotIn("l.id", parentProjectIDs),
)).
GroupBy("l.id")
}
@ -436,7 +442,7 @@ func getUserProjectsStatement(userID int64, search string, getArchived bool) *bu
func getAllProjectsForUser(s *xorm.Session, userID int64, opts *projectOptions) (projects []*Project, totalCount int64, err error) {
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
query := getUserProjectsStatement(userID, opts.search, opts.getArchived)
query := getUserProjectsStatement(nil, userID, opts.search, opts.getArchived)
querySQLString, args, err := query.ToSQL()
if err != nil {
@ -453,26 +459,9 @@ UNION ALL
SELECT p.* FROM projects p
INNER JOIN all_projects ap ON p.parent_project_id = ap.id`
columnStr := strings.Join([]string{
"all_projects.id",
"all_projects.title",
"all_projects.description",
"all_projects.identifier",
"all_projects.hex_color",
"all_projects.owner_id",
"CASE WHEN np.id IS NULL THEN 0 ELSE all_projects.parent_project_id END AS parent_project_id",
"all_projects.is_archived",
"all_projects.background_file_id",
"all_projects.background_blur_hash",
"all_projects.position",
"all_projects.created",
"all_projects.updated",
}, ", ")
currentProjects := []*Project{}
err = s.SQL(`WITH RECURSIVE all_projects as (`+baseQuery+`)
SELECT DISTINCT `+columnStr+` FROM all_projects
LEFT JOIN all_projects np on all_projects.parent_project_id = np.id
ORDER BY all_projects.position `+limitSQL, args...).Find(&currentProjects)
SELECT DISTINCT * FROM all_projects ORDER BY position `+limitSQL, args...).Find(&currentProjects)
if err != nil {
return
}

View File

@ -280,13 +280,6 @@ func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, project
if err != nil {
return err
}
err = events.Dispatch(&TaskUpdatedEvent{
Task: t,
Doer: doer,
})
if err != nil {
return err
}
err = updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
return

View File

@ -24,9 +24,10 @@ import (
"strings"
"time"
"github.com/ganigeorgiev/fexpr"
"code.vikunja.io/api/pkg/config"
"github.com/ganigeorgiev/fexpr"
"github.com/iancoleman/strcase"
"github.com/jszwedko/go-datemath"
"xorm.io/xorm/schemas"
@ -154,27 +155,15 @@ func getTaskFiltersFromFilterString(filter string, filterTimezone string) (filte
filter = strings.ReplaceAll(filter, " in ", " ?= ")
// Replaces all occurrences with in with a string so that it passes the filter
pattern := `(\?=\s+([^&|']+))|(([<>]?=|[<>])[^&|')]+\/[^&|')]+([&|')]+))`
pattern := `\?=\s+([^&|']+)`
re := regexp.MustCompile(pattern)
filter = re.ReplaceAllStringFunc(filter, func(match string) string {
comparator := match[:2]
value := strings.TrimSpace(match[2:])
if match[1] == ' ' {
comparator = match[:1]
}
var end string
if value[len(value)-1:] == ")" {
end = ")"
value = value[0 : len(value)-1]
}
value := strings.TrimSpace(strings.TrimPrefix(match, "?="))
value = strings.ReplaceAll(value, "'", `\'`)
enclosedValue := "'" + value + "'"
return comparator + " " + enclosedValue + end
return "?= " + enclosedValue
})
parsedFilter, err := fexpr.Parse(filter)

View File

@ -672,21 +672,9 @@ func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, view *Projec
return
}
// If the task was marked as done and the view has a done bucket, move the task to the done bucket
if task.Done && originalTask != nil &&
(!originalTask.Done || task.ProjectID != originalTask.ProjectID) {
targetBucket.BucketID = view.DoneBucketID
// …and also reset the position so that it shows up at the top
// Note: this might result in an "off-looking" position when there is already a task with position 0.
// This is done by design, because recalculating all positions is really costly and will happen
// later anyway.
_, err = s.
Where("task_id = ? AND project_view_id = ?", task.ID, view.ID).
Cols("position").
Update(&TaskPosition{Position: 0})
if err != nil {
return
}
}
if targetBucket.BucketID == 0 && oldTaskBucket.BucketID != 0 {

View File

@ -143,13 +143,6 @@ func (bp *BackgroundProvider) SetBackground(c echo.Context) error {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
err = project.ReadOne(s, auth)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusOK, project)
}

View File

@ -51,7 +51,7 @@ func (s *MigrationListener) Handle(msg *message.Message) (err error) {
mstr := registeredMigrators[event.MigratorKind]
event.Migrator = mstr.MigrationStruct()
// unmarshalling again to make sure the migrator has the correct type now
// unmarshaling again to make sure the migrator has the correct type now
err = json.Unmarshal(msg.Payload, event)
if err != nil {
return

View File

@ -26,13 +26,8 @@ type Mail struct {
actionText string
actionURL string
greeting string
introLines []*mailLine
outroLines []*mailLine
}
type mailLine struct {
Text string
isHTML bool
introLines []string
outroLines []string
}
// NewMail creates a new mail object with a default greeting
@ -71,28 +66,14 @@ func (m *Mail) Action(text, url string) *Mail {
return m
}
// Line adds a line of Text to the mail
// Line adds a line of text to the mail
func (m *Mail) Line(line string) *Mail {
return m.appendLine(line, false)
}
func (m *Mail) HTML(line string) *Mail {
return m.appendLine(line, true)
}
func (m *Mail) appendLine(line string, isHTML bool) *Mail {
if m.actionURL == "" {
m.introLines = append(m.introLines, &mailLine{
Text: line,
isHTML: isHTML,
})
m.introLines = append(m.introLines, line)
return m
}
m.outroLines = append(m.outroLines, &mailLine{
Text: line,
isHTML: isHTML,
})
m.outroLines = append(m.outroLines, line)
return m
}

View File

@ -23,8 +23,6 @@ import (
templatehtml "html/template"
templatetext "text/template"
"github.com/microcosm-cc/bluemonday"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/mail"
"code.vikunja.io/api/pkg/utils"
@ -35,12 +33,12 @@ import (
const mailTemplatePlain = `
{{ .Greeting }}
{{ range $line := .IntroLines}}
{{ $line.Text }}
{{ $line }}
{{ end }}
{{ if .ActionURL }}{{ .ActionText }}:
{{ .ActionURL }}{{end}}
{{ range $line := .OutroLines}}
{{ $line.Text }}
{{ $line }}
{{ end }}`
const mailTemplateHTML = `
@ -50,9 +48,9 @@ const mailTemplateHTML = `
<meta name="viewport" content="width: display-width;">
</head>
<body style="width: 100%; padding: 0; margin: 0; background: #f3f4f6">
<div style="width: 100%; font-family: 'Open Sans', sans-serif; Text-rendering: optimizeLegibility">
<div style="width: 600px; margin: 0 auto; Text-align: justify;">
<h1 style="font-size: 30px; Text-align: center;">
<div style="width: 100%; font-family: 'Open Sans', sans-serif; text-rendering: optimizeLegibility">
<div style="width: 600px; margin: 0 auto; text-align: justify;">
<h1 style="font-size: 30px; text-align: center;">
<img src="cid:logo.png" style="height: 75px;" alt="Vikunja"/>
</h1>
<div style="border: 1px solid #dbdbdb; -webkit-box-shadow: 0.3em 0.3em 0.8em #e6e6e6; box-shadow: 0.3em 0.3em 0.8em #e6e6e6; color: #4a4a4a; padding: 5px 25px; border-radius: 3px; background: #fff;">
@ -66,7 +64,7 @@ const mailTemplateHTML = `
{{ if .ActionURL }}
<a href="{{ .ActionURL }}" title="{{ .ActionText }}"
style="position: relative;Text-decoration:none;display: block;border-radius: 4px;cursor: pointer;padding-bottom: 8px;padding-left: 14px;padding-right: 14px;padding-top: 8px;width:280px;margin:10px auto;Text-align: center;white-space: nowrap;border: 0;Text-transform: uppercase;font-size: 14px;font-weight: 700;-webkit-box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);background-color: #1973ff;border-color: transparent;color: #fff;">
style="position: relative;text-decoration:none;display: block;border-radius: 4px;cursor: pointer;padding-bottom: 8px;padding-left: 14px;padding-right: 14px;padding-top: 8px;width:280px;margin:10px auto;text-align: center;white-space: nowrap;border: 0;text-transform: uppercase;font-size: 14px;font-weight: 700;-webkit-box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);background-color: #1973ff;border-color: transparent;color: #fff;">
{{ .ActionText }}
</a>
{{end}}
@ -119,43 +117,29 @@ func RenderMail(m *Mail) (mailOpts *mail.Opts, err error) {
data["Boundary"] = boundary
data["FrontendURL"] = config.ServicePublicURL.GetString()
p := bluemonday.UGCPolicy()
var introLinesHTML []templatehtml.HTML
for _, line := range m.introLines {
if line.isHTML {
// #nosec G203 -- the html is sanitized
introLinesHTML = append(introLinesHTML, templatehtml.HTML(p.Sanitize(line.Text)))
continue
}
md := []byte(templatehtml.HTMLEscapeString(line.Text))
md := []byte(templatehtml.HTMLEscapeString(line))
var buf bytes.Buffer
err = goldmark.Convert(md, &buf)
if err != nil {
return nil, err
}
// #nosec G203 -- the html is sanitized
introLinesHTML = append(introLinesHTML, templatehtml.HTML(p.Sanitize(buf.String())))
//#nosec - the html is escaped few lines before
introLinesHTML = append(introLinesHTML, templatehtml.HTML(buf.String()))
}
data["IntroLinesHTML"] = introLinesHTML
var outroLinesHTML []templatehtml.HTML
for _, line := range m.outroLines {
if line.isHTML {
// #nosec G203 -- the html is sanitized
outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(p.Sanitize(line.Text)))
continue
}
md := []byte(templatehtml.HTMLEscapeString(line.Text))
md := []byte(templatehtml.HTMLEscapeString(line))
var buf bytes.Buffer
err = goldmark.Convert(md, &buf)
if err != nil {
return nil, err
}
// #nosec G203 -- the html is sanitized
outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(p.Sanitize(buf.String())))
//#nosec - the html is escaped few lines before
outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(buf.String()))
}
data["OutroLinesHTML"] = outroLinesHTML

View File

@ -41,15 +41,11 @@ func TestNewMail(t *testing.T) {
assert.Equal(t, "Testmail", mail.subject)
assert.Equal(t, "Hi there,", mail.greeting)
assert.Len(t, mail.introLines, 2)
assert.Equal(t, "This is a line", mail.introLines[0].Text)
assert.False(t, mail.introLines[0].isHTML)
assert.Equal(t, "And another one", mail.introLines[1].Text)
assert.False(t, mail.introLines[1].isHTML)
assert.Equal(t, "This is a line", mail.introLines[0])
assert.Equal(t, "And another one", mail.introLines[1])
assert.Len(t, mail.outroLines, 2)
assert.Equal(t, "This should be an outro line", mail.outroLines[0].Text)
assert.False(t, mail.outroLines[0].isHTML)
assert.Equal(t, "And one more, because why not?", mail.outroLines[1].Text)
assert.False(t, mail.outroLines[1].isHTML)
assert.Equal(t, "This should be an outro line", mail.outroLines[0])
assert.Equal(t, "And one more, because why not?", mail.outroLines[1])
})
t.Run("No greeting", func(t *testing.T) {
mail := NewMail().
@ -64,8 +60,8 @@ func TestNewMail(t *testing.T) {
assert.Equal(t, "Testmail", mail.subject)
assert.Equal(t, "", mail.greeting)
assert.Len(t, mail.introLines, 2)
assert.Equal(t, "This is a line", mail.introLines[0].Text)
assert.Equal(t, "And another one", mail.introLines[1].Text)
assert.Equal(t, "This is a line", mail.introLines[0])
assert.Equal(t, "And another one", mail.introLines[1])
})
t.Run("No action", func(t *testing.T) {
mail := NewMail().
@ -81,10 +77,10 @@ func TestNewMail(t *testing.T) {
assert.Equal(t, "test@otherdomain.com", mail.to)
assert.Equal(t, "Testmail", mail.subject)
assert.Len(t, mail.introLines, 4)
assert.Equal(t, "This is a line", mail.introLines[0].Text)
assert.Equal(t, "And another one", mail.introLines[1].Text)
assert.Equal(t, "This should be an outro line", mail.introLines[2].Text)
assert.Equal(t, "And one more, because why not?", mail.introLines[3].Text)
assert.Equal(t, "This is a line", mail.introLines[0])
assert.Equal(t, "And another one", mail.introLines[1])
assert.Equal(t, "This should be an outro line", mail.introLines[2])
assert.Equal(t, "And one more, because why not?", mail.introLines[3])
})
}
@ -129,9 +125,9 @@ And one more, because why not?
<meta name="viewport" content="width: display-width;">
</head>
<body style="width: 100%; padding: 0; margin: 0; background: #f3f4f6">
<div style="width: 100%; font-family: 'Open Sans', sans-serif; Text-rendering: optimizeLegibility">
<div style="width: 600px; margin: 0 auto; Text-align: justify;">
<h1 style="font-size: 30px; Text-align: center;">
<div style="width: 100%; font-family: 'Open Sans', sans-serif; text-rendering: optimizeLegibility">
<div style="width: 600px; margin: 0 auto; text-align: justify;">
<h1 style="font-size: 30px; text-align: center;">
<img src="cid:logo.png" style="height: 75px;" alt="Vikunja"/>
</h1>
<div style="border: 1px solid #dbdbdb; -webkit-box-shadow: 0.3em 0.3em 0.8em #e6e6e6; box-shadow: 0.3em 0.3em 0.8em #e6e6e6; color: #4a4a4a; padding: 5px 25px; border-radius: 3px; background: #fff;">
@ -143,7 +139,7 @@ And one more, because why not?
<p>This is a line</p>
<p>This <strong>line</strong> contains <a href="https://vikunja.io" rel="nofollow">a link</a></p>
<p>This <strong>line</strong> contains <a href="https://vikunja.io">a link</a></p>
<p>And another one</p>
@ -152,7 +148,7 @@ And one more, because why not?
<a href="https://example.com" title="The action"
style="position: relative;Text-decoration:none;display: block;border-radius: 4px;cursor: pointer;padding-bottom: 8px;padding-left: 14px;padding-right: 14px;padding-top: 8px;width:280px;margin:10px auto;Text-align: center;white-space: nowrap;border: 0;Text-transform: uppercase;font-size: 14px;font-weight: 700;-webkit-box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);background-color: #1973ff;border-color: transparent;color: #fff;">
style="position: relative;text-decoration:none;display: block;border-radius: 4px;cursor: pointer;padding-bottom: 8px;padding-left: 14px;padding-right: 14px;padding-top: 8px;width:280px;margin:10px auto;text-align: center;white-space: nowrap;border: 0;text-transform: uppercase;font-size: 14px;font-weight: 700;-webkit-box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);background-color: #1973ff;border-color: transparent;color: #fff;">
The action
</a>