wip: feat: abstract NotificationItem in dedicated component #2681
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<BaseButton
|
||||
class="trigger-button"
|
||||
:aria-pressed="pressed || undefined"
|
||||
>
|
||||
<slot />
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
defineProps<{
|
||||
pressed?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.trigger-button {
|
||||
cursor: pointer;
|
||||
color: var(--grey-400);
|
||||
transition: $transition;
|
||||
padding: .5rem;
|
||||
font-size: 1.25rem;
|
||||
position: relative;
|
||||
width: $navbar-icon-width;
|
||||
}
|
||||
|
||||
[aria-pressed] {
|
||||
color: var(--primary);
|
||||
}
|
||||
</style>
|
|
@ -26,14 +26,14 @@
|
|||
|
||||
<div class="navbar-end">
|
||||
<update/>
|
||||
<BaseButton
|
||||
<NavbarTriggerButton
|
||||
@click="openQuickActions"
|
||||
class="trigger-button pr-0"
|
||||
v-shortcut="'Control+k'"
|
||||
:pressed="quickActionsActive"
|
||||
:title="$t('keyboardShortcuts.quickSearch')"
|
||||
>
|
||||
<icon icon="search"/>
|
||||
</BaseButton>
|
||||
</NavbarTriggerButton>
|
||||
<notifications/>
|
||||
<div class="user">
|
||||
<dropdown class="is-right" ref="usernameDropdown">
|
||||
|
@ -101,6 +101,7 @@ import Dropdown from '@/components/misc/dropdown.vue'
|
|||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||
import Notifications from '@/components/notifications/notifications.vue'
|
||||
import Logo from '@/components/home/Logo.vue'
|
||||
import NavbarTriggerButton from '@/components/home/NavbarTriggerButton.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import MenuButton from '@/components/home/MenuButton.vue'
|
||||
|
||||
|
@ -134,6 +135,8 @@ onMounted(async () => {
|
|||
listTitle.value.style.setProperty('--nav-username-width', `${usernameWidth}px`)
|
||||
})
|
||||
|
||||
const quickActionsActive = computed(() => baseStore.quickActionsActive)
|
||||
|
||||
function openQuickActions() {
|
||||
baseStore.setQuickActionsActive(true)
|
||||
}
|
||||
|
@ -218,26 +221,11 @@ $hamburger-menu-icon-width: 28px;
|
|||
}
|
||||
|
||||
.navbar {
|
||||
// FIXME: notifications should provide a slot for the icon instead, so that we can style it as we want
|
||||
:deep() {
|
||||
.trigger-button {
|
||||
cursor: pointer;
|
||||
color: var(--grey-400);
|
||||
padding: .5rem;
|
||||
font-size: 1.25rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> * > .trigger-button {
|
||||
width: $navbar-icon-width;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
.username {
|
||||
font-family: $vikunja-font;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<div class="single-notification">
|
||||
<div class="read-indicator" :class="{'read': notification.readAt !== null}" />
|
||||
<user
|
||||
class="user"
|
||||
v-if="notification.notification.doer"
|
||||
:user="notification.notification.doer"
|
||||
:show-username="false"
|
||||
:avatar-size="16"
|
||||
/>
|
||||
<div class="detail">
|
||||
<div>
|
||||
<span class="has-text-weight-bold mr-1" v-if="notification.notification.doer">
|
||||
{{ getDisplayName(notification.notification.doer) }}
|
||||
</span>
|
||||
<BaseButton :to="to" @click="emit('markNotificationAsRead')">
|
||||
{{ notificationText }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<span class="created" v-tooltip="formatDateLong(notification.created)">
|
||||
{{ formatDateSince(notification.created) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
import {NOTIFICATION_NAMES, type INotification} from '@/modelTypes/INotification'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import User from '@/components/misc/user.vue'
|
||||
|
||||
import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
|
||||
import {getDisplayName} from '@/models/user'
|
||||
import {getTextIdentifier} from '@/models/task'
|
||||
|
||||
const props = defineProps<{
|
||||
notification: INotification
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'markNotificationAsRead'): void
|
||||
}>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const userInfo = computed(() => authStore.info)
|
||||
|
||||
const to = computed(() => {
|
||||
const to = {
|
||||
name: '',
|
||||
params: {},
|
||||
}
|
||||
|
||||
switch (props.notification.name) {
|
||||
case NOTIFICATION_NAMES.TASK_COMMENT:
|
||||
case NOTIFICATION_NAMES.TASK_ASSIGNED:
|
||||
to.name = 'task.detail'
|
||||
to.params.id = props.notification.notification.task.id
|
||||
break
|
||||
case NOTIFICATION_NAMES.TASK_DELETED:
|
||||
// Nothing
|
||||
break
|
||||
case NOTIFICATION_NAMES.LIST_CREATED:
|
||||
to.name = 'task.index'
|
||||
dpschen marked this conversation as resolved
Outdated
|
||||
to.params.listId = props.notification.notification.list.id
|
||||
break
|
||||
case NOTIFICATION_NAMES.TEAM_MEMBER_ADDED:
|
||||
to.name = 'teams.edit'
|
||||
to.params.id = props.notification.notification.team.id
|
||||
break
|
||||
default:
|
||||
}
|
||||
|
||||
return to
|
||||
})
|
||||
|
||||
const notificationText = computed(() => {
|
||||
const notification = props.notification.notification
|
||||
let who = ''
|
||||
|
||||
switch (props.notification.name) {
|
||||
case NOTIFICATION_NAMES.TASK_COMMENT:
|
||||
return `commented on ${getTextIdentifier(notification.task)}`
|
||||
konrad
commented
I wonder if we can translate these but that's for another PR. I wonder if we can translate these but that's for another PR.
dpschen
commented
Yes, saw that too! Yes, saw that too!
dpschen
commented
I added translations I added translations
|
||||
case NOTIFICATION_NAMES.TASK_ASSIGNED:
|
||||
if (userInfo.value !== null && userInfo.value.id === notification.assignee.id) {
|
||||
who = 'you'
|
||||
} else {
|
||||
who = `${getDisplayName(notification.assignee)}`
|
||||
}
|
||||
|
||||
return `assigned ${who} to ${getTextIdentifier(notification.task)}`
|
||||
case NOTIFICATION_NAMES.TASK_DELETED:
|
||||
return `deleted ${getTextIdentifier(notification.task)}`
|
||||
case NOTIFICATION_NAMES.LIST_CREATED:
|
||||
return `created ${notification.list.title}`
|
||||
case NOTIFICATION_NAMES.TEAM_MEMBER_ADDED:
|
||||
if (userInfo.value !== null && userInfo.value.id === notification.member.id) {
|
||||
who = 'you'
|
||||
} else {
|
||||
who = `${getDisplayName(notification.member)}`
|
||||
}
|
||||
|
||||
return `added ${who} to the ${notification.team.name} team`
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.single-notification {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0;
|
||||
transition: background-color $transition;
|
||||
|
||||
&:hover {
|
||||
background: var(--grey-100);
|
||||
border-radius: $radius;
|
||||
}
|
||||
}
|
||||
|
||||
.read-indicator {
|
||||
width: .35rem;
|
||||
height: .35rem;
|
||||
background: var(--primary);
|
||||
border-radius: 100%;
|
||||
margin-left: .5rem;
|
||||
|
||||
&.read {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: this deep styling of user should not be in here
|
||||
.user {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
margin: 0 .5rem;
|
||||
|
||||
span {
|
||||
font-family: $family-sans-serif;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.created {
|
||||
color: var(--grey-400);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--grey-800);
|
||||
}
|
||||
</style>
|
|
@ -1,40 +1,28 @@
|
|||
<template>
|
||||
<div class="notifications">
|
||||
<div class="is-flex is-justify-content-center">
|
||||
<BaseButton @click.stop="showNotifications = !showNotifications" class="trigger-button">
|
||||
<span class="unread-indicator" v-if="unreadNotifications > 0"></span>
|
||||
<icon icon="bell"/>
|
||||
</BaseButton>
|
||||
</div>
|
||||
<!-- FIXME: add label -->
|
||||
<slot :togglePopup="togglePopup" :hasUnreadNotifications="hasUnreadNotifications">
|
||||
<NavbarTriggerButton
|
||||
:pressed="showNotifications"
|
||||
ref="toggleButton"
|
||||
@click="togglePopup"
|
||||
>
|
||||
<span v-if="hasUnreadNotifications" class="unread-indicator" />
|
||||
<icon icon="bell"/>
|
||||
</NavbarTriggerButton>
|
||||
</slot>
|
||||
|
||||
<!-- FIXME: create dedicated dropdown menu -->
|
||||
<CustomTransition name="fade">
|
||||
<div class="notifications-list" v-if="showNotifications" ref="popup">
|
||||
<span class="head">{{ $t('notification.title') }}</span>
|
||||
<div
|
||||
<h3 class="head">{{ $t('notification.title') }}</h3>
|
||||
<NotificationItem
|
||||
v-for="(n, index) in notifications"
|
||||
:key="n.id"
|
||||
class="single-notification"
|
||||
>
|
||||
<div class="read-indicator" :class="{'read': n.readAt !== null}"></div>
|
||||
<user
|
||||
:user="n.notification.doer"
|
||||
:show-username="false"
|
||||
:avatar-size="16"
|
||||
v-if="n.notification.doer"/>
|
||||
<div class="detail">
|
||||
<div>
|
||||
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
|
||||
{{ getDisplayName(n.notification.doer) }}
|
||||
</span>
|
||||
<BaseButton @click="() => to(n, index)()">
|
||||
{{ n.toText(userInfo) }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<span class="created" v-tooltip="formatDateLong(n.created)">
|
||||
{{ formatDateSince(n.created) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
class="notification-item"
|
||||
:notification="n"
|
||||
@markNotificationAsRead="markNotificationAsRead(index, n)"
|
||||
/>
|
||||
<p class="nothing" v-if="notifications.length === 0">
|
||||
{{ $t('notification.none') }}<br/>
|
||||
<span class="explainer">
|
||||
|
@ -48,203 +36,118 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import {computed, onMounted, onUnmounted, ref} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {onClickOutside} from '@vueuse/core'
|
||||
|
||||
import NotificationService from '@/services/notification'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import User from '@/components/misc/user.vue'
|
||||
import { NOTIFICATION_NAMES as names, type INotification} from '@/modelTypes/INotification'
|
||||
dpschen marked this conversation as resolved
Outdated
konrad
commented
If vueuse has a helper like ours, do we even need ours? If vueuse has a helper like ours, do we even need ours?
dpschen
commented
No, I'm replacing ours piece by piece. No, I'm replacing ours piece by piece.
Not all vueuse composables are perfect though.
E.g. there is one `useTextareaAutosize`, but it e.g. doesn't cover the case where the textarea scales with the width of the window. So we should be careful. The `onClickoutside` seems well written though!
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
|
||||
import {getDisplayName} from '@/models/user'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import type {INotification} from '@/modelTypes/INotification'
|
||||
|
||||
const LOAD_NOTIFICATIONS_INTERVAL = 10000
|
||||
import NavbarTriggerButton from '@/components/home/NavbarTriggerButton.vue'
|
||||
import NotificationItem from '@/components/notifications/NotificationItem.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
const NOTIFICATIONS_PULL_INTERVAL = 10000
|
||||
|
||||
const allNotifications = ref<INotification[]>([])
|
||||
const showNotifications = ref(false)
|
||||
const popup = ref(null)
|
||||
const toggleButton = ref(null)
|
||||
|
||||
function togglePopup() {
|
||||
showNotifications.value = !showNotifications.value
|
||||
}
|
||||
|
||||
onClickOutside(
|
||||
popup,
|
||||
() => {
|
||||
if (!showNotifications.value) {
|
||||
return
|
||||
}
|
||||
showNotifications.value = false
|
||||
},
|
||||
{ ignore: [toggleButton]},
|
||||
)
|
||||
|
||||
const unreadNotifications = computed(() => {
|
||||
return notifications.value.filter(n => n.readAt === null).length
|
||||
})
|
||||
const notifications = computed(() => {
|
||||
return allNotifications.value ? allNotifications.value.filter(n => n.name !== '') : []
|
||||
})
|
||||
const userInfo = computed(() => authStore.info)
|
||||
const unreadNotifications = computed(() => {
|
||||
return notifications.value.filter(n => n.readAt === null).length
|
||||
})
|
||||
|
||||
const hasUnreadNotifications = computed(() => unreadNotifications.value > 0)
|
||||
|
||||
let interval: ReturnType<typeof setInterval>
|
||||
|
||||
onMounted(() => {
|
||||
loadNotifications()
|
||||
document.addEventListener('click', hidePopup)
|
||||
interval = setInterval(loadNotifications, LOAD_NOTIFICATIONS_INTERVAL)
|
||||
interval = setInterval(loadNotifications, NOTIFICATIONS_PULL_INTERVAL)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', hidePopup)
|
||||
clearInterval(interval)
|
||||
})
|
||||
onUnmounted(() => clearInterval(interval))
|
||||
|
||||
const notificationService = new NotificationService()
|
||||
|
||||
loadNotifications()
|
||||
async function loadNotifications() {
|
||||
// We're recreating the notification service here to make sure it uses the latest api user token
|
||||
const notificationService = new NotificationService()
|
||||
allNotifications.value = await notificationService.getAll()
|
||||
}
|
||||
|
||||
function hidePopup(e) {
|
||||
if (showNotifications.value) {
|
||||
closeWhenClickedOutside(e, popup.value, () => showNotifications.value = false)
|
||||
}
|
||||
}
|
||||
|
||||
function to(n, index) {
|
||||
const to = {
|
||||
name: '',
|
||||
params: {},
|
||||
}
|
||||
|
||||
switch (n.name) {
|
||||
case names.TASK_COMMENT:
|
||||
case names.TASK_ASSIGNED:
|
||||
to.name = 'task.detail'
|
||||
to.params.id = n.notification.task.id
|
||||
break
|
||||
case names.TASK_DELETED:
|
||||
// Nothing
|
||||
break
|
||||
case names.LIST_CREATED:
|
||||
to.name = 'task.index'
|
||||
to.params.listId = n.notification.list.id
|
||||
break
|
||||
case names.TEAM_MEMBER_ADDED:
|
||||
to.name = 'teams.edit'
|
||||
to.params.id = n.notification.team.id
|
||||
break
|
||||
}
|
||||
|
||||
return async () => {
|
||||
if (to.name !== '') {
|
||||
router.push(to)
|
||||
}
|
||||
|
||||
n.read = true
|
||||
const notificationService = new NotificationService()
|
||||
allNotifications.value[index] = await notificationService.update(n)
|
||||
}
|
||||
async function markNotificationAsRead(index: number, notification: INotification) {
|
||||
allNotifications.value[index] = await notificationService.update({
|
||||
...notification,
|
||||
read: true,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notifications {
|
||||
width: $navbar-icon-width;
|
||||
.unread-indicator {
|
||||
position: absolute;
|
||||
top: .75rem;
|
||||
right: 1.15rem;
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
|
||||
.unread-indicator {
|
||||
position: absolute;
|
||||
top: .75rem;
|
||||
right: 1.15rem;
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
background: var(--primary);
|
||||
border-radius: 100%;
|
||||
border: 2px solid var(--white);
|
||||
}
|
||||
|
||||
background: var(--primary);
|
||||
border-radius: 100%;
|
||||
border: 2px solid var(--white);
|
||||
}
|
||||
.notifications-list {
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
margin-top: 1rem;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
|
||||
.notifications-list {
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
margin-top: 1rem;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
background: var(--white);
|
||||
width: 350px;
|
||||
max-width: calc(100vw - 2rem);
|
||||
padding: .75rem .25rem;
|
||||
border-radius: $radius;
|
||||
box-shadow: var(--shadow-sm);
|
||||
font-size: .85rem;
|
||||
|
||||
background: var(--white);
|
||||
width: 350px;
|
||||
max-width: calc(100vw - 2rem);
|
||||
padding: .75rem .25rem;
|
||||
border-radius: $radius;
|
||||
box-shadow: var(--shadow-sm);
|
||||
font-size: .85rem;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
max-height: calc(100vh - 1rem - #{$navbar-height});
|
||||
}
|
||||
|
||||
.head {
|
||||
font-family: $vikunja-font;
|
||||
font-size: 1rem;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.single-notification {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0;
|
||||
|
||||
transition: background-color $transition;
|
||||
|
||||
&:hover {
|
||||
background: var(--grey-100);
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
.read-indicator {
|
||||
width: .35rem;
|
||||
height: .35rem;
|
||||
background: var(--primary);
|
||||
border-radius: 100%;
|
||||
margin-left: .5rem;
|
||||
|
||||
&.read {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
margin: 0 .5rem;
|
||||
|
||||
span {
|
||||
font-family: $family-sans-serif;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.created {
|
||||
color: var(--grey-400);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--grey-800);
|
||||
}
|
||||
}
|
||||
|
||||
.nothing {
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
color: var(--grey-500);
|
||||
|
||||
.explainer {
|
||||
font-size: .75rem;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: $tablet) {
|
||||
max-height: calc(100vh - 1rem - #{$navbar-height});
|
||||
}
|
||||
}
|
||||
|
||||
.head {
|
||||
font-family: $vikunja-font;
|
||||
font-size: 1rem;
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.notification-item:last-child {
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
.nothing {
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
color: var(--grey-500);
|
||||
}
|
||||
|
||||
.explainer {
|
||||
font-size: .75rem;
|
||||
}
|
||||
</style>
|
|
@ -3,7 +3,7 @@ import type {IUser} from './IUser'
|
|||
import type {ITask} from './ITask'
|
||||
import type {ITaskComment} from './ITaskComment'
|
||||
import type {ITeam} from './ITeam'
|
||||
import type { IList } from './IList'
|
||||
import type {IList} from './IList'
|
||||
|
||||
export const NOTIFICATION_NAMES = {
|
||||
'TASK_COMMENT': 'task.comment',
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import AbstractModel from './abstractModel'
|
||||
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
||||
import UserModel, {getDisplayName} from '@/models/user'
|
||||
import UserModel from '@/models/user'
|
||||
import TaskModel from '@/models/task'
|
||||
import TaskCommentModel from '@/models/taskComment'
|
||||
import ListModel from '@/models/list'
|
||||
import TeamModel from '@/models/team'
|
||||
|
||||
import {NOTIFICATION_NAMES, type INotification} from '@/modelTypes/INotification'
|
||||
import type { IUser } from '@/modelTypes/IUser'
|
||||
|
||||
export default class NotificationModel extends AbstractModel<INotification> implements INotification {
|
||||
id = 0
|
||||
|
@ -61,35 +60,4 @@ export default class NotificationModel extends AbstractModel<INotification> impl
|
|||
this.created = new Date(this.created)
|
||||
this.readAt = parseDateOrNull(this.readAt)
|
||||
}
|
||||
|
||||
toText(user: IUser | null = null) {
|
||||
let who = ''
|
||||
|
||||
switch (this.name) {
|
||||
case NOTIFICATION_NAMES.TASK_COMMENT:
|
||||
return `commented on ${this.notification.task.getTextIdentifier()}`
|
||||
case NOTIFICATION_NAMES.TASK_ASSIGNED:
|
||||
who = `${getDisplayName(this.notification.assignee)}`
|
||||
|
||||
if (user !== null && user.id === this.notification.assignee.id) {
|
||||
who = 'you'
|
||||
}
|
||||
|
||||
return `assigned ${who} to ${this.notification.task.getTextIdentifier()}`
|
||||
case NOTIFICATION_NAMES.TASK_DELETED:
|
||||
return `deleted ${this.notification.task.getTextIdentifier()}`
|
||||
case NOTIFICATION_NAMES.LIST_CREATED:
|
||||
return `created ${this.notification.list.title}`
|
||||
case NOTIFICATION_NAMES.TEAM_MEMBER_ADDED:
|
||||
who = `${getDisplayName(this.notification.member)}`
|
||||
|
||||
if (user !== null && user.id === this.notification.member.id) {
|
||||
who = 'you'
|
||||
}
|
||||
|
||||
return `added ${who} to the ${this.notification.team.name} team`
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,14 @@ export function getHexColor(hexColor: string): string {
|
|||
return hexColor
|
||||
}
|
||||
|
||||
export function getTextIdentifier(task: ITask) {
|
||||
if (task.identifier === '') {
|
||||
return `#${task.index}`
|
||||
}
|
||||
|
||||
return task.identifier
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses `repeatAfterSeconds` into a usable js object.
|
||||
*/
|
||||
|
@ -159,11 +167,7 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
|
|||
}
|
||||
|
||||
getTextIdentifier() {
|
||||
if (this.identifier === '') {
|
||||
return `#${this.index}`
|
||||
}
|
||||
|
||||
return this.identifier
|
||||
return getTextIdentifier(this)
|
||||
}
|
||||
|
||||
getHexColor() {
|
||||
|
|
Reference in New Issue
Shouldn't this be
list.index
? (not sure if the route is actually called that)This name is correct.