Skip to content

Commit

Permalink
Merge pull request #16278 from itisAliRH/notifications-admin
Browse files Browse the repository at this point in the history
Notifications admin panel
  • Loading branch information
davelopez authored Sep 13, 2023
2 parents af8edd2 + 4b23c31 commit 7822a0c
Show file tree
Hide file tree
Showing 24 changed files with 1,421 additions and 170 deletions.
7 changes: 6 additions & 1 deletion client/src/components/Common/AsyncButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ const props = defineProps({
required: false,
default: "link",
},
disabled: {
type: Boolean,
required: false,
default: false,
},
});
async function onClick() {
Expand All @@ -44,7 +49,7 @@ async function onClick() {
:title="title"
:size="size"
:variant="variant"
:disabled="loading"
:disabled="loading || disabled"
@click="onClick">
<span v-if="loading" class="loading-icon fa fa-spinner fa-spin" title="loading"></span>
<FontAwesomeIcon v-else :icon="props.icon" @click="onClick" />
Expand Down
155 changes: 155 additions & 0 deletions client/src/components/Notifications/Broadcasts/BroadcastContainer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faInfoCircle, faTimes } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BButton, BCol, BRow } from "bootstrap-vue";
import { storeToRefs } from "pinia";
import { computed } from "vue";
import { useRouter } from "vue-router/composables";
import { useMarkdown } from "@/composables/markdown";
import { type components } from "@/schema";
import { type BroadcastNotification, useBroadcastsStore } from "@/stores/broadcastsStore";
import Heading from "@/components/Common/Heading.vue";
library.add(faInfoCircle, faTimes);
type BroadcastNotificationCreateRequest = components["schemas"]["BroadcastNotificationCreateRequest"];
type Options =
| {
previewMode?: false;
broadcast: BroadcastNotification;
}
| {
previewMode: true;
broadcast: BroadcastNotificationCreateRequest;
};
const props = defineProps<{
options: Options;
}>();
const router = useRouter();
const broadcastsStore = useBroadcastsStore();
const { activeBroadcasts } = storeToRefs(broadcastsStore);
const { renderMarkdown } = useMarkdown({ openLinksInNewPage: true });
const remainingBroadcastsCountText = computed(() => {
const count = activeBroadcasts.value.length - 1;
return count > 0 ? `${count} more` : "";
});
function getBroadcastVariant(item: { variant: string }) {
switch (item.variant) {
case "urgent":
return "danger";
default:
return item.variant;
}
}
function onActionClick(item: BroadcastNotification, link: string) {
if (link.startsWith("/")) {
router.push(link);
} else {
window.open(link, "_blank");
}
if (!props.options.previewMode) {
onDismiss(item);
}
}
function onDismiss(item: BroadcastNotification) {
broadcastsStore.dismissBroadcast(item);
}
</script>

<template>
<BRow
align-v="center"
class="broadcast-banner"
:class="{
'non-urgent': props.options.broadcast.variant !== 'urgent',
'fixed-position': !props.options.previewMode,
}"
no-gutters>
<BCol cols="auto">
<FontAwesomeIcon
class="mx-2"
size="2xl"
:class="`text-${getBroadcastVariant(props.options.broadcast)}`"
:icon="faInfoCircle" />
</BCol>

<BCol>
<BRow align-v="center" no-gutters>
<Heading size="md" bold>
{{ props.options.broadcast.content.subject }}
</Heading>
</BRow>

<BRow align-v="center" no-gutters>
<span class="broadcast-message" v-html="renderMarkdown(props.options.broadcast.content.message)" />
</BRow>

<BRow no-gutters>
<div v-if="props.options.broadcast.content.action_links">
<BButton
v-for="actionLink in props.options.broadcast.content.action_links"
:key="actionLink.action_name"
class="mx-1"
:title="actionLink.action_name"
variant="primary"
@click="onActionClick(props.options.broadcast, actionLink.link)">
{{ actionLink.action_name }}
</BButton>
</div>
</BRow>
</BCol>

<BCol v-if="!props.options.previewMode" cols="auto" align-self="center" class="p-0">
<BButton
id="dismiss-button"
variant="light"
class="align-items-center d-flex"
@click="broadcastsStore.dismissBroadcast(props.options.broadcast)">
<FontAwesomeIcon class="mx-1" :icon="faTimes" />
Dismiss
</BButton>

<div v-if="remainingBroadcastsCountText" class="text-center mt-2">
{{ remainingBroadcastsCountText }}...
</div>
</BCol>
</BRow>
</template>

<style lang="scss" scoped>
.broadcast-banner {
width: 100%;
color: white;
display: flex;
z-index: 9999;
padding: 1rem;
min-height: 6rem;
backdrop-filter: blur(0.2rem);
justify-content: space-between;
background-color: rgb(0, 0, 0, 0.7);
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.5);
.broadcast-message {
font-size: large;
}
}
.fixed-position {
position: fixed;
}
.non-urgent {
bottom: 0;
}
</style>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createTestingPinia } from "@pinia/testing";
import { getLocalVue } from "@tests/jest/helpers";
import { shallowMount } from "@vue/test-utils";
import { mount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { setActivePinia } from "pinia";

Expand Down Expand Up @@ -46,9 +46,12 @@ async function mountBroadcastsOverlayWith(broadcasts: BroadcastNotification[] =
broadcastsStore.broadcasts = broadcastsStore.broadcasts.filter((b) => b.id !== broadcast.id);
});

const wrapper = shallowMount(BroadcastsOverlay, {
const wrapper = mount(BroadcastsOverlay, {
localVue,
pinia,
stubs: {
BroadcastContainer: true,
},
});

await flushPromises();
Expand Down
117 changes: 4 additions & 113 deletions client/src/components/Notifications/Broadcasts/BroadcastsOverlay.vue
Original file line number Diff line number Diff line change
@@ -1,135 +1,26 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faInfoCircle, faTimes } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BButton } from "bootstrap-vue";
import { storeToRefs } from "pinia";
import { computed } from "vue";
import { useRouter } from "vue-router/composables";
import { useMarkdown } from "@/composables/markdown";
import { type BroadcastNotification, useBroadcastsStore } from "@/stores/broadcastsStore";
import Heading from "@/components/Common/Heading.vue";
import BroadcastContainer from "@/components/Notifications/Broadcasts/BroadcastContainer.vue";
library.add(faInfoCircle, faTimes);
const router = useRouter();
const broadcastsStore = useBroadcastsStore();
const { activeBroadcasts } = storeToRefs(useBroadcastsStore());
const { renderMarkdown } = useMarkdown({ openLinksInNewPage: true });
const currentBroadcast = computed(() => getNextActiveBroadcast());
const remainingBroadcastsCountText = computed(() => {
const count = activeBroadcasts.value.length - 1;
return count > 0 ? `${count} more` : "";
});
function getNextActiveBroadcast(): BroadcastNotification | undefined {
return activeBroadcasts.value.sort(sortByPublicationTime).at(0);
}
function sortByPublicationTime(a: BroadcastNotification, b: BroadcastNotification) {
return new Date(a.publication_time).getTime() - new Date(b.publication_time).getTime();
}
function getBroadcastVariant(item: BroadcastNotification) {
switch (item.variant) {
case "urgent":
return "danger";
default:
return item.variant;
}
}
function onActionClick(item: BroadcastNotification, link: string) {
if (link.startsWith("/")) {
router.push(link);
} else {
window.open(link, "_blank");
}
onDismiss(item);
}
function onDismiss(item: BroadcastNotification) {
broadcastsStore.dismissBroadcast(item);
function getNextActiveBroadcast(): BroadcastNotification | undefined {
return activeBroadcasts.value.sort(sortByPublicationTime).at(0);
}
</script>

<template>
<div v-if="currentBroadcast">
<BRow
align-v="center"
class="broadcast-banner m-0"
:class="{ 'non-urgent': currentBroadcast.variant !== 'urgent' }">
<BCol cols="auto">
<FontAwesomeIcon
class="mx-2"
size="2xl"
:class="`text-${getBroadcastVariant(currentBroadcast)}`"
:icon="faInfoCircle" />
</BCol>
<BCol>
<BRow align-v="center">
<Heading size="md" bold>
{{ currentBroadcast.content.subject }}
</Heading>
</BRow>
<BRow align-v="center">
<span class="broadcast-message" v-html="renderMarkdown(currentBroadcast.content.message)" />
</BRow>
<BRow>
<div v-if="currentBroadcast.content.action_links">
<BButton
v-for="actionLink in currentBroadcast.content.action_links"
:key="actionLink.action_name"
:title="actionLink.action_name"
variant="primary"
@click="onActionClick(currentBroadcast, actionLink.link)">
{{ actionLink.action_name }}
</BButton>
</div>
</BRow>
</BCol>
<BCol cols="auto" align-self="center" class="p-0">
<BButton
id="dismiss-button"
variant="light"
class="align-items-center d-flex"
@click="onDismiss(currentBroadcast)">
<FontAwesomeIcon class="mx-1" icon="times" />
Dismiss
</BButton>
<div v-if="remainingBroadcastsCountText" class="text-center mt-2">
{{ remainingBroadcastsCountText }}...
</div>
</BCol>
</BRow>
<BroadcastContainer :options="{ broadcast: currentBroadcast }" />
</div>
</template>

<style lang="scss" scoped>
.broadcast-banner {
width: 100%;
color: white;
display: flex;
z-index: 9999;
padding: 1rem;
position: fixed;
min-height: 6rem;
backdrop-filter: blur(0.2rem);
justify-content: space-between;
background-color: rgb(0, 0, 0, 0.7);
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.5);
.broadcast-message {
font-size: large;
}
}
.non-urgent {
bottom: 0;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import SharedItemNotification from "@/components/Notifications/Categories/Shared

const localVue = getLocalVue(true);

async function mountComponent(
component: typeof MessageNotification | typeof SharedItemNotification,
propsData: object = {}
): Promise<Wrapper<Vue>> {
async function mountComponent(component: object, propsData: object = {}): Promise<Wrapper<Vue>> {
const pinia = createTestingPinia();
setActivePinia(pinia);

Expand All @@ -38,7 +35,9 @@ describe("Notifications categories", () => {
notification.content.message = "This is a **markdown** message to test _rendering_";

const wrapper = await mountComponent(MessageNotification, {
notification,
options: {
notification,
},
});

expect(wrapper.find("#notification-message").html()).toContain(
Expand Down
Loading

0 comments on commit 7822a0c

Please sign in to comment.