Compare commits
45 Commits
c0f9b19c15
...
212d11ae72
Author | SHA1 | Date |
---|---|---|
kolaente | 212d11ae72 | |
kolaente | 51459f9a52 | |
renovate | c8754b122d | |
kolaente | 4d78ae7fa8 | |
kolaente | c1d06c5e5a | |
kolaente | f1c3ce5eeb | |
kolaente | 2f6b395334 | |
kolaente | 1cd5dd2b2f | |
kolaente | 521300613f | |
kolaente | 037022e857 | |
kolaente | ec1ff80791 | |
kolaente | 7b8fab33a5 | |
kolaente | e0417c8bda | |
kolaente | 6fbd24d5f6 | |
kolaente | e534a6a5bf | |
kolaente | bf85cb0505 | |
kolaente | 20e2314128 | |
kolaente | 1ebb551864 | |
renovate | 30c1a46ed4 | |
kolaente | 1910f69392 | |
renovate | fe4a093825 | |
renovate | 90055d063c | |
waza-ari | f0d695e789 | |
kolaente | 95276ceebe | |
kolaente | 1558921f42 | |
kolaente | bf5088e546 | |
kolaente | 6f366d4907 | |
kolaente | d7554d9e70 | |
kolaente | 8a72fe26f8 | |
kolaente | 13cab62d14 | |
kolaente | 81de986d8d | |
kolaente | 915f677c2a | |
kolaente | 8a6e3d5bd7 | |
kolaente | 81fe8391e4 | |
kolaente | 89e37b88d9 | |
kolaente | cc6801c5b1 | |
kolaente | 767b058915 | |
kolaente | 2c0d3f2885 | |
renovate | fa170b9397 | |
kolaente | a5fd6f834a | |
renovate | 8984e0e9f0 | |
renovate | 176c41dc40 | |
waza-ari | c4d3d99cd4 | |
renovate | 30b4ed6b23 | |
renovate | aed92d1cd2 |
|
@ -461,7 +461,6 @@ steps:
|
|||
- cp -r dist-test dist-preview
|
||||
# Override the default api url used for preview
|
||||
- sed -i 's|http://localhost:3456|https://try.vikunja.io|g' dist-preview/index.html
|
||||
- apk add --no-cache perl-utils
|
||||
# create via:
|
||||
# `shasum -a 384 ./scripts/deploy-preview-netlify.mjs > ./scripts/deploy-preview-netlify.mjs.sha384`
|
||||
- shasum -a 384 -c ./scripts/deploy-preview-netlify.mjs.sha384
|
||||
|
@ -1401,6 +1400,6 @@ steps:
|
|||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: b34dc8d5220db311afb5f214eb3395188be38dcf677bae13bfea5d78ca6df450
|
||||
hmac: ad1d7014fb230dd4ced032ea8995b7a7dc18fcc7cf0805ee38dcbcd6413325af
|
||||
|
||||
...
|
||||
|
|
|
@ -28,3 +28,4 @@ vendor/
|
|||
os-packages/
|
||||
mage_output_file.go
|
||||
mage-static
|
||||
.direnv/
|
||||
|
|
|
@ -51,11 +51,11 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "29.1.4",
|
||||
"electron": "29.2.0",
|
||||
"electron-builder": "24.13.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"express": "4.19.0"
|
||||
"express": "4.19.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -769,10 +769,10 @@ electron-publish@24.13.1:
|
|||
lazy-val "^1.0.5"
|
||||
mime "^2.5.2"
|
||||
|
||||
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==
|
||||
electron@29.2.0:
|
||||
version "29.2.0"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-29.2.0.tgz#98e9d45dcebda124fb0bd1ff20fc509ec692101c"
|
||||
integrity sha512-ALKrCN52RG4g9prx4DriXSPnY5WoiyRUCNp7zEVQuoiNOpHTNqMMpRidQAHzntV4hajF1LMWHVoBkwqIs1jHhg==
|
||||
dependencies:
|
||||
"@electron/get" "^2.0.0"
|
||||
"@types/node" "^20.9.0"
|
||||
|
@ -830,10 +830,10 @@ etag@~1.8.1:
|
|||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
||||
|
||||
express@4.19.0:
|
||||
version "4.19.0"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.19.0.tgz#c9f689a62522f3399132d49eacd9af177d8ccb9e"
|
||||
integrity sha512-/ERliX0l7UuHEgAy7HU2FRsiz3ScIKNl/iwnoYzHTJC0Sqj3ctWDD3MQ9CbUEfjshvxXImWaeukD0Xo7a2lWLA==
|
||||
express@4.19.2:
|
||||
version "4.19.2"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
|
||||
integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
|
||||
dependencies:
|
||||
accepts "~1.3.8"
|
||||
array-flatten "1.1.1"
|
||||
|
|
|
@ -72,6 +72,7 @@ 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
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1701336116,
|
||||
"narHash": "sha256-kEmpezCR/FpITc6yMbAh4WrOCiT2zg5pSjnKrq51h5Y=",
|
||||
"lastModified": 1712449641,
|
||||
"narHash": "sha256-U9DDWMexN6o5Td2DznEgguh8TRIUnIl9levmit43GcI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f5c27c6136db4d76c30e533c20517df6864c46ee",
|
||||
"rev": "600b15aea1b36eeb43833a50b0e96579147099ff",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
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
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
|
@ -13,7 +13,6 @@ node_modules
|
|||
/dist*
|
||||
coverage
|
||||
*.zip
|
||||
.direnv/
|
||||
|
||||
# Test files
|
||||
cypress/screenshots
|
||||
|
|
|
@ -21,7 +21,7 @@ describe('Project View Table', () => {
|
|||
TaskFactory.create(1)
|
||||
cy.visit('/projects/1/3')
|
||||
|
||||
cy.get('.project-table .filter-container .items .button')
|
||||
cy.get('.project-table .filter-container .button')
|
||||
.contains('Columns')
|
||||
.click()
|
||||
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox')
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
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 ]; };
|
||||
};
|
||||
}
|
|
@ -58,8 +58,8 @@
|
|||
"@infectoone/vue-ganttastic": "2.3.1",
|
||||
"@intlify/unplugin-vue-i18n": "3.0.1",
|
||||
"@kyvg/vue3-notification": "3.2.1",
|
||||
"@sentry/tracing": "7.107.0",
|
||||
"@sentry/vue": "7.107.0",
|
||||
"@sentry/tracing": "7.109.0",
|
||||
"@sentry/vue": "7.109.0",
|
||||
"@tiptap/core": "2.2.4",
|
||||
"@tiptap/extension-blockquote": "2.2.4",
|
||||
"@tiptap/extension-bold": "2.2.4",
|
||||
|
@ -103,7 +103,7 @@
|
|||
"camel-case": "4.1.2",
|
||||
"date-fns": "3.6.0",
|
||||
"dayjs": "1.11.10",
|
||||
"dompurify": "3.0.10",
|
||||
"dompurify": "3.0.11",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.31",
|
||||
|
@ -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.14",
|
||||
"@histoire/plugin-vue": "0.17.14",
|
||||
"@rushstack/eslint-patch": "1.8.0",
|
||||
"@tsconfig/node18": "18.2.2",
|
||||
"@histoire/plugin-screenshot": "0.17.15",
|
||||
"@histoire/plugin-vue": "0.17.15",
|
||||
"@rushstack/eslint-patch": "1.10.1",
|
||||
"@tsconfig/node18": "18.2.4",
|
||||
"@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.11.30",
|
||||
"@types/node": "20.12.5",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "7.3.1",
|
||||
"@typescript-eslint/parser": "7.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "7.5.0",
|
||||
"@typescript-eslint/parser": "7.5.0",
|
||||
"@vitejs/plugin-legacy": "5.3.2",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vue/eslint-config-typescript": "13.0.0",
|
||||
"@vue/test-utils": "2.4.5",
|
||||
"@vue/tsconfig": "0.5.1",
|
||||
"autoprefixer": "10.4.18",
|
||||
"autoprefixer": "10.4.19",
|
||||
"browserslist": "4.23.0",
|
||||
"caniuse-lite": "1.0.30001599",
|
||||
"css-has-pseudo": "6.0.2",
|
||||
"caniuse-lite": "1.0.30001606",
|
||||
"css-has-pseudo": "6.0.3",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "13.7.0",
|
||||
"cypress": "13.7.2",
|
||||
"esbuild": "0.20.2",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-vue": "9.23.0",
|
||||
"happy-dom": "14.0.0",
|
||||
"histoire": "0.17.14",
|
||||
"postcss": "8.4.37",
|
||||
"eslint-plugin-vue": "9.24.0",
|
||||
"happy-dom": "14.6.1",
|
||||
"histoire": "0.17.15",
|
||||
"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.2",
|
||||
"rollup": "4.13.0",
|
||||
"postcss-preset-env": "9.5.4",
|
||||
"rollup": "4.14.0",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"sass": "1.72.0",
|
||||
"sass": "1.74.1",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"typescript": "5.4.2",
|
||||
"vite": "5.1.6",
|
||||
"typescript": "5.4.4",
|
||||
"vite": "5.2.8",
|
||||
"vite-plugin-inject-preload": "1.3.3",
|
||||
"vite-plugin-pwa": "0.19.5",
|
||||
"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.6",
|
||||
"vue-tsc": "2.0.10",
|
||||
"wait-on": "7.2.0",
|
||||
"workbox-cli": "7.0.0"
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -26,7 +26,6 @@
|
|||
:project="project"
|
||||
:is-loading="projectUpdating[project.id]"
|
||||
:can-collapse="canCollapse"
|
||||
:level="level"
|
||||
:data-project-id="project.id"
|
||||
/>
|
||||
</template>
|
||||
|
@ -49,7 +48,6 @@ const props = defineProps<{
|
|||
modelValue?: IProject[],
|
||||
canEditOrder: boolean,
|
||||
canCollapse?: boolean,
|
||||
level?: number,
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', projects: IProject[]): void
|
||||
|
|
|
@ -58,7 +58,6 @@
|
|||
<ProjectSettingsDropdown
|
||||
class="menu-list-dropdown"
|
||||
:project="project"
|
||||
:level="level"
|
||||
>
|
||||
<template #trigger="{toggleOpen}">
|
||||
<BaseButton
|
||||
|
@ -78,7 +77,6 @@
|
|||
:model-value="childProjects"
|
||||
:can-edit-order="true"
|
||||
:can-collapse="canCollapse"
|
||||
:level="level + 1"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
|
@ -101,12 +99,10 @@ const {
|
|||
project,
|
||||
isLoading,
|
||||
canCollapse,
|
||||
level = 0,
|
||||
} = defineProps<{
|
||||
project: IProject,
|
||||
isLoading?: boolean,
|
||||
canCollapse?: boolean,
|
||||
level?: number,
|
||||
}>()
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
|
|
|
@ -16,9 +16,11 @@ import {useColorScheme} from '@/composables/useColorScheme'
|
|||
const {
|
||||
entityKind,
|
||||
entityId,
|
||||
disabled = false,
|
||||
} = defineProps<{
|
||||
entityKind: ReactionKind,
|
||||
entityId: number,
|
||||
disabled?: boolean,
|
||||
}>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
@ -143,11 +145,13 @@ async function toggleReaction(value: string) {
|
|||
v-tooltip="getReactionTooltip(users, value)"
|
||||
class="reaction-button"
|
||||
:class="{'current-user-has-reacted': hasCurrentUserReactedWithEmoji(value)}"
|
||||
:disabled
|
||||
@click="toggleReaction(value)"
|
||||
>
|
||||
{{ value }} {{ users.length }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="!disabled"
|
||||
ref="emojiPickerButtonRef"
|
||||
v-tooltip="$t('reaction.add')"
|
||||
class="reaction-button"
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
}"
|
||||
>
|
||||
<template v-if="icon">
|
||||
<icon
|
||||
<icon
|
||||
v-if="showIconOnly"
|
||||
:icon="icon"
|
||||
:style="{'color': iconColor !== '' ? iconColor : undefined}"
|
||||
|
@ -22,7 +22,7 @@
|
|||
v-else
|
||||
class="icon is-small"
|
||||
>
|
||||
<icon
|
||||
<icon
|
||||
:icon="icon"
|
||||
:style="{'color': iconColor !== '' ? iconColor : undefined}"
|
||||
/>
|
||||
|
@ -34,20 +34,20 @@
|
|||
|
||||
<script lang="ts">
|
||||
const BUTTON_TYPES_MAP = {
|
||||
primary: 'is-primary',
|
||||
secondary: 'is-outlined',
|
||||
tertiary: 'is-text is-inverted underline-none',
|
||||
primary: 'is-primary',
|
||||
secondary: 'is-outlined',
|
||||
tertiary: 'is-text is-inverted underline-none',
|
||||
} as const
|
||||
|
||||
export type ButtonTypes = keyof typeof BUTTON_TYPES_MAP
|
||||
|
||||
export default { name: 'XButton' }
|
||||
export default {name: 'XButton'}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, useSlots} from 'vue'
|
||||
import BaseButton, {type BaseButtonProps} from '@/components/base/BaseButton.vue'
|
||||
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
// extending the props of the BaseButton
|
||||
export interface ButtonProps extends /* @vue-ignore */ BaseButtonProps {
|
||||
|
@ -76,37 +76,38 @@ const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'und
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.button {
|
||||
transition: all $transition;
|
||||
border: 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
height: auto;
|
||||
min-height: $button-height;
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: inline-flex;
|
||||
white-space: var(--button-white-space);
|
||||
transition: all $transition;
|
||||
border: 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
height: auto;
|
||||
min-height: $button-height;
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: inline-flex;
|
||||
white-space: var(--button-white-space);
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
&.fullheight {
|
||||
padding-right: 7px;
|
||||
height: 100%;
|
||||
}
|
||||
&.fullheight {
|
||||
padding-right: 7px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.is-active,
|
||||
&.is-focused,
|
||||
&:active,
|
||||
&:focus,
|
||||
&:focus:not(:active) {
|
||||
box-shadow: var(--shadow-xs) !important;
|
||||
}
|
||||
&.is-active,
|
||||
&.is-focused,
|
||||
&:active,
|
||||
&:focus,
|
||||
&:focus:not(:active) {
|
||||
box-shadow: var(--shadow-xs) !important;
|
||||
}
|
||||
|
||||
&.is-primary.is-outlined:hover {
|
||||
color: var(--white);
|
||||
}
|
||||
&.is-primary.is-outlined:hover {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.is-small {
|
||||
|
@ -114,6 +115,6 @@ const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'und
|
|||
}
|
||||
|
||||
.underline-none {
|
||||
text-decoration: none !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
</style>
|
|
@ -188,12 +188,6 @@ $modal-width: 1024px;
|
|||
.info {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,14 @@
|
|||
{{ getProjectTitle(currentProject) }}
|
||||
</h1>
|
||||
|
||||
<div class="switch-view-container d-print-none">
|
||||
<div class="switch-view">
|
||||
<div
|
||||
class="switch-view-container d-print-none"
|
||||
:class="{'is-justify-content-flex-end': views.length === 1}"
|
||||
>
|
||||
<div
|
||||
v-if="views.length > 1"
|
||||
class="switch-view"
|
||||
>
|
||||
<BaseButton
|
||||
v-for="v in views"
|
||||
:key="v.id"
|
||||
|
@ -149,8 +155,14 @@ function getViewTitle(view: IProjectView) {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.switch-view-container {
|
||||
min-height: $switch-view-height;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -162,8 +174,6 @@ function getViewTitle(view: IProjectView) {
|
|||
border-radius: $radius;
|
||||
font-size: .75rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
height: $switch-view-height;
|
||||
margin: 0 auto 1rem;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
|
|
|
@ -91,7 +91,15 @@ const highlightedFilterQuery = computed(() => {
|
|||
value = ''
|
||||
}
|
||||
|
||||
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>`
|
||||
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}`
|
||||
})
|
||||
})
|
||||
ASSIGNEE_FIELDS
|
||||
|
@ -317,6 +325,11 @@ 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);
|
||||
|
@ -379,6 +392,7 @@ const blurDebounced = useDebounceFn(() => emit('blur'), 500)
|
|||
}
|
||||
|
||||
.filter-input-highlight {
|
||||
background: var(--white);
|
||||
height: 2.5em;
|
||||
line-height: 1.5;
|
||||
padding: .5em .75em;
|
||||
|
|
|
@ -96,7 +96,6 @@
|
|||
{{ $t('project.webhooks.title') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-if="level < 2"
|
||||
:to="{ name: 'project.createFromParent', params: { parentProjectId: project.id } }"
|
||||
icon="layer-group"
|
||||
>
|
||||
|
@ -135,9 +134,6 @@ const props = defineProps({
|
|||
type: Object as PropType<IProject>,
|
||||
required: true,
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
},
|
||||
})
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
|
|
|
@ -1,26 +1,10 @@
|
|||
<!-- 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/>. -->
|
||||
|
||||
<template>
|
||||
<ProjectWrapper
|
||||
class="project-gantt"
|
||||
:project-id="filters.projectId"
|
||||
:view
|
||||
:view-id
|
||||
>
|
||||
<template #header>
|
||||
<template #default>
|
||||
<card :has-content="false">
|
||||
<div class="gantt-options">
|
||||
<div class="field">
|
||||
|
@ -61,9 +45,7 @@
|
|||
</Fancycheckbox>
|
||||
</div>
|
||||
</card>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<div class="gantt-chart-container">
|
||||
<card
|
||||
:has-content="false"
|
||||
|
@ -95,7 +77,7 @@ import {useI18n} from 'vue-i18n'
|
|||
import type {RouteLocationNormalized} from 'vue-router'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
|
||||
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
|
||||
|
||||
import Foo from '@/components/misc/flatpickr/Flatpickr.vue'
|
||||
import ProjectWrapper from '@/components/project/ProjectWrapper.vue'
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
@click="() => unCollapseBucket(bucket)"
|
||||
>
|
||||
<span
|
||||
v-if="project?.doneBucketId === bucket.id"
|
||||
v-if="view?.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 === project?.doneBucketId}"
|
||||
:icon-class="{'has-text-success': bucket.id === view?.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 === project.defaultBucketId}"
|
||||
:icon-class="{'has-text-primary': bucket.id === view?.defaultBucketId}"
|
||||
icon="th"
|
||||
@click.stop="toggleDefaultBucket(bucket)"
|
||||
>
|
||||
|
@ -304,6 +304,8 @@ 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,
|
||||
|
@ -393,6 +395,8 @@ 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(
|
||||
|
@ -701,26 +705,46 @@ function dragstart(bucket: IBucket) {
|
|||
}
|
||||
|
||||
async function toggleDefaultBucket(bucket: IBucket) {
|
||||
const defaultBucketId = project.value.defaultBucketId === bucket.id
|
||||
const defaultBucketId = view.value?.defaultBucketId === bucket.id
|
||||
? 0
|
||||
: bucket.id
|
||||
|
||||
await projectStore.updateProject({
|
||||
...project.value,
|
||||
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 = {
|
||||
...project.value,
|
||||
views,
|
||||
}
|
||||
|
||||
projectStore.setProject(updatedProject)
|
||||
|
||||
success({message: t('project.kanban.defaultBucketSavedSuccess')})
|
||||
}
|
||||
|
||||
async function toggleDoneBucket(bucket: IBucket) {
|
||||
const doneBucketId = project.value?.doneBucketId === bucket.id
|
||||
const doneBucketId = view.value?.doneBucketId === bucket.id
|
||||
? 0
|
||||
: bucket.id
|
||||
|
||||
await projectStore.updateProject({
|
||||
...project.value,
|
||||
|
||||
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 = {
|
||||
...project.value,
|
||||
views,
|
||||
}
|
||||
|
||||
projectStore.setProject(updatedProject)
|
||||
|
||||
success({message: t('project.kanban.doneBucketSavedSuccess')})
|
||||
}
|
||||
|
||||
|
|
|
@ -6,69 +6,68 @@
|
|||
>
|
||||
<template #header>
|
||||
<div class="filter-container">
|
||||
<div class="items">
|
||||
<Popup>
|
||||
<template #trigger="{toggle}">
|
||||
<x-button
|
||||
icon="th"
|
||||
variant="secondary"
|
||||
@click.prevent.stop="toggle()"
|
||||
>
|
||||
{{ $t('project.table.columns') }}
|
||||
</x-button>
|
||||
</template>
|
||||
<template #content="{isOpen}">
|
||||
<card
|
||||
class="columns-filter"
|
||||
:class="{'is-open': isOpen}"
|
||||
>
|
||||
<Fancycheckbox v-model="activeColumns.index">
|
||||
#
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.done">
|
||||
{{ $t('task.attributes.done') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.title">
|
||||
{{ $t('task.attributes.title') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.priority">
|
||||
{{ $t('task.attributes.priority') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.labels">
|
||||
{{ $t('task.attributes.labels') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.assignees">
|
||||
{{ $t('task.attributes.assignees') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.dueDate">
|
||||
{{ $t('task.attributes.dueDate') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.startDate">
|
||||
{{ $t('task.attributes.startDate') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.endDate">
|
||||
{{ $t('task.attributes.endDate') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.percentDone">
|
||||
{{ $t('task.attributes.percentDone') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.doneAt">
|
||||
{{ $t('task.attributes.doneAt') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.created">
|
||||
{{ $t('task.attributes.created') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.updated">
|
||||
{{ $t('task.attributes.updated') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.createdBy">
|
||||
{{ $t('task.attributes.createdBy') }}
|
||||
</Fancycheckbox>
|
||||
</card>
|
||||
</template>
|
||||
</Popup>
|
||||
<FilterPopup v-model="params" />
|
||||
</div>
|
||||
<Popup>
|
||||
<template #trigger="{toggle}">
|
||||
<x-button
|
||||
icon="th"
|
||||
variant="secondary"
|
||||
class="mr-2"
|
||||
@click.prevent.stop="toggle()"
|
||||
>
|
||||
{{ $t('project.table.columns') }}
|
||||
</x-button>
|
||||
</template>
|
||||
<template #content="{isOpen}">
|
||||
<card
|
||||
class="columns-filter"
|
||||
:class="{'is-open': isOpen}"
|
||||
>
|
||||
<Fancycheckbox v-model="activeColumns.index">
|
||||
#
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.done">
|
||||
{{ $t('task.attributes.done') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.title">
|
||||
{{ $t('task.attributes.title') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.priority">
|
||||
{{ $t('task.attributes.priority') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.labels">
|
||||
{{ $t('task.attributes.labels') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.assignees">
|
||||
{{ $t('task.attributes.assignees') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.dueDate">
|
||||
{{ $t('task.attributes.dueDate') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.startDate">
|
||||
{{ $t('task.attributes.startDate') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.endDate">
|
||||
{{ $t('task.attributes.endDate') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.percentDone">
|
||||
{{ $t('task.attributes.percentDone') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.doneAt">
|
||||
{{ $t('task.attributes.doneAt') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.created">
|
||||
{{ $t('task.attributes.created') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.updated">
|
||||
{{ $t('task.attributes.updated') }}
|
||||
</Fancycheckbox>
|
||||
<Fancycheckbox v-model="activeColumns.createdBy">
|
||||
{{ $t('task.attributes.createdBy') }}
|
||||
</Fancycheckbox>
|
||||
</card>
|
||||
</template>
|
||||
</Popup>
|
||||
<FilterPopup v-model="params" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -397,4 +396,8 @@ const taskDetailRoutes = computed(() => Object.fromEntries(
|
|||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.filter-container :deep(.popup) {
|
||||
top: 7rem;
|
||||
}
|
||||
</style>
|
|
@ -2,12 +2,65 @@
|
|||
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||
import XButton from '@/components/input/button.vue'
|
||||
import FilterInput from '@/components/project/partials/FilterInput.vue'
|
||||
import {ref} from 'vue'
|
||||
import {ref, watch} from 'vue'
|
||||
import {transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
} = defineProps<{
|
||||
modelValue: IProjectView,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const view = ref<IProjectView>()
|
||||
|
||||
const labelStore = useLabelStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
watch(
|
||||
() => modelValue,
|
||||
newValue => {
|
||||
const transformed = {
|
||||
...newValue,
|
||||
filter: transformFilterStringFromApi(
|
||||
newValue.filter,
|
||||
labelId => labelStore.getLabelById(labelId)?.title,
|
||||
projectId => projectStore.projects[projectId]?.title || null,
|
||||
),
|
||||
}
|
||||
|
||||
if (JSON.stringify(view.value) !== JSON.stringify(transformed)) {
|
||||
view.value = transformed
|
||||
}
|
||||
},
|
||||
{immediate: true, deep: true},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => view.value,
|
||||
newView => {
|
||||
emit('update:modelValue', {
|
||||
...newView,
|
||||
filter: transformFilterStringForApi(
|
||||
newView.filter,
|
||||
labelTitle => labelStore.filterLabelsByQuery([], labelTitle)[0]?.id || null,
|
||||
projectTitle => {
|
||||
const found = projectStore.findProjectByExactname(projectTitle)
|
||||
return found?.id || null
|
||||
},
|
||||
),
|
||||
})
|
||||
},
|
||||
{deep: true},
|
||||
)
|
||||
|
||||
const model = defineModel<IProjectView>()
|
||||
const titleValid = ref(true)
|
||||
|
||||
function validateTitle() {
|
||||
titleValid.value = model.value.title !== ''
|
||||
titleValid.value = view.value?.title !== ''
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -23,14 +76,14 @@ function validateTitle() {
|
|||
<div class="control">
|
||||
<input
|
||||
id="title"
|
||||
v-model="model.title"
|
||||
v-model="view.title"
|
||||
v-focus
|
||||
class="input"
|
||||
:placeholder="$t('project.share.links.namePlaceholder')"
|
||||
@blur="validateTitle"
|
||||
>
|
||||
</div>
|
||||
<p
|
||||
<p
|
||||
v-if="!titleValid"
|
||||
class="help is-danger"
|
||||
>
|
||||
|
@ -49,7 +102,7 @@ function validateTitle() {
|
|||
<div class="select">
|
||||
<select
|
||||
id="kind"
|
||||
v-model="model.viewKind"
|
||||
v-model="view.viewKind"
|
||||
>
|
||||
<option value="list">
|
||||
{{ $t('project.list.title') }}
|
||||
|
@ -69,12 +122,12 @@ function validateTitle() {
|
|||
</div>
|
||||
|
||||
<FilterInput
|
||||
v-model="model.filter"
|
||||
v-model="view.filter"
|
||||
:input-label="$t('project.views.filter')"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="model.viewKind === 'kanban'"
|
||||
v-if="view.viewKind === 'kanban'"
|
||||
class="field"
|
||||
>
|
||||
<label
|
||||
|
@ -87,7 +140,7 @@ function validateTitle() {
|
|||
<div class="select">
|
||||
<select
|
||||
id="configMode"
|
||||
v-model="model.bucketConfigurationMode"
|
||||
v-model="view.bucketConfigurationMode"
|
||||
>
|
||||
<option value="manual">
|
||||
{{ $t('project.views.bucketConfigManual') }}
|
||||
|
@ -101,7 +154,7 @@ function validateTitle() {
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="model.viewKind === 'kanban' && model.bucketConfigurationMode === 'filter'"
|
||||
v-if="view.viewKind === 'kanban' && view.bucketConfigurationMode === 'filter'"
|
||||
class="field"
|
||||
>
|
||||
<label class="label">
|
||||
|
@ -109,13 +162,13 @@ function validateTitle() {
|
|||
</label>
|
||||
<div class="control">
|
||||
<div
|
||||
v-for="(b, index) in model.bucketConfiguration"
|
||||
v-for="(b, index) in view.bucketConfiguration"
|
||||
:key="'bucket_'+index"
|
||||
class="filter-bucket"
|
||||
>
|
||||
<button
|
||||
class="is-danger"
|
||||
@click.prevent="() => model.bucketConfiguration.splice(index, 1)"
|
||||
@click.prevent="() => view.bucketConfiguration.splice(index, 1)"
|
||||
>
|
||||
<icon icon="trash-alt" />
|
||||
</button>
|
||||
|
@ -130,7 +183,7 @@ function validateTitle() {
|
|||
<div class="control">
|
||||
<input
|
||||
:id="'bucket_'+index+'_title'"
|
||||
v-model="model.bucketConfiguration[index].title"
|
||||
v-model="view.bucketConfiguration[index].title"
|
||||
class="input"
|
||||
:placeholder="$t('project.share.links.namePlaceholder')"
|
||||
>
|
||||
|
@ -138,7 +191,7 @@ function validateTitle() {
|
|||
</div>
|
||||
|
||||
<FilterInput
|
||||
v-model="model.bucketConfiguration[index].filter"
|
||||
v-model="view.bucketConfiguration[index].filter"
|
||||
:input-label="$t('project.views.filter')"
|
||||
/>
|
||||
</div>
|
||||
|
@ -147,7 +200,7 @@ function validateTitle() {
|
|||
<XButton
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
@click="() => model.bucketConfiguration.push({title: '', filter: ''})"
|
||||
@click="() => view.bucketConfiguration.push({title: '', filter: ''})"
|
||||
>
|
||||
{{ $t('project.kanban.addBucket') }}
|
||||
</XButton>
|
||||
|
|
|
@ -16,7 +16,22 @@
|
|||
:search-results="found"
|
||||
:label="searchLabel"
|
||||
@search="find"
|
||||
/>
|
||||
>
|
||||
<template #searchResult="{option: result}">
|
||||
<User
|
||||
v-if="shareType === 'user'"
|
||||
:avatar-size="24"
|
||||
:show-username="true"
|
||||
:user="result"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="search-result"
|
||||
>
|
||||
{{ result.name }}
|
||||
</span>
|
||||
</template>
|
||||
</Multiselect>
|
||||
</p>
|
||||
<p class="control">
|
||||
<x-button @click="add()">
|
||||
|
@ -173,6 +188,7 @@ import Nothing from '@/components/misc/nothing.vue'
|
|||
import {success} from '@/message'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
import User from '@/components/misc/user.vue'
|
||||
|
||||
// FIXME: I think this whole thing can now only manage user/team sharing for projects? Maybe remove a little generalization?
|
||||
|
||||
|
|
|
@ -21,12 +21,12 @@
|
|||
@dragendBar="updateGanttTask"
|
||||
@dblclickBar="openTask"
|
||||
>
|
||||
<template #timeunit="{value, date}">
|
||||
<template #timeunit="{date}">
|
||||
<div
|
||||
class="timeunit-wrapper"
|
||||
:class="{'today': dateIsToday(date)}"
|
||||
>
|
||||
<span>{{ value }}</span>
|
||||
<span>{{ date.getDate() }}</span>
|
||||
<span class="weekday">
|
||||
{{ weekDayFromDate(date) }}
|
||||
</span>
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
class="mt-2"
|
||||
entity-kind="comments"
|
||||
:entity-id="c.id"
|
||||
:disabled="!canWrite"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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,6 +117,16 @@ 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', () => {
|
||||
|
@ -198,5 +208,15 @@ 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -27,13 +27,13 @@ export const AUTOCOMPLETE_FIELDS = [
|
|||
]
|
||||
|
||||
export const AVAILABLE_FILTER_FIELDS = [
|
||||
'done',
|
||||
'priority',
|
||||
'percentDone',
|
||||
...DATE_FIELDS,
|
||||
...ASSIGNEE_FIELDS,
|
||||
...LABEL_FIELDS,
|
||||
...PROJECT_FIELDS,
|
||||
'done',
|
||||
'priority',
|
||||
'percentDone',
|
||||
]
|
||||
|
||||
export const FILTER_OPERATORS = [
|
||||
|
@ -127,7 +127,7 @@ export function transformFilterStringForApi(
|
|||
|
||||
// Transform all attributes to snake case
|
||||
AVAILABLE_FILTER_FIELDS.forEach(f => {
|
||||
filter = filter.replace(f, snakeCase(f))
|
||||
filter = filter.replaceAll(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.replace(snakeCase(f), f)
|
||||
filter = filter.replaceAll(snakeCase(f), f)
|
||||
})
|
||||
|
||||
// Transform labels to their titles
|
||||
|
|
|
@ -20,8 +20,6 @@ export interface IProject extends IAbstract {
|
|||
position: number
|
||||
backgroundBlurHash: string
|
||||
parentProjectId: number
|
||||
doneBucketId: number
|
||||
defaultBucketId: number
|
||||
views: IProjectView[]
|
||||
|
||||
created: Date
|
||||
|
|
|
@ -24,8 +24,6 @@ export default class ProjectModel extends AbstractModel<IProject> implements IPr
|
|||
position = 0
|
||||
backgroundBlurHash = ''
|
||||
parentProjectId = 0
|
||||
doneBucketId = 0
|
||||
defaultBucketId = 0
|
||||
views = []
|
||||
|
||||
created: Date = null
|
||||
|
|
|
@ -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', () => {
|
||||
Storage.prototype.getItem = vi.fn(() => null)
|
||||
vi.spyOn(localStorage, 'getItem').mockImplementation(() => null)
|
||||
const h = getHistory()
|
||||
expect(h).toStrictEqual([])
|
||||
})
|
||||
|
||||
test('return a saved history', () => {
|
||||
const saved = [{id: 1}, {id: 2}]
|
||||
Storage.prototype.getItem = vi.fn(() => JSON.stringify(saved))
|
||||
vi.spyOn(localStorage, 'getItem').mockImplementation(() => 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 = {}
|
||||
Storage.prototype.getItem = vi.fn(() => null)
|
||||
Storage.prototype.setItem = vi.fn((key, projects) => {
|
||||
vi.spyOn(localStorage, 'getItem').mockImplementation(() => null)
|
||||
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
|
||||
saved = projects
|
||||
})
|
||||
|
||||
|
@ -28,8 +28,8 @@ test('store project in history', () => {
|
|||
|
||||
test('store only the last 5 projects in history', () => {
|
||||
let saved: string | null = null
|
||||
Storage.prototype.getItem = vi.fn(() => saved)
|
||||
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
|
||||
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
|
||||
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
|
||||
saved = projects
|
||||
})
|
||||
|
||||
|
@ -44,8 +44,8 @@ test('store only the last 5 projects in history', () => {
|
|||
|
||||
test('don\'t store the same project twice', () => {
|
||||
let saved: string | null = null
|
||||
Storage.prototype.getItem = vi.fn(() => saved)
|
||||
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
|
||||
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
|
||||
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
|
||||
saved = projects
|
||||
})
|
||||
|
||||
|
@ -56,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
|
||||
Storage.prototype.getItem = vi.fn(() => saved)
|
||||
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
|
||||
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
|
||||
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
|
||||
saved = projects
|
||||
})
|
||||
|
||||
|
@ -69,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}]'
|
||||
Storage.prototype.getItem = vi.fn(() => null)
|
||||
Storage.prototype.setItem = vi.fn((key: string, projects: string) => {
|
||||
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
|
||||
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
|
||||
saved = projects
|
||||
})
|
||||
Storage.prototype.removeItem = vi.fn((key: string) => {
|
||||
vi.spyOn(localStorage, 'removeItem').mockImplementation((key: string) => {
|
||||
saved = null
|
||||
})
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ export function getDefaultTaskFilterParams(): TaskFilterParams {
|
|||
return {
|
||||
sort_by: ['position', 'id'],
|
||||
order_by: ['asc', 'desc'],
|
||||
filter: 'done = false',
|
||||
filter: '',
|
||||
filter_include_nulls: false,
|
||||
filter_timezone: '',
|
||||
s: '',
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
@import "tooltip";
|
||||
@import "labels";
|
||||
@import "project";
|
||||
@import "task";
|
||||
@import "tasks";
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
// FIXME: should be a component <FilterContainer>
|
||||
// used in
|
||||
// - Kanban.vue
|
||||
// - Project.vue
|
||||
// - Table.vue
|
||||
|
||||
$filter-container-top-default: -59px;
|
||||
$filter-container-top-link-share-gantt: -133px;
|
||||
$filter-container-top-link-share-list: -47px;
|
||||
|
||||
.filter-container {
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
min-width: 400px;
|
||||
max-width: 180px;
|
||||
position: absolute;
|
||||
right: 1.5rem;
|
||||
margin-top: $filter-container-top-default;
|
||||
z-index: 4;
|
||||
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.button:not(:last-of-type) {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
height: $switch-view-height;
|
||||
}
|
||||
|
||||
.card {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
position: static;
|
||||
margin: 0 0 1rem 0 !important;
|
||||
max-width: 100%;
|
||||
min-width: auto;
|
||||
|
||||
.items {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
|
||||
.control:first-child {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.link-share-container .gantt-chart-container .filter-container,
|
||||
.gantt-chart-container .filter-container {
|
||||
right: 0;
|
||||
margin-top: calc(#{$filter-container-top-link-share-gantt - 2} - 7rem);
|
||||
}
|
||||
|
||||
.link-share-container .gantt-chart-container .filter-container {
|
||||
margin-top: calc(#{$filter-container-top-link-share-gantt} - 5rem);
|
||||
}
|
||||
|
||||
.link-share-container .list-view .filter-container {
|
||||
margin-top: $filter-container-top-link-share-list - 10px;
|
||||
}
|
||||
|
||||
.link-share-container.project\.table-view,
|
||||
.link-share-container.project\.list-view {
|
||||
.filter-container {
|
||||
right: 9rem;
|
||||
margin-top: $filter-container-top-default;
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ const currentView = computed(() => {
|
|||
})
|
||||
|
||||
function redirectToFirstViewIfNecessary() {
|
||||
if (viewId === 0) {
|
||||
if (viewId === 0 || !projectStore.projects[projectId]?.views.find(v => v.id === viewId)) {
|
||||
// Ideally, we would do that in the router redirect, but the projects (and therefore, the views)
|
||||
// are not always loaded then.
|
||||
const firstViewId = projectStore.projects[projectId]?.views[0].id
|
||||
|
|
|
@ -89,7 +89,7 @@ async function saveView() {
|
|||
v-model="newView"
|
||||
class="mb-4"
|
||||
/>
|
||||
<div class="is-flex is-justify-content-end">
|
||||
<div class="is-flex is-justify-content-end mb-4">
|
||||
<XButton
|
||||
:loading="projectViewService.loading"
|
||||
@click="createView"
|
||||
|
|
|
@ -328,6 +328,7 @@
|
|||
entity-kind="tasks"
|
||||
:entity-id="task.id"
|
||||
class="details"
|
||||
:disabled="!canWrite"
|
||||
/>
|
||||
|
||||
<!-- Attachments -->
|
||||
|
|
4
go.mod
4
go.mod
|
@ -33,7 +33,7 @@ require (
|
|||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/ganigeorgiev/fexpr v0.4.0
|
||||
github.com/getsentry/sentry-go v0.27.0
|
||||
github.com/go-sql-driver/mysql v1.8.0
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.10.0
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
|
@ -77,7 +77,7 @@ require (
|
|||
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.20240305180710-770b8eae9cec
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20240403232151-e01c4fbef884
|
||||
src.techknowlogick.com/xormigrate v1.7.1
|
||||
xorm.io/builder v0.3.13
|
||||
xorm.io/xorm v1.3.9
|
||||
|
|
4
go.sum
4
go.sum
|
@ -158,6 +158,8 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
|
|||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
|
||||
github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.10.0 h1:BrBwN7AuC+74g5qtk9D59TLGOaEa8Bw1WmIsf+SyzWc=
|
||||
|
@ -786,6 +788,8 @@ 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=
|
||||
|
|
|
@ -78,8 +78,9 @@ func init() {
|
|||
if view.ViewKind == 3 { // Kanban view
|
||||
|
||||
pos := taskBuckets20240315110428{
|
||||
TaskID: task.ID,
|
||||
BucketID: task.BucketID,
|
||||
TaskID: task.ID,
|
||||
BucketID: task.BucketID,
|
||||
ProjectViewID: view.ID,
|
||||
}
|
||||
|
||||
_, err = tx.Insert(pos)
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
// 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 (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type projectView20240329170952 struct {
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"view"`
|
||||
Filter string `xorm:"text null default null" query:"filter" json:"filter"`
|
||||
ViewKind int `xorm:"not null" json:"view_kind"`
|
||||
}
|
||||
|
||||
func (projectView20240329170952) TableName() string {
|
||||
return "project_views"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20240329170952",
|
||||
Description: "Update default filter for list views to hide completed tasks",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
|
||||
// Update the filter for all list views to hide completed tasks unless the filter is already set
|
||||
_, err := tx.Where("view_kind = ? AND filter = ?", 0, "").Cols("filter").Update(&projectView20240329170952{Filter: "done = false"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
// 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
|
||||
},
|
||||
})
|
||||
}
|
|
@ -384,27 +384,27 @@ func (b *Bucket) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
|
||||
// Get the default bucket
|
||||
p, err := GetProjectViewByIDAndProject(s, b.ProjectViewID, b.ProjectID)
|
||||
pv, err := GetProjectViewByIDAndProject(s, b.ProjectViewID, b.ProjectID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var updateProject bool
|
||||
if b.ID == p.DefaultBucketID {
|
||||
p.DefaultBucketID = 0
|
||||
updateProject = true
|
||||
var updateProjectView bool
|
||||
if b.ID == pv.DefaultBucketID {
|
||||
pv.DefaultBucketID = 0
|
||||
updateProjectView = true
|
||||
}
|
||||
if b.ID == p.DoneBucketID {
|
||||
p.DoneBucketID = 0
|
||||
updateProject = true
|
||||
if b.ID == pv.DoneBucketID {
|
||||
pv.DoneBucketID = 0
|
||||
updateProjectView = true
|
||||
}
|
||||
if updateProject {
|
||||
err = p.Update(s, a)
|
||||
if updateProjectView {
|
||||
err = pv.Update(s, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
defaultBucketID, err := getDefaultBucketID(s, p)
|
||||
defaultBucketID, err := getDefaultBucketID(s, pv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -216,10 +216,10 @@ func TestBucket_Delete(t *testing.T) {
|
|||
err := b.Delete(s, u)
|
||||
require.NoError(t, err)
|
||||
|
||||
db.AssertMissing(t, "project_views", map[string]interface{}{
|
||||
db.AssertExists(t, "project_views", map[string]interface{}{
|
||||
"id": b.ProjectViewID,
|
||||
"done_bucket_id": 0,
|
||||
})
|
||||
}, false)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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(nil, u.ID, "", false).Select("l.id"))),
|
||||
Where(builder.In("project_id", getUserProjectsStatement(u.ID, "", false).Select("l.id"))),
|
||||
)
|
||||
|
||||
ll := &LabelTask{}
|
||||
|
|
|
@ -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, _ web.Auth) (err error) {
|
||||
func (lt *LabelTask) Create(s *xorm.Session, auth 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,6 +100,20 @@ func (lt *LabelTask) Create(s *xorm.Session, _ 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
|
||||
}
|
||||
|
|
|
@ -368,7 +368,7 @@ type projectOptions struct {
|
|||
getArchived bool
|
||||
}
|
||||
|
||||
func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search string, getArchived bool) *builder.Builder {
|
||||
func getUserProjectsStatement(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,18 +413,13 @@ func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search str
|
|||
),
|
||||
)
|
||||
}
|
||||
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."+projectCol).
|
||||
Join("LEFT", "team_projects tl", "tl.project_id = l.id").
|
||||
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
|
||||
Join("LEFT", "users_projects ul", "ul.project_id = l."+projectCol).
|
||||
Join("LEFT", "users_projects ul", "ul.project_id = l.id").
|
||||
Where(builder.And(
|
||||
builder.Or(
|
||||
builder.Eq{"tm2.user_id": userID},
|
||||
|
@ -434,7 +429,6 @@ func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search str
|
|||
filterCond,
|
||||
getArchivedCond,
|
||||
parentCondition,
|
||||
builder.NotIn("l.id", parentProjectIDs),
|
||||
)).
|
||||
GroupBy("l.id")
|
||||
}
|
||||
|
@ -442,7 +436,7 @@ func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search str
|
|||
func getAllProjectsForUser(s *xorm.Session, userID int64, opts *projectOptions) (projects []*Project, totalCount int64, err error) {
|
||||
|
||||
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
||||
query := getUserProjectsStatement(nil, userID, opts.search, opts.getArchived)
|
||||
query := getUserProjectsStatement(userID, opts.search, opts.getArchived)
|
||||
|
||||
querySQLString, args, err := query.ToSQL()
|
||||
if err != nil {
|
||||
|
@ -459,9 +453,26 @@ 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 * FROM all_projects ORDER BY position `+limitSQL, args...).Find(¤tProjects)
|
||||
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(¤tProjects)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -991,7 +1002,12 @@ func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
return p.ReadOne(s, a)
|
||||
fullProject, err := GetProjectSimpleByID(s, p.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return fullProject.ReadOne(s, a)
|
||||
}
|
||||
|
||||
func (p *Project) isDefaultProject(s *xorm.Session) (is bool, err error) {
|
||||
|
|
|
@ -331,11 +331,19 @@ func (p *ProjectView) Update(s *xorm.Session, _ web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
_, err = s.ID(p.ID).Update(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
ID(p.ID).
|
||||
Cols(
|
||||
"title",
|
||||
"view_kind",
|
||||
"filter",
|
||||
"position",
|
||||
"bucket_configuration_mode",
|
||||
"bucket_configuration",
|
||||
"default_bucket_id",
|
||||
"done_bucket_id",
|
||||
).
|
||||
Update(p)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -383,6 +391,7 @@ func CreateDefaultViewsForProject(s *xorm.Session, project *Project, a web.Auth,
|
|||
Title: "List",
|
||||
ViewKind: ProjectViewKindList,
|
||||
Position: 100,
|
||||
Filter: "done = false",
|
||||
}
|
||||
err = createProjectView(s, list, a, createBacklogBucket)
|
||||
if err != nil {
|
||||
|
|
|
@ -280,6 +280,13 @@ 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
|
||||
|
|
|
@ -24,10 +24,9 @@ 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"
|
||||
|
@ -155,15 +154,27 @@ 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 {
|
||||
value := strings.TrimSpace(strings.TrimPrefix(match, "?="))
|
||||
|
||||
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.ReplaceAll(value, "'", `\'`)
|
||||
enclosedValue := "'" + value + "'"
|
||||
|
||||
return "?= " + enclosedValue
|
||||
return comparator + " " + enclosedValue + end
|
||||
})
|
||||
|
||||
parsedFilter, err := fexpr.Parse(filter)
|
||||
|
|
|
@ -672,9 +672,21 @@ 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 {
|
||||
|
|
Loading…
Reference in New Issue