Browse Source

fix(Dashboard): replace empty content with harmonized layout

Signed-off-by: Dorra Jaouad <dorra.jaoued7@gmail.com>
pull/15697/head
Dorra Jaouad 2 months ago
parent
commit
fdcb0fb98d
  1. 97
      src/components/Dashboard/Section.vue
  2. 310
      src/components/Dashboard/TalkDashboard.vue

97
src/components/Dashboard/Section.vue

@ -0,0 +1,97 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script lang="ts" setup>
import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile'
const { wide = false, title = '', subtitle = '', description = '' } = defineProps<{
wide?: boolean
title?: string
subtitle?: string
description?: string
}>()
const isMobile = useIsMobile()
</script>
<template>
<div class="dashboard-section"
:class="{
'dashboard-section--wide': wide,
'dashboard-section--list': $slots.list,
}">
<div v-if="!isMobile"
class="dashboard-section__bar"
:class="{ 'dashboard-section__bar--narrow': $slots.list }" />
<div class="dashboard-section__content-wrapper">
<div class="dashboard-section__content">
<h3 class="dashboard-section__title">
{{ title }}
</h3>
<span class="dashboard-section__subtitle">{{ subtitle }}</span>
<span class="dashboard-section__description">{{ description }}</span>
<slot name="list" />
<div v-if="$slots.action" class="dashboard-section__action">
<slot name="action" />
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.dashboard-section {
display: flex;
border-radius: var(--border-radius-large);
overflow: hidden;
box-shadow: 0 2px 10px 0 rgba(0,0,0, 0.2);
height: 100%;
flex-direction: column;
&--wide {
flex-direction: row;
}
&__content-wrapper {
display: flex;
flex: auto;
height: 96%; // bar is 4%
padding: var(--default-grid-baseline);
}
&__content {
position: relative;
display: flex;
flex-direction: column;
flex-grow: inherit;
padding: 0 calc(var(--default-grid-baseline) * 3) calc(var(--default-grid-baseline) * 2) calc(var(--default-grid-baseline) * 4);
}
&__bar {
background: linear-gradient(100deg, var(--color-primary) 0%, var(--color-main-background) 130%);
flex: 0 0 50%;
&--narrow {
flex: 0 0 4%;
}
}
&__title {
font-size: 1.25rem;
font-weight: bold;
}
&__subtitle {
font-weight: bold;
}
&__action {
margin-top: auto;
}
}
h3 {
margin-block: calc(var(--default-grid-baseline) * 2);
}
</style>

310
src/components/Dashboard/TalkDashboard.vue

@ -7,17 +7,15 @@ import { showError } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { isRTL, t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile'
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import NcInputField from '@nextcloud/vue/components/NcInputField'
import NcPopover from '@nextcloud/vue/components/NcPopover'
import IconAlarm from 'vue-material-design-icons/Alarm.vue'
import IconArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue'
import IconAt from 'vue-material-design-icons/At.vue'
import IconCalendarBlankOutline from 'vue-material-design-icons/CalendarBlankOutline.vue'
import IconList from 'vue-material-design-icons/FormatListBulleted.vue'
import IconMicrophoneOutline from 'vue-material-design-icons/MicrophoneOutline.vue'
@ -28,6 +26,7 @@ import ConversationsListVirtual from '../LeftSidebar/ConversationsList/Conversat
import SearchMessageItem from '../RightSidebar/SearchMessages/SearchMessageItem.vue'
import LoadingPlaceholder from '../UIShared/LoadingPlaceholder.vue'
import EventCard from './EventCard.vue'
import Section from './Section.vue'
import { CONVERSATION } from '../../constants.ts'
import { getTalkConfig, hasTalkFeature } from '../../services/CapabilitiesManager.ts'
import { EventBus } from '../../services/EventBus.ts'
@ -43,6 +42,7 @@ const canModerateSipDialOut = hasTalkFeature('local', 'sip-support-dialout')
&& getTalkConfig('local', 'call', 'can-enable-sip')
const canStartConversations = getTalkConfig('local', 'conversations', 'can-create')
const isDirectionRTL = isRTL()
const isMobile = useIsMobile()
const store = useStore()
const router = useRouter()
@ -177,7 +177,8 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
</script>
<template>
<div class="talk-dashboard-wrapper">
<div class="talk-dashboard-wrapper"
:class="{ 'talk-dashboard-wrapper--mobile': isMobile }">
<h2 class="talk-dashboard__header">
{{ t('spreed', 'Hello, {displayName}', { displayName: actorStore.displayName }, { escape: false }) }}
</h2>
@ -221,127 +222,141 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
{{ t('spreed', 'Join open conversations') }}
</NcButton>
<NcButton v-if="canModerateSipDialOut"
@click="EventBus.emit('call-phone-dialog:show')">
<template #icon>
<IconPhoneOutline :size="20" />
</template>
{{ t('spreed', 'Call a phone number') }}
</NcButton>
<NcButton variant="tertiary"
@click="emit('talk:media-settings:show', 'device-check')">
<template #icon>
<IconMicrophoneOutline :size="20" />
</template>
{{ t('spreed', 'Check devices') }}
</NcButton>
</div>
<h3 class="title">
{{ t('spreed', 'Upcoming meetings') }}
</h3>
<div v-if="eventsInitialised && eventRooms.length > 0"
class="talk-dashboard__event-cards-wrapper"
:class="{ 'forward-scrollable': forwardScrollable, 'backward-scrollable': backwardScrollable }">
<div ref="eventCardsWrapper"
class="talk-dashboard__event-cards"
@scroll.passive="updateScrollableFlags">
<EventCard v-for="eventRoom in eventRooms"
:key="eventRoom.eventLink"
:event-room="eventRoom"
class="talk-dashboard__event-card" />
</div>
<div class="talk-dashboard__event-cards__scroll-indicator">
<NcButton v-show="backwardScrollable"
class="button-slide backward"
variant="tertiary"
:title="t('spreed', 'Scroll backward')"
:aria-label="t('spreed', 'Scroll backward')"
@click="scrollEventCards({ direction: 'backward' })">
<NcButton v-if="canModerateSipDialOut"
@click="EventBus.emit('call-phone-dialog:show')">
<template #icon>
<IconArrowLeft class="bidirectional-icon" />
<IconPhoneOutline :size="20" />
</template>
{{ t('spreed', 'Call a phone number') }}
</NcButton>
<NcButton v-show="forwardScrollable"
class="button-slide forward"
variant="tertiary"
:title="t('spreed', 'Scroll forward')"
:aria-label="t('spreed', 'Scroll forward')"
@click="scrollEventCards({ direction: 'forward' })">
<NcButton variant="secondary"
@click="emit('talk:media-settings:show', 'device-check')">
<template #icon>
<IconArrowRight class="bidirectional-icon" />
<IconMicrophoneOutline :size="20" />
</template>
{{ t('spreed', 'Check devices') }}
</NcButton>
</div>
</div>
<LoadingPlaceholder v-else-if="!eventsInitialised"
type="event-cards" />
<div v-else class="talk-dashboard__empty-event-card">
<span class="title"> {{ t('spreed', 'You have no upcoming meetings') }}</span>
<span class="secondary_text">
{{ t('spreed', 'Schedule a meeting with a colleague from your calendar') }}
</span>
<NcButton class="talk-dashboard__calendar-button"
variant="secondary"
:href="generateUrl('apps/calendar')"
target="_blank">
<template #icon>
<IconCalendarBlankOutline :size="20" />
<div class="talk-dashboard__items">
<div class="event-section">
<template v-if="eventsInitialised && eventRooms.length > 0">
<h3 class="title">
{{ t('spreed', 'Upcoming meetings') }}
</h3>
<div class="talk-dashboard__event-cards-wrapper"
:class="{ 'forward-scrollable': forwardScrollable, 'backward-scrollable': backwardScrollable }">
<div ref="eventCardsWrapper"
class="talk-dashboard__event-cards"
@scroll.passive="updateScrollableFlags">
<EventCard v-for="eventRoom in eventRooms"
:key="eventRoom.eventLink"
:event-room="eventRoom"
class="talk-dashboard__event-card" />
</div>
<div class="talk-dashboard__event-cards__scroll-indicator">
<NcButton v-show="backwardScrollable"
class="button-slide backward"
variant="tertiary"
:title="t('spreed', 'Scroll backward')"
:aria-label="t('spreed', 'Scroll backward')"
@click="scrollEventCards({ direction: 'backward' })">
<template #icon>
<IconArrowLeft class="bidirectional-icon" />
</template>
</NcButton>
<NcButton v-show="forwardScrollable"
class="button-slide forward"
variant="tertiary"
:title="t('spreed', 'Scroll forward')"
:aria-label="t('spreed', 'Scroll forward')"
@click="scrollEventCards({ direction: 'forward' })">
<template #icon>
<IconArrowRight class="bidirectional-icon" />
</template>
</NcButton>
</div>
</div>
</template>
{{ t('spreed', 'Open calendar') }}
</NcButton>
</div>
<div class="talk-dashboard__chats">
<div class="talk-dashboard__unread-mentions">
<h3 class="title">
{{ t('spreed', 'Unread mentions') }}
</h3>
<ConversationsListVirtual v-if="filteredConversations.length > 0 || !conversationsInitialised"
class="talk-dashboard__conversations-list"
:conversations="filteredConversations"
:loading="!conversationsInitialised" />
<NcEmptyContent v-else
class="talk-dashboard__empty-content"
:name="t('spreed', 'All caught up!')"
:description="t('spreed', 'You have no unread mentions')">
<template #icon>
<IconAt :size="40" />
<LoadingPlaceholder v-else-if="!eventsInitialised"
type="event-cards" />
<DashboardSection v-else
class="event-section--empty"
wide
:title="t('spreed', 'Schedule meetings')"
:subtitle="t('spreed', 'You don\'t have any upcoming meetings')"
:description="t('spreed', 'Schedule a meeting from your calendar. A Talk conversation needs to be set as location to show up here')">
<template #image>
<img :src="imagePath('spreed', 'dashboard/meetings.png')">
</template>
<template #action>
<NcButton
variant="secondary"
:href="generateUrl('apps/calendar')"
target="_blank">
<template #icon>
<IconCalendarBlankOutline :size="20" />
</template>
{{ t('spreed', 'Open calendar') }}
</NcButton>
</template>
</NcEmptyContent>
</DashboardSection>
</div>
<div v-if="supportsUpcomingReminders"
class="talk-dashboard__upcoming-reminders">
<h3 class="title">
{{ t('spreed', 'Upcoming reminders') }}
</h3>
<div v-if="upcomingReminders.length > 0" class="upcoming-reminders-list">
<SearchMessageItem v-for="reminder in upcomingReminders"
:key="reminder.messageId"
:message-id="reminder.messageId"
:title="reminder.actorDisplayName"
:subline="reminder.message"
:message-parameters="reminder.messageParameters"
:token="reminder.roomToken"
:to="{
name: 'conversation',
params: { token: reminder.roomToken },
hash: `#message_${reminder.messageId}`,
}"
:actor-id="reminder.actorId"
:actor-type="reminder.actorType"
:timestamp="reminder.reminderTimestamp"
is-reminder />
<div class="talk-dashboard__chats">
<div class="talk-dashboard__unread-mentions">
<DashboardSection v-if="filteredConversations.length > 0 || !conversationsInitialised"
:title="t('spreed', 'Unread mentions')">
<template #list>
<ConversationsListVirtual
class="talk-dashboard__conversations-list"
:conversations="filteredConversations"
:loading="!conversationsInitialised" />
</template>
</DashboardSection>
<DashboardSection v-else
:title="t('spreed', 'Unread mentions')"
:description="t('spreed', 'Messages where you were mentioned will show up here\. You can mention people by typing @ followed by their name')">
<template #image>
<img :src="imagePath('spreed', 'dashboard/mentions.png')">
</template>
</DashboardSection>
</div>
<div v-if="supportsUpcomingReminders"
class="talk-dashboard__upcoming-reminders">
<DashboardSection v-if="upcomingReminders.length > 0 || !remindersInitialised"
:title="t('spreed', 'Upcoming reminders')">
<template #list>
<ul v-if="remindersInitialised" class="upcoming-reminders-list">
<SearchMessageItem v-for="reminder in upcomingReminders"
:key="reminder.messageId"
:message-id="reminder.messageId"
:title="reminder.actorDisplayName"
:subline="reminder.message"
:message-parameters="reminder.messageParameters"
:token="reminder.roomToken"
:to="{
name: 'conversation',
params: { token: reminder.roomToken },
hash: `#message_${reminder.messageId}`,
}"
:actor-id="reminder.actorId"
:actor-type="reminder.actorType"
:timestamp="reminder.reminderTimestamp"
is-reminder />
</ul>
<LoadingPlaceholder v-else
class="upcoming-reminders__loading-placeholder"
type="conversations" />
</template>
</DashboardSection>
<DashboardSection v-else
:title="t('spreed', 'Message reminders')"
:description="t('spreed', 'Set a reminder on a message to be notified')">
<template #image>
<img :src="imagePath('spreed', 'dashboard/reminders.png')">
</template>
</DashboardSection>
</div>
<LoadingPlaceholder v-else-if="!remindersInitialised"
class="upcoming-reminders__loading-placeholder"
type="conversations" />
<NcEmptyContent v-else
class="talk-dashboard__empty-content"
:name="t('spreed', 'No reminders scheduled')"
:description="t('spreed', 'You have no reminders scheduled')">
<template #icon>
<IconAlarm :size="40" />
</template>
</NcEmptyContent>
</div>
</div>
</div>
@ -356,8 +371,12 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
--section-height: 300px;
--content-height: calc(100% - var(--title-height));
padding: 0 calc(var(--default-grid-baseline) * 3);
max-width: calc($messages-list-max-width + 400px); // FIXME: to change to a readable value
max-width: calc(100vw - 300px - var(--body-container-margin) * 2); // 300px for the left sidebar and body container margins
margin: 0 auto;
&--mobile {
max-width: 100%;
}
}
.talk-dashboard__header {
@ -376,11 +395,18 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
flex-wrap: wrap;
:deep(.button-vue) {
height: var(--header-menu-item-height);
padding-inline: calc(var(--default-grid-baseline) * 2) calc(var(--default-grid-baseline) * 4);
}
}
.event-section {
margin-block: calc(var(--default-grid-baseline) * 8);
&--empty {
height: 225px;
}
}
.talk-dashboard__event-cards {
display: flex;
flex-wrap: nowrap;
@ -436,32 +462,19 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
.talk-dashboard__chats {
display: flex;
gap: calc(var(--default-grid-baseline) * 2);
padding-block-end: calc(var(--default-grid-baseline) * 2);
flex-wrap: wrap;
}
gap: calc(var(--default-grid-baseline) * 8);
justify-content: space-between;
flex-direction: row;
height: 300px;
.talk-dashboard__unread-mentions {
height: var(--section-height);
width: var(--section-width);
flex-shrink: 0;
}
.talk-dashboard__upcoming-reminders {
height: var(--section-height);
width: var(--section-width);
flex-shrink: 0;
&-list {
overflow-y: auto;
height: var(--content-height);
&> div {
width: calc(50% - calc(var(--default-grid-baseline) * 4));
}
}
.upcoming-reminders {
&-list {
overflow-y: auto;
height: var(--content-height);
}
&__loading-placeholder {
@ -470,26 +483,6 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
}
}
.talk-dashboard__empty-content {
border-radius: var(--border-radius-large);
padding: calc(var(--default-grid-baseline) * 2);
margin: var(--default-grid-baseline) 0;
border: 3px solid var(--color-border);
height: var(--content-height);
}
.talk-dashboard__empty-event-card {
display: flex;
flex-direction: column;
position: relative;
height: 225px;
width: var(--section-width);
border-radius: var(--border-radius-large);
border: 3px solid var(--color-border);
padding: calc(var(--default-grid-baseline) * 2);
margin-bottom: calc(var(--default-grid-baseline) * 2);
}
.talk-dashboard__conversations-list {
margin: var(--default-grid-baseline) 0;
height: var(--content-height);
@ -508,13 +501,6 @@ function scrollEventCards({ direction }: { direction: 'backward' | 'forward' })
margin-inline: var(--default-grid-baseline);
}
.secondary_text {
color: var(--color-text-maxcontrast);
font-size: var(--font-size-small);
overflow: hidden;
text-overflow: ellipsis;
}
.instant-meeting__dialog {
padding: 8px;
display: flex;

Loading…
Cancel
Save