Merge branch 'main' into feature/login-improvements
# Conflicts: # src/components/misc/no-auth-wrapper.vue # src/styles/components/_index.scss # src/views/user/Login.vue # src/views/user/Register.vue
This commit is contained in:
118
src/components/base/BaseButton.vue
Normal file
118
src/components/base/BaseButton.vue
Normal file
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<component
|
||||
:is="componentNodeName"
|
||||
class="base-button"
|
||||
:class="{ 'base-button--type-button': isButton }"
|
||||
v-bind="elementBindings"
|
||||
:disabled="disabled || undefined"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// see https://v3.vuejs.org/api/sfc-script-setup.html#usage-alongside-normal-script
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// this component removes styling differences between links / vue-router links and button elements
|
||||
// by doing so we make it easy abstract the functionality from style and enable easier and semantic
|
||||
// correct button and link usage. Also see: https://css-tricks.com/a-complete-guide-to-links-and-buttons/#accessibility-considerations
|
||||
|
||||
// the component tries to heuristically determine what it should be checking the props (see the
|
||||
// componentNodeName and elementBindings ref for this).
|
||||
|
||||
// NOTE: Do NOT use buttons with @click to push routes. => Use router-links instead!
|
||||
|
||||
import { ref, watchEffect, computed, useAttrs, PropType } from 'vue'
|
||||
|
||||
const BASE_BUTTON_TYPES_MAP = Object.freeze({
|
||||
button: 'button',
|
||||
submit: 'submit',
|
||||
})
|
||||
|
||||
type BaseButtonTypes = keyof typeof BASE_BUTTON_TYPES_MAP
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String as PropType<BaseButtonTypes>,
|
||||
default: 'button',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const componentNodeName = ref<Node['nodeName']>('button')
|
||||
interface ElementBindings {
|
||||
type?: string;
|
||||
rel?: string,
|
||||
}
|
||||
|
||||
const elementBindings = ref({})
|
||||
|
||||
const attrs = useAttrs()
|
||||
watchEffect(() => {
|
||||
// by default this component is a button element with the attribute of the type "button" (default prop value)
|
||||
let nodeName = 'button'
|
||||
let bindings: ElementBindings = {type: props.type}
|
||||
|
||||
// if we find a "to" prop we set it as router-link
|
||||
if ('to' in attrs) {
|
||||
nodeName = 'router-link'
|
||||
bindings = {}
|
||||
}
|
||||
|
||||
// if there is a href we assume the user wants an external link via a link element
|
||||
// we also set the attribute rel to "noopener" but make it possible to overwrite this by the user.
|
||||
if ('href' in attrs) {
|
||||
nodeName = 'a'
|
||||
bindings = {rel: 'noopener'}
|
||||
}
|
||||
|
||||
componentNodeName.value = nodeName
|
||||
elementBindings.value = {
|
||||
...bindings,
|
||||
...attrs,
|
||||
}
|
||||
})
|
||||
|
||||
const isButton = computed(() => componentNodeName.value === 'button')
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// NOTE: we do not use scoped styles to reduce specifity and make it easy to overwrite
|
||||
|
||||
// We reset the default styles of a button element to enable easier styling
|
||||
:where(.base-button--type-button) {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
text-align: center;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
:where(.base-button) {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
user-select: none;
|
||||
pointer-events: auto; // disable possible resets
|
||||
|
||||
&:focus {
|
||||
outline: transparent;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[background ? 'has-background' : '', $route.name+'-view']"
|
||||
:class="[background ? 'has-background' : '', $route.name as string +'-view']"
|
||||
:style="{'background-image': `url(${background})`}"
|
||||
class="link-share-container"
|
||||
>
|
||||
|
@ -555,4 +555,8 @@ $vikunja-nav-selected-width: 0.4rem;
|
||||
width: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.namespaces-list.loader-container.is-loading {
|
||||
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem + 1.5rem});
|
||||
}
|
||||
</style>
|
||||
|
@ -37,7 +37,7 @@
|
||||
<dropdown class="is-right" ref="usernameDropdown">
|
||||
<template #trigger>
|
||||
<x-button
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
:shadow="false">
|
||||
<span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span>
|
||||
<span class="icon is-small">
|
||||
|
@ -1,79 +1,64 @@
|
||||
<template>
|
||||
<a
|
||||
<BaseButton
|
||||
class="button"
|
||||
:class="{
|
||||
'is-loading': loading,
|
||||
'has-no-shadow': !shadow,
|
||||
'is-primary': type === 'primary',
|
||||
'is-outlined': type === 'secondary',
|
||||
'is-text is-inverted has-no-shadow underline-none':
|
||||
type === 'tertary',
|
||||
}"
|
||||
:disabled="disabled || null"
|
||||
@click="click"
|
||||
:href="href !== '' ? href : null"
|
||||
:class="[
|
||||
variantClass,
|
||||
{
|
||||
'is-loading': loading,
|
||||
'has-no-shadow': !shadow || variant === 'tertiary',
|
||||
}
|
||||
]"
|
||||
>
|
||||
<icon :icon="icon" v-if="showIconOnly"/>
|
||||
<span class="icon is-small" v-else-if="icon !== ''">
|
||||
<icon :icon="icon"/>
|
||||
</span>
|
||||
<slot></slot>
|
||||
</a>
|
||||
<slot />
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'x-button',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
to: {
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shadow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
computed: {
|
||||
showIconOnly() {
|
||||
return this.icon !== '' && typeof this.$slots.default === 'undefined'
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
click(e) {
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.to !== false) {
|
||||
this.$router.push(this.to)
|
||||
}
|
||||
|
||||
this.$emit('click', e)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, useSlots, PropType} from 'vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
const BUTTON_TYPES_MAP = Object.freeze({
|
||||
primary: 'is-primary',
|
||||
secondary: 'is-outlined',
|
||||
tertiary: 'is-text is-inverted underline-none',
|
||||
})
|
||||
|
||||
type ButtonTypes = keyof typeof BUTTON_TYPES_MAP
|
||||
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String as PropType<ButtonTypes>,
|
||||
default: 'primary',
|
||||
},
|
||||
icon: {
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shadow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const variantClass = computed(() => BUTTON_TYPES_MAP[props.variant])
|
||||
|
||||
const slots = useSlots()
|
||||
const showIconOnly = computed(() => props.icon !== '' && typeof slots.default === 'undefined')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button {
|
||||
transition: all $transition;
|
||||
@ -83,8 +68,8 @@ export default {
|
||||
font-weight: bold;
|
||||
height: $button-height;
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: inline-flex;
|
||||
|
||||
&.is-hovered,
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
@ -106,9 +91,10 @@ export default {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
&.is-small {
|
||||
border-radius: $radius;
|
||||
}
|
||||
}
|
||||
|
||||
.is-small {
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
.underline-none {
|
||||
|
@ -27,7 +27,7 @@
|
||||
@click="reset"
|
||||
class="is-small ml-2"
|
||||
:shadow="false"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
>
|
||||
{{ $t('input.resetColor') }}
|
||||
</x-button>
|
||||
|
@ -101,6 +101,7 @@
|
||||
class="is-fullwidth"
|
||||
:shadow="false"
|
||||
@click="close"
|
||||
v-cy="'closeDatepicker'"
|
||||
>
|
||||
{{ $t('misc.confirm') }}
|
||||
</x-button>
|
||||
|
@ -35,7 +35,7 @@
|
||||
<a @click="toggleEdit">{{ $t('input.editor.edit') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<x-button v-else-if="isEditActive" @click="toggleEdit" type="secondary" :shadow="false">
|
||||
<x-button v-else-if="isEditActive" @click="toggleEdit" variant="secondary" :shadow="false" v-cy="'saveEditor'">
|
||||
{{ $t('misc.save') }}
|
||||
</x-button>
|
||||
</template>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<x-button
|
||||
v-if="hasFilters"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
@click="clearFilters"
|
||||
>
|
||||
{{ $t('filters.clear') }}
|
||||
@ -10,7 +10,7 @@
|
||||
<template #trigger="{toggle}">
|
||||
<x-button
|
||||
@click.prevent.stop="toggle()"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="filter"
|
||||
>
|
||||
{{ $t('filters.title') }}
|
||||
|
@ -33,7 +33,9 @@ import {useStore} from 'vuex'
|
||||
|
||||
import ListService from '@/services/list'
|
||||
|
||||
const background = ref(null)
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
|
||||
const background = ref<string | null>(null)
|
||||
const backgroundLoading = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -14,25 +14,25 @@
|
||||
</div>
|
||||
<footer class="modal-card-foot is-flex is-justify-content-flex-end">
|
||||
<x-button
|
||||
v-if="tertiary !== ''"
|
||||
:shadow="false"
|
||||
type="tertary"
|
||||
@click.prevent.stop="$emit('tertary')"
|
||||
v-if="tertary !== ''"
|
||||
variant="tertiary"
|
||||
@click.prevent.stop="$emit('tertiary')"
|
||||
>
|
||||
{{ tertary }}
|
||||
{{ tertiary }}
|
||||
</x-button>
|
||||
<x-button
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
@click.prevent.stop="$router.back()"
|
||||
>
|
||||
{{ $t('misc.cancel') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
type="primary"
|
||||
v-if="primaryLabel !== ''"
|
||||
variant="primary"
|
||||
@click.prevent.stop="primary"
|
||||
:icon="primaryIcon"
|
||||
:disabled="primaryDisabled"
|
||||
v-if="primaryLabel !== ''"
|
||||
>
|
||||
{{ primaryLabel }}
|
||||
</x-button>
|
||||
@ -65,7 +65,7 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tertary: {
|
||||
tertiary: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
@ -78,7 +78,7 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['create', 'primary', 'tertary'],
|
||||
emits: ['create', 'primary', 'tertiary'],
|
||||
methods: {
|
||||
primary() {
|
||||
this.$emit('create')
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="dropdown is-right is-active" ref="dropdown">
|
||||
<div class="dropdown-trigger" @click="open = !open">
|
||||
<div class="dropdown-trigger is-flex" @click="open = !open">
|
||||
<slot name="trigger" :close="close">
|
||||
<icon :icon="triggerIcon" class="icon"/>
|
||||
</slot>
|
||||
|
@ -26,7 +26,7 @@
|
||||
@click="action.callback"
|
||||
:shadow="false"
|
||||
class="is-small"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
v-for="(action, i) in item.data.actions"
|
||||
>
|
||||
{{ action.title }}
|
||||
|
@ -50,11 +50,12 @@ import Message from '@/components/misc/message.vue'
|
||||
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
|
||||
|
||||
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
|
||||
import {useOnline} from '@/composables/useOnline'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const ready = computed(() => store.state.vikunjaReady)
|
||||
const online = computed(() => store.state.online)
|
||||
const online = useOnline()
|
||||
|
||||
const error = ref('')
|
||||
const showLoading = computed(() => !ready.value && error.value === '')
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<x-button
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
:icon="icon"
|
||||
v-tooltip="tooltipText"
|
||||
@click="changeSubscription"
|
||||
|
@ -31,14 +31,15 @@
|
||||
<div class="actions">
|
||||
<x-button
|
||||
@click="$emit('close')"
|
||||
type="tertary"
|
||||
variant="tertiary"
|
||||
class="has-text-danger"
|
||||
>
|
||||
{{ $t('misc.cancel') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="$emit('submit')"
|
||||
type="primary"
|
||||
variant="primary"
|
||||
v-cy="'modalPrimary'"
|
||||
:shadow="false"
|
||||
>
|
||||
{{ $t('misc.doit') }}
|
||||
|
@ -42,7 +42,7 @@ import {useI18n} from 'vue-i18n'
|
||||
import {useStore} from 'vuex'
|
||||
import { tryOnMounted, debouncedWatch, useWindowSize, MaybeRef } from '@vueuse/core'
|
||||
|
||||
import TaskService from '../../services/task'
|
||||
import TaskService from '@/services/task'
|
||||
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
|
||||
|
||||
function cleanupTitle(title: string) {
|
||||
@ -117,9 +117,6 @@ function useAutoHeightTextarea(value: MaybeRef<string>) {
|
||||
return textarea
|
||||
}
|
||||
|
||||
const emit = defineEmits(['taskAdded'])
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
defaultPosition: {
|
||||
type: Number,
|
||||
@ -127,8 +124,7 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
const errorMessage = ref('')
|
||||
const emit = defineEmits(['taskAdded'])
|
||||
|
||||
const newTaskTitle = ref('')
|
||||
const newTaskInput = useAutoHeightTextarea(newTaskTitle)
|
||||
@ -136,6 +132,9 @@ const newTaskInput = useAutoHeightTextarea(newTaskTitle)
|
||||
const { t } = useI18n()
|
||||
const store = useStore()
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
const errorMessage = ref('')
|
||||
|
||||
async function addTask() {
|
||||
if (newTaskTitle.value === '') {
|
||||
errorMessage.value = t('list.create.addTitleRequired')
|
||||
|
@ -78,7 +78,6 @@
|
||||
<script>
|
||||
import AsyncEditor from '@/components/input/AsyncEditor'
|
||||
|
||||
import ListService from '../../services/list'
|
||||
import TaskService from '../../services/task'
|
||||
import TaskModel from '../../models/task'
|
||||
import priorities from '../../models/constants/priorities'
|
||||
@ -90,14 +89,10 @@ export default {
|
||||
name: 'edit-task',
|
||||
data() {
|
||||
return {
|
||||
listId: this.$route.params.id,
|
||||
listService: new ListService(),
|
||||
taskService: new TaskService(),
|
||||
|
||||
priorities: priorities,
|
||||
list: {},
|
||||
editorActive: false,
|
||||
newTask: new TaskModel(),
|
||||
isTaskEdit: false,
|
||||
taskEditTask: TaskModel,
|
||||
}
|
||||
|
@ -183,6 +183,8 @@ import {mapState} from 'vuex'
|
||||
import Rights from '../../models/constants/rights.json'
|
||||
import FilterPopup from '@/components/list/partials/filter-popup.vue'
|
||||
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
|
||||
export default {
|
||||
name: 'GanttChart',
|
||||
components: {
|
||||
@ -201,10 +203,10 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
dateFrom: {
|
||||
default: new Date(new Date().setDate(new Date().getDate() - 15)),
|
||||
default: () => new Date(new Date().setDate(new Date().getDate() - 15)),
|
||||
},
|
||||
dateTo: {
|
||||
default: new Date(new Date().setDate(new Date().getDate() + 30)),
|
||||
default: () => new Date(new Date().setDate(new Date().getDate() + 30)),
|
||||
},
|
||||
// The width of a day in pixels, used to calculate all sorts of things.
|
||||
dayWidth: {
|
||||
@ -252,6 +254,7 @@ export default {
|
||||
canWrite: (state) => state.currentList.maxRight > Rights.READ,
|
||||
}),
|
||||
methods: {
|
||||
colorIsDark,
|
||||
buildTheGanttChart() {
|
||||
this.setDates()
|
||||
this.prepareGanttDays()
|
||||
|
@ -83,7 +83,7 @@
|
||||
@click="$refs.files.click()"
|
||||
class="mb-4"
|
||||
icon="cloud-upload-alt"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
>
|
||||
{{ $t('task.attachment.upload') }}
|
||||
|
@ -8,21 +8,21 @@
|
||||
<x-button
|
||||
@click.prevent.stop="() => deferDays(1)"
|
||||
:shadow="false"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
>
|
||||
{{ $t('task.deferDueDate.1day') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click.prevent.stop="() => deferDays(3)"
|
||||
:shadow="false"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
>
|
||||
{{ $t('task.deferDueDate.3days') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click.prevent.stop="() => deferDays(7)"
|
||||
:shadow="false"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
>
|
||||
{{ $t('task.deferDueDate.1week') }}
|
||||
</x-button>
|
||||
|
@ -73,6 +73,8 @@ import Done from '@/components/misc/Done.vue'
|
||||
import Labels from '../../../components/tasks/partials/labels'
|
||||
import ChecklistSummary from './checklist-summary'
|
||||
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
|
||||
export default {
|
||||
name: 'kanban-card',
|
||||
components: {
|
||||
@ -98,6 +100,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
colorIsDark,
|
||||
async toggleTaskDone(task) {
|
||||
this.loadingInternal = true
|
||||
try {
|
||||
|
@ -6,7 +6,7 @@
|
||||
class="is-pulled-right add-task-relation-button"
|
||||
:class="{'is-active': showNewRelationForm}"
|
||||
v-tooltip="$t('task.relation.add')"
|
||||
type="secondary"
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
:shadow="false"
|
||||
/>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="control repeat-after-input">
|
||||
<div class="buttons has-addons is-centered mt-2">
|
||||
<x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'days')">{{ $t('task.repeat.everyDay') }}</x-button>
|
||||
<x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">{{ $t('task.repeat.everyWeek') }}</x-button>
|
||||
<x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">{{ $t('task.repeat.everyMonth') }}</x-button>
|
||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'days')">{{ $t('task.repeat.everyDay') }}</x-button>
|
||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">{{ $t('task.repeat.everyWeek') }}</x-button>
|
||||
<x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">{{ $t('task.repeat.everyMonth') }}</x-button>
|
||||
</div>
|
||||
<div class="is-flex is-align-items-center mb-2">
|
||||
<label for="repeatMode" class="is-fullwidth">
|
||||
|
Reference in New Issue
Block a user