feat(assignees): improve avatar list consistency

Resolves vikunja/frontend#3354
This commit is contained in:
kolaente 2023-09-04 13:03:39 +02:00
parent 270e32290a
commit f63c39a578
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
7 changed files with 129 additions and 71 deletions

View File

@ -11,7 +11,12 @@
class="input-wrapper input" class="input-wrapper input"
:class="{'has-multiple': hasMultiple}" :class="{'has-multiple': hasMultiple}"
> >
<template v-if="Array.isArray(internalValue)"> <slot
v-if="Array.isArray(internalValue)"
name="items"
:items="internalValue"
:remove="remove"
>
<template v-for="(item, key) in internalValue"> <template v-for="(item, key) in internalValue">
<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">
@ -20,7 +25,7 @@
</span> </span>
</slot> </slot>
</template> </template>
</template> </slot>
<input <input
type="text" type="text"
@ -85,7 +90,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, toRefs, watch, type ComponentPublicInstance, type PropType} from 'vue' import {
computed, onBeforeUnmount, onMounted, ref, toRefs, watch, type ComponentPublicInstance, type PropType,
watchEffect,
} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'

View File

@ -0,0 +1,87 @@
<script setup lang="ts">
import type {IUser} from '@/modelTypes/IUser'
import BaseButton from '@/components/base/BaseButton.vue'
import User from '@/components/misc/user.vue'
import {computed} from 'vue'
type removeFunction = (item: any) => {}
const {
assignees,
remove,
disabled,
avatarSize = 30,
inline = false,
} = defineProps<{
assignees: IUser[],
remove?: removeFunction,
disabled?: boolean,
avatarSize?: number,
inline?: boolean,
}>()
const hasDelete = computed(() => typeof remove !== 'undefined' && !disabled)
</script>
<template>
<div class="assignees-list" :class="{'is-inline': inline}">
<span class="assignee" v-for="user in assignees">
<User
:avatar-size="avatarSize"
:show-username="false"
:user="user"
:class="{'m-2': hasDelete, 'mr-3': !hasDelete}"
/>
<BaseButton
v-if="hasDelete"
@click="remove(user)"
class="remove-assignee"
>
<icon icon="times"/>
</BaseButton>
</span>
</div>
</template>
<style scoped lang="scss">
.assignees-list {
display: flex;
&.is-inline :deep(.user) {
display: inline;
}
&:hover .assignee:not(:first-child) {
margin-left: -1rem;
}
}
.assignee {
position: relative;
transition: all $transition;
&:not(:first-child) {
margin-left: -1.5rem;
}
:deep(.user img) {
border: 2px solid var(--white);
margin-right: 0;
}
}
.remove-assignee {
position: absolute;
top: 4px;
left: 2px;
color: var(--danger);
background: var(--white);
padding: 0 4px;
display: block;
border-radius: 100%;
font-size: .75rem;
width: 18px;
height: 18px;
z-index: 100;
}
</style>

View File

@ -11,13 +11,8 @@
v-model="assignees" v-model="assignees"
:autocomplete-enabled="false" :autocomplete-enabled="false"
> >
<template #tag="{item: user}"> <template #items="{items, remove}">
<span class="assignee"> <assignee-list :assignees="items" :remove="removeAssignee"/>
<user :avatar-size="32" :show-username="false" :user="user" class="m-2"/>
<BaseButton @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled">
<icon icon="times"/>
</BaseButton>
</span>
</template> </template>
<template #searchResult="{option: user}"> <template #searchResult="{option: user}">
<user :avatar-size="24" :show-username="true" :user="user"/> <user :avatar-size="24" :show-username="true" :user="user"/>
@ -40,6 +35,7 @@ import {useTaskStore} from '@/stores/tasks'
import type {IUser} from '@/modelTypes/IUser' import type {IUser} from '@/modelTypes/IUser'
import {getDisplayName} from '@/models/user' import {getDisplayName} from '@/models/user'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
const props = defineProps({ const props = defineProps({
taskId: { taskId: {
@ -120,34 +116,3 @@ async function findUser(query: string) {
}) })
} }
</script> </script>
<style lang="scss" scoped>
.assignee {
position: relative;
&:not(:first-child) {
margin-left: -1.5rem;
}
:deep(.user img) {
border: 2px solid var(--white);
margin-right: 0;
}
}
.remove-assignee {
position: absolute;
top: 4px;
left: 2px;
color: var(--danger);
background: var(--white);
padding: 0 4px;
display: block;
border-radius: 100%;
font-size: .75rem;
width: 18px;
height: 18px;
z-index: 100;
}
</style>

View File

@ -49,15 +49,13 @@
<div class="footer"> <div class="footer">
<labels :labels="task.labels"/> <labels :labels="task.labels"/>
<priority-label :priority="task.priority" :done="task.done"/> <priority-label :priority="task.priority" :done="task.done"/>
<div class="assignees" v-if="task.assignees.length > 0"> <assignee-list
<user v-if="task.assignees.length > 0"
v-for="u in task.assignees" :assignees="task.assignees"
:avatar-size="24" :avatar-size="24"
:key="task.id + 'assignee' + u.id" class="ml-1"
:show-username="false" :inline="true"
:user="u" />
/>
</div>
<checklist-summary :task="task"/> <checklist-summary :task="task"/>
<span class="icon" v-if="task.attachments.length > 0"> <span class="icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/> <icon icon="paperclip"/>
@ -91,6 +89,7 @@ import AttachmentService from '@/services/attachment'
import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate' import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate'
import {colorIsDark} from '@/helpers/color/colorIsDark' import {colorIsDark} from '@/helpers/color/colorIsDark'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
const router = useRouter() const router = useRouter()

View File

@ -49,14 +49,12 @@
:labels="task.labels" :labels="task.labels"
/> />
<User <assignee-list
v-for="(a, i) in task.assignees" v-if="task.assignees.length > 0"
:avatar-size="27" :assignees="task.assignees"
:is-inline="true" :avatar-size="25"
:key="task.id + 'assignee' + a.id + i" class="ml-1"
:show-username="false" :inline="true"
:user="a"
class="m-2"
/> />
<!-- FIXME: use popup --> <!-- FIXME: use popup -->
@ -152,6 +150,7 @@ import {success} from '@/message'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
const { const {
theTask, theTask,

View File

@ -33,13 +33,12 @@
:labels="task.labels" :labels="task.labels"
/> />
<User <assignee-list
v-for="(a, i) in task.assignees" v-if="task.assignees.length > 0"
:assignees="task.assignees"
:avatar-size="20" :avatar-size="20"
:key="task.id + 'assignee' + a.id + i" class="ml-1"
:show-username="false" :inline="true"
:user="a"
class="avatar"
/> />
<span <span
@ -98,6 +97,7 @@ import ColorBubble from '@/components/misc/colorBubble.vue'
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate' import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
const { const {
task, task,

View File

@ -143,13 +143,12 @@
<labels :labels="t.labels"/> <labels :labels="t.labels"/>
</td> </td>
<td v-if="activeColumns.assignees"> <td v-if="activeColumns.assignees">
<user <assignee-list
:avatar-size="27" v-if="t.assignees.length > 0"
:is-inline="true" :assignees="t.assignees"
:key="t.id + 'assignee' + a.id + i" :avatar-size="28"
:show-username="false" class="ml-1"
:user="a" :inline="true"
v-for="(a, i) in t.assignees"
/> />
</td> </td>
<date-table-cell :date="t.dueDate" v-if="activeColumns.dueDate"/> <date-table-cell :date="t.dueDate" v-if="activeColumns.dueDate"/>
@ -201,6 +200,7 @@ import {useTaskList} from '@/composables/useTaskList'
import type {SortBy} from '@/composables/useTaskList' import type {SortBy} from '@/composables/useTaskList'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
const ACTIVE_COLUMNS_DEFAULT = { const ACTIVE_COLUMNS_DEFAULT = {
index: true, index: true,