refactor: allow banner service to hold multiple banners and display the banner with the highest score (#3556)

This commit is contained in:
Joel Jacob Stephen
2023-11-17 20:31:34 +05:30
committed by GitHub
parent a3aa9b68fc
commit 24ae090916
4 changed files with 112 additions and 48 deletions

View File

@@ -8,10 +8,10 @@
<span class="text-white"> <span class="text-white">
<span v-if="banner.alternateText" class="md:hidden"> <span v-if="banner.alternateText" class="md:hidden">
{{ banner.alternateText }} {{ banner.alternateText(t) }}
</span> </span>
<span class="<md:hidden"> <span :class="banner.alternateText ? '<md:hidden' : ''">
{{ banner.text }} {{ banner.text(t) }}
</span> </span>
</span> </span>
</div> </div>
@@ -19,8 +19,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue" import { computed } from "vue"
import { BannerContent, BannerType } from "~/services/banner.service" import { BannerContent, BannerType } from "~/services/banner.service"
import { useI18n } from "@composables/i18n"
import IconAlertCircle from "~icons/lucide/alert-circle" import IconAlertCircle from "~icons/lucide/alert-circle"
import IconAlertTriangle from "~icons/lucide/alert-triangle" import IconAlertTriangle from "~icons/lucide/alert-triangle"
@@ -30,6 +30,8 @@ const props = defineProps<{
banner: BannerContent banner: BannerContent
}>() }>()
const t = useI18n()
const ariaRoles: Record<BannerType, string> = { const ariaRoles: Record<BannerType, string> = {
error: "alert", error: "alert",
warning: "status", warning: "status",

View File

@@ -217,7 +217,7 @@
</div> </div>
</div> </div>
</header> </header>
<AppBanner v-if="banner" :banner="banner" /> <AppBanner v-if="bannerContent" :banner="bannerContent" />
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" /> <TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
<TeamsInvite <TeamsInvite
v-if="workspace.type === 'team' && workspace.teamID" v-if="workspace.type === 'team' && workspace.teamID"
@@ -266,7 +266,11 @@ import IconUsers from "~icons/lucide/users"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team" import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
import { BannerService } from "~/services/banner.service" import {
BannerService,
BannerContent,
BANNER_PRIORITY_HIGH,
} from "~/services/banner.service"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -284,18 +288,29 @@ const showTeamsModal = ref(false)
const breakpoints = useBreakpoints(breakpointsTailwind) const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greater("md") const mdAndLarger = breakpoints.greater("md")
const { content: banner } = useService(BannerService) const banner = useService(BannerService)
const network = reactive(useNetwork()) const bannerContent = computed(() => banner.content.value?.content)
let bannerID: number | null = null
watch(network, () => { const offlineBanner: BannerContent = {
if (network.isOnline) { type: "info",
banner.value = null text: (t) => t("helpers.offline"),
alternateText: (t) => t("helpers.offline_short"),
score: BANNER_PRIORITY_HIGH,
}
const network = reactive(useNetwork())
const isOnline = computed(() => network.isOnline)
// Show the offline banner if the user is offline
watch(isOnline, () => {
if (!isOnline.value) {
bannerID = banner.showBanner(offlineBanner)
return return
} } else {
banner.value = { if (banner.content && bannerID) {
type: "info", banner.removeBanner(bannerID)
text: t("helpers.offline"), }
alternateText: t("helpers.offline_short"),
} }
}) })

View File

@@ -1,38 +1,53 @@
import { describe, expect, it } from "vitest" import { describe, expect, it } from "vitest"
import { BannerContent, BannerService } from "../banner.service"
import { TestContainer } from "dioc/testing" import { TestContainer } from "dioc/testing"
import { getI18n } from "~/modules/i18n"
import {
BannerService,
BANNER_PRIORITY_LOW,
BANNER_PRIORITY_HIGH,
BannerContent,
} from "../banner.service"
describe("BannerService", () => { describe("BannerService", () => {
const container = new TestContainer() const container = new TestContainer()
const service = container.bind(BannerService) const banner = container.bind(BannerService)
it("initally there are no banners defined", () => { it("should be able to show and remove a banner", () => {
expect(service.content.value).toEqual(null) const bannerContent: BannerContent = {
})
it("should be able to set and retrieve banner content", () => {
const sampleBanner: BannerContent = {
type: "info", type: "info",
text: "Info Banner", text: (t: ReturnType<typeof getI18n>) => t("Info Banner"),
score: BANNER_PRIORITY_LOW,
} }
const banner = service.content const bannerId = banner.showBanner(bannerContent)
banner.value = sampleBanner expect(banner.content.value).toEqual({
const retrievedBanner = service.content.value id: bannerId,
content: bannerContent,
})
expect(retrievedBanner).toEqual(sampleBanner) banner.removeBanner(bannerId)
expect(banner.content.value).toBeNull()
}) })
it("should be able to update the banner content", () => { it("should show the banner with the highest score", () => {
const updatedBanner: BannerContent = { const lowPriorityBanner: BannerContent = {
type: "warning", type: "info",
text: "Updated Banner Content", text: (t: ReturnType<typeof getI18n>) => t("Low Priority Banner"),
alternateText: "Updated Banner", score: BANNER_PRIORITY_LOW,
} }
service.content.value = updatedBanner const highPriorityBanner: BannerContent = {
const retrievedBanner = service.content.value type: "warning",
text: (t: ReturnType<typeof getI18n>) => t("High Priority Banner"),
score: BANNER_PRIORITY_HIGH,
}
expect(retrievedBanner).toEqual(updatedBanner) banner.showBanner(lowPriorityBanner)
const highPriorityBannerID = banner.showBanner(highPriorityBanner)
expect(banner.content.value).toEqual({
id: highPriorityBannerID,
content: highPriorityBanner,
})
}) })
}) })

View File

@@ -1,16 +1,35 @@
import { Service } from "dioc" import { Service } from "dioc"
import { ref } from "vue" import { computed, ref } from "vue"
import { getI18n } from "~/modules/i18n"
export const BANNER_PRIORITY_LOW = 1
export const BANNER_PRIORITY_MEDIUM = 3
export const BANNER_PRIORITY_HIGH = 5
/**
* The different types of banners that can be used.
*/
export type BannerType = "info" | "warning" | "error" export type BannerType = "info" | "warning" | "error"
export type BannerContent = { export type BannerContent = {
type: BannerType type: BannerType
text: string text: (t: ReturnType<typeof getI18n>) => string
// Can be used to display an alternate text when display size is small // Can be used to display an alternate text when display size is small
alternateText?: string alternateText?: (t: ReturnType<typeof getI18n>) => string
// Used to determine which banner should be displayed when multiple banners are present
score: number
}
export type Banner = {
id: number
content: BannerContent
}
// Returns the banner with the highest score
const getBannerWithHighestScore = (list: Banner[]) => {
if (list.length === 0) return null
else if (list.length === 1) return list[0]
else {
const highestScore = Math.max(...list.map((banner) => banner.content.score))
return list.find((banner) => banner.content.score === highestScore)
}
} }
/** /**
@@ -20,9 +39,22 @@ export type BannerContent = {
export class BannerService extends Service { export class BannerService extends Service {
public static readonly ID = "BANNER_SERVICE" public static readonly ID = "BANNER_SERVICE"
/** private bannerID = 0
* This is a reactive variable that can be used to set the contents of the banner private bannerList = ref<Banner[]>([])
* and use it to render the banner on components.
*/ public content = computed(() =>
public content = ref<BannerContent | null>(null) getBannerWithHighestScore(this.bannerList.value)
)
public showBanner(banner: BannerContent) {
this.bannerID = this.bannerID + 1
this.bannerList.value.push({ id: this.bannerID, content: banner })
return this.bannerID
}
public removeBanner(id: number) {
this.bannerList.value = this.bannerList.value.filter(
(banner) => id !== banner.id
)
}
} }