From 7cd214725fd9681d85e8bd6990c0c11af49eadb7 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 19 Nov 2024 13:09:47 -0500 Subject: [PATCH 01/29] This really stablizes these tests - they sort of runaway without this. --- client/src/components/History/HistoryView.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/src/components/History/HistoryView.test.js b/client/src/components/History/HistoryView.test.js index 85dc95106ede..b00e2647f7a9 100644 --- a/client/src/components/History/HistoryView.test.js +++ b/client/src/components/History/HistoryView.test.js @@ -17,6 +17,11 @@ jest.mock("stores/services/history.services"); const { server, http } = useServerMock(); +jest.mock("vue-router/composables", () => ({ + useRoute: jest.fn(() => ({})), + useRouter: jest.fn(() => ({})), +})); + function create_history(historyId, userId, purged = false, archived = false) { const historyName = `${userId}'s History ${historyId}`; return { From abe326a4b0997f99e39ec003282cd7485fd643e6 Mon Sep 17 00:00:00 2001 From: Laila Los <44241786+ElectronicBlueberry@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:52:28 +0100 Subject: [PATCH 02/29] fix pesky warning --- ...PersistentTaskProgressMonitorAlert.test.ts | 2 +- client/src/composables/genericTaskMonitor.ts | 18 +++++------ .../persistentProgressMonitor.test.ts | 6 ++-- .../composables/persistentProgressMonitor.ts | 14 ++++---- .../shortTermStorageMonitor.test.ts | 28 ++++++++-------- client/src/composables/taskMonitor.test.ts | 32 +++++++++---------- 6 files changed, 50 insertions(+), 50 deletions(-) diff --git a/client/src/components/Common/PersistentTaskProgressMonitorAlert.test.ts b/client/src/components/Common/PersistentTaskProgressMonitorAlert.test.ts index bc49c53c547d..266d2128a1b7 100644 --- a/client/src/components/Common/PersistentTaskProgressMonitorAlert.test.ts +++ b/client/src/components/Common/PersistentTaskProgressMonitorAlert.test.ts @@ -32,7 +32,7 @@ const FAKE_MONITOR: TaskMonitor = { isCompleted: ref(false), hasFailed: ref(false), requestHasFailed: ref(false), - status: ref(), + taskStatus: ref(""), expirationTime: FAKE_EXPIRATION_TIME, isFinalState: jest.fn(), loadStatus: jest.fn(), diff --git a/client/src/composables/genericTaskMonitor.ts b/client/src/composables/genericTaskMonitor.ts index cb0c1f7c7137..e13876dda9b1 100644 --- a/client/src/composables/genericTaskMonitor.ts +++ b/client/src/composables/genericTaskMonitor.ts @@ -42,7 +42,7 @@ export interface TaskMonitor { * The meaning of the status string is up to the monitor implementation. * In case of an error, this will be the error message. */ - status: Readonly>; + taskStatus: Readonly>; /** * Loads the status of the task from a stored value. @@ -96,19 +96,19 @@ export function useGenericMonitor(options: { let pollDelay = options.defaultPollDelay ?? DEFAULT_POLL_DELAY; const isRunning = ref(false); - const status = ref(); + const taskStatus = ref(); const requestId = ref(); const requestHasFailed = ref(false); - const isCompleted = computed(() => options.completedCondition(status.value)); - const hasFailed = computed(() => options.failedCondition(status.value)); + const isCompleted = computed(() => options.completedCondition(taskStatus.value)); + const hasFailed = computed(() => options.failedCondition(taskStatus.value)); function isFinalState(status?: string) { return options.completedCondition(status) || options.failedCondition(status); } function loadStatus(storedStatus: string) { - status.value = storedStatus; + taskStatus.value = storedStatus; } async function waitForTask(taskId: string, pollDelayInMs?: number) { @@ -122,7 +122,7 @@ export function useGenericMonitor(options: { async function fetchTaskStatus(taskId: string) { try { const result = await options.fetchStatus(taskId); - status.value = result; + taskStatus.value = result; if (isCompleted.value || hasFailed.value) { isRunning.value = false; } else { @@ -141,7 +141,7 @@ export function useGenericMonitor(options: { } function handleError(err: string) { - status.value = err.toString(); + taskStatus.value = err.toString(); requestHasFailed.value = true; isRunning.value = false; resetTimeout(); @@ -156,7 +156,7 @@ export function useGenericMonitor(options: { function resetState() { resetTimeout(); - status.value = undefined; + taskStatus.value = undefined; requestHasFailed.value = false; isRunning.value = false; } @@ -169,7 +169,7 @@ export function useGenericMonitor(options: { isCompleted: readonly(isCompleted), hasFailed: readonly(hasFailed), requestHasFailed: readonly(requestHasFailed), - status: readonly(status), + taskStatus: readonly(taskStatus), expirationTime: options.expirationTime, }; } diff --git a/client/src/composables/persistentProgressMonitor.test.ts b/client/src/composables/persistentProgressMonitor.test.ts index 2a5f9c547dd0..88e21b79e7a0 100644 --- a/client/src/composables/persistentProgressMonitor.test.ts +++ b/client/src/composables/persistentProgressMonitor.test.ts @@ -20,7 +20,7 @@ jest.mock("@vueuse/core", () => ({ function useMonitorMock(): TaskMonitor { const isRunning = ref(false); - const status = ref(); + const taskStatus = ref(); return { waitForTask: jest.fn().mockImplementation(() => { @@ -30,11 +30,11 @@ function useMonitorMock(): TaskMonitor { isCompleted: ref(false), hasFailed: ref(false), requestHasFailed: ref(false), - status, + taskStatus, expirationTime: 1000, isFinalState: jest.fn(), loadStatus(storedStatus) { - status.value = storedStatus; + taskStatus.value = storedStatus; }, }; } diff --git a/client/src/composables/persistentProgressMonitor.ts b/client/src/composables/persistentProgressMonitor.ts index fc2c9067771a..a54773d718d2 100644 --- a/client/src/composables/persistentProgressMonitor.ts +++ b/client/src/composables/persistentProgressMonitor.ts @@ -98,7 +98,7 @@ export interface MonitoringData { * The meaning of the status string is up to the monitor implementation. * In case of an error, this will be the error message. */ - status?: string; + taskStatus?: string; } /** @@ -120,7 +120,7 @@ export function usePersistentProgressTaskMonitor( isCompleted, hasFailed, requestHasFailed, - status, + taskStatus, expirationTime, } = useMonitor; @@ -152,12 +152,12 @@ export function usePersistentProgressTaskMonitor( }); watch( - status, + () => taskStatus.value, (newStatus) => { if (newStatus && currentMonitoringData.value) { currentMonitoringData.value = { ...currentMonitoringData.value, - status: newStatus, + taskStatus: newStatus, }; } }, @@ -173,10 +173,10 @@ export function usePersistentProgressTaskMonitor( throw new Error("No monitoring data provided or stored. Cannot start monitoring progress."); } - if (isFinalState(currentMonitoringData.value.status)) { + if (isFinalState(currentMonitoringData.value.taskStatus)) { // The task has already finished no need to start monitoring again. // Instead, reload the stored status to update the UI. - return loadStatus(currentMonitoringData.value.status!); + return loadStatus(currentMonitoringData.value.taskStatus!); } if (hasExpired.value) { @@ -240,7 +240,7 @@ export function usePersistentProgressTaskMonitor( * The meaning of the status string is up to the monitor implementation. * In case of an error, this will be the error message. */ - status, + status: taskStatus, /** * True if the monitoring data can expire. diff --git a/client/src/composables/shortTermStorageMonitor.test.ts b/client/src/composables/shortTermStorageMonitor.test.ts index f76e4c1eabcc..2dc3974c0668 100644 --- a/client/src/composables/shortTermStorageMonitor.test.ts +++ b/client/src/composables/shortTermStorageMonitor.test.ts @@ -31,28 +31,28 @@ describe("useShortTermStorageMonitor", () => { }); it("should indicate the task is running when it is still not ready", async () => { - const { waitForTask, isRunning, status } = useShortTermStorageMonitor(); + const { waitForTask, isRunning, taskStatus } = useShortTermStorageMonitor(); expect(isRunning.value).toBe(false); waitForTask(PENDING_TASK_ID); await flushPromises(); expect(isRunning.value).toBe(true); - expect(status.value).toBe("PENDING"); + expect(taskStatus.value).toBe("PENDING"); }); it("should indicate the task is successfully completed when the state is ready", async () => { - const { waitForTask, isRunning, isCompleted, status } = useShortTermStorageMonitor(); + const { waitForTask, isRunning, isCompleted, taskStatus } = useShortTermStorageMonitor(); expect(isCompleted.value).toBe(false); waitForTask(COMPLETED_TASK_ID); await flushPromises(); expect(isCompleted.value).toBe(true); expect(isRunning.value).toBe(false); - expect(status.value).toBe("READY"); + expect(taskStatus.value).toBe("READY"); }); it("should indicate the task status request failed when the request failed", async () => { - const { waitForTask, requestHasFailed, isRunning, isCompleted, status } = useShortTermStorageMonitor(); + const { waitForTask, requestHasFailed, isRunning, isCompleted, taskStatus } = useShortTermStorageMonitor(); expect(requestHasFailed.value).toBe(false); waitForTask(REQUEST_FAILED_TASK_ID); @@ -60,16 +60,16 @@ describe("useShortTermStorageMonitor", () => { expect(requestHasFailed.value).toBe(true); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(false); - expect(status.value).toBe("Request failed"); + expect(taskStatus.value).toBe("Request failed"); }); it("should load the status from the stored monitoring data", async () => { - const { loadStatus, isRunning, isCompleted, hasFailed, status } = useShortTermStorageMonitor(); + const { loadStatus, isRunning, isCompleted, hasFailed, taskStatus } = useShortTermStorageMonitor(); const storedStatus = "READY"; loadStatus(storedStatus); - expect(status.value).toBe(storedStatus); + expect(taskStatus.value).toBe(storedStatus); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(true); expect(hasFailed.value).toBe(false); @@ -77,26 +77,26 @@ describe("useShortTermStorageMonitor", () => { describe("isFinalState", () => { it("should indicate is final state when the task is completed", async () => { - const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, status } = + const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, taskStatus } = useShortTermStorageMonitor(); - expect(isFinalState(status.value)).toBe(false); + expect(isFinalState(taskStatus.value)).toBe(false); waitForTask(COMPLETED_TASK_ID); await flushPromises(); - expect(isFinalState(status.value)).toBe(true); + expect(isFinalState(taskStatus.value)).toBe(true); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(true); expect(hasFailed.value).toBe(false); }); it("should indicate is final state when the task has failed", async () => { - const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, status } = + const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, taskStatus } = useShortTermStorageMonitor(); - expect(isFinalState(status.value)).toBe(false); + expect(isFinalState(taskStatus.value)).toBe(false); waitForTask(REQUEST_FAILED_TASK_ID); await flushPromises(); - expect(isFinalState(status.value)).toBe(true); + expect(isFinalState(taskStatus.value)).toBe(true); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(false); expect(hasFailed.value).toBe(true); diff --git a/client/src/composables/taskMonitor.test.ts b/client/src/composables/taskMonitor.test.ts index b62dd22bf93e..60702963e983 100644 --- a/client/src/composables/taskMonitor.test.ts +++ b/client/src/composables/taskMonitor.test.ts @@ -35,39 +35,39 @@ describe("useTaskMonitor", () => { }); it("should indicate the task is running when it is still pending", async () => { - const { waitForTask, isRunning, status } = useTaskMonitor(); + const { waitForTask, isRunning, taskStatus } = useTaskMonitor(); expect(isRunning.value).toBe(false); waitForTask(PENDING_TASK_ID); await flushPromises(); expect(isRunning.value).toBe(true); - expect(status.value).toBe("PENDING"); + expect(taskStatus.value).toBe("PENDING"); }); it("should indicate the task is successfully completed when the state is SUCCESS", async () => { - const { waitForTask, isRunning, isCompleted, status } = useTaskMonitor(); + const { waitForTask, isRunning, isCompleted, taskStatus } = useTaskMonitor(); expect(isCompleted.value).toBe(false); waitForTask(COMPLETED_TASK_ID); await flushPromises(); expect(isCompleted.value).toBe(true); expect(isRunning.value).toBe(false); - expect(status.value).toBe("SUCCESS"); + expect(taskStatus.value).toBe("SUCCESS"); }); it("should indicate the task has failed when the state is FAILED", async () => { - const { waitForTask, isRunning, hasFailed, status } = useTaskMonitor(); + const { waitForTask, isRunning, hasFailed, taskStatus } = useTaskMonitor(); expect(hasFailed.value).toBe(false); waitForTask(FAILED_TASK_ID); await flushPromises(); expect(hasFailed.value).toBe(true); expect(isRunning.value).toBe(false); - expect(status.value).toBe("FAILURE"); + expect(taskStatus.value).toBe("FAILURE"); }); it("should indicate the task status request failed when the request failed", async () => { - const { waitForTask, requestHasFailed, isRunning, isCompleted, status } = useTaskMonitor(); + const { waitForTask, requestHasFailed, isRunning, isCompleted, taskStatus } = useTaskMonitor(); expect(requestHasFailed.value).toBe(false); waitForTask(REQUEST_FAILED_TASK_ID); @@ -75,16 +75,16 @@ describe("useTaskMonitor", () => { expect(requestHasFailed.value).toBe(true); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(false); - expect(status.value).toBe("Request failed"); + expect(taskStatus.value).toBe("Request failed"); }); it("should load the status from the stored monitoring data", async () => { - const { loadStatus, isRunning, isCompleted, hasFailed, status } = useTaskMonitor(); + const { loadStatus, isRunning, isCompleted, hasFailed, taskStatus } = useTaskMonitor(); const storedStatus = "SUCCESS"; loadStatus(storedStatus); - expect(status.value).toBe(storedStatus); + expect(taskStatus.value).toBe(storedStatus); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(true); expect(hasFailed.value).toBe(false); @@ -92,24 +92,24 @@ describe("useTaskMonitor", () => { describe("isFinalState", () => { it("should indicate is final state when the task is completed", async () => { - const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, status } = useTaskMonitor(); + const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, taskStatus } = useTaskMonitor(); - expect(isFinalState(status.value)).toBe(false); + expect(isFinalState(taskStatus.value)).toBe(false); waitForTask(COMPLETED_TASK_ID); await flushPromises(); - expect(isFinalState(status.value)).toBe(true); + expect(isFinalState(taskStatus.value)).toBe(true); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(true); expect(hasFailed.value).toBe(false); }); it("should indicate is final state when the task has failed", async () => { - const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, status } = useTaskMonitor(); + const { waitForTask, isFinalState, isRunning, isCompleted, hasFailed, taskStatus } = useTaskMonitor(); - expect(isFinalState(status.value)).toBe(false); + expect(isFinalState(taskStatus.value)).toBe(false); waitForTask(FAILED_TASK_ID); await flushPromises(); - expect(isFinalState(status.value)).toBe(true); + expect(isFinalState(taskStatus.value)).toBe(true); expect(isRunning.value).toBe(false); expect(isCompleted.value).toBe(false); expect(hasFailed.value).toBe(true); From d44f03758471a3eca0a6227c3719d1a4ada9942f Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Thu, 19 Sep 2024 15:48:33 -0500 Subject: [PATCH 03/29] add button for creating a list from run form field This is an initial/draft implementation. Some of the next steps are: - By default, instead of the modals treating the items as selections from the current history, automatically filter items valid for the list (e.g.: for a list with csv elements, filter out csvs from the history in this list). - In case nothing can be auto paried for `list:paired`, do not attempt to auto pair by default and simply show all items. - In case the current history is empty and to make it clearer in general, allow history to be switched from within the modal? - Allow files to be uploaded (and dropped) directly to either the form field or within the list builder once it is opened. One thing I have not planned for yet is the rule builder. I can see that for `list` and `list:paired`, we get that from the `props.collectionTypes` in `FormData`. But when would we use the rule builder instead? Fixes https://github.com/galaxyproject/galaxy/issues/18704 --- .../Form/Elements/FormData/FormData.vue | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/client/src/components/Form/Elements/FormData/FormData.vue b/client/src/components/Form/Elements/FormData/FormData.vue index 6a97be9c2ec5..d42c024dd651 100644 --- a/client/src/components/Form/Elements/FormData/FormData.vue +++ b/client/src/components/Form/Elements/FormData/FormData.vue @@ -8,9 +8,12 @@ import { computed, onMounted, type Ref, ref, watch } from "vue"; import { isDatasetElement, isDCE } from "@/api"; import { getGalaxyInstance } from "@/app"; +import { buildCollectionModal } from "@/components/History/adapters/buildCollectionModal"; import { useDatatypesMapper } from "@/composables/datatypesMapper"; import { useUid } from "@/composables/utils/uid"; import { type EventData, useEventStore } from "@/stores/eventStore"; +import { useHistoryItemsStore } from "@/stores/historyItemsStore"; +import { useHistoryStore } from "@/stores/historyStore"; import { orList } from "@/utils/strings"; import type { DataOption } from "./types"; @@ -471,6 +474,21 @@ function canAcceptSrc(historyContentType: "dataset" | "dataset_collection", coll } } +const historyStore = useHistoryStore(); +const historyItemsStore = useHistoryItemsStore(); +// Build a new collection +async function buildNewCollection(collectionType: string) { + if (!historyStore.currentHistoryId) { + return; + } + const modalResult = await buildCollectionModal( + collectionType, + historyItemsStore.getHistoryItems(historyStore.currentHistoryId, ""), + historyStore.currentHistoryId + ); + // TODO: Implement handling `modalResult` as input for the field +} + // Drag/Drop event handlers function onDragEnter(evt: MouseEvent) { const eventData = eventStore.getDragData(); @@ -632,7 +650,20 @@ const noOptionsWarningMessage = computed(() => { :placeholder="`Select a ${placeholder}`"> From 6d903746d123b1bb911be53ab3bbf56397a377ad Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Thu, 26 Sep 2024 18:13:53 -0500 Subject: [PATCH 04/29] fully implement `list` collection creator in `FormData` This allows a collection type `list` to be created via the collection creater from the workflow/tool form directly. It tracks the current history changes via the new `useHistoryItemsForType` composable. It utilises the `FormSelectMany` component to easily move items between selected and unselected for list columns. The items in the list creator can be filtered for extension, parent datatype or all items in the history, based on whether the form field required a certain extension(s) as input for the list. --- client/src/api/histories.ts | 3 + .../Collections/ListCollectionCreator.vue | 338 ++++++++++++++---- .../Collections/ListCollectionCreatorModal.js | 16 +- .../ListDatasetCollectionElementView.vue | 43 ++- .../Collections/PairCollectionCreator.vue | 1 + .../PairedListCollectionCreator.vue | 1 + .../Collections/common/ClickToEdit.vue | 52 +-- .../Collections/common/CollectionCreator.vue | 163 +++++---- .../src/components/Common/ButtonSpinner.vue | 17 +- client/src/components/Datatypes/model.ts | 10 + .../Form/Elements/FormData/FormData.vue | 93 +++-- .../components/Form/Elements/FormSelect.vue | 3 +- .../FormSelectMany/FormSelectMany.vue | 8 +- client/src/components/Help/terms.yml | 11 + .../HistoryOperations/SelectionOperations.vue | 4 +- .../History/adapters/HistoryPanelProxy.js | 2 +- ...ectionModal.js => buildCollectionModal.ts} | 43 ++- .../src/composables/useHistoryItemsForType.ts | 90 +++++ 18 files changed, 665 insertions(+), 233 deletions(-) create mode 100644 client/src/api/histories.ts rename client/src/components/History/adapters/{buildCollectionModal.js => buildCollectionModal.ts} (55%) create mode 100644 client/src/composables/useHistoryItemsForType.ts diff --git a/client/src/api/histories.ts b/client/src/api/histories.ts new file mode 100644 index 000000000000..ed10bb8efb1f --- /dev/null +++ b/client/src/api/histories.ts @@ -0,0 +1,3 @@ +import { type components } from "@/api"; + +export type HistoryContentsResult = components["schemas"]["HistoryContentsResult"]; diff --git a/client/src/components/Collections/ListCollectionCreator.vue b/client/src/components/Collections/ListCollectionCreator.vue index 5798187943f2..e5590be15fd1 100644 --- a/client/src/components/Collections/ListCollectionCreator.vue +++ b/client/src/components/Collections/ListCollectionCreator.vue @@ -1,21 +1,29 @@ diff --git a/client/src/components/Collections/PairCollectionCreator.vue b/client/src/components/Collections/PairCollectionCreator.vue index 2520a3a022f8..e0641fe50f34 100644 --- a/client/src/components/Collections/PairCollectionCreator.vue +++ b/client/src/components/Collections/PairCollectionCreator.vue @@ -298,6 +298,7 @@ onMounted(() => { :oncancel="oncancel" :hide-source-items="hideSourceItems" :suggested-name="initialSuggestedName" + :extensions-toggle="removeExtensions" @onUpdateHideSourceItems="onUpdateHideSourceItems" @clicked-create="clickedCreate" @remove-extensions-toggle="removeExtensionsToggle"> diff --git a/client/src/components/Collections/PairedListCollectionCreator.vue b/client/src/components/Collections/PairedListCollectionCreator.vue index c699c0de7077..d18767c32572 100644 --- a/client/src/components/Collections/PairedListCollectionCreator.vue +++ b/client/src/components/Collections/PairedListCollectionCreator.vue @@ -102,6 +102,7 @@ :oncancel="oncancel" :hide-source-items="hideSourceItems" :render-extensions-toggle="true" + :extensions-toggle="removeExtensions" @onUpdateHideSourceItems="onUpdateHideSourceItems" @clicked-create="clickedCreate" @remove-extensions-toggle="removeExtensionsToggle"> diff --git a/client/src/components/Collections/common/ClickToEdit.vue b/client/src/components/Collections/common/ClickToEdit.vue index da60f01880d9..84c413d7f1f1 100644 --- a/client/src/components/Collections/common/ClickToEdit.vue +++ b/client/src/components/Collections/common/ClickToEdit.vue @@ -1,4 +1,7 @@ diff --git a/client/src/components/Collections/common/CollectionCreator.vue b/client/src/components/Collections/common/CollectionCreator.vue index 8ef14660d999..e257df302654 100644 --- a/client/src/components/Collections/common/CollectionCreator.vue +++ b/client/src/components/Collections/common/CollectionCreator.vue @@ -1,28 +1,38 @@ diff --git a/client/src/components/Datatypes/model.ts b/client/src/components/Datatypes/model.ts index 77826b73fc13..591f8f2814b0 100644 --- a/client/src/components/Datatypes/model.ts +++ b/client/src/components/Datatypes/model.ts @@ -39,4 +39,14 @@ export class DatatypesMapperModel { isSubTypeOfAny(child: string, parents: DatatypesCombinedMap["datatypes"]): boolean { return parents.some((parent) => this.isSubType(child, parent)); } + + /** For classes like `galaxy.datatypes.{parent}.{extension}`, get the extension's parent */ + getParentDatatype(extension: string) { + const fullClassName = this.datatypesMapping.ext_to_class_name[extension]; + return fullClassName?.split(".")[2]; + } + + isSubClassOfAny(child: string, parents: DatatypesCombinedMap["datatypes"]): boolean { + return parents.every((parent) => this.getParentDatatype(parent) === this.getParentDatatype(child)); + } } diff --git a/client/src/components/Form/Elements/FormData/FormData.vue b/client/src/components/Form/Elements/FormData/FormData.vue index d42c024dd651..e87a104c7eb2 100644 --- a/client/src/components/Form/Elements/FormData/FormData.vue +++ b/client/src/components/Form/Elements/FormData/FormData.vue @@ -1,7 +1,7 @@ diff --git a/client/src/components/Form/Elements/FormSelectMany/FormSelectMany.vue b/client/src/components/Form/Elements/FormSelectMany/FormSelectMany.vue index 95581fbc3972..58ecf975cf72 100644 --- a/client/src/components/Form/Elements/FormSelectMany/FormSelectMany.vue +++ b/client/src/components/Form/Elements/FormSelectMany/FormSelectMany.vue @@ -364,7 +364,9 @@ const selectedCount = computed(() => { :class="{ highlighted: highlightUnselected.highlightedIndexes.includes(i) }" @click="(e) => selectOption(e, i)" @keydown="(e) => optionOnKey('unselected', e, i)"> - {{ option.label }} + + {{ option.label }} + @@ -396,7 +398,9 @@ const selectedCount = computed(() => { :class="{ highlighted: highlightSelected.highlightedIndexes.includes(i) }" @click="(e) => deselectOption(e, i)" @keydown="(e) => optionOnKey('selected', e, i)"> - {{ option.label }} + + {{ option.label }} + diff --git a/client/src/components/Help/terms.yml b/client/src/components/Help/terms.yml index 536d08f3dee1..bc1caa1ccb22 100644 --- a/client/src/components/Help/terms.yml +++ b/client/src/components/Help/terms.yml @@ -59,6 +59,17 @@ galaxy: These lists will be gathered together in a nested list structured (collection type ``list:list``) where the outer element count and structure matches that of the input and the inner list for each of those is just the outputs of the tool for the corresponding element of the input. + collectionBuilder: + hideOriginalElements: | + Toggling this on means that the original history items that will become a part of the collection + will be hidden from the history panel (they will still be searchable via the 'visible: false' filter). + filterForDatatypes: | + This option allows you to filter items shown here by datatype because this input requires specific + extension(s). By default, the toggle is at "Extension" and the list is filtered for the explicit extension(s) + required by the input; *if datasets with that extension(s) are available*. If you toggle to "Datatype", + the list will be filtered for the "parent" datatype of the required extension (for implicit conversion). + If you toggle to "All", the list will show all items regardless of datatype. + jobs: states: # upload, waiting, failed, paused, deleting, deleted, stop, stopped, skipped. diff --git a/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue b/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue index 313846e57546..e9f6591723ae 100644 --- a/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue +++ b/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue @@ -392,7 +392,9 @@ export default { if (contents === undefined) { contents = this.contentSelection; } - const modalResult = await buildCollectionModal(collectionType, contents, this.history.id); + const modalResult = await buildCollectionModal(collectionType, contents, this.history.id, { + fromSelection: true, + }); await createDatasetCollection(this.history, modalResult); // have to hide the source items if that was requested diff --git a/client/src/components/History/adapters/HistoryPanelProxy.js b/client/src/components/History/adapters/HistoryPanelProxy.js index 28b95b38627f..e79466d45052 100644 --- a/client/src/components/History/adapters/HistoryPanelProxy.js +++ b/client/src/components/History/adapters/HistoryPanelProxy.js @@ -56,7 +56,7 @@ export class HistoryPanelProxy { selectionContent.set(obj.id, obj); }); } - const modalResult = await buildCollectionModal(collectionType, selectionContent, historyId, fromRulesInput); + const modalResult = await buildCollectionModal(collectionType, selectionContent, historyId, { fromRulesInput }); if (modalResult) { console.debug("Submitting collection build request.", modalResult); await createDatasetCollection({ id: historyId }, modalResult); diff --git a/client/src/components/History/adapters/buildCollectionModal.js b/client/src/components/History/adapters/buildCollectionModal.ts similarity index 55% rename from client/src/components/History/adapters/buildCollectionModal.js rename to client/src/components/History/adapters/buildCollectionModal.ts index f5b5c030ea27..06be87275012 100644 --- a/client/src/components/History/adapters/buildCollectionModal.js +++ b/client/src/components/History/adapters/buildCollectionModal.ts @@ -8,15 +8,33 @@ * deprecated jquery Deferred object. */ -import LIST_COLLECTION_CREATOR from "components/Collections/ListCollectionCreatorModal"; -import PAIR_COLLECTION_CREATOR from "components/Collections/PairCollectionCreatorModal"; -import LIST_OF_PAIRS_COLLECTION_CREATOR from "components/Collections/PairedListCollectionCreatorModal"; -import RULE_BASED_COLLECTION_CREATOR from "components/Collections/RuleBasedCollectionCreatorModal"; import jQuery from "jquery"; +import type { HistoryItemSummary } from "@/api"; +import LIST_COLLECTION_CREATOR from "@/components/Collections/ListCollectionCreatorModal"; +import PAIR_COLLECTION_CREATOR from "@/components/Collections/PairCollectionCreatorModal"; +import LIST_OF_PAIRS_COLLECTION_CREATOR from "@/components/Collections/PairedListCollectionCreatorModal"; +import RULE_BASED_COLLECTION_CREATOR from "@/components/Collections/RuleBasedCollectionCreatorModal"; + +export type CollectionType = "list" | "paired" | "list:paired" | "rules"; +export interface BuildCollectionOptions { + fromRulesInput?: boolean; + fromSelection?: boolean; + extensions?: string[]; + title?: string; + defaultHideSourceItems?: boolean; + historyId?: string; +} + // stand-in for buildCollection from history-view-edit.js -export async function buildCollectionModal(collectionType, selectedContent, historyId, fromRulesInput = false) { +export async function buildCollectionModal( + collectionType: CollectionType, + selectedContent: HistoryItemSummary[], + historyId: string, + options: BuildCollectionOptions = {} +) { // select legacy function + const { fromRulesInput = false } = options; let createFunc; if (collectionType == "list") { createFunc = LIST_COLLECTION_CREATOR.createListCollection; @@ -33,19 +51,25 @@ export async function buildCollectionModal(collectionType, selectedContent, hist if (fromRulesInput) { return await createFunc(selectedContent); } else { - const fakeBackboneContent = createBackboneContent(historyId, selectedContent); + const fakeBackboneContent = createBackboneContent(historyId, selectedContent, options); return await createFunc(fakeBackboneContent); } } -const createBackboneContent = (historyId, selection) => { +const createBackboneContent = (historyId: string, selection: HistoryItemSummary[], options: BuildCollectionOptions) => { const selectionJson = Array.from(selection.values()); return { historyId, toJSON: () => selectionJson, // result must be a $.Deferred object instead of a promise because // that's the kind of deprecated data format that backbone likes to use. - createHDCA(element_identifiers, collection_type, name, hide_source_items, options = {}) { + createHDCA( + element_identifiers: any, + collection_type: CollectionType, + name: string, + hide_source_items: boolean, + options = {} + ) { const def = jQuery.Deferred(); return def.resolve(null, { collection_type, @@ -55,5 +79,8 @@ const createBackboneContent = (historyId, selection) => { options, }); }, + fromSelection: options.fromSelection, + extensions: options.extensions, + defaultHideSourceItems: options.defaultHideSourceItems === undefined ? true : options.defaultHideSourceItems, }; }; diff --git a/client/src/composables/useHistoryItemsForType.ts b/client/src/composables/useHistoryItemsForType.ts new file mode 100644 index 000000000000..765e411637da --- /dev/null +++ b/client/src/composables/useHistoryItemsForType.ts @@ -0,0 +1,90 @@ +import { computed, type Ref, ref, watch } from "vue"; + +import { GalaxyApi, type HistoryItemSummary } from "@/api"; +import { filtersToQueryValues } from "@/components/History/model/queries"; +import { useHistoryStore } from "@/stores/historyStore"; +import { errorMessageAsString } from "@/utils/simple-error"; + +const DEFAULT_FILTERS = { visible: true, deleted: false }; + +let singletonInstance: { + isFetchingItems: Ref; + errorMessage: Ref; + historyItems: Ref; +} | null = null; + +/** + * Creates a composable that fetches the given type of items from a history reactively. + * @param historyId The history ID to fetch items for. (TODO: make this a required parameter; only `string` allowed) + * @param type The type of items to fetch. Default is "dataset". + * @param filters Filters to apply to the items. + * @returns An object containing reactive properties for the fetch status and the fetched items. + */ +export function useHistoryItemsForType( + historyId: Ref, + type: "dataset" | "dataset_collection" = "dataset", + filters = DEFAULT_FILTERS +) { + if (singletonInstance) { + return singletonInstance; + } + const isFetchingItems = ref(false); + const errorMessage = ref(null); + const historyItems = ref([]); + const counter = ref(0); + + const historyStore = useHistoryStore(); + + const historyUpdateTime = computed( + () => historyId.value && historyStore.getHistoryById(historyId.value)?.update_time + ); + + // Fetch items when history ID or update time changes + watch( + () => ({ + time: historyUpdateTime.value, + id: historyId.value, + }), + async (newValues, oldValues) => { + if (newValues.time !== oldValues?.time || newValues.id !== oldValues?.id) { + await fetchItems(); + counter.value++; + } + }, + { immediate: true } + ); + + async function fetchItems() { + if (!historyId.value) { + errorMessage.value = "No history ID provided"; + return; + } + if (isFetchingItems.value) { + return; + } + const filterQuery = filtersToQueryValues(filters); + isFetchingItems.value = true; + const { data, error } = await GalaxyApi().GET("/api/histories/{history_id}/contents/{type}s", { + params: { + path: { history_id: historyId.value, type: type }, + query: { ...filterQuery, v: "dev" }, + }, + }); + isFetchingItems.value = false; + if (error) { + errorMessage.value = errorMessageAsString(error); + console.error("Error fetching history items", errorMessage.value); + } else { + historyItems.value = data as HistoryItemSummary[]; + errorMessage.value = null; + } + } + + singletonInstance = { + isFetchingItems, + errorMessage, + historyItems, + }; + + return singletonInstance; +} From 4b2027667eb39e3e8099b7f0e84a67c7cb82988e Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Thu, 26 Sep 2024 18:17:50 -0500 Subject: [PATCH 05/29] add a `maintain-selection-order` prop to `FormSelectMany` This keeps the order in which the user adds items to the selection evident in the selected column. --- .../components/Collections/ListCollectionCreator.vue | 1 + .../Form/Elements/FormSelectMany/FormSelectMany.vue | 6 ++++++ .../Elements/FormSelectMany/worker/selectMany.d.ts | 1 + .../Form/Elements/FormSelectMany/worker/selectMany.js | 2 ++ .../FormSelectMany/worker/selectMany.worker.js | 2 ++ .../Elements/FormSelectMany/worker/selectManyMain.ts | 10 ++++++++++ 6 files changed, 22 insertions(+) diff --git a/client/src/components/Collections/ListCollectionCreator.vue b/client/src/components/Collections/ListCollectionCreator.vue index e5590be15fd1..f26282248a2f 100644 --- a/client/src/components/Collections/ListCollectionCreator.vue +++ b/client/src/components/Collections/ListCollectionCreator.vue @@ -648,6 +648,7 @@ function renameElement(element: any, name: string) { From f53170f430078096cad67668c2c1beb8065e4bed Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Tue, 1 Oct 2024 11:48:02 -0500 Subject: [PATCH 07/29] `ListCollectionCreator`: add more types --- .../Collections/ListCollectionCreator.vue | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/client/src/components/Collections/ListCollectionCreator.vue b/client/src/components/Collections/ListCollectionCreator.vue index f26282248a2f..b3d5e398091c 100644 --- a/client/src/components/Collections/ListCollectionCreator.vue +++ b/client/src/components/Collections/ListCollectionCreator.vue @@ -26,10 +26,10 @@ const DEFAULT_DATATYPE_FILTER_OPTIONS = [ type DatatypeToggle = "all" | "datatype" | "ext" | undefined; interface Props { - initialElements: Array; + initialElements: HistoryItemSummary[]; oncancel: () => void; oncreate: () => void; - creationFn: (workingElements: any, collectionName: string, hideSourceItems: boolean) => any; + creationFn: (workingElements: HDASummary[], collectionName: string, hideSourceItems: boolean) => any; defaultHideSourceItems?: boolean; fromSelection?: boolean; extensions?: string[]; @@ -38,7 +38,7 @@ interface Props { const props = defineProps(); const emit = defineEmits<{ - (e: "clicked-create", workingElements: any, collectionName: string, hideSourceItems: boolean): void; + (e: "clicked-create", workingElements: HDASummary[], collectionName: string, hideSourceItems: boolean): void; }>(); const state = ref("build"); @@ -75,7 +75,7 @@ const allElementsAreInvalid = computed(() => { }); /** If not `fromSelection`, the list of elements that will become the collection */ -const inListElements = ref([]); +const inListElements = ref([]); // variables for datatype mapping and then filtering const datatypesMapperStore = useDatatypesMapperStore(); @@ -121,21 +121,21 @@ function _elementsSetUp() { // reverse the order of the elements to emulate what we have in the history panel workingElements.value.reverse(); - _ensureElementIds(); + // _ensureElementIds(); _validateElements(); _mangleDuplicateNames(); } -/** add ids to dataset objs in initial list if none */ -function _ensureElementIds() { - workingElements.value.forEach((element) => { - if (!Object.prototype.hasOwnProperty.call(element, "id")) { - console.warn("Element missing id", element); - } - }); +// TODO: not sure if this is needed +// function _ensureElementIds() { +// workingElements.value.forEach((element) => { +// if (!Object.prototype.hasOwnProperty.call(element, "id")) { +// console.warn("Element missing id", element); +// } +// }); - return workingElements.value; -} +// return workingElements.value; +// } // /** separate working list into valid and invalid elements for this collection */ function _validateElements() { From 35ff19da970a9e2fb7a33116e121c708ae01a3f4 Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Tue, 1 Oct 2024 11:49:13 -0500 Subject: [PATCH 08/29] modernize/refactor `PairedListCollectionCreator` for input forms - Converted the file(s) to composition API and typescript - Improved styling of the modal and its components Still need to add `extensions` handling for cases where a certain extension is required for the collection. --- .../Collections/PairedElementView.vue | 49 +- .../PairedListCollectionCreator.vue | 1786 +++++++++-------- .../PairedListCollectionCreatorModal.js | 16 +- .../UnpairedDatasetElementView.vue | 63 +- client/src/utils/natural-sort.js | 38 - client/src/utils/naturalSort.ts | 45 + 6 files changed, 1127 insertions(+), 870 deletions(-) delete mode 100644 client/src/utils/natural-sort.js create mode 100644 client/src/utils/naturalSort.ts diff --git a/client/src/components/Collections/PairedElementView.vue b/client/src/components/Collections/PairedElementView.vue index 364832a22db2..42cd1d9e8af0 100644 --- a/client/src/components/Collections/PairedElementView.vue +++ b/client/src/components/Collections/PairedElementView.vue @@ -1,69 +1,58 @@ diff --git a/client/src/components/Collections/PairedListCollectionCreator.vue b/client/src/components/Collections/PairedListCollectionCreator.vue index d18767c32572..6e9ada15396d 100644 --- a/client/src/components/Collections/PairedListCollectionCreator.vue +++ b/client/src/components/Collections/PairedListCollectionCreator.vue @@ -1,115 +1,898 @@ + + - diff --git a/client/src/utils/natural-sort.js b/client/src/utils/natural-sort.js deleted file mode 100644 index c620f169163f..000000000000 --- a/client/src/utils/natural-sort.js +++ /dev/null @@ -1,38 +0,0 @@ -// Alphanumeric/natural sort fn -function naturalSort(a, b) { - // setup temp-scope variables for comparison evauluation - var re = /(-?[0-9.]+)/g; - - var x = a.toString().toLowerCase() || ""; - var y = b.toString().toLowerCase() || ""; - var nC = String.fromCharCode(0); - var xN = x.replace(re, `${nC}$1${nC}`).split(nC); - var yN = y.replace(re, `${nC}$1${nC}`).split(nC); - var xD = new Date(x).getTime(); - var yD = xD ? new Date(y).getTime() : null; - // natural sorting of dates - if (yD) { - if (xD < yD) { - return -1; - } else if (xD > yD) { - return 1; - } - } - - // natural sorting through split numeric strings and default strings - var oFxNcL; - - var oFyNcL; - for (var cLoc = 0, numS = Math.max(xN.length, yN.length); cLoc < numS; cLoc++) { - oFxNcL = parseFloat(xN[cLoc]) || xN[cLoc]; - oFyNcL = parseFloat(yN[cLoc]) || yN[cLoc]; - if (oFxNcL < oFyNcL) { - return -1; - } else if (oFxNcL > oFyNcL) { - return 1; - } - } - return 0; -} - -export default naturalSort; diff --git a/client/src/utils/naturalSort.ts b/client/src/utils/naturalSort.ts new file mode 100644 index 000000000000..91a047b90dd0 --- /dev/null +++ b/client/src/utils/naturalSort.ts @@ -0,0 +1,45 @@ +// Alphanumeric/natural sort fn +export function naturalSort(a = "", b = ""): number { + // setup temp-scope variables for comparison evauluation + const re = /(-?[0-9.]+)/g; + + const x = a.toString().toLowerCase() || ""; + const y = b.toString().toLowerCase() || ""; + const nC = String.fromCharCode(0); + const xN = x.replace(re, `${nC}$1${nC}`).split(nC); + const yN = y.replace(re, `${nC}$1${nC}`).split(nC); + const xD = new Date(x).getTime(); + const yD = xD ? new Date(y).getTime() : null; + // natural sorting of dates + if (yD) { + if (xD < yD) { + return -1; + } else if (xD > yD) { + return 1; + } + } + + // natural sorting through split numeric strings and default strings + let oFxNcL; + + let oFyNcL; + for (let cLoc = 0, numS = Math.max(xN.length, yN.length); cLoc < numS; cLoc++) { + oFxNcL = parseFloat(xN[cLoc] || "") || xN[cLoc]; + oFyNcL = parseFloat(yN[cLoc] || "") || yN[cLoc]; + + // Check if either part is undefined + if (oFxNcL === undefined) { + return -1; + } + if (oFyNcL === undefined) { + return 1; + } + + if (oFxNcL < oFyNcL) { + return -1; + } else if (oFxNcL > oFyNcL) { + return 1; + } + } + return 0; +} From 6a504c0afadc27ccee8603fded2886bf606df91d Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Tue, 1 Oct 2024 12:25:09 -0500 Subject: [PATCH 09/29] remove the extensions toggle, only include `isSubTypeOfAny` items The `isSubClassOfAny` was incorrect logic for implicit conversions. `isSubTypeOfAny` gives us what we want as far as filtering items that would be valid for implicit conversions goes. Also, we concluded that the `All` option was also not acceptable as only valid extensions must be enforced in the collection creator. --- .../Collections/ListCollectionCreator.vue | 65 +++---------------- .../Collections/common/CollectionCreator.vue | 49 +++----------- client/src/components/Datatypes/model.ts | 4 -- .../Form/Elements/FormData/FormData.vue | 17 ++--- client/src/components/Help/terms.yml | 9 +-- 5 files changed, 30 insertions(+), 114 deletions(-) diff --git a/client/src/components/Collections/ListCollectionCreator.vue b/client/src/components/Collections/ListCollectionCreator.vue index b3d5e398091c..a9750da933f4 100644 --- a/client/src/components/Collections/ListCollectionCreator.vue +++ b/client/src/components/Collections/ListCollectionCreator.vue @@ -17,14 +17,6 @@ import FormSelectMany from "../Form/Elements/FormSelectMany/FormSelectMany.vue"; import CollectionCreator from "@/components/Collections/common/CollectionCreator.vue"; import DatasetCollectionElementView from "@/components/Collections/ListDatasetCollectionElementView.vue"; -const DEFAULT_DATATYPE_FILTER_OPTIONS = [ - { text: "All", value: "all" }, - { text: "Datatype", value: "datatype" }, - { text: "Extension", value: "ext" }, -]; - -type DatatypeToggle = "all" | "datatype" | "ext" | undefined; - interface Props { initialElements: HistoryItemSummary[]; oncancel: () => void; @@ -81,21 +73,7 @@ const inListElements = ref([]); const datatypesMapperStore = useDatatypesMapperStore(); const datatypesMapper = computed(() => datatypesMapperStore.datatypesMapper); -const datatypeToggleOptions = ref<{ text: string; value: string }[] | undefined>( - props.extensions?.length ? DEFAULT_DATATYPE_FILTER_OPTIONS : undefined -); -const baseDatatypeToggle = computed(() => { - const options = datatypeToggleOptions.value || []; - return options.find((option) => option.value === "ext") - ? "ext" - : options.find((option) => option.value === "datatype") - ? "datatype" - : "all"; -}); - -const datatypeToggle = ref(props.extensions?.length ? "ext" : undefined); - -/** Are we filtering by extension or datatype? */ +/** Are we filtering by datatype? */ const filterExtensions = computed(() => !!datatypesMapper.value && !!props.extensions?.length); /** set up instance vars function */ @@ -149,28 +127,11 @@ function _validateElements() { return !problem; }); - // if all elements are invalid, try again by switching to the next datatypeToggle value - // until we've tried both `ext` and `datatype` and then we revert to showing everything - if (props.initialElements.length && allElementsAreInvalid.value && !props.fromSelection && filterExtensions.value) { - if (datatypeToggle.value === "ext") { - datatypeToggleOptions.value = datatypeToggleOptions.value?.filter((option) => option.value !== "ext"); - datatypeToggle.value = "datatype"; - } else if (datatypeToggle.value === "datatype") { - // we've tried both `ext` and `datatype`, so we remove the toggle options - datatypeToggleOptions.value = undefined; - datatypeToggle.value = "all"; - } else { - return; - } - // re-run `_elementsSetUp` to filter out invalid elements for the new datatypeToggle value - _elementsSetUp(); - } - return workingElements.value; } /** describe what is wrong with a particular element if anything */ -function _isElementInvalid(element: HistoryItemSummary) { +function _isElementInvalid(element: HistoryItemSummary): string | null { if (element.history_content_type === "dataset_collection") { return localize("is a collection, this is not allowed"); } @@ -185,17 +146,13 @@ function _isElementInvalid(element: HistoryItemSummary) { return localize("has been deleted or purged"); } - if (filterExtensions.value && datatypeToggle.value !== "all" && element.extension) { - if ( - // is the element's extension not a subtype of any of the required extensions? - (datatypeToggle.value === "ext" && - !datatypesMapper.value?.isSubTypeOfAny(element.extension, props.extensions!)) || - // else, does the element's extension have the same parent class as any of the required extensions? - (datatypeToggle.value === "datatype" && - !datatypesMapper.value?.isSubClassOfAny(element.extension, props.extensions!)) - ) { - return localize("has an invalid extension"); - } + // is the element's extension not a subtype of any of the required extensions? + if ( + filterExtensions.value && + element.extension && + !datatypesMapper.value?.isSubTypeOfAny(element.extension, props.extensions!) + ) { + return localize("has an invalid extension"); } return null; } @@ -219,7 +176,6 @@ function _mangleDuplicateNames() { } function changeDatatypeFilter(newFilter: "all" | "datatype" | "ext") { - datatypeToggle.value = newFilter; _elementsSetUp(); } @@ -305,7 +261,6 @@ function checkForDuplicates() { /** reset all data to the initial state */ function reset() { - datatypeToggle.value = baseDatatypeToggle.value; _instanceSetUp(); getOriginalNames(); } @@ -462,8 +417,6 @@ function renameElement(element: any, name: string) { :oncancel="oncancel" :hide-source-items="hideSourceItems" :extensions="extensions" - :datatype-toggle="filterExtensions && datatypeToggleOptions ? datatypeToggle : undefined" - :datatype-toggle-options="datatypeToggleOptions" @on-update-datatype-toggle="changeDatatypeFilter" @onUpdateHideSourceItems="onUpdateHideSourceItems" @clicked-create="clickedCreate"> diff --git a/client/src/components/Collections/common/CollectionCreator.vue b/client/src/components/Collections/common/CollectionCreator.vue index e257df302654..02eea2bee594 100644 --- a/client/src/components/Collections/common/CollectionCreator.vue +++ b/client/src/components/Collections/common/CollectionCreator.vue @@ -1,7 +1,7 @@ diff --git a/client/src/components/Form/Elements/FormData/FormData.vue b/client/src/components/Form/Elements/FormData/FormData.vue index 2173b62bec91..49e31cfc6782 100644 --- a/client/src/components/Form/Elements/FormData/FormData.vue +++ b/client/src/components/Form/Elements/FormData/FormData.vue @@ -656,6 +656,18 @@ const noOptionsWarningMessage = computed(() => { ... + + + +
@@ -689,25 +701,6 @@ const noOptionsWarningMessage = computed(() => { {{ noOptionsWarningMessage }} - - { From 6862f64692400b27e941de53c153bc8654864360 Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Thu, 3 Oct 2024 15:25:44 -0500 Subject: [PATCH 11/29] change create new collection `ButtonSpinner` variant --- client/src/components/Form/Elements/FormData/FormData.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Form/Elements/FormData/FormData.vue b/client/src/components/Form/Elements/FormData/FormData.vue index 49e31cfc6782..a5f6db0a1258 100644 --- a/client/src/components/Form/Elements/FormData/FormData.vue +++ b/client/src/components/Form/Elements/FormData/FormData.vue @@ -661,7 +661,7 @@ const noOptionsWarningMessage = computed(() => { v-for="collectionType in effectiveCollectionTypes" :key="collectionType" :tooltip="collectionType" - variant="secondary" + :variant="formattedOptions.length === 0 ? 'warning' : 'secondary'" :disabled="isFetchingItems" :icon="faPlus" :wait="isFetchingItems" From 297a529dde53420cef855e9feb67365c2c5ddc7a Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Thu, 3 Oct 2024 15:34:35 -0500 Subject: [PATCH 12/29] change create new collection `ButtonSpinner` title; fix icon imports --- .../Form/Elements/FormData/FormData.vue | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/client/src/components/Form/Elements/FormData/FormData.vue b/client/src/components/Form/Elements/FormData/FormData.vue index a5f6db0a1258..faffa91cb75e 100644 --- a/client/src/components/Form/Elements/FormData/FormData.vue +++ b/client/src/components/Form/Elements/FormData/FormData.vue @@ -1,7 +1,13 @@ + + + + diff --git a/client/src/components/Collections/ListCollectionCreator.vue b/client/src/components/Collections/ListCollectionCreator.vue index a9750da933f4..8d1e5be51705 100644 --- a/client/src/components/Collections/ListCollectionCreator.vue +++ b/client/src/components/Collections/ListCollectionCreator.vue @@ -19,9 +19,6 @@ import DatasetCollectionElementView from "@/components/Collections/ListDatasetCo interface Props { initialElements: HistoryItemSummary[]; - oncancel: () => void; - oncreate: () => void; - creationFn: (workingElements: HDASummary[], collectionName: string, hideSourceItems: boolean) => any; defaultHideSourceItems?: boolean; fromSelection?: boolean; extensions?: string[]; @@ -31,6 +28,7 @@ const props = defineProps(); const emit = defineEmits<{ (e: "clicked-create", workingElements: HDASummary[], collectionName: string, hideSourceItems: boolean): void; + (e: "on-cancel"): void; }>(); const state = ref("build"); @@ -152,7 +150,7 @@ function _isElementInvalid(element: HistoryItemSummary): string | null { element.extension && !datatypesMapper.value?.isSubTypeOfAny(element.extension, props.extensions!) ) { - return localize("has an invalid extension"); + return localize(`has an invalid extension: ${element.extension}`); } return null; } @@ -231,13 +229,6 @@ function clickedCreate(collectionName: string) { if (state.value !== "error") { emit("clicked-create", returnedElements, collectionName, hideSourceItems.value); - - return props - .creationFn(returnedElements, collectionName, hideSourceItems.value) - .done(props.oncreate) - .fail(() => { - state.value = "error"; - }); } } @@ -339,14 +330,14 @@ function renameElement(element: any, name: string) { {{ localize("No datasets were selected") }} {{ localize("At least one element is needed for the collection. You may need to") }} - + {{ localize("cancel") }} {{ localize("and reselect new elements.") }}
-
@@ -375,14 +366,14 @@ function renameElement(element: any, name: string) { {{ localize("At least one element is needed for the collection. You may need to") }} - + {{ localize("cancel") }} {{ localize("and reselect new elements.") }}
-
@@ -414,7 +405,7 @@ function renameElement(element: any, name: string) {
(); @@ -21,7 +23,7 @@ const emit = defineEmits<{ (event: "element-is-discarded", element: any): void; }>(); -const elementName = ref(props.element.name); +const elementName = ref(props.element.name || "..."); watch(elementName, () => { emit("onRename", elementName.value); @@ -43,13 +45,16 @@ function clickDiscard() { {{ element.hid }}: - + + {{ elementName }} - ({{ element.extension }}) + ({{ element.extension }})
- Added to list + + Added to collection + Selected