chore: use the <dropdown> and <dropdown-item> components everywhere

Resolves https://kolaente.dev/vikunja/frontend/issues/2176
This commit is contained in:
kolaente 2022-07-20 17:08:46 +02:00
parent d6a10b01dd
commit cdb63b578d
No known key found for this signature in database
GPG key ID: F40E70337AB24C9B
11 changed files with 146 additions and 76 deletions

View file

@ -554,8 +554,4 @@ $vikunja-nav-selected-width: 0.4rem;
.namespaces-list.loader-container.is-loading { .namespaces-list.loader-container.is-loading {
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem + 1.5rem}); min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem + 1.5rem});
} }
a.dropdown-item:hover {
background: var(--dropdown-item-hover-background-color) !important;
}
</style> </style>

View file

@ -56,12 +56,13 @@
{{ $t('menu.archive') }} {{ $t('menu.archive') }}
</dropdown-item> </dropdown-item>
<task-subscription <task-subscription
class="dropdown-item has-no-shadow" class="has-no-shadow"
:is-button="false" :is-button="false"
entity="list" entity="list"
:entity-id="list.id" :entity-id="list.id"
:subscription="list.subscription" :subscription="list.subscription"
@change="sub => subscription = sub" @change="sub => subscription = sub"
type="dropdown"
/> />
<dropdown-item <dropdown-item
:to="{ name: 'list.settings.delete', params: { listId: list.id } }" :to="{ name: 'list.settings.delete', params: { listId: list.id } }"

View file

@ -1,25 +1,95 @@
<template> <template>
<router-link <component
:is="componentNodeName"
v-bind="elementBindings"
:to="to" :to="to"
class="dropdown-item"> class="dropdown-item">
<span class="icon" v-if="icon !== ''"> <span class="icon" v-if="icon">
<icon :icon="icon"/> <icon :icon="icon"/>
</span> </span>
<span> <span>
<slot></slot> <slot></slot>
</span> </span>
</router-link> </component>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineProps({ import {ref, useAttrs, watchEffect} from 'vue'
to: {
required: true, const props = defineProps<{
}, to?: object,
icon: { icon?: string | string[],
type: String, }>()
required: false,
default: '', const componentNodeName = ref<Node['nodeName']>('a')
}, const elementBindings = ref({})
const attrs = useAttrs()
watchEffect(() => {
let nodeName = 'a'
if (props.to) {
nodeName = 'router-link'
}
if ('href' in attrs) {
nodeName = 'BaseButton'
}
componentNodeName.value = nodeName
elementBindings.value = {
...attrs,
}
}) })
</script> </script>
<style scoped lang="scss">
.dropdown-item {
color: var(--text);
display: block;
font-size: 0.875rem;
line-height: 1.5;
padding: $item-padding;
position: relative;
}
a.dropdown-item,
button.dropdown-item {
text-align: inherit;
white-space: nowrap;
width: 100%;
display: flex;
align-items: center;
justify-content: left !important;
&:hover {
background-color: var(--grey-100) !important;
}
&.is-active {
background-color: var(--link);
color: var(--link-invert);
}
.icon {
padding-right: .5rem;
}
.icon:not(.has-text-success) {
color: var(--grey-300) !important;
}
&.has-text-danger .icon {
color: var(--danger) !important;
}
&.is-disabled {
cursor: not-allowed;
&:hover {
background-color: transparent;
}
}
}
</style>

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="dropdown is-right is-active" ref="dropdown"> <div class="dropdown" ref="dropdown">
<slot name="trigger" :close="close" :toggleOpen="toggleOpen"> <slot name="trigger" :close="close" :toggleOpen="toggleOpen">
<BaseButton class="dropdown-trigger is-flex" @click="toggleOpen"> <BaseButton class="dropdown-trigger is-flex" @click="toggleOpen">
<icon :icon="triggerIcon" class="icon"/> <icon :icon="triggerIcon" class="icon"/>
@ -51,7 +51,36 @@ onClickOutside(dropdown, (e: Event) => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.dropdown-menu .dropdown-content { .dropdown {
display: inline-flex;
position: relative;
vertical-align: top;
}
.dropdown-menu {
min-width: 12rem;
padding-top: 4px;
position: absolute;
top: 100%;
z-index: 20;
display: block;
left: auto;
right: 0;
}
.dropdown-content {
background-color: var(--scheme-main);
border-radius: $radius;
padding-bottom: .5rem;
padding-top: .5rem;
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
} }
</style>
.dropdown-divider {
background-color: var(--border-light);
border: none;
display: block;
height: 1px;
margin: 0.5rem 0;
}
</style>

View file

@ -1,6 +1,6 @@
<template> <template>
<x-button <x-button
v-if="isButton" v-if="type === 'button'"
variant="secondary" variant="secondary"
:icon="iconName" :icon="iconName"
v-tooltip="tooltipText" v-tooltip="tooltipText"
@ -9,6 +9,15 @@
> >
{{ buttonText }} {{ buttonText }}
</x-button> </x-button>
<DropdownItem
v-else-if="type === 'dropdown'"
v-tooltip="tooltipText"
@click="changeSubscription"
:class="{'is-disabled': disabled}"
:icon="iconName"
>
{{ buttonText }}
</DropdownItem>
<BaseButton <BaseButton
v-else v-else
v-tooltip="tooltipText" v-tooltip="tooltipText"
@ -27,6 +36,7 @@ import {computed, shallowRef} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import SubscriptionService from '@/services/subscription' import SubscriptionService from '@/services/subscription'
import SubscriptionModel from '@/models/subscription' import SubscriptionModel from '@/models/subscription'
@ -34,15 +44,15 @@ import SubscriptionModel from '@/models/subscription'
import {success} from '@/message' import {success} from '@/message'
interface Props { interface Props {
entity: string entity: string
entityId: number entityId: number
subscription: SubscriptionModel | null subscription: SubscriptionModel | null
isButton?: boolean type?: 'button' | 'dropdown' | null
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
isButton: true,
subscription: null, subscription: null,
type: 'button',
}) })
const subscriptionEntity = computed<string | null>(() => props.subscription?.entity ?? null) const subscriptionEntity = computed<string | null>(() => props.subscription?.entity ?? null)

View file

@ -34,12 +34,13 @@
{{ $t('menu.archive') }} {{ $t('menu.archive') }}
</dropdown-item> </dropdown-item>
<task-subscription <task-subscription
class="dropdown-item has-no-shadow" class="has-no-shadow"
:is-button="false" :is-button="false"
entity="namespace" entity="namespace"
:entity-id="namespace.id" :entity-id="namespace.id"
:subscription="subscription" :subscription="subscription"
@change="sub => subscription = sub" @change="sub => subscription = sub"
type="dropdown"
/> />
<dropdown-item <dropdown-item
:to="{ name: 'namespace.settings.delete', params: { id: namespace.id } }" :to="{ name: 'namespace.settings.delete', params: { id: namespace.id } }"

View file

@ -50,7 +50,6 @@ const props = defineProps({
for (const e of prop) { for (const e of prop) {
if (!isDate(e) && !isString(e)) { if (!isDate(e) && !isString(e)) {
console.log('validation failed', e, e instanceof Date)
return false return false
} }
} }

View file

@ -24,9 +24,6 @@ $vikunja-font: 'Quicksand', sans-serif;
$pagination-current-border: var(--primary); $pagination-current-border: var(--primary);
$navbar-item-active-color: var(--primary); $navbar-item-active-color: var(--primary);
$dropdown-content-shadow: none;
$dropdown-item-hover-background-color: var(--grey-100);
$site-background: var(--grey-100); $site-background: var(--grey-100);
$transition-duration: 150ms; $transition-duration: 150ms;

View file

@ -44,7 +44,7 @@
// imports from "bulma-css-variables/sass/components/_all"; // imports from "bulma-css-variables/sass/components/_all";
// @import "bulma-css-variables/sass/components/breadcrumb"; // not used // @import "bulma-css-variables/sass/components/breadcrumb"; // not used
@import "bulma-css-variables/sass/components/card"; @import "bulma-css-variables/sass/components/card";
@import "bulma-css-variables/sass/components/dropdown"; // @import "bulma-css-variables/sass/components/dropdown"; // moved to component
// @import "bulma-css-variables/sass/components/level"; // not used // @import "bulma-css-variables/sass/components/level"; // not used
@import "bulma-css-variables/sass/components/media"; @import "bulma-css-variables/sass/components/media";
@import "bulma-css-variables/sass/components/menu"; @import "bulma-css-variables/sass/components/menu";

View file

@ -91,34 +91,6 @@ button.table {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
// FIXME: merge with dropdown-item.vue
// for this to happen the component has to be used everywhere
.dropdown-item {
display: flex;
align-items: center;
justify-content: left !important;
.icon {
padding-right: .5rem;
}
.icon:not(.has-text-success) {
color: var(--grey-300) !important;
}
&.has-text-danger .icon {
color: var(--danger) !important;
}
&.is-disabled {
cursor: not-allowed;
&:hover {
background-color: transparent;
}
}
}
.is-max-width-desktop { .is-max-width-desktop {
width: 100%; width: 100%;
max-width: $desktop; max-width: $desktop;

View file

@ -62,9 +62,8 @@
trigger-icon="ellipsis-v" trigger-icon="ellipsis-v"
@close="() => showSetLimitInput = false" @close="() => showSetLimitInput = false"
> >
<a <dropdown-item
@click.stop="showSetLimitInput = true" @click.stop="showSetLimitInput = true"
class="dropdown-item"
> >
<div class="field has-addons" v-if="showSetLimitInput"> <div class="field has-addons" v-if="showSetLimitInput">
<div class="control"> <div class="control">
@ -93,34 +92,32 @@
$t('list.kanban.limit', {limit: bucket.limit > 0 ? bucket.limit : $t('list.kanban.noLimit')}) $t('list.kanban.limit', {limit: bucket.limit > 0 ? bucket.limit : $t('list.kanban.noLimit')})
}} }}
</template> </template>
</a> </dropdown-item>
<a <dropdown-item
@click.stop="toggleDoneBucket(bucket)" @click.stop="toggleDoneBucket(bucket)"
class="dropdown-item"
v-tooltip="$t('list.kanban.doneBucketHintExtended')" v-tooltip="$t('list.kanban.doneBucketHintExtended')"
> >
<span class="icon is-small" :class="{'has-text-success': bucket.isDoneBucket}"> <span class="icon is-small" :class="{'has-text-success': bucket.isDoneBucket}">
<icon icon="check-double"/> <icon icon="check-double"/>
</span> </span>
{{ $t('list.kanban.doneBucket') }} {{ $t('list.kanban.doneBucket') }}
</a> </dropdown-item>
<a <dropdown-item
class="dropdown-item"
@click.stop="() => collapseBucket(bucket)" @click.stop="() => collapseBucket(bucket)"
> >
{{ $t('list.kanban.collapse') }} {{ $t('list.kanban.collapse') }}
</a> </dropdown-item>
<a <dropdown-item
:class="{'is-disabled': buckets.length <= 1}" :class="{'is-disabled': buckets.length <= 1}"
@click.stop="() => deleteBucketModal(bucket.id)" @click.stop="() => deleteBucketModal(bucket.id)"
class="dropdown-item has-text-danger" class="has-text-danger"
v-tooltip="buckets.length <= 1 ? $t('list.kanban.deleteLast') : ''" v-tooltip="buckets.length <= 1 ? $t('list.kanban.deleteLast') : ''"
> >
<span class="icon is-small"> <span class="icon is-small">
<icon icon="trash-alt"/> <icon icon="trash-alt"/>
</span> </span>
{{ $t('misc.delete') }} {{ $t('misc.delete') }}
</a> </dropdown-item>
</dropdown> </dropdown>
</div> </div>
@ -241,6 +238,7 @@ import Dropdown from '@/components/misc/dropdown.vue'
import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState' import {getCollapsedBucketState, saveCollapsedBucketState} from '@/helpers/saveCollapsedBucketState'
import {calculateItemPosition} from '../../helpers/calculateItemPosition' import {calculateItemPosition} from '../../helpers/calculateItemPosition'
import KanbanCard from '@/components/tasks/partials/kanban-card.vue' import KanbanCard from '@/components/tasks/partials/kanban-card.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
const DRAG_OPTIONS = { const DRAG_OPTIONS = {
// sortable options // sortable options
@ -256,6 +254,7 @@ const MIN_SCROLL_HEIGHT_PERCENT = 0.25
export default defineComponent({ export default defineComponent({
name: 'Kanban', name: 'Kanban',
components: { components: {
DropdownItem,
ListWrapper, ListWrapper,
KanbanCard, KanbanCard,
Dropdown, Dropdown,
@ -728,10 +727,6 @@ $filter-container-height: '1rem - #{$switch-view-height}';
} }
} }
a.dropdown-item {
padding-right: 1rem;
}
&.is-collapsed { &.is-collapsed {
align-self: flex-start; align-self: flex-start;
transform: rotate(90deg) translateY(-100%); transform: rotate(90deg) translateY(-100%);