Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion client/src/api/users.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { GalaxyApi } from "@/api";
import { type components, GalaxyApi } from "@/api";
import { toQuotaUsage } from "@/components/User/DiskUsage/Quota/model";
import { rethrowSimple } from "@/utils/simple-error";

export { type QuotaUsage } from "@/components/User/DiskUsage/Quota/model";

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

export async function fetchCurrentUserQuotaUsages() {
const { data, error } = await GalaxyApi().GET("/api/users/{user_id}/usage", {
params: { path: { user_id: "current" } },
Expand Down
23 changes: 22 additions & 1 deletion client/src/components/BaseComponents/GModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const props = withDefaults(
okDisabledTitle?: string;
/** 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) */
overflowVisible?: boolean;
}>(),
{
id: undefined,
Expand All @@ -57,6 +59,7 @@ const props = withDefaults(
okDisabled: false,
okDisabledTitle: undefined,
closeOnOk: true,
overflowVisible: false,
},
);

Expand Down Expand Up @@ -168,7 +171,12 @@ defineExpose({ showModal, hideModal });
<template>
<!-- This is a convenience shortcut for mouse-users to close the dialog, so disabling this warning is fine here -->
<!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions, vuejs-accessibility/click-events-have-key-events -->
<dialog :id="currentId" ref="dialog" class="g-dialog" :class="sizeClass" @click="onClickDialog">
<dialog
:id="currentId"
ref="dialog"
class="g-dialog"
:class="[sizeClass, { 'g-overflow-visible': props.overflowVisible }]"
@click="onClickDialog">
<section>
<header>
<Heading
Expand Down Expand Up @@ -240,6 +248,19 @@ defineExpose({ showModal, hideModal });
}
}

&.g-overflow-visible {
overflow: visible;

section {
overflow: visible;
}

.g-modal-content {
overflow: visible;
max-height: none;
}
}

&::backdrop {
background-color: var(--color-blue-800);
opacity: 0.33;
Expand Down
5 changes: 2 additions & 3 deletions client/src/components/Collections/ListCollectionCreator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,11 @@ async function attemptCreate() {
const returnedElements = props.fromSelection ? workingElements.value : inListElements.value;
atLeastOneElement.value = returnedElements.length > 0;

let confirmed = false;
let confirmed: boolean | null = false;
if (!atLeastOneElement.value) {
confirmed = await confirm("Are you sure you want to create a list with no datasets?", {
title: "Create an empty list",
okTitle: "Create",
okVariant: "primary",
okText: "Create",
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -474,12 +474,11 @@ async function attemptCreate() {
} else {
listIdentifiers = pairedListIdentifiers();
}
let confirmed = false;
let confirmed: boolean | null = false;
if (listIdentifiers.length == 0) {
confirmed = await confirm("Are you sure you want to create a list with no entries?", {
title: "Create an empty list",
okTitle: "Create",
okVariant: "primary",
okText: "Create",
});
if (!confirmed) {
return;
Expand Down
15 changes: 0 additions & 15 deletions client/src/components/ConfirmDialog.js

This file was deleted.

73 changes: 73 additions & 0 deletions client/src/components/ConfirmDialog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { getLocalVue } from "@tests/vitest/helpers";
import { mount } from "@vue/test-utils";
import { afterEach, beforeEach, describe, expect, it } from "vitest";

import ConfirmDialog from "./ConfirmDialog.vue";

const localVue = getLocalVue();

type ConfirmDialogVM = InstanceType<typeof ConfirmDialog>;

describe("ConfirmDialog", () => {
let wrapper: ReturnType<typeof mount>;
let vm: ConfirmDialogVM;

beforeEach(() => {
wrapper = mount(ConfirmDialog as object, { localVue, attachTo: document.body });
vm = wrapper.vm as unknown as ConfirmDialogVM;
});

afterEach(() => {
wrapper.destroy();
document.body.innerHTML = "";
});

it("resolves true when OK is clicked", async () => {
const promise = vm.confirm("Are you sure?");
await localVue.nextTick();
await wrapper.find('[data-description="confirm dialog ok"]').trigger("click");
expect(await promise).toBe(true);
});

it("resolves false when Cancel is clicked", async () => {
const promise = vm.confirm("Are you sure?");
await localVue.nextTick();
await wrapper.find('[data-description="confirm dialog cancel"]').trigger("click");
expect(await promise).toBe(false);
});

it("resolves null when dialog is dismissed (closed without choosing)", async () => {
const promise = vm.confirm("Are you sure?");
await localVue.nextTick();
wrapper.find("dialog").element.dispatchEvent(new Event("close"));
await localVue.nextTick();
expect(await promise).toBe(null);
});

it("renders message and respects custom options", async () => {
vm.confirm("Delete this item?", { title: "Confirm deletion", okText: "Delete" });
await localVue.nextTick();
const dialogText = wrapper.find("dialog").text();
expect(dialogText).toContain("Delete this item?");
expect(dialogText).toContain("Confirm deletion");
expect(dialogText).toContain("Delete");
});

it("resolves first pending promise as false on concurrent call", async () => {
const first = vm.confirm("First message");
await localVue.nextTick();
const second = vm.confirm("Second message");
expect(await first).toBe(false);
// resolve second cleanly
await wrapper.find('[data-description="confirm dialog cancel"]').trigger("click");
await second;
});

it("resolves false when abort signal fires", async () => {
const controller = new AbortController();
const promise = vm.confirm("Are you sure?", { signal: controller.signal });
await localVue.nextTick();
controller.abort();
expect(await promise).toBe(false);
});
});
84 changes: 84 additions & 0 deletions client/src/components/ConfirmDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<script setup lang="ts">
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert } from "bootstrap-vue";
import { ref } from "vue";

import { type ConfirmDialogOptions, DEFAULT_CONFIRM_OPTIONS } from "@/composables/confirmDialog";

import GButton from "@/components/BaseComponents/GButton.vue";
import GModal from "@/components/BaseComponents/GModal.vue";

const show = ref(false);
const message = ref("");
const currentOptions = ref<ConfirmDialogOptions>({ ...DEFAULT_CONFIRM_OPTIONS });

let resolveCallback: ((value: boolean | null) => void) | null = null;

function confirm(msg: string, options: ConfirmDialogOptions = {}): Promise<boolean | null> {
// Resolve any pending dialog as false before showing a new one
resolveCallback?.(false);
resolveCallback = null;

message.value = msg;
currentOptions.value = { ...DEFAULT_CONFIRM_OPTIONS, ...options };

if (!show.value) {
show.value = true;
}

return new Promise((resolve) => {
resolveCallback = resolve;

options.signal?.addEventListener("abort", () => handleResponse(false), { once: true });
});
}

function handleResponse(isOk: boolean | null) {
resolveCallback?.(isOk);
resolveCallback = null;
show.value = false;
}

defineExpose({ confirm });
</script>

<template>
<GModal
id="galaxy-confirm-dialog"
footer
:show="show"
size="small"
:title="currentOptions.title"
@close="handleResponse(null)">
<BAlert class="mb-0" data-description="confirm dialog message" variant="info" show>
{{ message }}
</BAlert>
<template v-slot:footer>
<div class="button-container">
<GButton class="confirm-button" data-description="confirm dialog cancel" @click="handleResponse(false)">
{{ currentOptions.cancelText }}
</GButton>
<GButton
class="confirm-button"
:color="currentOptions.okColor"
data-description="confirm dialog ok"
@click="handleResponse(true)">
<FontAwesomeIcon v-if="currentOptions.okIcon" :icon="currentOptions.okIcon" fixed-width />
{{ currentOptions.okText }}
</GButton>
</div>
</template>
</GModal>
</template>

<style scoped lang="scss">
.button-container {
display: flex;
width: 100%;
gap: 0.5rem;
}

.confirm-button {
flex: 1;
}
</style>
8 changes: 3 additions & 5 deletions client/src/components/Dataset/DatasetList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,10 @@ async function onBulkDelete() {
const totalSelected = selectedItemIds.value.length;

const confirmed = await confirm(`Are you sure you want to delete ${totalSelected} datasets?`, {
id: "bulk-delete-datasets-confirmation",
title: "Delete datasets",
okTitle: "Delete datasets",
okVariant: "danger",
cancelVariant: "outline-primary",
centered: true,
okText: "Delete datasets",
okColor: "red",
okIcon: faTrash,
});

if (confirmed) {
Expand Down
5 changes: 3 additions & 2 deletions client/src/components/Dataset/useDatasetTableActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ export function useDatasetTableActions(refreshList: () => Promise<void>) {
`Are you sure you want to ${purge ? "purge" : "delete"} the dataset "${item.name}"?`,
{
title: purge ? "Purge Dataset" : "Delete Dataset",
okTitle: purge ? "Purge" : "Delete",
okVariant: "danger",
okText: purge ? "Purge" : "Delete",
okColor: "red",
okIcon: purge ? faFire : faTrash,
},
);

Expand Down
Loading
Loading