feat(web): support markdown in notification messages

This commit is contained in:
Pujit Mehrotra
2024-11-18 15:27:39 -05:00
parent 4c663dc69c
commit abcaa5aedb
4 changed files with 43 additions and 14 deletions

View File

@@ -1,39 +1,47 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
ArchiveBoxIcon, ArchiveBoxIcon,
ShieldExclamationIcon,
CheckBadgeIcon, CheckBadgeIcon,
ExclamationTriangleIcon, ExclamationTriangleIcon,
LinkIcon, LinkIcon,
ShieldExclamationIcon,
TrashIcon, TrashIcon,
} from "@heroicons/vue/24/solid"; } from '@heroicons/vue/24/solid';
import { useMutation } from "@vue/apollo-composable"; import { useMutation } from '@vue/apollo-composable';
import type { NotificationFragmentFragment } from "~/composables/gql/graphql"; import type { NotificationFragmentFragment } from '~/composables/gql/graphql';
import { NotificationType } from '~/composables/gql/graphql';
import { NotificationType } from "~/composables/gql/graphql"; import { safeParseMarkdown } from '~/helpers/markdown';
import { import {
archiveNotification as archiveMutation, archiveNotification as archiveMutation,
deleteNotification as deleteMutation, deleteNotification as deleteMutation,
} from "./graphql/notification.query"; } from './graphql/notification.query';
const props = defineProps<NotificationFragmentFragment>(); const props = defineProps<NotificationFragmentFragment>();
const descriptionMarkup = computedAsync(async () => {
try {
return await safeParseMarkdown(props.description);
} catch (e) {
return props.description;
}
}, '');
const icon = computed<{ component: Component; color: string } | null>(() => { const icon = computed<{ component: Component; color: string } | null>(() => {
switch (props.importance) { switch (props.importance) {
case "INFO": case 'INFO':
return { return {
component: CheckBadgeIcon, component: CheckBadgeIcon,
color: "text-green-500", color: 'text-green-500',
}; };
case "WARNING": case 'WARNING':
return { return {
component: ExclamationTriangleIcon, component: ExclamationTriangleIcon,
color: "text-yellow-500", color: 'text-yellow-500',
}; };
case "ALERT": case 'ALERT':
return { return {
component: ShieldExclamationIcon, component: ShieldExclamationIcon,
color: "text-red-500", color: 'text-red-500',
}; };
} }
return null; return null;
@@ -86,7 +94,7 @@ const mutationError = computed(() => {
<div <div
class="w-full flex flex-row items-center justify-between gap-2 opacity-75 group-hover/item:opacity-100 group-focus/item:opacity-100" class="w-full flex flex-row items-center justify-between gap-2 opacity-75 group-hover/item:opacity-100 group-focus/item:opacity-100"
> >
<p class="text-secondary-foreground">{{ description }}</p> <div class="text-secondary-foreground" v-html="descriptionMarkup" />
</div> </div>
<p v-if="mutationError" class="text-red-600">Error: {{ mutationError }}</p> <p v-if="mutationError" class="text-red-600">Error: {{ mutationError }}</p>

13
web/helpers/markdown.ts Normal file
View File

@@ -0,0 +1,13 @@
import DOMPurify from 'dompurify';
import { marked } from 'marked';
/**
* Parses arbitrary markdown content as sanitized html. May throw if parsing fails.
*
* @param markdownContent string of markdown content
* @returns safe, sanitized html content
*/
export async function safeParseMarkdown(markdownContent: string) {
const parsed = await marked.parse(markdownContent);
return DOMPurify.sanitize(parsed);
}

7
web/package-lock.json generated
View File

@@ -23,6 +23,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"dompurify": "^3.2.0",
"focus-trap": "^7.5.4", "focus-trap": "^7.5.4",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
@@ -8329,6 +8330,12 @@
"url": "https://github.com/fb55/domhandler?sponsor=1" "url": "https://github.com/fb55/domhandler?sponsor=1"
} }
}, },
"node_modules/dompurify": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.0.tgz",
"integrity": "sha512-AMdOzK44oFWqHEi0wpOqix/fUNY707OmoeFDnbi3Q5I8uOpy21ufUA5cDJPr0bosxrflOVD/H2DMSvuGKJGfmQ==",
"license": "(MPL-2.0 OR Apache-2.0)"
},
"node_modules/domutils": { "node_modules/domutils": {
"version": "2.8.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",

View File

@@ -69,6 +69,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"dompurify": "^3.2.0",
"focus-trap": "^7.5.4", "focus-trap": "^7.5.4",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",