diff --git a/.releaserc b/.releaserc index fb38cd11b552d58f64613e34ddb72f7f6571869e..43921b6ddff5e838465b1c774f114e41e3c3ae32 100644 --- a/.releaserc +++ b/.releaserc @@ -4,13 +4,12 @@ "preset": "eslint" }], ["@semantic-release/release-notes-generator", { - "preset": "eslint", + "preset": "eslint" }], ["@semantic-release/gitlab", { "preset": "eslint", "gitlabUrl": "https://git.rwth-aachen.de" }], - #Currently not supported ["@semantic-release/npm", { "preset": "eslint", "tarballDir": "dist", diff --git a/.vscode/settings.json b/.vscode/settings.json index 3783a132066bbe437c1f5cca2002038242a10458..9fb1253b2d20a546904bdda95407f9e731c94f55 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,12 @@ { + "workbench.colorCustomizations": { + "activityBar.background": "#64b687", + "activityBar.foreground": "#ffffff", + "activityBar.inactiveForeground": "#32485c", + "activityBar.activeBorder": "#32485c", + "activityBarBadge.background": "#32485c", + "activityBarBadge.foreground": "#ffffff", + }, "search.exclude": { "**/.yarn": true, "**/.pnp.*": true @@ -11,4 +19,4 @@ "vite", "Vuelidate" ] -} +} \ No newline at end of file diff --git a/package.json b/package.json index ca3bca21e72020331bac47e8ba55cdf367bf8b4f..93d98c5d9f9f4e7eca708c4fead9206e629c2812 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "coverage": "vitest run --coverage" }, "dependencies": { - "@coscine/api-client": "^3.10.0", + "@coscine/api-client": "3.8.0-issue-2881-messagecontroller.11", "@coscine/form-generator": "^4.0.5", "@dynamic-mapper/mapper": "^1.10.4", "@pinia/testing": "^0.1.3", @@ -30,6 +30,7 @@ "http-status-codes": "^2.3.0", "jose": "^5.1.1", "lodash": "^4.17.21", + "markdown-it": "^14.1.0", "moment": "^2.29.4", "pinia": "^2.1.7", "rdf-ext": "^2.5.2", @@ -58,6 +59,7 @@ "@semantic-release/release-notes-generator": "^11.0.7", "@types/file-saver": "^2.0.7", "@types/lodash": "^4.14.201", + "@types/markdown-it": "^14", "@types/rdf-ext": "^2.5.0", "@types/rdf-validate-shacl": "^0.4.7", "@types/uuid": "^9.0.7", diff --git a/src/App.vue b/src/App.vue index 7e7f18e93580cdcebf99ae88ebba4cd458116030..a8f3d2cf343fcd9fc161be6caf50e3090eaf3b0e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,7 +10,8 @@ <SidebarMenu v-if="isLoggedIn && areTosAccepted" /> <main :class="mainContainerSizing"> <b-container fluid> - <Maintenance /> + <Noc /> + <Internal /> <BreadCrumbs v-if="isLoggedIn && areTosAccepted" /> <RouterView /> </b-container> diff --git a/src/components.d.ts b/src/components.d.ts index 4967504046f19d4d12a36d8403c0c85a6cafc602..c7a30233671e9f7e8ed879a29e83dcedd7c3a3ef 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -68,6 +68,7 @@ declare module 'vue' { BToastOrchestrator: typeof import('bootstrap-vue-next')['BToastOrchestrator'] BTooltip: typeof import('bootstrap-vue-next')['BTooltip'] BTr: typeof import('bootstrap-vue-next')['BTr'] + copy: typeof import('./components/banner/Noc copy.vue')['default'] CoscineCard: typeof import('./components/coscine/CoscineCard.vue')['default'] CoscineFormGroup: typeof import('./components/coscine/CoscineFormGroup.vue')['default'] CoscineHeadline: typeof import('./components/coscine/CoscineHeadline.vue')['default'] @@ -108,10 +109,12 @@ declare module 'vue' { IBiSortUp: typeof import('~icons/bi/sort-up')['default'] IBiSunFill: typeof import('~icons/bi/sun-fill')['default'] IBiThreeDotsVertical: typeof import('~icons/bi/three-dots-vertical')['default'] + Internal: typeof import('./components/banner/Internal.vue')['default'] LoadingIndicator: typeof import('./components/elements/LoadingIndicator.vue')['default'] LoadingSpinner: typeof import('./components/coscine/LoadingSpinner.vue')['default'] Maintenance: typeof import('./components/banner/Maintenance.vue')['default'] Navbar: typeof import('./components/elements/Navbar.vue')['default'] + Noc: typeof import('./components/banner/Noc.vue')['default'] NotificationToast: typeof import('./components/toasts/NotificationToast.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/src/components/banner/Internal.spec.ts b/src/components/banner/Internal.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..bac1416607bf5dd560b406c1c9c15d8ebd014a81 --- /dev/null +++ b/src/components/banner/Internal.spec.ts @@ -0,0 +1,107 @@ +/* Testing imports */ +import { mount } from "@vue/test-utils"; +import { createTestingPinia } from "@pinia/testing"; + +/* Vue i18n */ +import i18n, { def } from "@/plugins/vue-i18n"; +i18n.global.availableLocales.forEach((locale) => { + i18n.global.setLocaleMessage(locale, def[locale]); // default locale messages +}); + +/* Tested Component */ +import Internal from "./Internal.vue"; + +import { getTestShibbolethUserState } from "@/data/mockup/testUser"; +import { + getInternalMessagesResponse, + testSystemStatusState, +} from "@/data/mockup/testSystemStatus"; +import useSystemStatusStore from "@/store/systemStatus"; +import { getTestMainState } from "@/data/mockup/testMain"; + +describe("Internal", async () => { + // Create a mocked pinia instance with initial state + const testingPinia = createTestingPinia({ + createSpy: vitest.fn, + initialState: { + main: await getTestMainState(), + systemStatus: testSystemStatusState, + user: getTestShibbolethUserState(), + }, + }); + + const createWrapper = () => { + return mount(Internal, { + global: { + plugins: [testingPinia, i18n], + }, + }); + }; + + let wrapper: ReturnType<typeof createWrapper>; + + beforeEach(() => { + // shallowMount does not work here! + wrapper = createWrapper(); + }); + + const systemStatusStore = useSystemStatusStore(testingPinia); + + vi.mock("vue-i18n", () => ({ + useI18n: () => ({ + locale: { + value: "en", + }, + t: (key: string) => key, + d: (key: string) => key, + }), + })); + + it("Should render internal messages if they exist", async () => { + await nextTick(); + + expect(wrapper.findAll(".alert").length).toBe(1); + expect(wrapper.text()).toEqual(getInternalMessagesResponse[0].body?.en); + }); + + it("Should not render any messages if internalMessages array is empty", async () => { + await nextTick(); + + // Overwrite the internalMessages array with an empty array + systemStatusStore.banner.internal.messages = []; + + await nextTick(); + + expect(wrapper.findAll(".alert").length).toBe(0); + + // Restore the store's state, as tests are dependent on each other + systemStatusStore.banner.internal.messages = getInternalMessagesResponse; + }); + + it("Should use the correct variant for each message type", async () => { + await nextTick(); + + const alerts = wrapper.findAll(".alert"); + expect(alerts.at(0)?.attributes("class")).toContain("info"); + }); + + it("Should call hideMessage when an alert is closed", async () => { + await nextTick(); + + const banner = wrapper.find(".alert"); + const closeButton = banner.findAll(".btn-close").at(0); + closeButton?.trigger("click"); + + expect(systemStatusStore.hideMessage).toHaveBeenCalledOnce(); + }); + + it("Should not display hidden messages", async () => { + await nextTick(); + + systemStatusStore.banner.hidden = [getInternalMessagesResponse[0].id!]; + + await nextTick(); + + expect(wrapper.findAll(".alert").length).toBe(0); + }); +}); diff --git a/src/components/banner/Internal.vue b/src/components/banner/Internal.vue new file mode 100644 index 0000000000000000000000000000000000000000..db6b64079becf4d32884605a0401e3587dd081ed --- /dev/null +++ b/src/components/banner/Internal.vue @@ -0,0 +1,81 @@ +<template> + <div v-if="internalMessages.length"> + <b-alert + v-for="(message, index) in internalMessages" + :key="index" + model-value + show + dismissible + :locale="$i18n.locale" + :variant="getBannerVariant(message.type)" + @closed="hideMessage(message.id)" + > + <p v-dompurify-html="renderMarkdown(message.body?.[locale] || '~')" /> + </b-alert> + </div> +</template> + +<script lang="ts"> +import { defineComponent, computed } from "vue"; +import { useI18n } from "vue-i18n"; +import { useSystemStatusStore } from "@/store/systemStatus"; +import { mapMessageTypeToBannerVariant } from "@/util/messageTypeMap"; +import type { MessageType } from "@coscine/api-client/dist/types/Coscine.Api"; +import type { BaseColorVariant } from "bootstrap-vue-next/dist/src/types"; +import markdownIt from "markdown-it"; + +export default defineComponent({ + setup() { + const i18n = useI18n(); + const systemStatusStore = useSystemStatusStore(); + + const md = markdownIt(); + + const locale = computed(() => i18n.locale.value); + + /** + * Computes the list of visible internal messages from the system status store. + * @returns {MessageDto[]} The list of visible internal messages. + */ + const internalMessages = computed( + () => systemStatusStore.visibleInternalMessages, + ); + + /** + * Hides a message by its ID. + * @param {string | undefined} id - The ID of the message to hide. + */ + const hideMessage = (id?: string) => { + if (id) systemStatusStore.hideMessage(id); + }; + + /** + * Maps a message type to a Bootstrap Vue variant. + * @param {MessageType | undefined} type - The type of the message. + * @returns {keyof BaseColorVariant | null | undefined} The corresponding Bootstrap Vue variant. + */ + const getBannerVariant = ( + type?: MessageType, + ): keyof BaseColorVariant | null | undefined => { + return mapMessageTypeToBannerVariant(type); + }; + + /** + * Renders markdown to HTML. + * @param {string} markdownText - The markdown text to render. + * @returns {string} The rendered HTML. + */ + const renderMarkdown = (markdownText: string): string => { + return md.render(markdownText); + }; + + return { + internalMessages, + locale, + hideMessage, + getBannerVariant, + renderMarkdown, + }; + }, +}); +</script> diff --git a/src/components/banner/Maintenance.vue b/src/components/banner/Maintenance.vue deleted file mode 100644 index 56978935aa3a0220d25dd0c7eec0f21e3d478e8c..0000000000000000000000000000000000000000 --- a/src/components/banner/Maintenance.vue +++ /dev/null @@ -1,121 +0,0 @@ -<template> - <b-alert - v-if="visibility" - :model-value="show" - dismissible - :variant="maintenance.type !== 'Info' ? 'warning' : 'info'" - @closed="saveVisibility" - > - <p> - <span class="font-weight-bold"> - {{ `${messageType}${$t("banner.separator")}` }} - </span> - <span v-dompurify-html="messageBody"></span> - <span v-if="maintenance.href"> - {{ $t("banner.maintenance.linkText") }} - <a :href="maintenance.href" target="_blank" - >{{ $t("banner.maintenance.moreInformation") }} - </a> - </span> - </p> - </b-alert> -</template> - -<script lang="ts"> -// import the main store -import useMainStore from "@/store/index"; -import type { MaintenanceDto } from "@coscine/api-client/dist/types/Coscine.Api"; - -export default defineComponent({ - setup() { - const mainStore = useMainStore(); - return { mainStore }; - }, - - computed: { - maintenance(): MaintenanceDto { - return this.mainStore.coscine.banner.maintenance; - }, - visibility(): boolean { - return ( - this.mainStore.coscine.banner.maintenanceVisibility !== - this.mainStore.coscine.banner.dateString && - this.mainStore.coscine.banner.dateString.trim() !== "" - ); - }, - show(): boolean { - return this.visibility && this.maintenance.type && this.maintenance.body - ? true - : false; - }, - locale(): string { - return this.$i18n.locale; - }, - messageBody(): string | undefined { - const notificationText = this.createNotificationText()?.trim(); - // empty texts - if (!notificationText) { - return this.$t("banner.maintenance.notificationDefaultText").toString(); - } - - const languageSpecificNotificationTexts = - notificationText.split(/(?<!:)\/\//); - if (languageSpecificNotificationTexts?.length === 1) { - return languageSpecificNotificationTexts[0]; - } - - return this.$i18n.locale === "de" - ? languageSpecificNotificationTexts[1] - : languageSpecificNotificationTexts[0]; - }, - messageType(): string { - if (this.maintenance.type) { - switch (this.maintenance.type) { - case "Eingriff": - return this.$t("banner.maintenance.type.intervention").toString(); - case "Störung": - return this.$t("banner.maintenance.type.disturbance").toString(); - case "Teilstörung": - return this.$t( - "banner.maintenance.type.partialDisturbance", - ).toString(); - case "Unterbrechung": - return this.$t("banner.maintenance.type.interruption").toString(); - case "eingeschränkt betriebsfähig": - return this.$t( - "banner.maintenance.type.limitedOperation", - ).toString(); - case "Wartung": - return this.$t("banner.maintenance.type.maintenance").toString(); - case "Teilwartung": - return this.$t( - "banner.maintenance.type.partialMaintenance", - ).toString(); - case "Info": - return this.$t("banner.maintenance.type.info").toString(); - default: - return this.$t("banner.maintenance.type.maintenance").toString(); - } - } else { - return this.$t("banner.maintenance.type.maintenance").toString(); - } - }, - }, - - methods: { - saveVisibility() { - this.mainStore.coscine.banner.maintenanceVisibility = - this.mainStore.coscine.banner.dateString; - }, - createNotificationText(): string | undefined { - if (this.maintenance && this.maintenance.body) { - return this.maintenance.body; - } else { - return ""; - } - }, - }, -}); -</script> - -<style></style> diff --git a/src/components/banner/Noc.spec.ts b/src/components/banner/Noc.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c82070e0f5f581915a30bb0e9059b145fbd4490 --- /dev/null +++ b/src/components/banner/Noc.spec.ts @@ -0,0 +1,102 @@ +/* Testing imports */ +import { mount } from "@vue/test-utils"; +import { createTestingPinia } from "@pinia/testing"; + +/* Vue i18n */ +import i18n, { def } from "@/plugins/vue-i18n"; +i18n.global.availableLocales.forEach((locale) => { + i18n.global.setLocaleMessage(locale, def[locale]); // default locale messages +}); + +/* Tested Component */ +import Noc from "./Noc.vue"; + +import { getTestShibbolethUserState } from "@/data/mockup/testUser"; +import { + getNocMessagesResponse, + testSystemStatusState, +} from "@/data/mockup/testSystemStatus"; +import useSystemStatusStore from "@/store/systemStatus"; +import { getTestMainState } from "@/data/mockup/testMain"; + +describe("Noc", async () => { + // Create a mocked pinia instance with initial state + const testingPinia = createTestingPinia({ + createSpy: vitest.fn, + initialState: { + main: await getTestMainState(), + systemStatus: testSystemStatusState, + user: getTestShibbolethUserState(), + }, + }); + + const createWrapper = () => { + return mount(Noc, { + global: { + plugins: [testingPinia, i18n], + }, + }); + }; + + let wrapper: ReturnType<typeof createWrapper>; + + beforeEach(() => { + wrapper = createWrapper(); + }); + + const systemStatusStore = useSystemStatusStore(testingPinia); + + it("Should render NOC messages if they exist", async () => { + await nextTick(); + + const banners = wrapper.findAll(".alert"); + expect(banners.length).toBe(getNocMessagesResponse.length); + const text = i18n.global.t("banner.noc.text.Disturbance").substring(0, 10); + expect(banners.at(0)?.text()).toContain(text); + expect(banners.at(0)?.attributes("class")).toContain("danger"); + }); + + it("Should not render any messages if nocMessages array is empty", async () => { + await nextTick(); + + // Overwrite the nocMessages array with an empty array + systemStatusStore.banner.noc.messages = []; + + await nextTick(); + + expect(wrapper.findAll(".alert").length).toBe(0); + + // Restore the store's state, as tests are dependent on each other + systemStatusStore.banner.noc.messages = getNocMessagesResponse; + }); + + it("Should call hideMessage when an alert is closed", async () => { + await nextTick(); + + const banner = wrapper.find(".alert"); + const closeButton = banner.findAll(".btn-close").at(0); + closeButton?.trigger("click"); + + expect(systemStatusStore.hideMessage).toHaveBeenCalledOnce(); + }); + + it("Should use the correct variant for each message type", async () => { + await nextTick(); + + const alerts = wrapper.findAll(".alert"); + expect(alerts.at(0)?.attributes("class")).toContain("danger"); + expect(alerts.at(1)?.attributes("class")).toContain("warning"); + }); + + it("Should render a link within the message if href is provided", async () => { + await nextTick(); + + const links = wrapper.findAll("a"); + expect(links.at(0)?.attributes("href")).toBe( + "https://example.com/noc-5555", + ); + expect(links.at(1)?.attributes("href")).toBe( + "https://example.com/noc-5572", + ); + }); +}); diff --git a/src/components/banner/Noc.vue b/src/components/banner/Noc.vue new file mode 100644 index 0000000000000000000000000000000000000000..6e30f6ea0f12539a7574b935014155e2e07b9483 --- /dev/null +++ b/src/components/banner/Noc.vue @@ -0,0 +1,72 @@ +<template> + <div v-if="nocMessages.length"> + <b-alert + v-for="(message, index) in nocMessages" + :key="index" + model-value + show + dismissible + :variant="getBannerVariant(message.type)" + @closed="hideMessage(message.id)" + > + <i18n-t + scope="global" + :keypath="`banner.noc.text.${message.type}`" + tag="p" + > + <template #nocPortal> + <a :href="message.href ?? undefined" target="_blank"> + <i>{{ $t("banner.noc.nocPortal") }}</i> + </a> + </template> + </i18n-t> + </b-alert> + </div> +</template> + +<script lang="ts"> +import { defineComponent, computed } from "vue"; +import { useSystemStatusStore } from "@/store/systemStatus"; +import { mapMessageTypeToBannerVariant } from "@/util/messageTypeMap"; +import type { MessageType } from "@coscine/api-client/dist/types/Coscine.Api"; +import type { BaseColorVariant } from "bootstrap-vue-next/dist/src/types"; + +export default defineComponent({ + setup() { + const systemStatusStore = useSystemStatusStore(); + + /** + * Computes the list of visible NOC (Network Operations Center) messages. + * @returns {MessageDto[]} The list of visible NOC messages. + */ + const nocMessages = computed(() => systemStatusStore.visibleNocMessages); + + /** + * Hides a NOC message by its ID. + * @param {string | undefined} id - The ID of the message to hide. + */ + const hideMessage = (id?: string) => { + if (id) systemStatusStore.hideMessage(id); + }; + + /** + * Maps a message type to a Bootstrap Vue variant. + * @param {MessageType | undefined} type - The type of the NOC message. + * @returns {keyof BaseColorVariant | null | undefined} The corresponding Bootstrap Vue variant. + */ + const getBannerVariant = ( + type?: MessageType, + ): keyof BaseColorVariant | null | undefined => { + return mapMessageTypeToBannerVariant(type); + }; + + return { + nocMessages, + hideMessage, + getBannerVariant, + }; + }, +}); +</script> + +<style scoped></style> diff --git a/src/components/elements/LoadingIndicator.vue b/src/components/elements/LoadingIndicator.vue index 9c0186c616f1cce5c377a265ce5907a593a1a701..3dd3b024e8ce76430e6d929f47cb6592c51282cf 100644 --- a/src/components/elements/LoadingIndicator.vue +++ b/src/components/elements/LoadingIndicator.vue @@ -14,6 +14,7 @@ import usePidStore from "@/modules/pid/store"; import useProjectStore from "@/modules/project/store"; import useResourceStore from "@/modules/resource/store"; import useSearchStore from "@/modules/search/store"; +import useSystemStatusStore from "@/store/systemStatus"; import useUserStore from "@/modules/user/store"; import { loadingCounterEventHandler } from "@/plugins/loadingCounter"; @@ -29,6 +30,7 @@ export default defineComponent({ loadingCounterEventHandler(useProjectStore); loadingCounterEventHandler(useResourceStore); loadingCounterEventHandler(useSearchStore); + loadingCounterEventHandler(useSystemStatusStore); loadingCounterEventHandler(useUserStore); return { mainStore }; diff --git a/src/data/mockup/testMain.ts b/src/data/mockup/testMain.ts index ce0fb06922ff7fbc570aff5e8a406f18af46b14f..8ab0f6c3fac2a87a6c51c2a4ca0d4e70bb6ce68b 100644 --- a/src/data/mockup/testMain.ts +++ b/src/data/mockup/testMain.ts @@ -1,4 +1,4 @@ -import { type MainState } from "@/store/types"; +import type { MainState, Theme } from "@/store/types"; import { useLocalStorage } from "@vueuse/core"; import { v4 as uuidv4 } from "uuid"; import * as jose from "jose"; @@ -28,14 +28,7 @@ export const getTestMainState: () => Promise<MainState> = async () => { counter: 0, }, locale: useLocalStorage("coscine.locale", "en"), - banner: { - maintenance: {}, - maintenanceVisibility: useLocalStorage( - "coscine.banner.maintenanceVisibility", - "", - ), - dateString: useLocalStorage("coscine.banner.dateString", ""), - }, + theme: useLocalStorage<Theme>("coscine.theme", "light"), }, sidebarActive: useLocalStorage("coscine.sidebar.active", true), }; diff --git a/src/data/mockup/testSystemStatus.ts b/src/data/mockup/testSystemStatus.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b9e3eeaec6da55ffe5a0fd4d1cbd792cfcaffe3 --- /dev/null +++ b/src/data/mockup/testSystemStatus.ts @@ -0,0 +1,47 @@ +import moment from "moment"; +import { SystemStatusState } from "@/store/types"; +import type { MessageDto } from "@coscine/api-client/dist/types/Coscine.Api"; + +export const getNocMessagesResponse: MessageDto[] = [ + { + id: "noc-5555", + href: "https://example.com/noc-5555", + type: "Disturbance", + startDate: moment().subtract(1, "days").toISOString(), // yesterday + endDate: moment().add(1, "days").toISOString(), // tomorrow + }, + { + id: "noc-5572", + href: "https://example.com/noc-5572", + type: "Maintenance", + startDate: moment().subtract(1, "days").toISOString(), // yesterday + endDate: moment().add(5, "hours").toISOString(), // in 5 hours + }, +]; + +export const getInternalMessagesResponse: MessageDto[] = [ + { + id: "int-6551c5a7ba1a3870450bb960f93ba912f175bc2a31992bc776413c4cb542f5b3", + body: { + de: "Das ist eine Testnachricht", + en: "This is a test message", + }, + type: "Information", + startDate: moment().subtract(1, "days").toISOString(), // yesterday + endDate: moment().add(1, "days").toISOString(), // tomorrow + }, +]; + +export const testSystemStatusState: SystemStatusState = { + banner: { + noc: { + messages: getNocMessagesResponse, + lastFetched: new Date(), + }, + internal: { + messages: getInternalMessagesResponse, + lastFetched: new Date(), + }, + hidden: useLocalStorage("coscine.system.status.banner.hidden", []), + }, +}; diff --git a/src/i18n/de.ts b/src/i18n/de.ts index cf57c896c46019a6fce458c288c473b808bb99a1..a00c3076090b7fdaf9f57f03a61779a63b5845f4 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -274,23 +274,30 @@ export default { }, banner: { - maintenance: { - type: { - intervention: "Eingriff", - disturbance: "Störung", - partialDisturbance: "Teilstörung", - interruption: "Unterbrechung", - limitedOperation: "Eingeschränkt betriebsfähig", - maintenance: "Wartung", - partialMaintenance: "Teilwartung", - info: "Information", + noc: { + text: { + Disturbance: + "Derzeit gibt es ein Problem, das Ihre Nutzung beeinträchtigen könnte. Unser Team arbeitet daran. Weitere Informationen finden Sie im {nocPortal}.", + PartialDisturbance: + "Derzeit gibt es ein Problem, das Ihre Nutzung beeinträchtigen könnte. Unser Team arbeitet daran. Weitere Informationen finden Sie im {nocPortal}.", + Interruption: + "Derzeit gibt es ein Problem, das Ihre Nutzung beeinträchtigen könnte. Unser Team arbeitet daran. Weitere Informationen finden Sie im {nocPortal}.", + Maintenance: + "Geplante Wartungsarbeiten finden derzeit statt, um unseren Service zu verbessern. Wir bemühen uns, Störungen zu minimieren. Vielen Dank für Ihr Verständnis. Weitere Details finden Sie im {nocPortal}.", + PartialMaintenance: + "Geplante Wartungsarbeiten finden derzeit statt, um unseren Service zu verbessern. Wir bemühen uns, Störungen zu minimieren. Vielen Dank für Ihr Verständnis. Weitere Details finden Sie im {nocPortal}.", + LimitedOperation: + "Wir arbeiten derzeit mit eingeschränkter Funktionalität. Unser Team bemüht sich, den vollen Service so schnell wie möglich wiederherzustellen. Wir danken für Ihr Verständnis. Weitere Informationen finden Sie im {nocPortal}.", + Change: + "Eine kürzliche Änderung wurde in unserem System vorgenommen. Wir erwarten, dass dies Ihre Erfahrung verbessert. Weitere Details finden Sie im {nocPortal}.", + Hint: "Wir haben Neuigkeiten, die für Sie interessant sein könnten. Bitte nehmen Sie sich einen Moment Zeit, um sie zu lesen. Weitere Details finden Sie im {nocPortal}.", + Information: + "Wir haben Neuigkeiten, die für Sie interessant sein könnten. Bitte nehmen Sie sich einen Moment Zeit, um sie zu lesen. Weitere Details finden Sie im {nocPortal}.", + Warning: + "Es wurde eine Systemwarnung ausgegeben. Bitte überprüfen Sie die neuesten Informationen im {nocPortal}.", }, - notificationDefaultText: - "Derzeit kann es unter Umständen zu Einschränkungen bei der Nutzung von Coscine kommen. ", - linkText: "Details finden Sie ", - moreInformation: "im Statusmeldungsportal", + nocPortal: "Statusmeldungsportal", }, - separator: ": ", }, email: { diff --git a/src/i18n/en.ts b/src/i18n/en.ts index eef30aeed335df97e4dad9f245e3a3fccb9ec46a..d89fa8a2c6a54a417fa91e740ff5af4e70fa51a0 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -272,23 +272,30 @@ export default { }, banner: { - maintenance: { - type: { - intervention: "Intervention", - disturbance: "Disturbance", - partialDisturbance: "Partial disturbance", - interruption: "Interruption", - limitedOperation: "Limited operation", - maintenance: "Maintenance", - partialMaintenance: "Partial maintenance", - info: "Info", + noc: { + text: { + Disturbance: + "We are currently facing an issue that may affect your experience. Our team is on it. For more information, please visit the {nocPortal}.", + PartialDisturbance: + "We are currently facing an issue that may affect your experience. Our team is on it. For more information, please visit the {nocPortal}.", + Interruption: + "We are currently facing an issue that may affect your experience. Our team is on it. For more information, please visit the {nocPortal}.", + Maintenance: + "Scheduled maintenance is in progress to enhance our service. We're working to minimize any disruption. Thank you for your understanding. Visit the {nocPortal} for further details.", + PartialMaintenance: + "Scheduled maintenance is in progress to enhance our service. We're working to minimize any disruption. Thank you for your understanding. Visit the {nocPortal} for further details.", + LimitedOperation: + "We're currently operating with limited functionality. Our team is working to restore full service as soon as possible. We appreciate your understanding. For more details, visit the {nocPortal}.", + Change: + "A recent change has been applied to our system. We expect this to enhance your experience. For more details, please visit the {nocPortal}.", + Hint: "We have some news that may interest you. Please take a moment to read it. Further details can be found on the {nocPortal}.", + Information: + "We have some news that may interest you. Please take a moment to read it. Further details can be found on the {nocPortal}.", + Warning: + "A system warning has been issued. Please review the latest information on the {nocPortal}.", }, - notificationDefaultText: - "Currently, you may experience restrictions when using Coscine. ", - linkText: "Details can be found ", - moreInformation: "on the Status Updates Website", + nocPortal: "Status Updates Website", }, - separator: ": ", }, email: { diff --git a/src/modules/login/store.spec.ts b/src/modules/login/store.spec.ts index 24032787badfbad45b7e5eae40e7aeca2373b295..335ebffb23bc49a6e817ac9bdebfcb02347b94b8 100644 --- a/src/modules/login/store.spec.ts +++ b/src/modules/login/store.spec.ts @@ -31,11 +31,10 @@ describe("Login Store", () => { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", ); localStorage.setItem("coscine.locale", "en"); - localStorage.setItem("coscine.banner.maintenanceVisibility", ""); localStorage.setItem("coscine.login.storedData", ""); localStorage.setItem("coscine.sidebar.active", "true"); - localStorage.setItem("coscine.banner.dateString", ""); - expect(localStorage.length).toBe(7); + localStorage.setItem("coscine.system.status.banner.hidden", "[]"); + expect(localStorage.length).toBe(6); loginStore.logout(); diff --git a/src/modules/project/pages/components/data-publication/DataPublicationProjectData.vue b/src/modules/project/pages/components/data-publication/DataPublicationProjectData.vue index 0404122e4c936cfe6908f391f532fc5d48cd3cd7..03a4cdf0d9365f735d1379099a1ddf7666aa55a3 100644 --- a/src/modules/project/pages/components/data-publication/DataPublicationProjectData.vue +++ b/src/modules/project/pages/components/data-publication/DataPublicationProjectData.vue @@ -203,12 +203,12 @@ import useProjectStore from "../../../store"; import useUserStore from "@/modules/user/store"; import useResourceStore from "@/modules/resource/store"; import useMainStore from "@/store/index"; -import { - type DisciplineDto, - type UserDto, - type PublicationRequestForCreationDto, - type ResourceDto, - type OrganizationDto, +import type { + DisciplineDto, + UserDto, + PublicationRequestForCreationDto, + ResourceDto, + OrganizationDto, } from "@coscine/api-client/dist/types/Coscine.Api"; import type { VisitedProjectDto } from "@/modules/project/types"; diff --git a/src/router/index.ts b/src/router/index.ts index 0e8826444d43d5e9e3f1e9e8f6f67dd22d8787f0..578be194cf8907ffc7844cd22e77c4026e00740a 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -62,9 +62,10 @@ VueRouter.prototype.push = async function (location: RouteLocationRaw) { // Import the relevant stores import useMainStore from "@/store/index"; import useUserStore from "@/modules/user/store"; +import useLoginStore from "@/modules/login/store"; +import useSystemStatusStore from "@/store/systemStatus"; import i18n, { def } from "@/plugins/vue-i18n"; import type { DefaultLocaleMessageSchema, LocaleMessages } from "vue-i18n"; -import useLoginStore from "@/modules/login/store"; import type { ProjectDto } from "@coscine/api-client/dist/types/Coscine.Api"; router.beforeEach((to, _, next) => { @@ -83,6 +84,7 @@ router.beforeEach((to, _, next) => { } // Define the relevant stores const mainStore = useMainStore(); + const systemStatusStore = useSystemStatusStore(); const loginStore = useLoginStore(); const userStore = useUserStore(); @@ -90,8 +92,22 @@ router.beforeEach((to, _, next) => { mainStore.setAccessTokenFromRoute(router, to); // Handle contact change token from URL userStore.confirmUserEmail(to); - // Collect ongoing Maintenance information - mainStore.getMaintenance(); + // Collect current NOC information + if ( + systemStatusStore.shouldFetchMessages( + systemStatusStore.banner.noc.lastFetched, + ) + ) { + systemStatusStore.retrieveCurrentNocMessages(); + } + // Collect current internal information + if ( + systemStatusStore.shouldFetchMessages( + systemStatusStore.banner.internal.lastFetched, + ) + ) { + systemStatusStore.retrieveCurrentInternalMessages(); + } // Retrieve session status loginStore.retrieveSessionStatus(); diff --git a/src/store/index.ts b/src/store/index.ts index 8164121cbe73f6e64c52b00137f68bc3ea243378..ce6cbfa5339b0bd1cb34bf47925a1bba66726b3b 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -3,14 +3,12 @@ import { defineStore } from "pinia"; import type { Router, RouteLocationNormalized } from "vue-router"; import type { Theme, MainState } from "./types"; import { v4 as uuidv4 } from "uuid"; +import * as jose from "jose"; +import moment from "moment"; import { removeQueryParameterFromUrl } from "@/router"; -import { MaintenanceApi } from "@coscine/api-client"; import useLoginStore from "@/modules/login/store"; -import moment from "moment"; -import * as jose from "jose"; -import type { MaintenanceDto } from "@coscine/api-client/dist/types/Coscine.Api"; /* Store variable name is "this.<id>Store" @@ -36,14 +34,6 @@ export const useMainStore = defineStore({ counter: 0, }, locale: useLocalStorage("coscine.locale", "en"), - banner: { - maintenance: {}, - maintenanceVisibility: useLocalStorage( - "coscine.banner.maintenanceVisibility", - "", - ), - dateString: useLocalStorage("coscine.banner.dateString", ""), - }, theme: useLocalStorage<Theme>("coscine.theme", "light"), }, sidebarActive: useLocalStorage("coscine.sidebar.active", true), @@ -112,44 +102,6 @@ export const useMainStore = defineStore({ this.coscine.loading.counter++; }, - async getMaintenance() { - const apiResponse = await MaintenanceApi.getCurrentMaintenances(); - // TODO: Make it work with multiple maintenance messages - const maintenance = apiResponse.data.data?.at(0); // Take the first maintenance - if (maintenance?.startsDate) { - const now = new Date(Date.now()); - const startDate = new Date(maintenance.startsDate); - const endDate = maintenance.endsDate; - if (startDate <= now && (!endDate || new Date(endDate) >= now)) { - this.coscine.banner.dateString = - maintenance.startsDate + maintenance.endsDate; - this.coscine.banner.maintenance = maintenance; - } - } - // Inject banner - if (Object.keys(this.coscine.banner.maintenance).length === 0) { - // Javascript sets months to 0 - const startsDate = new Date(2024, 3, 22, 0, 0, 0).toUTCString(); - const endsDate = new Date(2024, 4, 7, 0, 0, 0).toUTCString(); - const injectedBanner = { - body: `Your opinion matters! The survey on the use of Coscine will take place from April 22 to May 6, 2024. Take the opportunity and help us to optimize the platform. The survey takes approximately 15 minutes. <a href="https://s2survey.net/coscine_2024/" target="_blank">https://s2survey.net/coscine_2024/</a> - // - Ihre Meinung ist uns wichtig! Vom 22. April bis 6. Mai 2024 findet die Umfrage zur Nutzung von Coscine statt. Nutzen Sie die Chance und tragen Sie aktiv dazu bei, die Plattform zu optimieren. Die Umfrage dauert ca. 15 Minuten. <a href="https://s2survey.net/coscine_2024/" target="_blank">https://s2survey.net/coscine_2024/</a>`, - displayName: "User Survey", - startsDate: startsDate, - endsDate: endsDate, - type: "Info", - } satisfies MaintenanceDto; - const now = new Date(Date.now()); - const startDate = new Date(startsDate); - if (startDate <= now && (!endsDate || new Date(endsDate) >= now)) { - this.coscine.banner.dateString = - injectedBanner.startsDate + injectedBanner.endsDate; - this.coscine.banner.maintenance = injectedBanner; - } - } - }, - getCurrentTokenExpirationDuration(): number { const loginStore = useLoginStore(); diff --git a/src/store/systemStatus.ts b/src/store/systemStatus.ts new file mode 100644 index 0000000000000000000000000000000000000000..a4756edfbeda217f3034c824f235bbac7ff308a6 --- /dev/null +++ b/src/store/systemStatus.ts @@ -0,0 +1,167 @@ +import { useLocalStorage } from "@vueuse/core"; +import { defineStore } from "pinia"; +import type { SystemStatusState } from "./types"; +import moment from "moment"; + +import useNotificationStore from "./notification"; + +import type { AxiosError } from "axios"; +import { SystemStatusApi } from "@coscine/api-client"; +import type { MessageDto } from "@coscine/api-client/dist/types/Coscine.Api"; + +/* + Store variable name is "this.<id>Store" + id: "notification" --> this.notificationStore + Important! The id must be unique for every store. +*/ +export const useSystemStatusStore = defineStore({ + id: "systemStatus", + /* + -------------------------------------------------------------------------------------- + STATES + -------------------------------------------------------------------------------------- + */ + state: (): SystemStatusState => ({ + banner: { + noc: { + messages: [], + lastFetched: null, + }, + internal: { + messages: [], + lastFetched: null, + }, + hidden: useLocalStorage("coscine.system.status.banner.hidden", []), + }, + }), + + /* + -------------------------------------------------------------------------------------- + GETTERS + -------------------------------------------------------------------------------------- + Synchronous code only. + + In a component use as e.g.: + :label = "this.notificationStore.<getter_name>; + */ + getters: { + /** + * Returns an array of visible NOC (Network Operations Center) messages. + * This filters out any messages that have been marked as hidden by the user. + * + * @returns {MessageDto[]} An array of NOC messages that are not hidden. + */ + visibleNocMessages(): MessageDto[] { + return this.banner.noc.messages.filter( + (m) => m.id && !this.banner.hidden.includes(m.id), + ); + }, + + /** + * Returns an array of visible internal messages. + * This filters out any messages that have been marked as hidden by the user. + * + * @returns {MessageDto[]} An array of internal messages that are not hidden. + */ + visibleInternalMessages(): MessageDto[] { + return this.banner.internal.messages.filter( + (m) => m.id && !this.banner.hidden.includes(m.id), + ); + }, + }, + /* + -------------------------------------------------------------------------------------- + ACTIONS + -------------------------------------------------------------------------------------- + Asynchronous & Synchronous code comes here (e.g. API calls and VueX mutations). + To change a state use an action. + + In a component use as e.g.: + @click = "this.notificationStore.<action_name>(); + */ + actions: { + /** + * Retroeves the current NOC (Network Operations Center) messages. + * + * @param {boolean} [silent=false] - If true, suppresses error notifications. + * @returns {Promise<MessageDto[]>} - A promise that resolves to an array of MessageDto objects. + * @throws {AxiosError} - Throws an error if the API call fails and silent is false. + */ + async retrieveCurrentNocMessages(silent: boolean = false): Promise<void> { + const notificationStore = useNotificationStore(); + const now = new Date().toISOString(); + try { + const apiResponse = await SystemStatusApi.getNocMessages({ + startDateBefore: now, + endDateAfter: now, + }); + this.banner.noc.messages = apiResponse.data.data ?? []; + // Update last fetched time to avoid fetching messages too often + this.banner.noc.lastFetched = new Date(); + } catch (error) { + if (!silent) { + // Handle other Status Codes + notificationStore.postApiErrorNotification(error as AxiosError); + } + } + }, + + /** + * Retrieves the current internal messages. + * + * @param {boolean} [silent=false] - If true, suppresses error notifications. + * @returns {Promise<MessageDto[]>} - A promise that resolves to an array of MessageDto objects. + * @throws {AxiosError} - Throws an error if the API call fails and silent is false. + */ + async retrieveCurrentInternalMessages( + silent: boolean = false, + ): Promise<void> { + const notificationStore = useNotificationStore(); + const now = new Date().toISOString(); + try { + const apiResponse = await SystemStatusApi.getInternalMessages({ + startDateBefore: now, + endDateAfter: now, + }); + this.banner.internal.messages = apiResponse.data.data ?? []; + // Update last fetched time to avoid fetching messages too often + this.banner.internal.lastFetched = new Date(); + } catch (error) { + if (!silent) { + // Handle other Status Codes + notificationStore.postApiErrorNotification(error as AxiosError); + } + } + }, + + /** + * Helper function to determine if messages should be fetched. + * + * @param {Date | null} lastFetched - The last fetched time. + * @returns {boolean} - True if messages should be fetched, false otherwise. + */ + shouldFetchMessages(lastFetched: Date | null): boolean { + const now = new Date(); + const cacheTime = moment.duration(1, "minutes").asMilliseconds(); + // Fetch if messages are never fetched or last fetch is older than 5 minutes + return ( + !lastFetched || + moment + .duration(now.getTime() - lastFetched.getTime()) + .asMilliseconds() > cacheTime + ); + }, + + /** + * Hide a banner message by its ID. + * @param id - The ID of the message to hide. + */ + hideMessage(id: string) { + if (!this.banner.hidden.includes(id)) { + this.banner.hidden.push(id); + } + }, + }, +}); + +export default useSystemStatusStore; diff --git a/src/store/types.ts b/src/store/types.ts index 1434977c937684f4a8d85dd7d28b0e7c095a820d..d66f13ec3ad6282774b6df5753266d9be41e8717 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -1,5 +1,5 @@ import type { RemovableRef } from "@vueuse/core"; -import type { MaintenanceDto } from "@coscine/api-client/dist/types/Coscine.Api"; +import type { MessageDto } from "@coscine/api-client/dist/types/Coscine.Api"; import type { OrchestratedToast } from "bootstrap-vue-next"; export type NotificationTranslationProps = { @@ -33,11 +33,6 @@ export interface MainState { counter: number; }; locale: RemovableRef<string>; - banner: { - maintenance: MaintenanceDto; - maintenanceVisibility: RemovableRef<string>; - dateString: RemovableRef<string>; - }; theme: RemovableRef<Theme>; }; sidebarActive: RemovableRef<boolean>; @@ -52,6 +47,25 @@ export interface NotificationState { notificationQueue: NotificationToast[]; } +export interface SystemStatusState { + /* + -------------------------------------------------------------------------------------- + STATE TYPE DEFINITION + -------------------------------------------------------------------------------------- + */ + banner: { + noc: { + messages: MessageDto[]; + lastFetched: Date | null; + }; + internal: { + messages: MessageDto[]; + lastFetched: Date | null; + }; + hidden: RemovableRef<string[]>; + }; +} + export interface CoscineErrorResponse { data: { status: number; diff --git a/src/util/messageTypeMap.ts b/src/util/messageTypeMap.ts new file mode 100644 index 0000000000000000000000000000000000000000..b2817f29ac56662bd8c03ba282943f904a6503a1 --- /dev/null +++ b/src/util/messageTypeMap.ts @@ -0,0 +1,31 @@ +import type { MessageType } from "@coscine/api-client/dist/types/Coscine.Api"; +import type { BaseColorVariant } from "bootstrap-vue-next"; + +export function mapMessageTypeToBannerVariant( + messageType: MessageType | undefined, +): keyof BaseColorVariant | null | undefined { + switch (messageType) { + case "Disturbance": + case "PartialDisturbance": + case "Interruption": + return "danger"; // Red color for serious issues + + case "Maintenance": + case "PartialMaintenance": + case "LimitedOperation": + return "warning"; // Yellow color for maintenance and partial operations + + case "Change": + return "primary"; // Blue color for changes + + case "Hint": + case "Information": + return "info"; // Light blue color for hints and information + + case "Warning": + return "warning"; // Red color for warnings + + default: + return "light"; // Fallback color for unknown types + } +} diff --git a/yarn.lock b/yarn.lock index a6290fccd16c22f6f5b541e9f7e7208ceffeb7e6..b54de9cf4257a0de50162adf424404dd3f4616fa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -487,18 +487,18 @@ __metadata: languageName: node linkType: hard -"@coscine/api-client@npm:^3.10.0": - version: 3.10.0 - resolution: "@coscine/api-client@npm:3.10.0::__archiveUrl=https%3A%2F%2Fgit.rwth-aachen.de%2Fapi%2Fv4%2Fprojects%2F61847%2Fpackages%2Fnpm%2F%40coscine%2Fapi-client%2F-%2F%40coscine%2Fapi-client-3.10.0.tgz" +"@coscine/api-client@npm:3.8.0-issue-2881-messagecontroller.11": + version: 3.8.0-issue-2881-messagecontroller.11 + resolution: "@coscine/api-client@npm:3.8.0-issue-2881-messagecontroller.11::__archiveUrl=https%3A%2F%2Fgit.rwth-aachen.de%2Fapi%2Fv4%2Fprojects%2F61847%2Fpackages%2Fnpm%2F%40coscine%2Fapi-client%2F-%2F%40coscine%2Fapi-client-3.8.0-issue-2881-messagecontroller.11.tgz" dependencies: axios: "npm:^1.6.2" - checksum: 10/90a56adf3f9af15da86fc85e02da1a2327bcebfc496986f764892bbe9fe629c67fbf857a7e1f03c96da994a6f86007429952ddf55154142159cfc1c61c0cf65d + checksum: 10/0fa32f0897bf72c84af4051acb540ff506702a865e67c251a2bee45a42911f4a62ce2324eabe2192357d1ed1155d635c4a1c58f3ac7d0f5698af80e66c638aa6 languageName: node linkType: hard -"@coscine/form-generator@npm:3.5.7-dev.39": - version: 3.5.7-dev.39 - resolution: "@coscine/form-generator@npm:3.5.7-dev.39::__archiveUrl=https%3A%2F%2Fgit.rwth-aachen.de%2Fapi%2Fv4%2Fprojects%2F35944%2Fpackages%2Fnpm%2F%40coscine%2Fform-generator%2F-%2F%40coscine%2Fform-generator-3.5.7-dev.39.tgz" +"@coscine/form-generator@npm:^4.0.5": + version: 4.0.5 + resolution: "@coscine/form-generator@npm:4.0.5::__archiveUrl=https%3A%2F%2Fgit.rwth-aachen.de%2Fapi%2Fv4%2Fprojects%2F35944%2Fpackages%2Fnpm%2F%40coscine%2Fform-generator%2F-%2F%40coscine%2Fform-generator-4.0.5.tgz" dependencies: "@vueuse/core": "npm:^10.9.0" "@zazuko/prefixes": "npm:^2.2.0" @@ -511,7 +511,7 @@ __metadata: vue-i18n: "npm:^9.10.1" vue-multiselect: "npm:^3.0.0-beta.3" vue-observe-visibility: "npm:^2.0.0-alpha.1" - checksum: 10/138ac91fa4af4f561058891079adf5f7b2991e6255fbcf28ede72fc415d4574c59f717afdd54a71a5ad85475c00066bbc940cb6088d66d3fb8d42e7fbd6ac2ad + checksum: 10/8257596e180b715e222f83c925763b64c2c51e1ed6a6fa68338f4544a95e03979dc77d4c05e20b4a37fbbf30a2aa5afb56686114880a5fb42f07f35621b1a66b languageName: node linkType: hard @@ -2798,6 +2798,13 @@ __metadata: languageName: node linkType: hard +"@types/linkify-it@npm:^5": + version: 5.0.0 + resolution: "@types/linkify-it@npm:5.0.0" + checksum: 10/c3919044d4876f9d71d037e861745cd2485c95ac8c36a4fa67b132d4e60eb1d067e123cc7965c9cf5110eea351517d767f0d306af5e9147d6d0af87bc374ddcf + languageName: node + linkType: hard + "@types/lodash@npm:^4.14.201": version: 4.14.201 resolution: "@types/lodash@npm:4.14.201" @@ -2805,6 +2812,23 @@ __metadata: languageName: node linkType: hard +"@types/markdown-it@npm:^14": + version: 14.1.2 + resolution: "@types/markdown-it@npm:14.1.2" + dependencies: + "@types/linkify-it": "npm:^5" + "@types/mdurl": "npm:^2" + checksum: 10/ca2f239c8d59610b9f936fd40261a6ccf2fa1ae27a21816c031e5712542dcf9ee01e2fe29b31118df90716e11ade54e47d92a498e9b6488800e77ca8827255a2 + languageName: node + linkType: hard + +"@types/mdurl@npm:^2": + version: 2.0.0 + resolution: "@types/mdurl@npm:2.0.0" + checksum: 10/78746e96c655ceed63db06382da466fd52c7e9dc54d60b12973dfdd110cae06b9439c4b90e17bb8d4461109184b3ea9f3e9f96b3e4bf4aa9fe18b6ac35f283c8 + languageName: node + linkType: hard + "@types/minimist@npm:^1.2.0": version: 1.2.5 resolution: "@types/minimist@npm:1.2.5" @@ -9215,6 +9239,15 @@ __metadata: languageName: node linkType: hard +"linkify-it@npm:^5.0.0": + version: 5.0.0 + resolution: "linkify-it@npm:5.0.0" + dependencies: + uc.micro: "npm:^2.0.0" + checksum: 10/ef3b7609dda6ec0c0be8a7b879cea195f0d36387b0011660cd6711bba0ad82137f59b458b7e703ec74f11d88e7c1328e2ad9b855a8500c0ded67461a8c4519e6 + languageName: node + linkType: hard + "lint-staged@npm:^15.1.0": version: 15.1.0 resolution: "lint-staged@npm:15.1.0" @@ -9598,6 +9631,22 @@ __metadata: languageName: node linkType: hard +"markdown-it@npm:^14.1.0": + version: 14.1.0 + resolution: "markdown-it@npm:14.1.0" + dependencies: + argparse: "npm:^2.0.1" + entities: "npm:^4.4.0" + linkify-it: "npm:^5.0.0" + mdurl: "npm:^2.0.0" + punycode.js: "npm:^2.3.1" + uc.micro: "npm:^2.1.0" + bin: + markdown-it: bin/markdown-it.mjs + checksum: 10/f34f921be178ed0607ba9e3e27c733642be445e9bb6b1dba88da7aafe8ba1bc5d2f1c3aa8f3fc33b49a902da4e4c08c2feadfafb290b8c7dda766208bb6483a9 + languageName: node + linkType: hard + "marked-terminal@npm:^5.1.1": version: 5.2.0 resolution: "marked-terminal@npm:5.2.0" @@ -9634,6 +9683,13 @@ __metadata: languageName: node linkType: hard +"mdurl@npm:^2.0.0": + version: 2.0.0 + resolution: "mdurl@npm:2.0.0" + checksum: 10/1720349d4a53e401aa993241368e35c0ad13d816ad0b28388928c58ca9faa0cf755fa45f18ccbf64f4ce54a845a50ddce5c84e4016897b513096a68dac4b0158 + languageName: node + linkType: hard + "meow@npm:^12.0.1": version: 12.1.1 resolution: "meow@npm:12.1.1" @@ -11489,6 +11545,13 @@ __metadata: languageName: node linkType: hard +"punycode.js@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode.js@npm:2.3.1" + checksum: 10/f0e946d1edf063f9e3d30a32ca86d8ff90ed13ca40dad9c75d37510a04473340cfc98db23a905cc1e517b1e9deb0f6021dce6f422ace235c60d3c9ac47c5a16a + languageName: node + linkType: hard + "punycode@npm:^1.4.1": version: 1.4.1 resolution: "punycode@npm:1.4.1" @@ -13659,6 +13722,13 @@ __metadata: languageName: node linkType: hard +"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0": + version: 2.1.0 + resolution: "uc.micro@npm:2.1.0" + checksum: 10/37197358242eb9afe367502d4638ac8c5838b78792ab218eafe48287b0ed28aaca268ec0392cc5729f6c90266744de32c06ae938549aee041fc93b0f9672d6b2 + languageName: node + linkType: hard + "ufo@npm:^1.1.2": version: 1.1.2 resolution: "ufo@npm:1.1.2" @@ -13686,8 +13756,8 @@ __metadata: version: 0.0.0-use.local resolution: "ui@workspace:." dependencies: - "@coscine/api-client": "npm:^3.10.0" - "@coscine/form-generator": "npm:3.5.7-dev.39" + "@coscine/api-client": "npm:3.8.0-issue-2881-messagecontroller.11" + "@coscine/form-generator": "npm:^4.0.5" "@dynamic-mapper/mapper": "npm:^1.10.4" "@iconify-json/bi": "npm:^1.1.23" "@pinia/testing": "npm:^0.1.3" @@ -13700,6 +13770,7 @@ __metadata: "@semantic-release/release-notes-generator": "npm:^11.0.7" "@types/file-saver": "npm:^2.0.7" "@types/lodash": "npm:^4.14.201" + "@types/markdown-it": "npm:^14" "@types/rdf-ext": "npm:^2.5.0" "@types/rdf-validate-shacl": "npm:^0.4.7" "@types/uuid": "npm:^9.0.7" @@ -13739,6 +13810,7 @@ __metadata: jose: "npm:^5.1.1" lint-staged: "npm:^15.1.0" lodash: "npm:^4.17.21" + markdown-it: "npm:^14.1.0" moment: "npm:^2.29.4" pinia: "npm:^2.1.7" prettier: "npm:^3.1.0"