Skip to content

Commit c3a4006

Browse files
committed
Harden tool request notification rendering
1 parent 3f22971 commit c3a4006

3 files changed

Lines changed: 115 additions & 83 deletions

File tree

client/src/components/Notifications/NotificationCard.test.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,26 @@ describe("Notifications categories", () => {
149149
notification,
150150
});
151151

152-
// Title should include the tool name
153-
expect(wrapper.text()).toContain(notification.content.tool_name);
152+
// Title should include the first tool name
153+
expect(wrapper.text()).toContain(notification.content.tool_names[0]);
154154

155155
// Description area should show tool request details
156156
const descriptionArea = wrapper.find(`#g-card-description-${notification.id}`);
157157
expect(descriptionArea.text()).toContain(notification.content.description);
158158
expect(descriptionArea.text()).toContain(notification.content.scientific_domain);
159159
expect(descriptionArea.text()).toContain(notification.content.requested_version);
160-
expect(descriptionArea.text()).toContain(notification.content.requester_name);
161-
expect(descriptionArea.text()).toContain(notification.content.requester_affiliation);
160+
expect(descriptionArea.text()).toContain(notification.content.requester_email);
161+
});
162+
163+
it("tool_request notification links workflow id and exposes anchor for deep-linking", async () => {
164+
const notification = generateToolRequestNotification();
165+
notification.content.workflow_id = "encoded-workflow-id-abc";
166+
167+
const wrapper = await mountComponent(NotificationCard, {
168+
notification,
169+
});
170+
171+
expect(wrapper.html()).toContain(`/workflows/run?id=${notification.content.workflow_id}`);
172+
expect(wrapper.find(`#notification-card-${notification.id}`).exists()).toBe(true);
162173
});
163174
});

client/src/components/Notifications/NotificationCard.vue

Lines changed: 99 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ const title = computed(() => {
6464
if (props.notification.category === "new_shared_item") {
6565
return `${sharedItemType.value} shared with you by ${props.notification.content.owner_name}`;
6666
} else if (props.notification.category === "tool_request") {
67-
return `Tool Request: ${props.notification.content.tool_name}`;
67+
const names = props.notification.content.tool_names;
68+
return names.length === 1 ? `Tool Request: ${names[0]}` : `Tool Request: ${names.length} tools`;
6869
} else {
6970
return props.notification.content.subject;
7071
}
@@ -153,80 +154,104 @@ function markNotificationAsSeen() {
153154
</script>
154155

155156
<template>
156-
<GCard
157-
v-if="props.notification"
158-
:id="props.notification.id"
159-
:title="title"
160-
:primary-actions="primaryActions"
161-
:title-size="'sm'"
162-
:title-icon="titleIcon"
163-
:selected="props.selected"
164-
:selectable="props.selectable"
165-
:update-time="props.notification.publication_time ?? props.notification.create_time"
166-
:update-time-icon="faClock"
167-
:update-time-title="`Sent ${props.notification.publication_time ? 'on' : 'at'}`"
168-
:content-class="[props.unreadBorder && !props.notification.seen_time ? 'border-dark unread-notification' : '']"
169-
@select="emit('select', [props.notification])">
170-
<template v-slot:description>
171-
<template v-if="props.notification.category === 'new_shared_item'">
172-
<span>The user</span>{{ " " }}<b>{{ props.notification.content.owner_name }}</b
173-
>{{ " " }}<span>shared </span>
174-
<BLink
175-
v-g-tooltip.bottom
176-
:title="`View ${props.notification.content.item_type} in new tab`"
177-
class="text-primary"
178-
:href="absPath(props.notification.content.slug)"
179-
target="_blank"
180-
@click="markNotificationAsSeen()">
181-
{{ props.notification.content.item_name }}
182-
<FontAwesomeIcon :icon="faExternalLinkAlt" fixed-width size="sm" />
183-
</BLink>
184-
<em>{{ props.notification.content.item_type }}</em
185-
>{{ " " }}<span> with you.</span>
157+
<div v-if="props.notification" :id="`notification-card-${props.notification.id}`">
158+
<GCard
159+
:id="props.notification.id"
160+
:title="title"
161+
:primary-actions="primaryActions"
162+
:title-size="'sm'"
163+
:title-icon="titleIcon"
164+
:selected="props.selected"
165+
:selectable="props.selectable"
166+
:update-time="props.notification.publication_time ?? props.notification.create_time"
167+
:update-time-icon="faClock"
168+
:update-time-title="`Sent ${props.notification.publication_time ? 'on' : 'at'}`"
169+
:content-class="[
170+
props.unreadBorder && !props.notification.seen_time ? 'border-dark unread-notification' : '',
171+
]"
172+
@select="emit('select', [props.notification])">
173+
<template v-slot:description>
174+
<template v-if="props.notification.category === 'new_shared_item'">
175+
<span>The user</span>{{ " " }}<b>{{ props.notification.content.owner_name }}</b
176+
>{{ " " }}<span>shared </span>
177+
<BLink
178+
v-g-tooltip.bottom
179+
:title="`View ${props.notification.content.item_type} in new tab`"
180+
class="text-primary"
181+
:href="absPath(props.notification.content.slug)"
182+
target="_blank"
183+
@click="markNotificationAsSeen()">
184+
{{ props.notification.content.item_name }}
185+
<FontAwesomeIcon :icon="faExternalLinkAlt" fixed-width size="sm" />
186+
</BLink>
187+
<em>{{ props.notification.content.item_type }}</em
188+
>{{ " " }}<span> with you.</span>
189+
</template>
190+
<template v-else-if="props.notification.category === 'tool_request'">
191+
<dl class="mb-0">
192+
<template v-if="props.notification.content.tool_names.length > 1">
193+
<dt>Tools</dt>
194+
<dd>
195+
<ul class="mb-0 pl-3">
196+
<li v-for="name in props.notification.content.tool_names" :key="name">
197+
{{ name }}
198+
</li>
199+
</ul>
200+
</dd>
201+
</template>
202+
<template v-else>
203+
<dt>Tool</dt>
204+
<dd>{{ props.notification.content.tool_names[0] }}</dd>
205+
</template>
206+
<template v-if="props.notification.content.description">
207+
<dt>Description</dt>
208+
<dd>{{ props.notification.content.description }}</dd>
209+
</template>
210+
<template v-if="props.notification.content.tool_url">
211+
<dt>URL</dt>
212+
<dd>
213+
<span class="text-break">{{ props.notification.content.tool_url }}</span>
214+
</dd>
215+
</template>
216+
<template v-if="props.notification.content.scientific_domain">
217+
<dt>Scientific domain</dt>
218+
<dd>{{ props.notification.content.scientific_domain }}</dd>
219+
</template>
220+
<template v-if="props.notification.content.requested_version">
221+
<dt>Version</dt>
222+
<dd>{{ props.notification.content.requested_version }}</dd>
223+
</template>
224+
<template v-if="props.notification.content.workflow_id">
225+
<dt>Workflow</dt>
226+
<dd>
227+
<RouterLink :to="`/workflows/run?id=${props.notification.content.workflow_id}`">
228+
{{ props.notification.content.workflow_id }}
229+
</RouterLink>
230+
</dd>
231+
</template>
232+
<template v-if="props.notification.content.additional_remarks">
233+
<dt>Additional remarks</dt>
234+
<dd>{{ props.notification.content.additional_remarks }}</dd>
235+
</template>
236+
<template v-if="props.notification.content.requester_email">
237+
<dt>Requested by</dt>
238+
<dd>
239+
<BLink :href="`mailto:${props.notification.content.requester_email}`">
240+
{{ props.notification.content.requester_email }}
241+
</BLink>
242+
</dd>
243+
</template>
244+
</dl>
245+
</template>
246+
<template v-else>
247+
<span
248+
class="notification-message"
249+
@click="handleMessageClick"
250+
v-html="renderMarkdown(props.notification.content.message)" />
251+
</template>
186252
</template>
187-
<template v-else-if="props.notification.category === 'tool_request'">
188-
<dl class="mb-0">
189-
<template v-if="props.notification.content.description">
190-
<dt>Description</dt>
191-
<dd>{{ props.notification.content.description }}</dd>
192-
</template>
193-
<template v-if="props.notification.content.tool_url">
194-
<dt>URL</dt>
195-
<dd>
196-
<span class="text-break">{{ props.notification.content.tool_url }}</span>
197-
</dd>
198-
</template>
199-
<template v-if="props.notification.content.scientific_domain">
200-
<dt>Scientific domain</dt>
201-
<dd>{{ props.notification.content.scientific_domain }}</dd>
202-
</template>
203-
<template v-if="props.notification.content.requested_version">
204-
<dt>Version</dt>
205-
<dd>{{ props.notification.content.requested_version }}</dd>
206-
</template>
207-
<dt>Requested by</dt>
208-
<dd>
209-
<span v-if="props.notification.content.requester_name">
210-
{{ props.notification.content.requester_name }}
211-
<span v-if="props.notification.content.requester_affiliation">
212-
({{ props.notification.content.requester_affiliation }})
213-
</span>
214-
&mdash;
215-
</span>
216-
<BLink :href="`mailto:${props.notification.content.requester_email}`">
217-
{{ props.notification.content.requester_email }}
218-
</BLink>
219-
</dd>
220-
</dl>
221-
</template>
222-
<template v-else>
223-
<span
224-
class="notification-message"
225-
@click="handleMessageClick"
226-
v-html="renderMarkdown(props.notification.content.message)" />
227-
</template>
228-
</template>
229-
</GCard>
253+
</GCard>
254+
</div>
230255
</template>
231256

232257
<style lang="scss">

client/src/components/Notifications/test-utils.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,12 @@ export function generateToolRequestNotification(): ToolRequestNotification {
7777
expiration_time: new Date(Date.now() + 86400000).toISOString(),
7878
content: {
7979
category: "tool_request",
80-
tool_name: generateRandomString(),
80+
tool_names: [generateRandomString()],
8181
tool_url: "https://github.com/example/tool",
8282
description: "A useful scientific analysis tool",
8383
scientific_domain: "Genomics",
8484
requested_version: "1.0.0",
85-
conda_available: true,
86-
test_data_available: true,
87-
requester_name: generateRandomString(),
8885
requester_email: "requester@example.com",
89-
requester_affiliation: "Example University",
9086
},
9187
seen_time: Math.random() > 0.5 ? new Date().toISOString() + 3 : undefined,
9288
deleted: false,

0 commit comments

Comments
 (0)