Compare commits

..

8 Commits

Author SHA1 Message Date
renovate d06cfc3d70 chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is failing Details
2024-04-02 12:07:58 +00:00
kolaente 6f366d4907
feat(views): lint
continuous-integration/drone/push Build is failing Details
2024-04-02 14:04:17 +02:00
kolaente d7554d9e70
feat(views): hide view switcher when there is only one view 2024-04-02 14:02:59 +02:00
kolaente 8a72fe26f8
fix(views): refactor filter button slot in wrapper
Before this change, the filter button on the top right was positioned using absolute positioning and plenty of tricks, which were brittle and not really maintainable. Now, the buttons are positioned using flexbox, which should make this a lot more maintainable.
2024-04-02 14:02:31 +02:00
kolaente 13cab62d14
fix(views): transform view filter before and after loading it from the api
continuous-integration/drone/push Build is failing Details
Previously, the actual filter was kept as-is when sending it to the api, essentially creating an invalid filter. This change fixes this, transforming the filter before saving and after loading.

Resolves #2233
2024-04-02 13:20:17 +02:00
kolaente 81de986d8d
fix(gantt): correctly show day in chart 2024-04-02 12:53:14 +02:00
kolaente 915f677c2a
fix(views): correctly pass view id to wrapper when gantt view is active 2024-04-02 12:50:10 +02:00
kolaente 8a6e3d5bd7
fix(views): use correct assertion in test
continuous-integration/drone/push Build is passing Details
2024-04-02 12:42:07 +02:00
13 changed files with 213 additions and 247 deletions

View File

@ -167,7 +167,7 @@
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-focus-within": "8.0.1",
"postcss-preset-env": "9.5.3",
"postcss-preset-env": "9.5.4",
"rollup": "4.13.2",
"rollup-plugin-visualizer": "5.12.0",
"sass": "1.72.0",

View File

@ -358,8 +358,8 @@ devDependencies:
specifier: 8.0.1
version: 8.0.1(postcss@8.4.38)
postcss-preset-env:
specifier: 9.5.3
version: 9.5.3(postcss@8.4.38)
specifier: 9.5.4
version: 9.5.4(postcss@8.4.38)
rollup:
specifier: 4.13.2
version: 4.13.2
@ -2628,7 +2628,7 @@ packages:
jsonc-eslint-parser: 2.3.0
magic-string: 0.30.7
mlly: 1.4.2
source-map-js: 1.2.0
source-map-js: 1.1.0
vue-i18n: 9.10.2(vue@3.4.21)
yaml-eslint-parser: 1.2.2
dev: false
@ -2646,7 +2646,7 @@ packages:
engines: {node: '>= 16'}
dependencies:
'@intlify/shared': 9.10.1
source-map-js: 1.2.0
source-map-js: 1.1.0
dev: false
/@intlify/message-compiler@9.10.2:
@ -3983,7 +3983,7 @@ packages:
'@vue/shared': 3.4.21
entities: 4.5.0
estree-walker: 2.0.2
source-map-js: 1.0.2
source-map-js: 1.2.0
/@vue/compiler-dom@3.4.21:
resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==}
@ -4002,7 +4002,7 @@ packages:
estree-walker: 2.0.2
magic-string: 0.30.7
postcss: 8.4.38
source-map-js: 1.2.0
source-map-js: 1.1.0
/@vue/compiler-ssr@3.4.21:
resolution: {integrity: sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==}
@ -5009,7 +5009,7 @@ packages:
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
dependencies:
mdn-data: 2.0.30
source-map-js: 1.0.2
source-map-js: 1.2.0
dev: true
/css-what@5.1.0:
@ -8121,8 +8121,8 @@ packages:
postcss-value-parser: 4.2.0
dev: true
/postcss-preset-env@9.5.3(postcss@8.4.38):
resolution: {integrity: sha512-uOBG5kvYMxZGuepbAKr563PCB+syENPa1C9kPA8IvDGraVkrEUk//31oaO06oj9VtuujVtsgXHI7qbQynCuaVQ==}
/postcss-preset-env@9.5.4(postcss@8.4.38):
resolution: {integrity: sha512-o/jOlJjhm4f6rI5q1f+4Og3tz1cjaO50er9ndk7ZdcXHjWOH49kMAhqDC/nQifypQkOAiAmF46dPt3pZM+Cwbg==}
engines: {node: ^14 || ^16 || >=18}
peerDependencies:
postcss: ^8.4
@ -9007,7 +9007,6 @@ packages:
/source-map-js@1.1.0:
resolution: {integrity: sha512-9vC2SfsJzlej6MAaMPLu8HiBSHGdRAJ9hVFYN1ibZoNkeanmDmLUcIrj6G9DGL7XMJ54AKg/G75akXl1/izTOw==}
engines: {node: '>=0.10.0'}
dev: false
/source-map-js@1.2.0:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
@import "tooltip";
@import "labels";
@import "project";
@import "task";
@import "tasks";

View File

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

View File

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

View File

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

View File

@ -344,10 +344,6 @@ func (p *ProjectView) Update(s *xorm.Session, _ web.Auth) (err error) {
"done_bucket_id",
).
Update(p)
if err != nil {
return
}
return
}