feat(assignees): improve avatar list consistency
Resolves https://kolaente.dev/vikunja/frontend/issues/3354
This commit is contained in:
parent
270e32290a
commit
f63c39a578
@ -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'
|
||||||
|
87
src/components/tasks/partials/assigneeList.vue
Normal file
87
src/components/tasks/partials/assigneeList.vue
Normal 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>
|
@ -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>
|
|
@ -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()
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user