Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
88e2715
Implement tool request form backend and admin notifications
arash77 Mar 25, 2026
30131e6
Update frontend API and types for tool requests
arash77 Mar 25, 2026
352197d
Add tool request form and integrate into toolbox
arash77 Mar 25, 2026
dacb75d
Enhance notification system to display tool requests
arash77 Mar 25, 2026
60af45c
Add tool request notification description and update ToolRequestForm …
arash77 Mar 25, 2026
726e050
Refactor ToolRequestForm and GModal integration; enhance modal handli…
arash77 Mar 26, 2026
1ea7040
fix: use .sync modifier for show prop in Vue 2, remove redundant moda…
arash77 Mar 26, 2026
7bd2530
fix: replace FormElement/FormBoolean with BFormSelect for nullable bo…
arash77 Mar 26, 2026
fa25b19
fix: clean up tool request form code quality issues
arash77 Apr 9, 2026
9697548
Show "Request a Tool" button only when tool search returns no results
arash77 Apr 9, 2026
9beac0d
feat: add workflow missing-tools install request button
arash77 Apr 10, 2026
a599e8b
fix: fill requester name/email from authenticated user, not form payload
arash77 Apr 20, 2026
4e63801
fix: remove requester fields from form, use FormElement, enforce http…
arash77 Apr 20, 2026
e2ec148
fix: exclude tool_request category from user notification preferences
arash77 Apr 20, 2026
7462a04
fix: render tool_url as plain text in notification card
arash77 Apr 20, 2026
366df07
fix: use errorMessageAsString abstraction in workflow run services
arash77 Apr 20, 2026
933ec7e
feat: add email wiring for tool_request notification category
arash77 Apr 20, 2026
ddec3d7
docs: run config-rebuild to document enable_tool_request_form option
arash77 Apr 20, 2026
31aab77
fix: remove requester_name from WorkflowMissingToolsRequest payload a…
arash77 Apr 20, 2026
6655b32
fix: remove extra blank line in WorkflowMissingToolsRequest.vue imports
arash77 Apr 20, 2026
21bd2db
fix: mock filter composable in ToolBoxSearch tests to avoid Worker un…
arash77 Apr 20, 2026
75cc18f
fix: lazy-mount ToolRequestForm with v-if and fix empty urlError caus…
arash77 Apr 20, 2026
3626ea1
Make requester_email required and requester_name optional, filled ser…
arash77 Apr 20, 2026
5b2a508
Use notifications API for tool requests
arash77 Apr 23, 2026
bdb9d14
Simplify tool request submission UI
arash77 Apr 23, 2026
836578f
Harden tool request notification rendering
arash77 Apr 23, 2026
b0d1de6
test: mock composables/config in WorkflowRun tests
arash77 Apr 27, 2026
cd28c97
Address tool request review cleanups
arash77 May 4, 2026
102575f
fix: dispatch email channel when force_sync=True and celery is enabled
arash77 May 5, 2026
672d956
Rename "Tool Request" → "Tool Installation Request" across all layers
arash77 May 6, 2026
ce0789a
fix: apply black formatting and regenerate OpenAPI schema
arash77 May 6, 2026
baec817
fix: remove invalid workflow annotation assignments
arash77 May 6, 2026
c61a52b
chore: regenerate OpenAPI client schema after rebase
arash77 May 21, 2026
154fb0f
fix: remove stale tool_request template and fix storage_operation v-if
arash77 May 21, 2026
622621e
fix: repair notification and toolbox references
arash77 May 21, 2026
cc814b8
Address review feedback on tool installation request form
arash77 May 21, 2026
e45d058
chore: regenerate OpenAPI client schema
arash77 May 22, 2026
36316b5
Streamlines client request submission flow
arash77 Jun 1, 2026
5544762
Unifies notification dispatch behavior
arash77 Jun 1, 2026
2035d71
Keeps success feedback visible
arash77 Jun 1, 2026
9825a52
Restore admin passthrough for arbitrary notification categories
arash77 Jun 2, 2026
95d4926
Remove notification ID dependency from client
arash77 Jun 2, 2026
6134234
Update tests for void-returning submitToolInstallationRequest
arash77 Jun 2, 2026
4a5c88e
Remove redundant mockResolvedValueOnce calls from tests
arash77 Jun 2, 2026
59e07dc
Fix tool installation request test by forcing sync notification creation
arash77 Jun 8, 2026
af2b7cc
Limit force_sync to only user-allowed categories when sending notific…
arash77 Jun 9, 2026
340db68
Configure force_sync=False globally for user notifications and update…
arash77 Jun 17, 2026
7ee614b
Add opt-in autoescape to template renderer
arash77 Jul 15, 2026
72a62ed
Route tool installation request emails per recipient
arash77 Jul 15, 2026
8d00368
Prevent dismissing the tool request modal during submit
arash77 Jul 15, 2026
0377172
Adapt tool request code to upstream typing modernization
arash77 Jul 15, 2026
ddd0e9d
Fix mypy and black lint on tool request changes
arash77 Jul 15, 2026
6a1356b
Make template autoescape secure-by-default and fix XSS vectors
arash77 Jul 22, 2026
80c3cd7
Fall back to default prefs when a notification category is absent
arash77 Jul 23, 2026
c12e3f6
Add is_confirmation field to ToolInstallationRequestNotificationContent
arash77 Jul 23, 2026
d86b932
Discriminate tool request confirmation at creation, not render time
arash77 Jul 23, 2026
c74de42
Add Jinja loader and factor shared tool-request fields into partials
arash77 Jul 23, 2026
1afb1c2
Send is_confirmation in frontend tool-request payloads
arash77 Jul 23, 2026
e8c7e29
Fix double blank line in tool-request text email and harden override …
arash77 Jul 23, 2026
88e6f93
Regenerate config type stubs for enable_tool_installation_request_form
arash77 Jul 23, 2026
44c1440
Narrow submitToolInstallationRequest input type to caller-supplied fi…
arash77 Jul 23, 2026
985f254
Derive tool-request confirmation copy via model_copy, not reconstruction
arash77 Jul 23, 2026
fff4cb2
Restructure tool requests around per-item RequestedTool entries
arash77 Aug 5, 2026
84b6cb3
Keep server-stamped fields out of the tool-request create schema
arash77 Aug 5, 2026
bdeaf92
Move tool-request handling behind a per-category request handler
arash77 Aug 5, 2026
02325c3
Preserve stamped tool-request fields across the Celery round-trip
arash77 Aug 5, 2026
ab33205
Reject notifications whose content category does not match the envelope
arash77 Aug 5, 2026
76e9820
Server-control the envelope of user-submitted tool requests
arash77 Aug 5, 2026
17ed707
Sanitize and bound user-submitted tool-request fields
arash77 Aug 5, 2026
10ae14e
Render requested tools faithfully in emails and notification cards
arash77 Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 136 additions & 2 deletions client/packages/api-client/src/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19777,6 +19777,7 @@ export interface components {
| components["schemas"]["MessageNotificationContent"]
| components["schemas"]["NewSharedItemNotificationContent"]
| components["schemas"]["StorageOperationNotificationContent"]
| components["schemas"]["ToolInstallationRequestCreateContent"]
| components["schemas"]["BroadcastNotificationContent"];
/**
* Expiration time
Expand Down Expand Up @@ -19866,6 +19867,7 @@ export interface components {
| components["schemas"]["MessageNotificationContent"]
| components["schemas"]["NewSharedItemNotificationContent"]
| components["schemas"]["StorageOperationNotificationContent"]
| components["schemas"]["ToolInstallationRequestNotificationContent"]
| components["schemas"]["BroadcastNotificationContent"];
/**
* Create time
Expand Down Expand Up @@ -21065,7 +21067,7 @@ export interface components {
* displayed in the notification preferences.
* @enum {string}
*/
PersonalNotificationCategory: "message" | "new_shared_item" | "storage_operation";
PersonalNotificationCategory: "message" | "new_shared_item" | "storage_operation" | "tool_installation_request";
/** PluginAspectStatus */
PluginAspectStatus: {
/** Message */
Expand Down Expand Up @@ -21719,6 +21721,48 @@ export interface components {
| "track_config"
| "genome_data"
| "in_use_state";
/**
* RequestedTool
* @description A single requested tool in a tool installation request.
*
* This is the per-item model: each entry describes one tool. An installation
* request submits an array of these, wrapped by
* :class:`ToolInstallationRequestNotificationContent` which carries the
* request-level metadata. All fields are sanitized on validation: control
* characters are collapsed and whitespace-only values become ``None``.
*/
RequestedTool: {
/**
* Description
* @description Short description of the tool and its scientific use case.
*/
description?: string | null;
/**
* Tool name
* @description The human-readable name of the tool, if known.
*/
name?: string | null;
/**
* Requested version
* @description The version of the tool being requested, if any.
*/
requested_version?: string | null;
/**
* Scientific domain
* @description The scientific domain for the requested tool.
*/
scientific_domain?: string | null;
/**
* Tool shed ID
* @description The fully qualified tool shed repository ID (e.g. ``toolshed.g2.bx.psu.edu/repos/devteam/bwa``), if known.
*/
tool_shed_id?: string | null;
/**
* Tool URL
* @description Homepage or repository URL for the requested tool. Must be an http(s) URL.
*/
tool_url?: string | null;
};
/**
* Requirement
* @description Available types of job sources (model classes) that produce dataset collections.
Expand Down Expand Up @@ -24683,6 +24727,81 @@ export interface components {
*/
values: string;
};
/**
* ToolInstallationRequestCreateContent
* @description The client-submittable (request) shape of a tool installation request.
*
* Carries only the fields a user supplies: the requested ``tools`` and
* request-level metadata (workflow context, remarks). The two server-stamped
* fields -- ``requester_email`` and ``is_confirmation`` -- are deliberately
* absent so they cannot be set by clients and do not appear in the POST
* request schema. The service stamps them, promoting the content to a
* :class:`ToolInstallationRequestNotificationContent` for persistence.
*/
ToolInstallationRequestCreateContent: {
/**
* Additional remarks
* @description Any additional information or context for the request.
*/
additional_remarks?: string | null;
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
*/
category: "tool_installation_request";
/**
* Requested tools
* @description The tools being requested. Each entry describes a single tool.
*/
tools: components["schemas"]["RequestedTool"][];
/**
* Workflow ID
* @description Encoded ID of the workflow requiring these tools, if applicable.
*/
workflow_id?: string | null;
};
/**
* ToolInstallationRequestNotificationContent
* @description The persisted/response shape of a tool installation request.
*
* Extends the create model with the two server-stamped fields. ``requester_email``
* is derived from the authenticated submitter; ``is_confirmation`` selects the
* confirmation vs. admin-facing email template. Both are written by the service
* and never trusted from the client.
*/
ToolInstallationRequestNotificationContent: {
/**
* Additional remarks
* @description Any additional information or context for the request.
*/
additional_remarks?: string | null;
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
*/
category: "tool_installation_request";
/**
* Is confirmation
* @description True on the copy sent to the user who made the request; False on the request sent to admins.
* @default false
*/
is_confirmation: boolean;
/**
* Requester email
* @description Email address of the user who made the request.
*/
requester_email?: string | null;
/**
* Requested tools
* @description The tools being requested. Each entry describes a single tool.
*/
tools: components["schemas"]["RequestedTool"][];
/**
* Workflow ID
* @description Encoded ID of the workflow requiring these tools, if applicable.
*/
workflow_id?: string | null;
};
/** ToolLandingRequest */
ToolLandingRequest: {
/** Origin */
Expand Down Expand Up @@ -25665,6 +25784,13 @@ export interface components {
* "push": true
* },
* "enabled": true
* },
* "tool_installation_request": {
* "channels": {
* "email": true,
* "push": true
* },
* "enabled": true
* }
* }
* }
Expand Down Expand Up @@ -26081,6 +26207,13 @@ export interface components {
* "push": true
* },
* "enabled": true
* },
* "tool_installation_request": {
* "channels": {
* "email": true,
* "push": true
* },
* "enabled": true
* }
* }
* }
Expand Down Expand Up @@ -26111,7 +26244,8 @@ export interface components {
content:
| components["schemas"]["MessageNotificationContent"]
| components["schemas"]["NewSharedItemNotificationContent"]
| components["schemas"]["StorageOperationNotificationContent"];
| components["schemas"]["StorageOperationNotificationContent"]
| components["schemas"]["ToolInstallationRequestNotificationContent"];
/**
* Create time
* Format: date-time
Expand Down
47 changes: 40 additions & 7 deletions client/packages/ui/src/components/GModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { watchImmediate } from "@vueuse/core";
import { faXmark } from "font-awesome-6";
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";

import { useUid } from "../composables/uid";
import { type ComponentColor, type ComponentSize, type ComponentSizeClassList, prefix } from "./componentVariants";
Expand Down Expand Up @@ -43,6 +43,8 @@ const props = withDefaults(
okDisabled?: boolean;
/** Title to show when the Ok button is disabled */
okDisabledTitle?: string;
/** Disables all cancel/dismiss actions (Cancel button, backdrop click, Escape) */
cancelDisabled?: boolean;
/** When false, keeps the modal open on "ok" */
closeOnOk?: boolean;
/** Allows content to overflow the modal body (e.g. for dropdowns/selectors inside the modal) */
Expand All @@ -62,6 +64,7 @@ const props = withDefaults(
okColor: "blue",
okDisabled: false,
okDisabledTitle: undefined,
cancelDisabled: false,
closeOnOk: true,
overflowVisible: false,
},
Expand Down Expand Up @@ -90,6 +93,7 @@ const dialog = ref<HTMLDialogElement | null>(null);
onMounted(() => {
if (dialog.value) {
dialog.value.addEventListener("close", onClose);
dialog.value.addEventListener("cancel", onCancel);
}
if (props.show) {
showModal();
Expand All @@ -99,11 +103,16 @@ onMounted(() => {
onBeforeUnmount(() => {
if (dialog.value) {
dialog.value.removeEventListener("close", onClose);
dialog.value.removeEventListener("cancel", onCancel);
}
});

function showModal() {
dialog.value?.showModal();
if (!dialog.value || dialog.value.open) {
return;
}

dialog.value.showModal();
onOpen();
}

Expand All @@ -114,22 +123,30 @@ function hideModal(ok = false) {
emit("ok");
} else {
isOk = ok;
dialog.value?.close();
if (dialog.value?.open) {
dialog.value.close();
}
}
}

watchImmediate(
() => props.show,
() => {
if (props.show) {
async (show) => {
await nextTick();

if (show) {
showModal();
} else {
hideModal();
}
},
{ flush: "post" },
);

function onClickDialog(event: MouseEvent) {
if (props.cancelDisabled) {
return;
}
if ((event.target as HTMLElement | null)?.tagName === "DIALOG") {
const rect = dialog.value?.getBoundingClientRect();
const insideDialogX = rect && event.clientX >= rect.left && event.clientX <= rect.right;
Expand All @@ -142,6 +159,14 @@ function onClickDialog(event: MouseEvent) {
}
}

function onCancel(event: Event) {
// The native <dialog> fires "cancel" on Escape; prevent dismissal while
// cancel actions are disabled (e.g. during an in-flight submit).
if (props.cancelDisabled) {
event.preventDefault();
}
}

function onOpen() {
emit("update:show", true);
emit("open");
Expand Down Expand Up @@ -191,7 +216,13 @@ defineExpose({ showModal, hideModal });

<slot name="header"></slot>

<GButton icon-only class="g-modal-close-button" transparent size="large" @click="hideModal(false)">
<GButton
icon-only
class="g-modal-close-button"
transparent
size="large"
:disabled="cancelDisabled"
@click="hideModal(false)">
<FontAwesomeIcon fixed-width :icon="faXmark" />
</GButton>
</header>
Expand All @@ -206,7 +237,9 @@ defineExpose({ showModal, hideModal });
</div>

<div v-if="props.confirm" class="g-modal-confirm-buttons">
<GButton @click="hideModal(false)"> {{ props.cancelText ?? "Cancel" }} </GButton>
<GButton :disabled="cancelDisabled" @click="hideModal(false)">
{{ props.cancelText ?? "Cancel" }}
</GButton>
<GButton
:disabled="okDisabled"
:disabled-title="okDisabledTitle"
Expand Down
51 changes: 50 additions & 1 deletion client/src/api/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { GalaxyApi } from "@/api";
import type { components } from "@/api/schema";
import { rethrowSimple } from "@/utils/simple-error";

export type BaseUserNotification = components["schemas"]["UserNotificationResponse"];
export type UserNotificationPreferences = components["schemas"]["UserNotificationPreferences"]["preferences"];
Expand Down Expand Up @@ -33,7 +35,33 @@ export interface MessageNotificationCreateRequest extends NotificationCreateRequ
notification: MessageNotificationCreateData;
}

export type UserNotification = MessageNotification | SharedItemNotification | StorageOperationNotification;
export type ToolInstallationRequestNotificationContent =
components["schemas"]["ToolInstallationRequestNotificationContent"];

export type ToolInstallationRequestCreateContent = components["schemas"]["ToolInstallationRequestCreateContent"];

export type RequestedTool = components["schemas"]["RequestedTool"];

export interface ToolInstallationRequestNotification extends BaseUserNotification {
category: "tool_installation_request";
content: ToolInstallationRequestNotificationContent;
}

/**
* Caller-supplied fields for a tool installation request.
*
* Derived from the create-only content model, which already omits the
* server-stamped `category`/`requester_email`/`is_confirmation` fields.
* `submitToolInstallationRequest` adds the `category` discriminator (required
* by the OpenAPI union) when building the request payload.
*/
export type ToolInstallationRequestInput = Omit<ToolInstallationRequestCreateContent, "category">;

export type UserNotification =
| MessageNotification
| SharedItemNotification
| StorageOperationNotification
| ToolInstallationRequestNotification;

export type NotificationChanges = components["schemas"]["UserNotificationUpdateRequest"];

Expand All @@ -43,3 +71,24 @@ export type NotificationVariants = components["schemas"]["NotificationVariant"];

export type NewSharedItemNotificationContentItemType =
components["schemas"]["NewSharedItemNotificationContent"]["item_type"];

/** Submit a tool installation request as the authenticated user. */
export async function submitToolInstallationRequest(content: ToolInstallationRequestInput) {
const { error } = await GalaxyApi().POST("/api/notifications", {
body: {
recipients: { user_ids: [], group_ids: [], role_ids: [] },
notification: {
source: "tool_installation_request_form",
category: "tool_installation_request",
variant: "info",
content: {
...content,
category: "tool_installation_request",
},
},
},
});
if (error) {
rethrowSimple(error);
}
}
Loading
Loading