feat: use BaseButton where easily possible #1803

Merged
dpschen merged 7 commits from dpschen/frontend:feature/feat-use-BaseButton-where-possible into main 2022-07-06 21:07:27 +00:00
31 changed files with 381 additions and 340 deletions

View File

@ -61,7 +61,7 @@ describe('List View List', () => {
}) })
cy.visit(`/lists/${lists[1].id}/`) cy.visit(`/lists/${lists[1].id}/`)
cy.get('.list-title a.icon') cy.get('.list-title .icon')
.should('not.exist') .should('not.exist')
cy.get('input.input[placeholder="Add a new task..."') cy.get('input.input[placeholder="Add a new task..."')
.should('not.exist') .should('not.exist')

View File

@ -52,9 +52,9 @@ describe('Lists', () => {
cy.get('.list-title h1') cy.get('.list-title h1')
.should('contain', 'First List') .should('contain', 'First List')
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-trigger') cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-trigger')
.click() .click()
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-content') cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
.contains('Edit') .contains('Edit')
.click() .click()
cy.get('#title') cy.get('#title')
@ -68,7 +68,7 @@ describe('Lists', () => {
cy.get('.list-title h1') cy.get('.list-title h1')
.should('contain', newListName) .should('contain', newListName)
.should('not.contain', lists[0].title) .should('not.contain', lists[0].title)
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child') cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child')
.should('contain', newListName) .should('contain', newListName)
.should('not.contain', lists[0].title) .should('not.contain', lists[0].title)
cy.visit('/') cy.visit('/')
@ -80,9 +80,9 @@ describe('Lists', () => {
it('Should remove a list', () => { it('Should remove a list', () => {
cy.visit(`/lists/${lists[0].id}`) cy.visit(`/lists/${lists[0].id}`)
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-trigger') cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-trigger')
.click() .click()
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-content') cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
.contains('Delete') .contains('Delete')
.click() .click()
cy.url() cy.url()
@ -93,7 +93,7 @@ describe('Lists', () => {
cy.get('.global-notification') cy.get('.global-notification')
.should('contain', 'Success') .should('contain', 'Success')
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list') cy.get('.namespace-container .menu.namespaces-lists .menu-list')
.should('not.contain', lists[0].title) .should('not.contain', lists[0].title)
cy.location('pathname') cy.location('pathname')
.should('equal', '/') .should('equal', '/')
@ -112,7 +112,7 @@ describe('Lists', () => {
cy.get('.modal-content [data-cy=modalPrimary]') cy.get('.modal-content [data-cy=modalPrimary]')
.click() .click()
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list') cy.get('.namespace-container .menu.namespaces-lists .menu-list')
.should('not.contain', lists[0].title) .should('not.contain', lists[0].title)
cy.get('main.app-content') cy.get('main.app-content')
.should('contain.text', 'This list is archived. It is not possible to create new or edit tasks for it.') .should('contain.text', 'This list is archived. It is not possible to create new or edit tasks for it.')

View File

@ -165,7 +165,7 @@ describe('Task', () => {
}) })
cy.visit(`/tasks/${tasks[0].id}`) cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .details.content.description .editor a') cy.get('.task-view .details.content.description .editor button')
.click() .click()
cy.get('.task-view .details.content.description .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll') cy.get('.task-view .details.content.description .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll')
.type('{selectall}New Description') .type('{selectall}New Description')
@ -297,7 +297,7 @@ describe('Task', () => {
cy.visit(`/tasks/${tasks[0].id}`) cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .column.assignees .multiselect .input-wrapper span.assignee') cy.get('.task-view .column.assignees .multiselect .input-wrapper span.assignee')
.get('a.remove-assignee') .get('.remove-assignee')
.click() .click()
cy.get('.global-notification') cy.get('.global-notification')
@ -402,7 +402,7 @@ describe('Task', () => {
.contains('Due Date') .contains('Due Date')
.get('.date-input .datepicker .show') .get('.date-input .datepicker .show')
.click() .click()
cy.get('.datepicker .datepicker-popup a') cy.get('.datepicker .datepicker-popup button')
.contains('Tomorrow') .contains('Tomorrow')
.click() .click()
cy.get('[data-cy="closeDatepicker"]') cy.get('[data-cy="closeDatepicker"]')

View File

@ -53,7 +53,8 @@ const props = defineProps({
const componentNodeName = ref<Node['nodeName']>('button') const componentNodeName = ref<Node['nodeName']>('button')
interface ElementBindings { interface ElementBindings {
type?: string; type?: string;
rel?: string, rel?: string;
target?: string;
} }
const elementBindings = ref({}) const elementBindings = ref({})
@ -74,7 +75,10 @@ watchEffect(() => {
// we also set a predefined value for the attribute rel, but make it possible to overwrite this by the user. // we also set a predefined value for the attribute rel, but make it possible to overwrite this by the user.
if ('href' in attrs) { if ('href' in attrs) {
nodeName = 'a' nodeName = 'a'
bindings = {rel: 'noreferrer noopener nofollow'} bindings = {
rel: 'noreferrer noopener nofollow',
target: '_blank',
}
} }
componentNodeName.value = nodeName componentNodeName.value = nodeName
@ -103,7 +107,7 @@ const isButton = computed(() => componentNodeName.value === 'button')
:where(.base-button) { :where(.base-button) {
cursor: pointer; cursor: pointer;
display: block; display: inline-block;
color: inherit; color: inherit;
font: inherit; font: inherit;
user-select: none; user-select: none;

View File

@ -22,14 +22,14 @@
<div class="navbar-end"> <div class="navbar-end">
<update/> <update/>
<a <BaseButton
@click="openQuickActions" @click="openQuickActions"
class="trigger-button pr-0" class="trigger-button pr-0"
v-shortcut="'Control+k'" v-shortcut="'Control+k'"
:title="$t('keyboardShortcuts.quickSearch')" :title="$t('keyboardShortcuts.quickSearch')"
> >
<icon icon="search"/> <icon icon="search"/>
</a> </BaseButton>
<notifications/> <notifications/>
<div class="user"> <div class="user">
<dropdown class="is-right" ref="usernameDropdown"> <dropdown class="is-right" ref="usernameDropdown">

View File

@ -49,13 +49,13 @@
</modal> </modal>
</transition> </transition>
<a <BaseButton
class="keyboard-shortcuts-button d-print-none" class="keyboard-shortcuts-button d-print-none"
@click="showKeyboardShortcuts()" @click="showKeyboardShortcuts()"
v-shortcut="'?'" v-shortcut="'?'"
> >
<icon icon="keyboard"/> <icon icon="keyboard"/>
</a> </BaseButton>
</main> </main>
</div> </div>
</div> </div>

View File

@ -51,7 +51,7 @@
<nav class="menu namespaces-lists loader-container is-loading-small" :class="{'is-loading': loading}"> <nav class="menu namespaces-lists loader-container is-loading-small" :class="{'is-loading': loading}">
<template v-for="(n, nk) in namespaces" :key="n.id"> <template v-for="(n, nk) in namespaces" :key="n.id">
<div class="namespace-title" :class="{'has-menu': n.id > 0}"> <div class="namespace-title" :class="{'has-menu': n.id > 0}">
<span <BaseButton
@click="toggleLists(n.id)" @click="toggleLists(n.id)"
class="menu-label" class="menu-label"
v-tooltip="namespaceTitles[nk]" v-tooltip="namespaceTitles[nk]"
@ -61,32 +61,25 @@
:style="{ backgroundColor: n.hexColor }" :style="{ backgroundColor: n.hexColor }"
class="color-bubble" class="color-bubble"
/> />
<span class="name"> <span class="name">{{ namespaceTitles[nk] }}</span>
{{ namespaceTitles[nk] }} <div
</span>
<a
class="icon is-small toggle-lists-icon pl-2" class="icon is-small toggle-lists-icon pl-2"
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}" :class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}"
@click="toggleLists(n.id)"
> >
<icon icon="chevron-down"/> <icon icon="chevron-down"/>
</a> </div>
<span class="count" :class="{'ml-2 mr-0': n.id > 0}"> <span class="count" :class="{'ml-2 mr-0': n.id > 0}">
({{ namespaceListsCount[nk] }}) ({{ namespaceListsCount[nk] }})
</span> </span>
</span> </BaseButton>
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/> <namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
</div> </div>
<div
v-if="listsVisible[n.id] ?? true"
:key="n.id + 'child'"
class="more-container"
>
<!-- <!--
NOTE: a v-model / computed setter is not possible, since the updateActiveLists function NOTE: a v-model / computed setter is not possible, since the updateActiveLists function
triggered by the change needs to have access to the current namespace triggered by the change needs to have access to the current namespace
--> -->
<draggable <draggable
v-if="listsVisible[n.id] ?? true"
v-bind="dragOptions" v-bind="dragOptions"
:modelValue="activeLists[nk]" :modelValue="activeLists[nk]"
@update:modelValue="(lists) => updateActiveLists(n, lists)" @update:modelValue="(lists) => updateActiveLists(n, lists)"
@ -111,45 +104,36 @@
> >
<template #item="{element: l}"> <template #item="{element: l}">
<li <li
class="loader-container is-loading-small" class="list-menu loader-container is-loading-small"
:class="{'is-loading': listUpdating[l.id]}" :class="{'is-loading': listUpdating[l.id]}"
> >
<router-link <BaseButton
:to="{ name: 'list.index', params: { listId: l.id} }" :to="{ name: 'list.index', params: { listId: l.id} }"
v-slot="{ href, navigate, isActive }" class="list-menu-link"
custom :class="{'router-link-exact-active': currentList.id === l.id}"
> >
<a <span class="icon handle">
@click="navigate" <icon icon="grip-lines"/>
:href="href" </span>
class="list-menu-link" <span
:class="{'router-link-exact-active': isActive || currentList?.id === l.id}" :style="{ backgroundColor: l.hexColor }"
> class="color-bubble"
<span class="icon handle"> v-if="l.hexColor !== ''">
<icon icon="grip-lines"/> </span>
</span> <span class="list-menu-title">{{ getListTitle(l) }}</span>
<span </BaseButton>
:style="{ backgroundColor: l.hexColor }" <BaseButton
class="color-bubble" class="favorite"
v-if="l.hexColor !== ''"> :class="{'is-favorite': l.isFavorite}"
</span> @click="toggleFavoriteList(l)"
<span class="list-menu-title"> >
{{ getListTitle(l) }} <icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
</span> </BaseButton>
<span
:class="{'is-favorite': l.isFavorite}"
@click.prevent.stop="toggleFavoriteList(l)"
class="favorite">
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
</span>
</a>
</router-link>
<list-settings-dropdown :list="l" v-if="l.id > 0"/> <list-settings-dropdown :list="l" v-if="l.id > 0"/>
<span class="list-setting-spacer" v-else></span> <span class="list-setting-spacer" v-else></span>
</li> </li>
</template> </template>
</draggable> </draggable>
</div>
</template> </template>
</nav> </nav>
<PoweredByLink/> <PoweredByLink/>
@ -162,6 +146,7 @@ import {useStore} from 'vuex'
import draggable from 'zhyswan-vuedraggable' import draggable from 'zhyswan-vuedraggable'
import {SortableEvent} from 'sortablejs' import {SortableEvent} from 'sortablejs'
import BaseButton from '@/components/base/BaseButton.vue'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue' import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue' import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
import PoweredByLink from '@/components/home/PoweredByLink.vue' import PoweredByLink from '@/components/home/PoweredByLink.vue'
@ -334,7 +319,7 @@ $vikunja-nav-selected-width: 0.4rem;
} }
.menu-label, .menu-label,
.menu-list span.list-menu-link, .menu-list .list-menu-link,
.menu-list a { .menu-list a {
display: flex; display: flex;
align-items: center; align-items: center;
@ -352,28 +337,21 @@ $vikunja-nav-selected-width: 0.4rem;
flex: 0 0 12px; flex: 0 0 12px;
} }
.favorite { }
margin-left: .25rem; .favorite {
transition: opacity $transition, color $transition; margin-left: .25rem;
opacity: 0; transition: opacity $transition, color $transition;
opacity: 0;
&:hover { &:hover,
color: var(--warning); &.is-favorite {
} color: var(--warning);
&.is-favorite {
opacity: 1;
color: var(--warning);
}
} }
}
&:hover .favorite { .favorite.is-favorite,
opacity: 1; .list-menu:hover .favorite {
} opacity: 1;
&:hover {
background: transparent;
}
} }
.menu-label { .menu-label {
@ -392,6 +370,8 @@ $vikunja-nav-selected-width: 0.4rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
color: $vikunja-nav-color;
padding: 0 .25rem;
.menu-label { .menu-label {
margin-bottom: 0; margin-bottom: 0;
@ -410,11 +390,6 @@ $vikunja-nav-selected-width: 0.4rem;
} }
} }
a:not(.dropdown-item) {
color: $vikunja-nav-color;
padding: 0 .25rem;
}
:deep(.dropdown-trigger) { :deep(.dropdown-trigger) {
padding: .5rem; padding: .5rem;
cursor: pointer; cursor: pointer;
@ -444,7 +419,7 @@ $vikunja-nav-selected-width: 0.4rem;
.menu-label, .menu-label,
.nsettings, .nsettings,
.menu-list span.list-menu-link, .menu-list .list-menu-link,
.menu-list a { .menu-list a {
color: $vikunja-nav-color; color: $vikunja-nav-color;
} }
@ -483,7 +458,11 @@ $vikunja-nav-selected-width: 0.4rem;
} }
} }
span.list-menu-link, li > a { a:hover {
background: transparent;
}
.list-menu-link, li > a {
padding: 0.75rem .5rem 0.75rem ($navbar-padding * 1.5 - 1.75rem); padding: 0.75rem .5rem 0.75rem ($navbar-padding * 1.5 - 1.75rem);
transition: all 0.2s ease; transition: all 0.2s ease;
@ -556,7 +535,7 @@ $vikunja-nav-selected-width: 0.4rem;
font-family: $vikunja-font; font-family: $vikunja-font;
} }
span.list-menu-link, li > a { .list-menu-link, li > a {
padding-left: 2rem; padding-left: 2rem;
display: inline-block; display: inline-block;

View File

@ -1,95 +1,73 @@
<template> <template>
<div class="datepicker" :class="{'disabled': disabled}"> <div class="datepicker">
<a @click.stop="toggleDatePopup" class="show"> <BaseButton @click.stop="toggleDatePopup" class="show" :disabled="disabled || undefined">
<template v-if="date === null"> {{ date === null ? chooseDateLabel : formatDateShort(date) }}
{{ chooseDateLabel }} </BaseButton>
</template>
<template v-else>
{{ formatDateShort(date) }}
</template>
</a>
<transition name="fade"> <transition name="fade">
<div v-if="show" class="datepicker-popup" ref="datepickerPopup"> <div v-if="show" class="datepicker-popup" ref="datepickerPopup">
<a @click.stop="() => setDate('today')" v-if="(new Date()).getHours() < 21"> <BaseButton
<span class="icon"> v-if="(new Date()).getHours() < 21"
<icon :icon="['far', 'calendar-alt']"/> class="datepicker__quick-select-date"
</span> @click.stop="setDate('today')"
>
<span class="icon"><icon :icon="['far', 'calendar-alt']"/></span>
<span class="text"> <span class="text">
<span> <span>{{ $t('input.datepicker.today') }}</span>
{{ $t('input.datepicker.today') }} <span class="weekday">{{ getWeekdayFromStringInterval('today') }}</span>
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('today') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('tomorrow')">
<span class="icon">
<icon :icon="['far', 'sun']"/>
</span> </span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('tomorrow')"
>
<span class="icon"><icon :icon="['far', 'sun']"/></span>
<span class="text"> <span class="text">
<span> <span>{{ $t('input.datepicker.tomorrow') }}</span>
{{ $t('input.datepicker.tomorrow') }} <span class="weekday">{{ getWeekdayFromStringInterval('tomorrow') }}</span>
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('tomorrow') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('nextMonday')">
<span class="icon">
<icon icon="coffee"/>
</span> </span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('nextMonday')"
>
<span class="icon"><icon icon="coffee"/></span>
<span class="text"> <span class="text">
<span> <span>{{ $t('input.datepicker.nextMonday') }}</span>
{{ $t('input.datepicker.nextMonday') }} <span class="weekday">{{ getWeekdayFromStringInterval('nextMonday') }}</span>
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('nextMonday') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('thisWeekend')">
<span class="icon">
<icon icon="cocktail"/>
</span> </span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('thisWeekend')"
>
<span class="icon"><icon icon="cocktail"/></span>
<span class="text"> <span class="text">
<span> <span>{{ $t('input.datepicker.thisWeekend') }}</span>
{{ $t('input.datepicker.thisWeekend') }} <span class="weekday">{{ getWeekdayFromStringInterval('thisWeekend') }}</span>
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('thisWeekend') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('laterThisWeek')">
<span class="icon">
<icon icon="chess-knight"/>
</span> </span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('laterThisWeek')"
>
<span class="icon"><icon icon="chess-knight"/></span>
<span class="text"> <span class="text">
<span> <span>{{ $t('input.datepicker.laterThisWeek') }}</span>
{{ $t('input.datepicker.laterThisWeek') }} <span class="weekday">{{ getWeekdayFromStringInterval('laterThisWeek') }}</span>
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('laterThisWeek') }}
</span>
</span>
</a>
<a @click.stop="() => setDate('nextWeek')">
<span class="icon">
<icon icon="forward"/>
</span> </span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('nextWeek')"
>
<span class="icon"><icon icon="forward"/></span>
<span class="text"> <span class="text">
<span> <span>{{ $t('input.datepicker.nextWeek') }}</span>
{{ $t('input.datepicker.nextWeek') }} <span class="weekday">{{ getWeekdayFromStringInterval('nextWeek') }}</span>
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('nextWeek') }}
</span>
</span> </span>
</a> </BaseButton>
<flat-pickr <flat-pickr
:config="flatPickerConfig" :config="flatPickerConfig"
@ -98,7 +76,7 @@
/> />
<x-button <x-button
class="is-fullwidth" class="datepicker__close-button is-fullwidth"
:shadow="false" :shadow="false"
@click="close" @click="close"
v-cy="'closeDatepicker'" v-cy="'closeDatepicker'"
@ -117,6 +95,8 @@ import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css' import 'flatpickr/dist/flatpickr.css'
import {i18n} from '@/i18n' import {i18n} from '@/i18n'
import BaseButton from '@/components/base/BaseButton.vue'
import {format} from 'date-fns' import {format} from 'date-fns'
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval' import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours' import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
@ -134,6 +114,7 @@ export default defineComponent({
}, },
components: { components: {
flatPickr, flatPickr,
BaseButton,
}, },
props: { props: {
modelValue: { modelValue: {
@ -263,68 +244,64 @@ export default defineComponent({
input.input { input.input {
display: none; display: none;
} }
}
&.disabled a { .datepicker-popup {
cursor: default; position: absolute;
} z-index: 99;
width: 320px;
background: var(--white);
border-radius: $radius;
box-shadow: $shadow;
.datepicker-popup { @media screen and (max-width: ($tablet)) {
position: absolute; width: calc(100vw - 5rem);
z-index: 99;
width: 320px;
background: var(--white);
border-radius: $radius;
box-shadow: $shadow;
@media screen and (max-width: ($tablet)) {
width: calc(100vw - 5rem);
}
a:not(.button) {
display: flex;
align-items: center;
padding: 0 .5rem;
width: 100%;
height: 2.25rem;
color: var(--text);
transition: all $transition;
&:first-child {
border-radius: $radius $radius 0 0;
}
&:hover {
background: var(--grey-100);
}
.text {
width: 100%;
font-size: .85rem;
display: flex;
justify-content: space-between;
padding-right: .25rem;
.weekday {
color: var(--text-light);
text-transform: capitalize;
}
}
.icon {
width: 2rem;
text-align: center;
}
}
a.button {
margin: 1rem;
width: calc(100% - 2rem);
}
:deep(.flatpickr-calendar) {
margin: 0 auto 8px;
box-shadow: none;
}
} }
} }
.datepicker__quick-select-date {
display: flex;
align-items: center;
padding: 0 .5rem;
width: 100%;
height: 2.25rem;
color: var(--text);
transition: all $transition;
&:first-child {
border-radius: $radius $radius 0 0;
}
&:hover {
background: var(--grey-100);
}
.text {
width: 100%;
font-size: .85rem;
display: flex;
justify-content: space-between;
padding-right: .25rem;
.weekday {
color: var(--text-light);
text-transform: capitalize;
}
}
.icon {
width: 2rem;
text-align: center;
}
}
.datepicker__close-button {
margin: 1rem;
width: calc(100% - 2rem);
}
:deep(.flatpickr-calendar) {
margin: 0 auto 8px;
box-shadow: none;
}
</style> </style>

View File

@ -16,23 +16,23 @@
<p class="has-text-centered has-text-grey is-italic my-5" v-if="showPreviewText"> <p class="has-text-centered has-text-grey is-italic my-5" v-if="showPreviewText">
{{ emptyText }} {{ emptyText }}
<template v-if="isEditEnabled"> <template v-if="isEditEnabled">
<a @click="toggleEdit" class="d-print-none">{{ $t('input.editor.edit') }}</a>. <ButtonLink @click="toggleEdit" class="d-print-none">{{ $t('input.editor.edit') }}</ButtonLink>.
</template> </template>
</p> </p>
<ul class="actions d-print-none" v-if="bottomActions.length > 0"> <ul class="actions d-print-none" v-if="bottomActions.length > 0">
<li v-if="isEditEnabled && !showPreviewText && showSave"> <li v-if="isEditEnabled && !showPreviewText && showSave">
<a v-if="showEditButton" @click="toggleEdit">{{ $t('input.editor.edit') }}</a> <BaseButton v-if="showEditButton" @click="toggleEdit">{{ $t('input.editor.edit') }}</BaseButton>
<a v-else-if="isEditActive" @click="toggleEdit" class="done-edit">{{ $t('misc.save') }}</a> <BaseButton v-else-if="isEditActive" @click="toggleEdit" class="done-edit">{{ $t('misc.save') }}</BaseButton>
</li> </li>
<li v-for="(action, k) in bottomActions" :key="k"> <li v-for="(action, k) in bottomActions" :key="k">
<a @click="action.action">{{ action.title }}</a> <BaseButton @click="action.action">{{ action.title }}</BaseButton>
</li> </li>
</ul> </ul>
<template v-else-if="isEditEnabled && showSave"> <template v-else-if="isEditEnabled && showSave">
<ul v-if="showEditButton" class="actions d-print-none"> <ul v-if="showEditButton" class="actions d-print-none">
<li> <li>
<a @click="toggleEdit">{{ $t('input.editor.edit') }}</a> <BaseButton @click="toggleEdit">{{ $t('input.editor.edit') }}</BaseButton>
</li> </li>
</ul> </ul>
<x-button <x-button
@ -62,10 +62,15 @@ import AttachmentService from '../../services/attachment'
import {findCheckboxesInText} from '../../helpers/checklistFromText' import {findCheckboxesInText} from '../../helpers/checklistFromText'
import {createRandomID} from '@/helpers/randomId' import {createRandomID} from '@/helpers/randomId'
import BaseButton from '@/components/base/BaseButton.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue'
export default defineComponent({ export default defineComponent({
name: 'editor', name: 'editor',
components: { components: {
VueEasymde, VueEasymde,
BaseButton,
ButtonLink,
}, },
props: { props: {
modelValue: { modelValue: {

View File

@ -15,7 +15,7 @@
<slot name="tag" :item="item"> <slot name="tag" :item="item">
<span :key="`item${key}`" class="tag ml-2 mt-2"> <span :key="`item${key}`" class="tag ml-2 mt-2">
{{ label !== '' ? item[label] : item }} {{ label !== '' ? item[label] : item }}
<a @click="() => remove(item)" class="delete is-small"></a> <BaseButton @click="() => remove(item)" class="delete is-small"></BaseButton>
</span> </span>
</slot> </slot>
</template> </template>
@ -37,7 +37,7 @@
<transition name="fade"> <transition name="fade">
<div class="search-results" :class="{'search-results-inline': inline}" v-if="searchResultsVisible"> <div class="search-results" :class="{'search-results-inline': inline}" v-if="searchResultsVisible">
<button <BaseButton
class="is-fullwidth" class="is-fullwidth"
v-for="(data, key) in filteredSearchResults" v-for="(data, key) in filteredSearchResults"
:key="key" :key="key"
@ -54,9 +54,9 @@
<span class="hint-text"> <span class="hint-text">
{{ selectPlaceholder }} {{ selectPlaceholder }}
</span> </span>
</button> </BaseButton>
<button <BaseButton
v-if="creatableAvailable" v-if="creatableAvailable"
class="is-fullwidth" class="is-fullwidth"
:ref="`result-${filteredSearchResults.length}`" :ref="`result-${filteredSearchResults.length}`"
@ -75,7 +75,7 @@
<span class="hint-text"> <span class="hint-text">
{{ createPlaceholder }} {{ createPlaceholder }}
</span> </span>
</button> </BaseButton>
</div> </div>
</transition> </transition>
@ -87,8 +87,13 @@ import {defineComponent} from 'vue'
import {i18n} from '@/i18n' import {i18n} from '@/i18n'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import BaseButton from '@/components/base/BaseButton.vue'
export default defineComponent({ export default defineComponent({
name: 'multiselect', name: 'multiselect',
components: {
BaseButton,
},
data() { data() {
return { return {
query: '', query: '',

View File

@ -15,19 +15,21 @@
<div <div
class="list-background background-fade-in" class="list-background background-fade-in"
:class="{'is-visible': background}" :class="{'is-visible': background}"
:style="{'background-image': background !== null ? `url(${background})` : false}"></div> :style="{'background-image': background !== null ? `url(${background})` : undefined}"
/>
<div class="list-content"> <div class="list-content">
<div class="is-archived-container">
<span class="is-archived" v-if="list.isArchived"> <span class="is-archived" v-if="list.isArchived">
{{ $t('namespace.archived') }} {{ $t('namespace.archived') }}
</span> </span>
<span <BaseButton
:class="{'is-favorite': list.isFavorite, 'is-archived': list.isArchived}" v-else
@click.stop="toggleFavoriteList(list)" :class="{'is-favorite': list.isFavorite}"
class="favorite"> @click.stop="toggleFavoriteList(list)"
class="favorite"
>
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']"/> <icon :icon="list.isFavorite ? 'star' : ['far', 'star']"/>
</span> </BaseButton>
</div>
<div class="title">{{ list.title }}</div> <div class="title">{{ list.title }}</div>
</div> </div>
</router-link> </router-link>
@ -43,6 +45,8 @@ import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
import {colorIsDark} from '@/helpers/color/colorIsDark' import {colorIsDark} from '@/helpers/color/colorIsDark'
import ListModel from '@/models/list' import ListModel from '@/models/list'
import BaseButton from '@/components/base/BaseButton.vue'
const background = ref<string | null>(null) const background = ref<string | null>(null)
const backgroundLoading = ref(false) const backgroundLoading = ref(false)
const blurHashUrl = ref('') const blurHashUrl = ref('')
@ -109,13 +113,14 @@ function toggleFavoriteList(list: ListModel) {
color: var(--grey-100); color: var(--grey-100);
} }
&.has-background, .list-background { &.has-background,
.list-background {
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
} }
&.has-background .list-content .title { &.has-background .title {
text-shadow: 0 0 10px var(--black), 1px 1px 5px var(--grey-700), -1px -1px 5px var(--grey-700); text-shadow: 0 0 10px var(--black), 1px 1px 5px var(--grey-700), -1px -1px 5px var(--grey-700);
color: var(--white); color: var(--white);
} }
@ -176,23 +181,35 @@ function toggleFavoriteList(list: ListModel) {
.list-content { .list-content {
display: flex; display: flex;
justify-content: space-between; align-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
padding: 1rem; padding: 1rem;
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 100%; width: 100%;
.is-archived-container {
width: 100%;
text-align: right;
.is-archived { .is-archived {
font-size: .75rem; font-size: .75rem;
float: left; }
.favorite {
margin-left: auto;
transition: opacity $transition, color $transition;
opacity: 0;
display: block;
&:hover,
&.is-favorite {
color: var(--warning);
} }
} }
.favorite.is-favorite,
&:hover .favorite {
opacity: 1;
}
.title { .title {
align-self: flex-end; align-self: flex-end;
font-family: $vikunja-font; font-family: $vikunja-font;
@ -209,30 +226,6 @@ function toggleFavoriteList(list: ListModel) {
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.favorite {
transition: opacity $transition, color $transition;
opacity: 0;
&:hover {
color: var(--warning);
}
&.is-archived {
display: none;
}
&.is-favorite {
display: inline-block;
opacity: 1;
color: var(--warning);
}
}
&:hover .favorite {
opacity: 1;
}
} }
} }
</style> </style>

View File

@ -0,0 +1,17 @@
<template>
<BaseButton class="button-link"><slot/></BaseButton>
</template>
<script setup lang="ts">
import BaseButton from '@/components/base/BaseButton.vue'
</script>
<style lang="scss">
.button-link {
color: var(--link);
&:hover {
color: var(--link-hover);
}
}
</style>

View File

@ -27,7 +27,7 @@
<span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span> <span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span>
</i18n-t> </i18n-t>
<br/> <br/>
<a @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</a> <ButtonLink class="api-config__change-button" @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</ButtonLink>
</div> </div>
<message variant="success" v-if="successMsg !== '' && errorMsg === ''" class="mt-2"> <message variant="success" v-if="successMsg !== '' && errorMsg === ''" class="mt-2">
@ -48,6 +48,7 @@ import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
import {success} from '@/message' import {success} from '@/message'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue'
const props = defineProps({ const props = defineProps({
configureOpen: { configureOpen: {
@ -117,4 +118,9 @@ async function setApiUrl() {
.url { .url {
border-bottom: 1px dashed var(--primary); border-bottom: 1px dashed var(--primary);
} }
.api-config__change-button {
display: inline-block;
color: var(--link);
}
</style> </style>

View File

@ -4,7 +4,7 @@
<p class="card-header-title"> <p class="card-header-title">
{{ title }} {{ title }}
</p> </p>
<a <BaseButton
v-if="hasClose" v-if="hasClose"
class="card-header-icon" class="card-header-icon"
:aria-label="$t('misc.close')" :aria-label="$t('misc.close')"
@ -14,7 +14,7 @@
<span class="icon"> <span class="icon">
<icon :icon="closeIcon"/> <icon :icon="closeIcon"/>
</span> </span>
</a> </BaseButton>
</header> </header>
<div class="card-content loader-container" :class="{'p-0': !padding, 'is-loading': loading}"> <div class="card-content loader-container" :class="{'p-0': !padding, 'is-loading': loading}">
<div :class="{'content': hasContent}"> <div :class="{'content': hasContent}">
@ -25,6 +25,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import BaseButton from '@/components/base/BaseButton.vue'
defineProps({ defineProps({
title: { title: {
type: String, type: String,

View File

@ -1,14 +1,15 @@
<template> <template>
<message variant="danger"> <message variant="danger">
<i18n-t keypath="loadingError.failed"> <i18n-t keypath="loadingError.failed">
<a @click="reload">{{ $t('loadingError.tryAgain') }}</a> <ButtonLink @click="reload">{{ $t('loadingError.tryAgain') }}</ButtonLink>
<a href="https://vikunja.io/contact/" rel="noreferrer noopener nofollow" target="_blank">{{ $t('loadingError.contact') }}</a> <ButtonLink href="https://vikunja.io/contact/">{{ $t('loadingError.contact') }}</ButtonLink>
</i18n-t> </i18n-t>
</message> </message>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue'
function reload() { function reload() {
window.location.reload() window.location.reload()

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="legal-links"> <div class="legal-links">
<a :href="imprintUrl" rel="noreferrer noopener nofollow" target="_blank" v-if="imprintUrl">{{ $t('navigation.imprint') }}</a> <BaseButton :href="imprintUrl" v-if="imprintUrl">{{ $t('navigation.imprint') }}</BaseButton>
<span v-if="imprintUrl && privacyPolicyUrl"> | </span> <span v-if="imprintUrl && privacyPolicyUrl"> | </span>
<a :href="privacyPolicyUrl" rel="noreferrer noopener nofollow" target="_blank" v-if="privacyPolicyUrl">{{ $t('navigation.privacy') }}</a> <BaseButton :href="privacyPolicyUrl" v-if="privacyPolicyUrl">{{ $t('navigation.privacy') }}</BaseButton>
</div> </div>
</template> </template>
@ -10,6 +10,8 @@
import {computed} from 'vue' import {computed} from 'vue'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import BaseButton from '@/components/base/BaseButton.vue'
const store = useStore() const store = useStore()
const imprintUrl = computed(() => store.state.config.legal.imprintUrl) const imprintUrl = computed(() => store.state.config.legal.imprintUrl)

View File

@ -1,6 +1,7 @@
<template> <template>
<notifications position="bottom left" :max="2" class="global-notification"> <notifications position="bottom left" :max="2" class="global-notification">
<template #body="{ item, close }"> <template #body="{ item, close }">
<!-- FIXME: overlay whole notification with button and add event listener on that button instead -->
<div <div
:class="[ :class="[
'vue-notification-template', 'vue-notification-template',

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="notifications"> <div class="notifications">
<div class="is-flex is-justify-content-center"> <div class="is-flex is-justify-content-center">
<a @click.stop="showNotifications = !showNotifications" class="trigger-button"> <BaseButton @click.stop="showNotifications = !showNotifications" class="trigger-button">
<span class="unread-indicator" v-if="unreadNotifications > 0"></span> <span class="unread-indicator" v-if="unreadNotifications > 0"></span>
<icon icon="bell"/> <icon icon="bell"/>
</a> </BaseButton>
</div> </div>
<transition name="fade"> <transition name="fade">
@ -26,9 +26,9 @@
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer"> <span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
{{ n.notification.doer.getDisplayName() }} {{ n.notification.doer.getDisplayName() }}
</span> </span>
<a @click="() => to(n, index)()"> <BaseButton @click="() => to(n, index)()">
{{ n.toText(userInfo) }} {{ n.toText(userInfo) }}
</a> </BaseButton>
</div> </div>
<span class="created" v-tooltip="formatDate(n.created)"> <span class="created" v-tooltip="formatDate(n.created)">
{{ formatDateSince(n.created) }} {{ formatDateSince(n.created) }}
@ -50,6 +50,7 @@
import {computed, onMounted, onUnmounted, ref} from 'vue' import {computed, onMounted, onUnmounted, ref} from 'vue'
import NotificationService from '@/services/notification' import NotificationService from '@/services/notification'
import BaseButton from '@/components/base/BaseButton.vue'
import User from '@/components/misc/user.vue' import User from '@/components/misc/user.vue'
import names from '@/models/constants/notificationNames.json' import names from '@/models/constants/notificationNames.json'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'

View File

@ -32,7 +32,7 @@
{{ r.title }} {{ r.title }}
</span> </span>
<div class="result-items"> <div class="result-items">
<button <BaseButton
v-for="(i, key) in r.items" v-for="(i, key) in r.items"
:key="key" :key="key"
:ref="`result-${k}_${key}`" :ref="`result-${k}_${key}`"
@ -44,7 +44,7 @@
:class="{'is-strikethrough': i.done}" :class="{'is-strikethrough': i.done}"
> >
{{ i.title }} {{ i.title }}
</button> </BaseButton>
</div> </div>
</div> </div>
</div> </div>
@ -63,6 +63,8 @@ import TeamModel from '@/models/team'
import {CURRENT_LIST, LOADING, LOADING_MODULE, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types' import {CURRENT_LIST, LOADING, LOADING_MODULE, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
import ListModel from '@/models/list' import ListModel from '@/models/list'
import BaseButton from '@/components/base/BaseButton.vue'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue' import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
import {getHistory} from '@/modules/listHistory' import {getHistory} from '@/modules/listHistory'
import {parseTaskText, PrefixMode} from '@/modules/parseTaskText' import {parseTaskText, PrefixMode} from '@/modules/parseTaskText'
@ -86,7 +88,10 @@ const SEARCH_MODE_TEAMS = 'teams'
export default defineComponent({ export default defineComponent({
name: 'quick-actions', name: 'quick-actions',
components: {QuickAddMagic}, components: {
BaseButton,
QuickAddMagic,
},
data() { data() {
return { return {
query: '', query: '',

View File

@ -96,9 +96,10 @@
</span> </span>
<priority-label :priority="t.priority" :done="t.done"/> <priority-label :priority="t.priority" :done="t.done"/>
<!-- using the key here forces vue to use the updated version model and not the response returned by the api --> <!-- using the key here forces vue to use the updated version model and not the response returned by the api -->
<a @click="editTask(theTasks[k])" class="edit-toggle"> <!-- FIXME: add label -->
<BaseButton @click="editTask(theTasks[k])" class="edit-toggle">
<icon icon="pen"/> <icon icon="pen"/>
</a> </BaseButton>
</VueDragResize> </VueDragResize>
</div> </div>
<template v-if="showTaskswithoutDates"> <template v-if="showTaskswithoutDates">
@ -184,12 +185,14 @@ import TaskCollectionService from '../../services/taskCollection'
import {mapState} from 'vuex' import {mapState} from 'vuex'
import Rights from '../../models/constants/rights.json' import Rights from '../../models/constants/rights.json'
import FilterPopup from '@/components/list/partials/filter-popup.vue' import FilterPopup from '@/components/list/partials/filter-popup.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import {colorIsDark} from '@/helpers/color/colorIsDark' import {colorIsDark} from '@/helpers/color/colorIsDark'
export default defineComponent({ export default defineComponent({
name: 'GanttChart', name: 'GanttChart',
components: { components: {
BaseButton,
FilterPopup, FilterPopup,
PriorityLabel, PriorityLabel,
EditTask, EditTask,

View File

@ -26,6 +26,9 @@
</progress> </progress>
<div class="files" v-if="attachments.length > 0"> <div class="files" v-if="attachments.length > 0">
<!-- FIXME: don't use a for element that wraps other links / buttons
Instead: overlay element with button that is inside.
-->
<a <a
class="attachment" class="attachment"
v-for="a in attachments" v-for="a in attachments"
@ -53,25 +56,28 @@
</span> </span>
</p> </p>
<p> <p>
<a <BaseButton
class="attachment-info-meta-button"
@click.prevent.stop="downloadAttachment(a)" @click.prevent.stop="downloadAttachment(a)"
v-tooltip="$t('task.attachment.downloadTooltip')" v-tooltip="$t('task.attachment.downloadTooltip')"
> >
{{ $t('misc.download') }} {{ $t('misc.download') }}
</a> </BaseButton>
<a <BaseButton
class="attachment-info-meta-button"
@click.stop="copyUrl(a)" @click.stop="copyUrl(a)"
v-tooltip="$t('task.attachment.copyUrlTooltip')" v-tooltip="$t('task.attachment.copyUrlTooltip')"
> >
{{ $t('task.attachment.copyUrl') }} {{ $t('task.attachment.copyUrl') }}
</a> </BaseButton>
<a <BaseButton
@click.prevent.stop="() => {attachmentToDelete = a; showDeleteModal = true}"
v-if="editEnabled" v-if="editEnabled"
class="attachment-info-meta-button"
@click.prevent.stop="() => {attachmentToDelete = a; showDeleteModal = true}"
v-tooltip="$t('task.attachment.deleteTooltip')" v-tooltip="$t('task.attachment.deleteTooltip')"
> >
{{ $t('misc.delete') }} {{ $t('misc.delete') }}
</a> </BaseButton>
</p> </p>
</div> </div>
</a> </a>
@ -148,9 +154,12 @@ import {mapState} from 'vuex'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard' import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { uploadFiles, generateAttachmentUrl } from '@/helpers/attachments' import { uploadFiles, generateAttachmentUrl } from '@/helpers/attachments'
import BaseButton from '@/components/base/BaseButton'
export default defineComponent({ export default defineComponent({
name: 'attachments', name: 'attachments',
components: { components: {
BaseButton,
User, User,
}, },
data() { data() {
@ -297,7 +306,7 @@ export default defineComponent({
display: flex; display: flex;
> span:not(:last-child):after, > span:not(:last-child):after,
> a:not(:last-child):after { > button:not(:last-child):after {
content: '·'; content: '·';
padding: 0 .25rem; padding: 0 .25rem;
} }
@ -377,7 +386,7 @@ export default defineComponent({
} }
> span:not(:last-child):after, > span:not(:last-child):after,
> a:not(:last-child):after { > button:not(:last-child):after {
display: none; display: none;
} }
@ -387,6 +396,10 @@ export default defineComponent({
} }
} }
.attachment-info-meta-button {
color: var(--link);
}
@keyframes bounce { @keyframes bounce {
from, from,
20%, 20%,

View File

@ -18,9 +18,9 @@
<template #tag="{item: user}"> <template #tag="{item: user}">
<span class="assignee"> <span class="assignee">
<user :avatar-size="32" :show-username="false" :user="user"/> <user :avatar-size="32" :show-username="false" :user="user"/>
<a @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled"> <BaseButton @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled">
<icon icon="times"/> <icon icon="times"/>
</a> </BaseButton>
</span> </span>
</template> </template>
</Multiselect> </Multiselect>
@ -34,6 +34,7 @@ import {useI18n} from 'vue-i18n'
import User from '@/components/misc/user.vue' import User from '@/components/misc/user.vue'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import {includesById} from '@/helpers/utils' import {includesById} from '@/helpers/utils'
import UserModel from '@/models/user' import UserModel from '@/models/user'
@ -95,7 +96,7 @@ async function removeAssignee(user: UserModel) {
success({message: t('task.assignee.unassignSuccess')}) success({message: t('task.assignee.unassignSuccess')})
} }
async function findUser(query) { async function findUser(query: string) {
if (query === '') { if (query === '') {
clearAllFoundUsers() clearAllFoundUsers()
return return

View File

@ -19,7 +19,7 @@
:style="{'background': label.hexColor, 'color': label.textColor}" :style="{'background': label.hexColor, 'color': label.textColor}"
class="tag"> class="tag">
<span>{{ label.title }}</span> <span>{{ label.title }}</span>
<button type="button" v-cy="'taskDetail.removeLabel'" @click="removeLabel(label)" class="delete is-small" /> <BaseButton v-cy="'taskDetail.removeLabel'" @click="removeLabel(label)" class="delete is-small" />
</span> </span>
</template> </template>
<template #searchResult="{option}"> <template #searchResult="{option}">
@ -47,6 +47,7 @@ import LabelModel from '@/models/label'
import LabelTaskService from '@/services/labelTask' import LabelTaskService from '@/services/labelTask'
import {success} from '@/message' import {success} from '@/message'
import BaseButton from '@/components/base/BaseButton.vue'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
const props = defineProps({ const props = defineProps({

View File

@ -2,7 +2,7 @@
<div v-if="available"> <div v-if="available">
<p class="help has-text-grey"> <p class="help has-text-grey">
{{ $t('task.quickAddMagic.hint') }}. {{ $t('task.quickAddMagic.hint') }}.
<a @click="() => visible = true">{{ $t('task.quickAddMagic.what') }}</a> <ButtonLink @click="() => visible = true">{{ $t('task.quickAddMagic.what') }}</ButtonLink>
</p> </p>
<modal <modal
@close="() => visible = false" @close="() => visible = false"
@ -86,13 +86,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed} from 'vue'
import ButtonLink from '@/components/misc/ButtonLink.vue'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode' import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {PREFIXES} from '@/modules/parseTaskText' import {PREFIXES} from '@/modules/parseTaskText'
import {ref, computed} from 'vue'
const visible = ref(false) const visible = ref(false)
const mode = ref(getQuickAddMagicMode()) const mode = ref(getQuickAddMagicMode())
const available = computed(() => mode.value !== 'disabled') const available = computed(() => mode.value !== 'disabled')
const prefixes = computed(() =>PREFIXES[mode.value]) const prefixes = computed(() => PREFIXES[mode.value])
</script> </script>

View File

@ -103,12 +103,13 @@
</span> </span>
{{ t.title }} {{ t.title }}
</router-link> </router-link>
<a <BaseButton
v-if="editEnabled"
@click="() => {showDeleteModal = true; relationToDelete = {relationKind: rts.kind, otherTaskId: t.id}}" @click="() => {showDeleteModal = true; relationToDelete = {relationKind: rts.kind, otherTaskId: t.id}}"
class="remove" class="remove"
v-if="editEnabled"> >
<icon icon="trash-alt"/> <icon icon="trash-alt"/>
</a> </BaseButton>
</div> </div>
</div> </div>
</div> </div>
@ -145,6 +146,7 @@ import TaskRelationService from '../../../services/taskRelation'
import relationKinds from '../../../models/constants/relationKinds' import relationKinds from '../../../models/constants/relationKinds'
import TaskRelationModel from '../../../models/taskRelation' import TaskRelationModel from '../../../models/taskRelation'
import BaseButton from '@/components/base/BaseButton.vue'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
export default defineComponent({ export default defineComponent({
@ -166,6 +168,7 @@ export default defineComponent({
} }
}, },
components: { components: {
BaseButton,
Multiselect, Multiselect,
}, },
props: { props: {

View File

@ -11,9 +11,9 @@
:disabled="disabled" :disabled="disabled"
@close-on-change="() => addReminderDate(index)" @close-on-change="() => addReminderDate(index)"
/> />
<a @click="removeReminderByIndex(index)" v-if="!disabled" class="remove"> <BaseButton @click="removeReminderByIndex(index)" v-if="!disabled" class="remove">
<icon icon="times"></icon> <icon icon="times"></icon>
</a> </BaseButton>
</div> </div>
<div class="reminder-input" v-if="!disabled"> <div class="reminder-input" v-if="!disabled">
<Datepicker <Datepicker
@ -28,10 +28,13 @@
<script setup lang="ts"> <script setup lang="ts">
import {PropType, ref, onMounted, watch} from 'vue' import {PropType, ref, onMounted, watch} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue'
import Datepicker from '@/components/input/datepicker.vue' import Datepicker from '@/components/input/datepicker.vue'
type Reminder = Date | string type Reminder = Date | string
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Array as PropType<Reminder[]>, type: Array as PropType<Reminder[]>,

View File

@ -39,17 +39,21 @@
:user="a" :user="a"
v-for="(a, i) in task.assignees" v-for="(a, i) in task.assignees"
/> />
<time <BaseButton
:datetime="formatISO(task.dueDate)"
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="is-italic"
@click.prevent.stop="showDefer = !showDefer"
v-if="+new Date(task.dueDate) > 0" v-if="+new Date(task.dueDate) > 0"
class="dueDate"
@click.prevent.stop="showDefer = !showDefer"
v-tooltip="formatDate(task.dueDate)" v-tooltip="formatDate(task.dueDate)"
:aria-expanded="showDefer ? 'true' : 'false'"
> >
- {{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }} <time
</time> :datetime="formatISO(task.dueDate)"
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="is-italic"
:aria-expanded="showDefer ? 'true' : 'false'"
>
- {{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
</time>
</BaseButton>
<transition name="fade"> <transition name="fade">
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/> <defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
</transition> </transition>
@ -80,13 +84,13 @@
v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})"> v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})">
{{ $store.getters['lists/getListById'](task.listId).title }} {{ $store.getters['lists/getListById'](task.listId).title }}
</router-link> </router-link>
<a <BaseButton
:class="{'is-favorite': task.isFavorite}" :class="{'is-favorite': task.isFavorite}"
@click="toggleFavorite" @click="toggleFavorite"
class="favorite"> class="favorite">
<icon icon="star" v-if="task.isFavorite"/> <icon icon="star" v-if="task.isFavorite"/>
<icon :icon="['far', 'star']" v-else/> <icon :icon="['far', 'star']" v-else/>
</a> </BaseButton>
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
@ -99,6 +103,7 @@ import PriorityLabel from './priorityLabel'
import TaskService from '../../../services/task' import TaskService from '../../../services/task'
import Labels from './labels' import Labels from './labels'
import User from '../../misc/user' import User from '../../misc/user'
import BaseButton from '@/components/base/BaseButton.vue'
import Fancycheckbox from '../../input/fancycheckbox' import Fancycheckbox from '../../input/fancycheckbox'
import DeferTask from './defer-task' import DeferTask from './defer-task'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
@ -115,6 +120,7 @@ export default defineComponent({
} }
}, },
components: { components: {
BaseButton,
ChecklistSummary, ChecklistSummary,
DeferTask, DeferTask,
Fancycheckbox, Fancycheckbox,
@ -249,6 +255,11 @@ export default defineComponent({
display: inline-block; display: inline-block;
flex: 1 0 50%; flex: 1 0 50%;
.dueDate {
display: inline-block;
margin-left: 5px;
}
.overdue { .overdue {
color: var(--danger); color: var(--danger);
} }

View File

@ -32,13 +32,13 @@
v-tooltip.bottom="$t('label.edit.forbidden')"> v-tooltip.bottom="$t('label.edit.forbidden')">
{{ l.title }} {{ l.title }}
</span> </span>
<a <BaseButton
:style="{'color': l.textColor}" :style="{'color': l.textColor}"
@click="editLabel(l)" @click="editLabel(l)"
v-else> v-else>
{{ l.title }} {{ l.title }}
</a> </BaseButton>
<a @click="showDeleteDialoge(l)" class="delete is-small" v-if="userInfo.id === l.createdBy.id"></a> <BaseButton @click="showDeleteDialoge(l)" class="delete is-small" v-if="userInfo.id === l.createdBy.id" />
</span> </span>
</div> </div>
<div class="column is-4" v-if="isLabelEdit"> <div class="column is-4" v-if="isLabelEdit">
@ -116,12 +116,14 @@ import {mapState} from 'vuex'
import LabelModel from '../../models/label' import LabelModel from '../../models/label'
import {LOADING, LOADING_MODULE} from '@/store/mutation-types' import {LOADING, LOADING_MODULE} from '@/store/mutation-types'
import BaseButton from '@/components/base/BaseButton.vue'
import AsyncEditor from '@/components/input/AsyncEditor' import AsyncEditor from '@/components/input/AsyncEditor'
import ColorPicker from '@/components/input/colorPicker' import ColorPicker from '@/components/input/colorPicker'
export default defineComponent({ export default defineComponent({
name: 'ListLabels', name: 'ListLabels',
components: { components: {
BaseButton,
ColorPicker, ColorPicker,
editor: AsyncEditor, editor: AsyncEditor,
}, },

View File

@ -65,9 +65,9 @@
<nothing v-if="ctaVisible && tasks.length === 0 && !loading"> <nothing v-if="ctaVisible && tasks.length === 0 && !loading">
{{ $t('list.list.empty') }} {{ $t('list.list.empty') }}
<a @click="focusNewTaskInput()"> <ButtonLink @click="focusNewTaskInput()">
{{ $t('list.list.newTaskCta') }} {{ $t('list.list.newTaskCta') }}
</a> </ButtonLink>
</nothing> </nothing>
<div class="tasks-container" :class="{ 'has-task-edit-open': isTaskEdit }"> <div class="tasks-container" :class="{ 'has-task-edit-open': isTaskEdit }">
@ -99,13 +99,13 @@
<span class="icon handle"> <span class="icon handle">
<icon icon="grip-lines"/> <icon icon="grip-lines"/>
</span> </span>
<div <BaseButton
@click="editTask(t.id)" @click="editTask(t.id)"
class="icon settings" class="icon settings"
v-if="!list.isArchived" v-if="!list.isArchived"
> >
<icon icon="pencil-alt"/> <icon icon="pencil-alt"/>
</div> </BaseButton>
</template> </template>
</single-task-in-list> </single-task-in-list>
</template> </template>
@ -134,6 +134,8 @@
<script lang="ts"> <script lang="ts">
import { ref, toRef, defineComponent } from 'vue' import { ref, toRef, defineComponent } from 'vue'
import BaseButton from '@/components/base/BaseButton.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue'
import ListWrapper from './ListWrapper.vue' import ListWrapper from './ListWrapper.vue'
import EditTask from '@/components/tasks/edit-task' import EditTask from '@/components/tasks/edit-task'
import AddTask from '@/components/tasks/add-task' import AddTask from '@/components/tasks/add-task'
@ -190,6 +192,7 @@ export default defineComponent({
} }
}, },
components: { components: {
BaseButton,
ListWrapper, ListWrapper,
Nothing, Nothing,
FilterPopup, FilterPopup,
@ -198,6 +201,7 @@ export default defineComponent({
AddTask, AddTask,
draggable, draggable,
Pagination, Pagination,
ButtonLink,
}, },
setup(props) { setup(props) {

View File

@ -35,9 +35,7 @@
v-model="backgroundSearchTerm" v-model="backgroundSearchTerm"
/> />
<p class="unsplash-link"> <p class="unsplash-link">
<a href="https://unsplash.com" rel="noreferrer noopener nofollow" target="_blank"> <BaseButton href="https://unsplash.com">{{ $t('list.background.poweredByUnsplash') }}</BaseButton>
{{ $t('list.background.poweredByUnsplash') }}
</a>
</p> </p>
<div class="image-search-result"> <div class="image-search-result">
<a <a
@ -83,12 +81,13 @@ import ListService from '@/services/list'
import {CURRENT_LIST} from '@/store/mutation-types' import {CURRENT_LIST} from '@/store/mutation-types'
import CreateEdit from '@/components/misc/create-edit.vue' import CreateEdit from '@/components/misc/create-edit.vue'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import BaseButton from '@/components/base/BaseButton.vue'
const SEARCH_DEBOUNCE = 300 const SEARCH_DEBOUNCE = 300
export default defineComponent({ export default defineComponent({
name: 'list-setting-background', name: 'list-setting-background',
components: {CreateEdit}, components: {CreateEdit, BaseButton},
data() { data() {
return { return {
backgroundService: new BackgroundUnsplashService(), backgroundService: new BackgroundUnsplashService(),

View File

@ -59,14 +59,14 @@
:disabled="taskService.loading || !canWrite" :disabled="taskService.loading || !canWrite"
ref="dueDate" ref="dueDate"
/> />
<a <BaseButton
@click="() => {task.dueDate = null;saveTask()}" @click="() => {task.dueDate = null;saveTask()}"
v-if="task.dueDate && canWrite" v-if="task.dueDate && canWrite"
class="remove"> class="remove">
<span class="icon is-small"> <span class="icon is-small">
<icon icon="times"></icon> <icon icon="times"></icon>
</span> </span>
</a> </BaseButton>
</div> </div>
</div> </div>
</transition> </transition>
@ -99,7 +99,7 @@
:disabled="taskService.loading || !canWrite" :disabled="taskService.loading || !canWrite"
ref="startDate" ref="startDate"
/> />
<a <BaseButton
@click="() => {task.startDate = null;saveTask()}" @click="() => {task.startDate = null;saveTask()}"
v-if="task.startDate && canWrite" v-if="task.startDate && canWrite"
class="remove" class="remove"
@ -107,7 +107,7 @@
<span class="icon is-small"> <span class="icon is-small">
<icon icon="times"></icon> <icon icon="times"></icon>
</span> </span>
</a> </BaseButton>
</div> </div>
</div> </div>
</transition> </transition>
@ -126,14 +126,14 @@
:disabled="taskService.loading || !canWrite" :disabled="taskService.loading || !canWrite"
ref="endDate" ref="endDate"
/> />
<a <BaseButton
@click="() => {task.endDate = null;saveTask()}" @click="() => {task.endDate = null;saveTask()}"
v-if="task.endDate && canWrite" v-if="task.endDate && canWrite"
class="remove"> class="remove">
<span class="icon is-small"> <span class="icon is-small">
<icon icon="times"></icon> <icon icon="times"></icon>
</span> </span>
</a> </BaseButton>
</div> </div>
</div> </div>
</transition> </transition>