Skip to content

Commit

Permalink
Merge pull request #17090 from davelopez/enhance_notification_broadca…
Browse files Browse the repository at this point in the history
…sts_admin_panel

Enhance Notification Broadcasts Admin Panel
  • Loading branch information
dannon authored Nov 29, 2023
2 parents c36d001 + 8034c39 commit 2283294
Show file tree
Hide file tree
Showing 4 changed files with 411 additions and 177 deletions.
206 changes: 206 additions & 0 deletions client/src/components/admin/Notifications/BroadcastCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faBroadcastTower,
faClock,
faEdit,
faHourglassHalf,
faInfoCircle,
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BButton, BCol, BInputGroup, BRow } from "bootstrap-vue";
import { computed } from "vue";
import { useConfirmDialog } from "@/composables/confirmDialog";
import { useMarkdown } from "@/composables/markdown";
import { BroadcastNotification } from "@/stores/broadcastsStore";
import Heading from "@/components/Common/Heading.vue";
import UtcDate from "@/components/UtcDate.vue";
library.add(faBroadcastTower, faClock, faEdit, faHourglassHalf, faInfoCircle, faTrash);
const { confirm } = useConfirmDialog();
const { renderMarkdown } = useMarkdown({ openLinksInNewPage: true });
interface Props {
broadcastNotification: BroadcastNotification;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "edit", broadcastNotification: BroadcastNotification): void;
(e: "expire", broadcastNotification: BroadcastNotification): void;
(e: "go-to-link", link: string): void;
}>();
const notification = computed(() => props.broadcastNotification);
const hasExpired = computed(() => {
return notification.value.expiration_time && new Date(notification.value.expiration_time) < new Date();
});
const hasBeenPublished = computed(() => {
return new Date(notification.value.publication_time) < new Date();
});
const notificationVariant = computed(() => {
switch (notification.value.variant) {
case "urgent":
return "danger";
default:
return notification.value.variant;
}
});
const publicationTimePrefix = computed(() => {
if (hasBeenPublished.value) {
return "Published";
}
return "Scheduled to be published";
});
const expirationTimePrefix = computed(() => {
if (notification.value.expiration_time) {
if (hasExpired.value) {
return "Expired";
}
return "Expires";
}
return "Does not expire";
});
function onEditClick() {
emit("edit", notification.value);
}
async function onForceExpirationClick() {
const confirmed = await confirm(
"Are you sure you want to expire this broadcast? It will be automatically deleted on the next cleanup cycle.",
"Expire broadcast"
);
if (confirmed) {
emit("expire", notification.value);
}
}
function onActionClick(link: string) {
emit("go-to-link", link);
}
</script>

<template>
<div class="broadcast-card mb-2">
<BRow align-v="center" align-h="between" no-gutters>
<Heading size="md" class="mb-0" :class="hasExpired ? 'expired-broadcast' : ''">
<FontAwesomeIcon :class="`text-${notificationVariant}`" :icon="faInfoCircle" />
{{ notification.content.subject }}
</Heading>

<BRow align-h="end" align-v="center" no-gutters>
<span>
created
<UtcDate class="mr-2" :date="notification.create_time" mode="elapsed" />
</span>

<BInputGroup v-if="!hasExpired">
<BButton
id="edit-broadcast-button"
v-b-tooltip.hover
variant="link"
title="Edit broadcast"
@click="onEditClick">
<FontAwesomeIcon :icon="faEdit" />
</BButton>

<BButton
id="delete-button"
v-b-tooltip.hover
variant="link"
title="Delete broadcast"
@click="onForceExpirationClick">
<FontAwesomeIcon :icon="faTrash" />
</BButton>
</BInputGroup>
</BRow>
</BRow>

<BRow align-v="center" align-h="between" no-gutters>
<BCol cols="auto">
<BRow align-v="center" no-gutters>
<span
:class="hasExpired ? 'expired-broadcast' : ''"
v-html="renderMarkdown(notification.content.message)" />
</BRow>

<BRow no-gutters>
<BButton
v-for="actionLink in notification.content.action_links"
:key="actionLink.action_name"
class="mr-1"
:title="actionLink.action_name"
variant="primary"
@click="onActionClick(actionLink.link)">
{{ actionLink.action_name }}
</BButton>
</BRow>
</BCol>

<BCol>
<BRow align-v="center" align-h="end" no-gutters>
<FontAwesomeIcon
:icon="hasBeenPublished ? faBroadcastTower : faClock"
:class="hasBeenPublished ? 'published' : 'scheduled'"
class="mx-1" />
{{ publicationTimePrefix }}
<UtcDate class="ml-1" :date="notification.publication_time" mode="elapsed" />
</BRow>

<BRow align-v="center" align-h="end" no-gutters>
<FontAwesomeIcon
variant="danger"
:icon="faHourglassHalf"
:class="hasExpired ? 'expired' : 'expires'"
class="mx-1" />
{{ expirationTimePrefix }}
<UtcDate
v-if="notification.expiration_time"
class="ml-1"
:date="notification.expiration_time"
mode="elapsed" />
</BRow>
</BCol>
</BRow>
</div>
</template>

<style scoped lang="scss">
@import "scss/theme/blue.scss";
.broadcast-card {
border-radius: 0.25rem;
border: 1px solid $gray-300;
padding: 1rem;
.expired-broadcast {
text-decoration: line-through;
}
.published {
color: $brand-primary;
}
.scheduled {
color: $brand-warning;
}
.expires {
color: $brand-info;
}
.expired {
color: $brand-danger;
}
}
</style>
101 changes: 101 additions & 0 deletions client/src/components/admin/Notifications/BroadcastsList.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { createTestingPinia } from "@pinia/testing";
import { getLocalVue } from "@tests/jest/helpers";
import { shallowMount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { setActivePinia } from "pinia";

import { mockFetcher } from "@/api/schema/__mocks__";
import { BroadcastNotification } from "@/stores/broadcastsStore";

import { generateNewBroadcast } from "./test.utils";

import BroadcastsList from "./BroadcastsList.vue";

jest.mock("@/api/schema");

const localVue = getLocalVue(true);

const selectors = {
emptyBroadcastsListAlert: "#empty-broadcast-list-alert",
showActiveFilterButton: "#show-active-filter-button",
showScheduledFilterButton: "#show-scheduled-filter-button",
showExpiredFilterButton: "#show-expired-filter-button",
broadcastItem: "[data-test-id='broadcast-item']",
} as const;

async function mountBroadcastsList(broadcasts?: BroadcastNotification[]) {
const pinia = createTestingPinia();
setActivePinia(pinia);

mockFetcher
.path("/api/notifications/broadcast")
.method("get")
.mock({ data: broadcasts ?? [] });

const wrapper = shallowMount(BroadcastsList as object, {
localVue,
pinia,
stubs: {
FontAwesomeIcon: true,
},
});

await flushPromises();

return wrapper;
}

describe("BroadcastsList.vue", () => {
it("should render empty list message when there are no broadcasts", async () => {
const wrapper = await mountBroadcastsList();

expect(wrapper.findAll(selectors.broadcastItem).length).toBe(0);
expect(wrapper.find(selectors.emptyBroadcastsListAlert).exists()).toBeTruthy();
});

it("should filter broadcasts by active, scheduled and expired", async () => {
const now = Date.now();

const activeBroadcast = {
...generateNewBroadcast({}),
publication_time: new Date(now - 1000).toISOString(),
expiration_time: new Date(now + 1000).toISOString(),
};

const scheduledBroadcast = {
...generateNewBroadcast({}),
publication_time: new Date(now + 1000).toISOString(),
expiration_time: new Date(now + 2000).toISOString(),
};

const expiredBroadcast = {
...generateNewBroadcast({}),
publication_time: new Date(now - 2000).toISOString(),
expiration_time: new Date(now - 1000).toISOString(),
};

const wrapper = await mountBroadcastsList([activeBroadcast, scheduledBroadcast, expiredBroadcast]);

// All broadcasts are shown by default
expect(wrapper.findAll(selectors.broadcastItem).length).toBe(3);

// Disable all filters
await wrapper.find(selectors.showActiveFilterButton).trigger("click");
await wrapper.find(selectors.showScheduledFilterButton).trigger("click");
await wrapper.find(selectors.showExpiredFilterButton).trigger("click");
expect(wrapper.findAll(selectors.broadcastItem).length).toBe(0);
expect(wrapper.find(selectors.emptyBroadcastsListAlert).exists()).toBeTruthy();

// Enable active broadcasts filter
await wrapper.find(selectors.showActiveFilterButton).trigger("click");
expect(wrapper.findAll(selectors.broadcastItem).length).toBe(1);

// Enable scheduled broadcasts filter
await wrapper.find(selectors.showScheduledFilterButton).trigger("click");
expect(wrapper.findAll(selectors.broadcastItem).length).toBe(2);

// Enable expired broadcasts filter
await wrapper.find(selectors.showExpiredFilterButton).trigger("click");
expect(wrapper.findAll(selectors.broadcastItem).length).toBe(3);
});
});
Loading

0 comments on commit 2283294

Please sign in to comment.