Merge branch 'develop' into fix/next-message-bubbles
This commit is contained in:
@@ -16,20 +16,41 @@ const { t } = useI18n();
|
||||
|
||||
const fileName = computed(() => {
|
||||
const url = attachment.dataUrl;
|
||||
if (url) {
|
||||
const filename = url.substring(url.lastIndexOf('/') + 1);
|
||||
return filename || t('CONVERSATION.UNKNOWN_FILE_TYPE');
|
||||
if (!url) return t('CONVERSATION.UNKNOWN_FILE_TYPE');
|
||||
|
||||
try {
|
||||
const encodedFilename = url.substring(url.lastIndexOf('/') + 1);
|
||||
return decodeURIComponent(encodedFilename);
|
||||
} catch {
|
||||
return t('CONVERSATION.UNKNOWN_FILE_TYPE');
|
||||
}
|
||||
return t('CONVERSATION.UNKNOWN_FILE_TYPE');
|
||||
});
|
||||
|
||||
const fileType = computed(() => {
|
||||
return fileName.value.split('.').pop();
|
||||
});
|
||||
const fileType = computed(() => fileName.value.split('.').pop()?.toLowerCase());
|
||||
|
||||
const fileNameWithoutExt = computed(() => {
|
||||
const parts = fileName.value.split('.');
|
||||
return parts.slice(0, -1).join('.') || fileName.value;
|
||||
|
||||
// If there's no extension (no dots in filename)
|
||||
if (parts.length === 1) {
|
||||
return fileName.value.trim();
|
||||
}
|
||||
|
||||
// Take all parts except the last one (extension)
|
||||
const nameWithoutExt = parts.slice(0, -1).join('.');
|
||||
return nameWithoutExt.trim();
|
||||
});
|
||||
|
||||
const displayFileName = computed(() => {
|
||||
const name = fileNameWithoutExt.value;
|
||||
const truncatedName = (str, maxLength, hasExt) =>
|
||||
str.length > maxLength
|
||||
? `${str.substring(0, maxLength).trimEnd()}${hasExt ? '..' : '...'}`
|
||||
: str;
|
||||
|
||||
return fileType.value
|
||||
? `${truncatedName(name, 14, true)}.${fileType.value}`
|
||||
: truncatedName(name, 16, false);
|
||||
});
|
||||
|
||||
const textColorClass = computed(() => {
|
||||
@@ -62,17 +83,15 @@ const textColorClass = computed(() => {
|
||||
>
|
||||
<FileIcon class="flex-shrink-0" :file-type="fileType" />
|
||||
<span
|
||||
class="flex-1 min-w-0 text-sm max-w-36"
|
||||
:title="fileName"
|
||||
:class="textColorClass"
|
||||
class="inline-flex items-center text-sm overflow-hidden"
|
||||
>
|
||||
<span class="truncate min-w-6">
|
||||
{{ fileNameWithoutExt }}
|
||||
</span>
|
||||
<span class="flex-shrink-0 whitespace-nowrap">.{{ fileType }}</span>
|
||||
{{ displayFileName }}
|
||||
</span>
|
||||
<a
|
||||
v-tooltip="t('CONVERSATION.DOWNLOAD')"
|
||||
class="flex-shrink-0 h-9 grid place-content-center cursor-pointer text-n-slate-11"
|
||||
class="flex-shrink-0 size-9 grid place-content-center cursor-pointer text-n-slate-11 hover:text-n-slate-12 transition-colors"
|
||||
:href="attachment.dataUrl"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
|
||||
@@ -151,6 +151,9 @@ const menuItems = computed(() => {
|
||||
icon: 'i-lucide-inbox',
|
||||
to: accountScopedRoute('inbox_view'),
|
||||
activeOn: ['inbox_view', 'inbox_view_conversation'],
|
||||
getterKeys: {
|
||||
badge: 'notifications/getHasUnreadNotifications',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Conversation',
|
||||
|
||||
@@ -15,6 +15,7 @@ const props = defineProps({
|
||||
to: { type: Object, default: null },
|
||||
activeOn: { type: Array, default: () => [] },
|
||||
children: { type: Array, default: undefined },
|
||||
getterKeys: { type: Object, default: () => ({}) },
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -143,6 +144,7 @@ onMounted(async () => {
|
||||
:name
|
||||
:label
|
||||
:to
|
||||
:getter-keys="getterKeys"
|
||||
:is-active="isActive"
|
||||
:has-active-child="hasActiveChild"
|
||||
:expandable="hasChildren"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup>
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
to: { type: [Object, String], default: '' },
|
||||
label: { type: String, default: '' },
|
||||
icon: { type: [String, Object], default: '' },
|
||||
@@ -9,9 +10,12 @@ defineProps({
|
||||
isExpanded: { type: Boolean, default: false },
|
||||
isActive: { type: Boolean, default: false },
|
||||
hasActiveChild: { type: Boolean, default: false },
|
||||
getterKeys: { type: Object, default: () => ({}) },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['toggle']);
|
||||
|
||||
const showBadge = useMapGetter(props.getterKeys.badge);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -28,7 +32,13 @@ const emit = defineEmits(['toggle']);
|
||||
}"
|
||||
@click.stop="emit('toggle')"
|
||||
>
|
||||
<Icon v-if="icon" :icon="icon" class="size-4" />
|
||||
<div v-if="icon" class="relative flex items-center gap-2">
|
||||
<Icon v-if="icon" :icon="icon" class="size-4" />
|
||||
<span
|
||||
v-if="showBadge"
|
||||
class="size-2 -top-px ltr:-right-px rtl:-left-px bg-n-brand absolute rounded-full border border-n-solid-2"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm font-medium leading-5 flex-grow">
|
||||
{{ label }}
|
||||
</span>
|
||||
|
||||
@@ -121,6 +121,7 @@ export default {
|
||||
})
|
||||
.then(() => {
|
||||
useAlert(this.$t('INBOX.ALERTS.MARK_AS_READ'));
|
||||
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
|
||||
});
|
||||
},
|
||||
markNotificationAsUnRead(notification) {
|
||||
@@ -133,6 +134,7 @@ export default {
|
||||
})
|
||||
.then(() => {
|
||||
useAlert(this.$t('INBOX.ALERTS.MARK_AS_UNREAD'));
|
||||
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
|
||||
});
|
||||
},
|
||||
deleteNotification(notification) {
|
||||
@@ -186,12 +188,16 @@ export default {
|
||||
notificationType,
|
||||
});
|
||||
|
||||
this.$store.dispatch('notifications/read', {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount: this.meta.unreadCount,
|
||||
});
|
||||
this.$store
|
||||
.dispatch('notifications/read', {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount: this.meta.unreadCount,
|
||||
})
|
||||
.then(() => {
|
||||
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
|
||||
});
|
||||
|
||||
this.$router.push({
|
||||
name: 'inbox_view_conversation',
|
||||
|
||||
@@ -35,4 +35,7 @@ export const getters = {
|
||||
getNotificationFilters($state) {
|
||||
return $state.notificationFilters;
|
||||
},
|
||||
getHasUnreadNotifications: $state => {
|
||||
return $state.meta.unreadCount > 0;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ const state = {
|
||||
meta: {
|
||||
count: 0,
|
||||
currentPage: 1,
|
||||
unReadCount: 0,
|
||||
unreadCount: 0,
|
||||
},
|
||||
records: {},
|
||||
uiFlags: {
|
||||
|
||||
@@ -95,4 +95,27 @@ describe('#getters', () => {
|
||||
state.notificationFilters
|
||||
);
|
||||
});
|
||||
|
||||
describe('getHasUnreadNotifications', () => {
|
||||
it('should return true when there are unread notifications', () => {
|
||||
const state = {
|
||||
meta: { unreadCount: 5 },
|
||||
};
|
||||
expect(getters.getHasUnreadNotifications(state)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when there are no unread notifications', () => {
|
||||
const state = {
|
||||
meta: { unreadCount: 0 },
|
||||
};
|
||||
expect(getters.getHasUnreadNotifications(state)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when meta is empty', () => {
|
||||
const state = {
|
||||
meta: {},
|
||||
};
|
||||
expect(getters.getHasUnreadNotifications(state)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user