Compare commits

..

17 Commits

Author SHA1 Message Date
Dominik Pschenitschni 551f3f54e1
chore: better variable typing
continuous-integration/drone/pr Build is failing Details
2022-07-06 23:59:58 +02:00
Dominik Pschenitschni 4005b91f88
chore: remove unnecessary defineComponent 2022-07-06 23:59:58 +02:00
Dominik Pschenitschni ab2dfca38c
chore: remove global mixing 2022-07-06 23:59:57 +02:00
Dominik Pschenitschni 0ae17f4f10
chore: remove date mixins 2022-07-06 23:59:57 +02:00
Dominik Pschenitschni 9b41024a00
feat: function attribute typing 2022-07-06 23:58:18 +02:00
Dominik Pschenitschni 2c00737092
feat: constants 2022-07-06 23:56:52 +02:00
Dominik Pschenitschni a27f09300c
chore: improve type imports 2022-07-06 23:56:52 +02:00
Dominik Pschenitschni f99e9ad595
feat: add properties to models 2022-07-06 23:19:47 +02:00
Dominik Pschenitschni 744eec9d5d
feat: convert abstractService to ts 2022-07-06 23:17:35 +02:00
Dominik Pschenitschni f3835d7dfe fix(quick-add-magic): use ButtonLink
continuous-integration/drone/push Build is passing Details
2022-07-06 21:07:26 +00:00
Dominik Pschenitschni 9a26310ad6 fix(ListList): use ButtonLink 2022-07-06 21:07:26 +00:00
Dominik Pschenitschni 6ddede4863 feat(BaseButton): add target _blank for links by default 2022-07-06 21:07:26 +00:00
Dominik Pschenitschni 12544c52ca fix: add ButtonLink component
Add ButtonLink component to fix occasions where the BaseButton needs to be styled in a link color.
2022-07-06 21:07:26 +00:00
Dominik Pschenitschni 02f985d8a3 fix: button styling 2022-07-06 21:07:26 +00:00
Dominik Pschenitschni 3b9bc5b2f8 feat: use BaseButton where easily possible
This replaces links with BaseButton components. BaseButton will use `<button type="button">` inside for this case. This improves accessibility a lot. Also we might be able to remove the `.stop` modifiers in some places because AFAIK the button element stops propagation by default.
2022-07-06 21:07:26 +00:00
Dominik Pschenitschni 9e1ec72739 feat: use inline-block for BaseButton 2022-07-06 21:07:26 +00:00
Dominik Pschenitschni 2c2fc4c9ee [skip ci] Updated translations via Crowdin 2022-07-05 00:12:36 +00:00
41 changed files with 405 additions and 387 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

@ -11,12 +11,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' export default { inheritAttrs: false }
// see https://v3.vuejs.org/api/sfc-script-setup.html#usage-alongside-normal-script
export default defineComponent({
inheritAttrs: false,
})
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
@ -53,7 +48,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 +70,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 +102,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 type {SortableEvent} from 'sortablejs' import type {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'
@ -170,10 +155,10 @@ import Logo from '@/components/home/Logo.vue'
import {MENU_ACTIVE} from '@/store/mutation-types' import {MENU_ACTIVE} from '@/store/mutation-types'
import {calculateItemPosition} from '@/helpers/calculateItemPosition' import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle' import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {getListTitle} from '@/helpers/getListTitle'
import {useEventListener} from '@vueuse/core' import {useEventListener} from '@vueuse/core'
import type NamespaceModel from '@/models/namespace' import type NamespaceModel from '@/models/namespace'
import type ListModel from '@/models/list' import type ListModel from '@/models/list'
import {getListTitle} from '@/helpers/getListTitle'
const drag = ref(false) const drag = ref(false)
const dragOptions = { const dragOptions = {
@ -335,7 +320,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;
@ -353,28 +338,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 {
@ -393,6 +371,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;
@ -411,11 +391,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;
@ -445,7 +420,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;
} }
@ -484,7 +459,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;
@ -557,7 +536,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,8 +95,9 @@ 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 {formatDate, formatDateShort} from '@/helpers/time/formatDate' import BaseButton from '@/components/base/BaseButton.vue'
import {formatDate, formatDateShort} from '@/helpers/time/formatDate'
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval' import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours' import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
@ -135,6 +114,7 @@ export default defineComponent({
}, },
components: { components: {
flatPickr, flatPickr,
BaseButton,
}, },
props: { props: {
modelValue: { modelValue: {
@ -265,68 +245,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 type ListModel from '@/models/list' import type 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="formatDateLong(n.created)"> <span class="created" v-tooltip="formatDateLong(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 NotificationModel, { NOTIFICATION_NAMES as names} from '@/models/notification' import NotificationModel, { NOTIFICATION_NAMES as names} from '@/models/notification'
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

@ -133,9 +133,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' export default {name: 'userTeamShare'}
export default defineComponent({name: 'userTeamShare'})
</script> </script>
<script setup lang="ts"> <script setup lang="ts">

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,15 @@ import TaskCollectionService from '../../services/taskCollection'
import {mapState} from 'vuex' import {mapState} from 'vuex'
import {RIGHTS as Rights} from '@/models/constants/rights' import {RIGHTS as Rights} from '@/models/constants/rights'
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'
import {formatDate} from '@/helpers/time/formatDate'
export default defineComponent({ export default defineComponent({
name: 'GanttChart', name: 'GanttChart',
components: { components: {
BaseButton,
FilterPopup, FilterPopup,
PriorityLabel, PriorityLabel,
EditTask, EditTask,
@ -434,7 +438,7 @@ export default defineComponent({
this.hideCrateNewTask() this.hideCrateNewTask()
}, },
formatYear(date) { formatYear(date) {
return this.formatDate(date, 'MMMM, yyyy') return formatDate(date, 'MMMM, yyyy')
}, },
}, },
}) })

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>
@ -150,9 +156,12 @@ import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { uploadFiles, generateAttachmentUrl } from '@/helpers/attachments' import { uploadFiles, generateAttachmentUrl } from '@/helpers/attachments'
import {formatDate, formatDateSince, formatDateLong} from '@/helpers/time/formatDate' import {formatDate, formatDateSince, formatDateLong} from '@/helpers/time/formatDate'
import BaseButton from '@/components/base/BaseButton'
export default defineComponent({ export default defineComponent({
name: 'attachments', name: 'attachments',
components: { components: {
BaseButton,
User, User,
}, },
data() { data() {
@ -225,6 +234,7 @@ export default defineComponent({
formatDate, formatDate,
formatDateSince, formatDateSince,
formatDateLong, formatDateLong,
downloadAttachment(attachment: AttachmentModel) { downloadAttachment(attachment: AttachmentModel) {
this.attachmentService.download(attachment) this.attachmentService.download(attachment)
}, },
@ -302,7 +312,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;
} }
@ -382,7 +392,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;
} }
@ -392,6 +402,10 @@ export default defineComponent({
} }
} }
.attachment-info-meta-button {
color: var(--link);
}
@keyframes bounce { @keyframes bounce {
from, from,
20%, 20%,

View File

@ -163,8 +163,8 @@ import TaskCommentModel from '@/models/taskComment'
import {uploadFile} from '@/helpers/attachments' import {uploadFile} from '@/helpers/attachments'
import {success} from '@/message' import {success} from '@/message'
import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate' import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
import type TaskModel from '@/models/task'
import type TaskModel from '@/models/task'
const props = defineProps({ const props = defineProps({
taskId: { taskId: {
type: Number, type: Number,

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 type UserModel from '@/models/user' import type UserModel from '@/models/user'

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>
@ -144,6 +145,7 @@ import TaskModel from '../../../models/task'
import TaskRelationService from '../../../services/taskRelation' import TaskRelationService from '../../../services/taskRelation'
import TaskRelationModel, {RELATION_KINDS} from '@/models/taskRelation' import TaskRelationModel, {RELATION_KINDS} 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({
@ -165,6 +167,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 {type PropType, ref, onMounted, watch} from 'vue' import {type 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="formatDateLong(task.dueDate)" v-tooltip="formatDateLong(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>
@ -97,14 +101,13 @@ import {defineComponent} from 'vue'
import TaskModel from '../../../models/task' import TaskModel from '../../../models/task'
import PriorityLabel from './priorityLabel.vue' import PriorityLabel from './priorityLabel.vue'
import TaskService from '../../../services/task' import TaskService from '../../../services/task'
import Labels from './labels.vue' import BaseButton from '@/components/base/BaseButton.vue'
import User from '../../misc/user.vue'
import Fancycheckbox from '../../input/fancycheckbox.vue' import Fancycheckbox from '../../input/fancycheckbox.vue'
import DeferTask from './defer-task.vue' import DeferTask from './defer-task.vue'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {playPop} from '@/helpers/playPop' import {playPop} from '@/helpers/playPop'
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
import ChecklistSummary from './checklist-summary.vue' import ChecklistSummary from './checklist-summary.vue'
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
export default defineComponent({ export default defineComponent({
name: 'singleTaskInList', name: 'singleTaskInList',
@ -116,6 +119,7 @@ export default defineComponent({
} }
}, },
components: { components: {
BaseButton,
ChecklistSummary, ChecklistSummary,
DeferTask, DeferTask,
Fancycheckbox, Fancycheckbox,
@ -182,6 +186,7 @@ export default defineComponent({
formatDateSince, formatDateSince,
formatISO, formatISO,
formatDateLong, formatDateLong,
async markAsDone(checked: boolean) { async markAsDone(checked: boolean) {
const updateFunc = async () => { const updateFunc = async () => {
const task = await this.taskService.update(this.task) const task = await this.taskService.update(this.task)
@ -253,6 +258,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

@ -77,7 +77,7 @@
"newName": "Il nuovo nome", "newName": "Il nuovo nome",
"savedSuccess": "Impostazioni salvate con successo.", "savedSuccess": "Impostazioni salvate con successo.",
"emailReminders": "Inviami promemoria per le attività via e-mail", "emailReminders": "Inviami promemoria per le attività via e-mail",
"overdueReminders": "Send me a summary of my undone overdue tasks every day", "overdueReminders": "Inviami ogni giorno un riassunto delle attività in ritardo",
"discoverableByName": "Lascia che altri utenti mi trovino cercando il mio nome", "discoverableByName": "Lascia che altri utenti mi trovino cercando il mio nome",
"discoverableByEmail": "Lascia che altri utenti mi trovino quando cercano il mio indirizzo e-mail completo", "discoverableByEmail": "Lascia che altri utenti mi trovino quando cercano il mio indirizzo e-mail completo",
"playSoundWhenDone": "Riproduci un suono quando le attività vengono segnate come fatte", "playSoundWhenDone": "Riproduci un suono quando le attività vengono segnate come fatte",
@ -87,7 +87,7 @@
"language": "Lingua", "language": "Lingua",
"defaultList": "Lista predefinita", "defaultList": "Lista predefinita",
"timezone": "Fuso Orario", "timezone": "Fuso Orario",
"overdueTasksRemindersTime": "Overdue tasks reminder email time" "overdueTasksRemindersTime": "Orario email del promemoria attività in ritardo"
}, },
"totp": { "totp": {
"title": "Autenticazione a due fattori", "title": "Autenticazione a due fattori",
@ -728,16 +728,16 @@
"removeSuccess": "Etichetta eliminata.", "removeSuccess": "Etichetta eliminata.",
"addCreateSuccess": "Etichetta creata e aggiunta.", "addCreateSuccess": "Etichetta creata e aggiunta.",
"delete": { "delete": {
"header": "Delete this label", "header": "Elimina etichetta",
"text1": "Are you sure you want to delete this label?", "text1": "Sei sicuro di voler eliminare questa etichetta?",
"text2": "This will remove it from all tasks and cannot be restored." "text2": "Verrà rimossa da tutte le attività e non potrà essere ripristinata."
} }
}, },
"priority": { "priority": {
"unset": "Azzera", "unset": "Azzera",
"low": "Bassa", "low": "Bassa",
"medium": "Media", "medium": "Media",
"high": "High", "high": "Alta",
"urgent": "Urgente", "urgent": "Urgente",
"doNow": "FARE ORA" "doNow": "FARE ORA"
}, },

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,6 +116,7 @@ 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.vue' import ColorPicker from '@/components/input/colorPicker.vue'
import { setTitle } from '@/helpers/setTitle' import { setTitle } from '@/helpers/setTitle'
@ -123,6 +124,7 @@ import { setTitle } from '@/helpers/setTitle'
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.vue' import EditTask from '@/components/tasks/edit-task.vue'
import AddTask from '@/components/tasks/add-task.vue' import AddTask from '@/components/tasks/add-task.vue'
@ -191,6 +193,7 @@ export default defineComponent({
} }
}, },
components: { components: {
BaseButton,
ListWrapper, ListWrapper,
Nothing, Nothing,
FilterPopup, FilterPopup,
@ -199,6 +202,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,13 +81,14 @@ 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'
import { setTitle } from '@/helpers/setTitle' import { setTitle } from '@/helpers/setTitle'
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

@ -74,6 +74,8 @@ import Logo from '@/assets/logo.svg?component'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import { setTitle } from '@/helpers/setTitle' import { setTitle } from '@/helpers/setTitle'
import {formatDateLong} from '@/helpers/time/formatDate'
import {MIGRATORS} from './migrators' import {MIGRATORS} from './migrators'
const PROGRESS_DOTS_COUNT = 8 const PROGRESS_DOTS_COUNT = 8
@ -119,6 +121,8 @@ export default defineComponent({
}, },
methods: { methods: {
formatDateLong,
async initMigration() { async initMigration() {
this.migrationService = this.migrator.isFileMigrator this.migrationService = this.migrator.isFileMigrator
? new AbstractMigrationFileService(this.migrator.id) ? new AbstractMigrationFileService(this.migrator.id)

View File

@ -21,11 +21,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' export default { name: 'namespace-setting-share' }
export default defineComponent({
name: 'namespace-setting-share',
})
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>

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>

View File

@ -58,11 +58,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' export default { name: 'user-settings-avatar' }
export default defineComponent({
name: 'user-settings-avatar',
})
</script> </script>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -39,11 +39,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' export default {name: 'user-settings-data-export'}
export default defineComponent({
name: 'user-settings-data-export',
})
</script> </script>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -37,10 +37,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' export default { name: 'user-settings-update-email' }
export default defineComponent({
name: 'user-settings-update-email',
})
</script> </script>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -52,11 +52,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' export default {name: 'user-settings-password-update'}
export default defineComponent({
name: 'user-settings-password-update',
})
</script> </script>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -65,8 +65,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' export default { name: 'user-settings-totp' }
export default defineComponent({ name: 'user-settings-totp' })
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>