2025-03-16 11:12:49 +01:00
|
|
|
<script setup>
|
|
|
|
|
|
2025-03-18 16:53:15 +01:00
|
|
|
import { onMounted, ref, useTemplateRef } from 'vue';
|
|
|
|
|
import { marked } from 'marked';
|
|
|
|
|
import { eachLimit } from 'async';
|
|
|
|
|
import { Button, Popover } from 'pankow';
|
|
|
|
|
import NotificationsModel from '../models/NotificationsModel.js';
|
|
|
|
|
|
2025-03-23 10:37:33 +01:00
|
|
|
const props = defineProps(['config', 'profile', 'subscription']);
|
2025-03-16 11:12:49 +01:00
|
|
|
|
2025-03-18 16:53:15 +01:00
|
|
|
const notificationModel = NotificationsModel.create();
|
|
|
|
|
const notificationButton = useTemplateRef('notificationButton');
|
|
|
|
|
const notificationPopover = useTemplateRef('notificationPopover');
|
|
|
|
|
const notifications = ref([]);
|
|
|
|
|
const notificationsAllBusy = ref(false);
|
|
|
|
|
|
|
|
|
|
function onOpenNotifications(popover, event, elem) {
|
|
|
|
|
popover.open(event, elem);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onMarkNotificationRead(notification) {
|
|
|
|
|
notification.busy = true;
|
|
|
|
|
const [error] = await notificationModel.update(notification.id, true);
|
|
|
|
|
if (error) return console.error(error);
|
|
|
|
|
|
|
|
|
|
await refresh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onMarkAllNotificationRead() {
|
|
|
|
|
notificationsAllBusy.value = true;
|
|
|
|
|
|
2025-03-18 16:58:00 +01:00
|
|
|
await eachLimit(notifications.value, 2, async (notification) => {
|
2025-03-18 16:53:15 +01:00
|
|
|
notification.busy = true;
|
|
|
|
|
const [error] = await notificationModel.update(notification.id, true);
|
|
|
|
|
if (error) return console.error(error);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await refresh();
|
|
|
|
|
|
|
|
|
|
notificationsAllBusy.value = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function refresh() {
|
|
|
|
|
const [error, result] = await notificationModel.list();
|
|
|
|
|
if (error) return console.error(error);
|
|
|
|
|
|
|
|
|
|
result.forEach(n => {
|
|
|
|
|
n.isCollapsed = true;
|
|
|
|
|
n.busy = false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
notifications.value = result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
2025-03-23 10:37:33 +01:00
|
|
|
if (props.profile.isAtLeastAdmin) await refresh();
|
2025-03-18 16:53:15 +01:00
|
|
|
});
|
|
|
|
|
|
2025-03-16 11:12:49 +01:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="headerbar">
|
2025-03-18 16:53:15 +01:00
|
|
|
<Popover ref="notificationPopover" :width="'min(80%, 400px)'" :height="'min(80%, 600px)'">
|
|
|
|
|
<div style="padding: 10px; display: flex; flex-direction: column; overflow: hidden; height: 100%;">
|
|
|
|
|
<div style="overflow: auto; margin-bottom: 10px">
|
|
|
|
|
<div class="notification-item" v-for="notification in notifications" :key="notification.id">
|
|
|
|
|
<div class="notification-item-title" @click="notification.isCollapsed = !notification.isCollapsed">
|
|
|
|
|
{{ notification.title }}
|
|
|
|
|
<!-- TODO translate -->
|
|
|
|
|
<Button plain small tool :loading="notification.busy" :disabled="notification.busy" class="notification-read-button" @click.stop="onMarkNotificationRead(notification)">Read</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="notification-item-body" v-if="!notification.isCollapsed">
|
|
|
|
|
<pre v-if="notification.messageJson" style="cursor: auto">{{ JSON.stringify(notification.messageJson, null, 4) }}</pre>
|
|
|
|
|
<div v-else style="cursor: auto; overflow: auto;" v-html="marked.parse(notification.message)"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- TODO translate -->
|
|
|
|
|
<Button @click="onMarkAllNotificationRead()" :loading="notificationsAllBusy" :disabled="notificationsAllBusy">Mark all as read</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</Popover>
|
|
|
|
|
|
2025-03-23 10:37:33 +01:00
|
|
|
<div v-if="!profile.isAtLeastUserManager">
|
2025-03-25 10:39:37 +01:00
|
|
|
<a href="#/" class="headerbar-logo">
|
2025-03-23 10:37:33 +01:00
|
|
|
<img :src="`https://${config.adminFqdn}/api/v1/cloudron/avatar`" width="40" height="40"/> {{ config.cloudronName || 'Cloudron' }}
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-03-16 11:12:49 +01:00
|
|
|
<div style="flex-grow: 1;"></div>
|
2025-03-23 10:37:33 +01:00
|
|
|
|
2025-03-16 11:12:49 +01:00
|
|
|
<div v-show="profile.isAtLeastOwner && (subscription.plan.id === 'free' || subscription.plan.id === 'expired')" ng-click="openSubscriptionSetup()" style="cursor: pointer">
|
|
|
|
|
<span class="badge" ng-class="{'badge-danger': subscription.plan.id !== 'free', 'badge-success': subscription.plan.id === 'free' }">
|
|
|
|
|
{{ $t(subscription.plan.id === 'free' ? 'settings.appstoreAccount.subscriptionSetupAction' : 'settings.appstoreAccount.subscriptionReactivateAction') }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-show="!profile.isAtLeastOwner && subscription.plan.id === 'expired'">
|
|
|
|
|
<span class="badge badge-danger">Subscription Expired</span>
|
|
|
|
|
</div>
|
2025-03-18 16:53:15 +01:00
|
|
|
<Button plain secondary tool v-if="profile.isAtLeastAdmin" ref="notificationButton" @click="onOpenNotifications(notificationPopover, $event, notificationButton.$el)" :icon="notifications.length ? 'fas fa-bell' : 'far fa-bell'">{{ notifications.length > 99 ? '99+' : notifications.length }}</Button>
|
|
|
|
|
<Button plain secondary tool v-if="profile.isAtLeastAdmin" href="#/support" icon="fa fa-question fa-fw"/>
|
2025-03-23 10:37:33 +01:00
|
|
|
<Button plain secondary href="#/profile"><img :src="profile.avatarUrl" class="headerbar-avatar"/> {{ profile.username }}</Button>
|
2025-03-16 11:12:49 +01:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
|
|
|
|
.headerbar {
|
|
|
|
|
display: flex;
|
2025-03-23 10:37:33 +01:00
|
|
|
padding: 10px;
|
|
|
|
|
padding-bottom: 6px;
|
2025-03-16 11:12:49 +01:00
|
|
|
align-items: center;
|
|
|
|
|
gap: 15px;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-25 16:11:59 +01:00
|
|
|
body.has-background .headerbar {
|
|
|
|
|
background-color: rgba(255,255,255,0.2);
|
|
|
|
|
filter: drop-shadow(0px 0px 14px white);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
|
|
|
body.has-background .headerbar {
|
|
|
|
|
background-color: rgba(0,0,0,0.2);
|
|
|
|
|
filter: drop-shadow(0px 0px 14px black);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-16 11:12:49 +01:00
|
|
|
.headerbar a {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.headerbar-avatar {
|
|
|
|
|
width: 32px;
|
|
|
|
|
height: 32px;
|
|
|
|
|
margin-right: 10px;
|
|
|
|
|
border-radius: var(--pankow-border-radius);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-23 10:37:33 +01:00
|
|
|
.headerbar-logo img {
|
|
|
|
|
margin-right: 10px;
|
|
|
|
|
border-radius: var(--pankow-border-radius);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.headerbar-logo,
|
|
|
|
|
.headerbar-logo:hover {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
color: var(--pankow-text-color);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
/* padding-left: 10px;*/
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-18 16:53:15 +01:00
|
|
|
.notification-item {
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
padding-bottom: 10px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
border-bottom: 1px solid var(--pankow-input-border-color);
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notification-item-title {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notification-item:hover .notification-item-title {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notification-read-button {
|
|
|
|
|
visibility: hidden;
|
|
|
|
|
margin-right: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notification-item:hover .notification-read-button {
|
|
|
|
|
visibility: visible;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-16 11:12:49 +01:00
|
|
|
</style>
|