diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index 02a3c86bc6fd..a8d706bbdfae 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -5512,6 +5512,15 @@ export interface components { */ url: string; }; + /** GroupUpdatePayload */ + GroupUpdatePayload: { + /** name of the group */ + name?: string | null; + /** role IDs */ + role_ids?: string[] | null; + /** user IDs */ + user_ids?: string[] | null; + }; /** GroupUserListResponse */ GroupUserListResponse: components["schemas"]["GroupUserResponse"][]; /** GroupUserResponse */ @@ -15744,7 +15753,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["GroupCreatePayload"]; + "application/json": components["schemas"]["GroupUpdatePayload"]; }; }; responses: { diff --git a/client/src/components/FilesDialog/FilesDialog.vue b/client/src/components/FilesDialog/FilesDialog.vue index 1fb89ae29130..5e6d2bde46e3 100644 --- a/client/src/components/FilesDialog/FilesDialog.vue +++ b/client/src/components/FilesDialog/FilesDialog.vue @@ -409,6 +409,7 @@ onMounted(() => { :is-busy="isBusy" :items="items" :items-provider="itemsProvider" + :provider-url="currentDirectory?.url" :total-items="totalItems" :modal-show="modalShow" :modal-static="modalStatic" diff --git a/client/src/components/Form/Elements/FormData/FormData.test.js b/client/src/components/Form/Elements/FormData/FormData.test.js index 65b7c9dd6100..bcff5d7e5589 100644 --- a/client/src/components/Form/Elements/FormData/FormData.test.js +++ b/client/src/components/Form/Elements/FormData/FormData.test.js @@ -3,6 +3,8 @@ import { mount } from "@vue/test-utils"; import { PiniaVuePlugin } from "pinia"; import { dispatchEvent, getLocalVue } from "tests/jest/helpers"; +import { testDatatypesMapper } from "@/components/Datatypes/test_fixtures"; +import { useDatatypesMapperStore } from "@/stores/datatypesMapperStore"; import { useEventStore } from "@/stores/eventStore"; import MountTarget from "./FormData.vue"; @@ -15,6 +17,8 @@ let eventStore; function createTarget(propsData) { const pinia = createTestingPinia({ stubActions: false }); eventStore = useEventStore(); + const datatypesStore = useDatatypesMapperStore(); + datatypesStore.datatypesMapper = testDatatypesMapper; return mount(MountTarget, { localVue, propsData, diff --git a/client/src/components/Form/Elements/FormData/FormData.vue b/client/src/components/Form/Elements/FormData/FormData.vue index 123d24bb43b5..b5f71c8cb2d1 100644 --- a/client/src/components/Form/Elements/FormData/FormData.vue +++ b/client/src/components/Form/Elements/FormData/FormData.vue @@ -7,6 +7,7 @@ import { BAlert, BButton, BButtonGroup, BCollapse, BFormCheckbox, BTooltip } fro import { computed, onMounted, type Ref, ref, watch } from "vue"; import { getGalaxyInstance } from "@/app"; +import { useDatatypesMapper } from "@/composables/datatypesMapper"; import { useUid } from "@/composables/utils/uid"; import { type EventData, useEventStore } from "@/stores/eventStore"; import { orList } from "@/utils/strings"; @@ -35,6 +36,7 @@ const props = withDefaults( }; extensions?: Array; type?: string; + collectionTypes?: Array; flavor?: string; tag?: string; }>(), @@ -45,14 +47,16 @@ const props = withDefaults( value: undefined, extensions: () => [], type: "data", + collectionTypes: undefined, flavor: undefined, tag: undefined, } ); const eventStore = useEventStore(); +const { datatypesMapper } = useDatatypesMapper(); -const $emit = defineEmits(["input"]); +const $emit = defineEmits(["input", "alert"]); // Determines wether values should be processed as linked or unlinked const currentLinked = ref(true); @@ -302,6 +306,10 @@ function getSourceType(val: DataOption) { function handleIncoming(incoming: Record, partial = true) { if (incoming) { const values = Array.isArray(incoming) ? incoming : [incoming]; + const extensions = values.map((v) => v.extension || v.elements_datatypes).filter((v) => (v ? true : false)); + if (!canAcceptDatatype(extensions)) { + return false; + } if (values.length > 0) { const incomingValues: Array = []; values.forEach((v) => { @@ -311,14 +319,28 @@ function handleIncoming(incoming: Record, partial = true) { const newName = v.name ? v.name : newId; const newSrc = v.src || (v.history_content_type === "dataset_collection" ? SOURCE.COLLECTION : SOURCE.DATASET); - const newValue = { + const newValue: DataOption = { id: newId, src: newSrc, + batch: false, + map_over_type: undefined, hid: newHid, name: newName, keep: true, tags: [], }; + if (v.collection_type && props.collectionTypes?.length > 0) { + if (!props.collectionTypes.includes(v.collection_type)) { + const mapOverType = props.collectionTypes.find((collectionType) => + v.collection_type.endsWith(collectionType) + ); + if (!mapOverType) { + return false; + } + newValue["batch"] = true; + newValue["map_over_type"] = mapOverType; + } + } // Verify that new value has corresponding option const keepKey = `${newId}_${newSrc}`; const existingOptions = props.options && props.options[newSrc]; @@ -349,6 +371,7 @@ function handleIncoming(incoming: Record, partial = true) { } } } + return true; } /** @@ -372,10 +395,36 @@ function onBrowse() { } } +function canAcceptDatatype(itemDatatypes: string | Array) { + if (!(props.extensions?.length > 0)) { + return true; + } + let datatypes: Array; + if (!Array.isArray(itemDatatypes)) { + datatypes = [itemDatatypes]; + } else { + datatypes = itemDatatypes; + } + const incompatibleItem = datatypes.find( + (extension) => !datatypesMapper.value?.isSubTypeOfAny(extension, props.extensions) + ); + if (incompatibleItem) { + return false; + } + return true; +} + // Drag/Drop event handlers function onDragEnter(evt: MouseEvent) { const eventData = eventStore.getDragData(); if (eventData) { + const extensions = (eventData.extension as string) || (eventData.elements_datatypes as Array); + if (!canAcceptDatatype(extensions)) { + currentHighlighting.value = "warning"; + $emit("alert", `${extensions} is not an acceptable format for this parameter.`); + } else { + currentHighlighting.value = "success"; + } dragTarget.value = evt.target; dragData.value = eventData; } @@ -384,23 +433,24 @@ function onDragEnter(evt: MouseEvent) { function onDragLeave(evt: MouseEvent) { if (dragTarget.value === evt.target) { currentHighlighting.value = null; - } -} - -function onDragOver() { - if (dragData.value !== null) { - currentHighlighting.value = "warning"; + $emit("alert", undefined); } } function onDrop() { if (dragData.value) { + let accept = false; if (eventStore.multipleDragData) { - handleIncoming(Object.values(dragData.value) as any, false); + accept = handleIncoming(Object.values(dragData.value) as any, false); + } else { + accept = handleIncoming(dragData.value); + } + if (accept) { + currentHighlighting.value = "success"; } else { - handleIncoming(dragData.value); + currentHighlighting.value = "warning"; } - currentHighlighting.value = "success"; + $emit("alert", undefined); dragData.value = null; clearHighlighting(); } @@ -468,7 +518,7 @@ const noOptionsWarningMessage = computed(() => { :class="currentHighlighting && `ui-dragover-${currentHighlighting}`" @dragenter.prevent="onDragEnter" @dragleave.prevent="onDragLeave" - @dragover.prevent="onDragOver" + @dragover.prevent @drop.prevent="onDrop">
diff --git a/client/src/components/Form/Elements/FormData/types.ts b/client/src/components/Form/Elements/FormData/types.ts index 52c8a418897e..acdf8299eca6 100644 --- a/client/src/components/Form/Elements/FormData/types.ts +++ b/client/src/components/Form/Elements/FormData/types.ts @@ -3,6 +3,7 @@ export type DataOption = { hid: number; is_dataset?: boolean; keep: boolean; + batch: boolean; map_over_type?: string; name: string; src: string; diff --git a/client/src/components/Form/FormElement.test.js b/client/src/components/Form/FormElement.test.js index 9aac1c55c0f6..a3cd69f50f74 100644 --- a/client/src/components/Form/FormElement.test.js +++ b/client/src/components/Form/FormElement.test.js @@ -30,7 +30,7 @@ describe("FormElement", () => { const error = wrapper.find(".ui-form-error-text"); expect(error.text()).toBe("error_text"); - await wrapper.setProps({ error: "" }); + await wrapper.setProps({ error: undefined }); const no_error = wrapper.findAll(".ui-form-error"); expect(no_error.length).toBe(0); diff --git a/client/src/components/Form/FormElement.vue b/client/src/components/Form/FormElement.vue index 1a7296f401ef..72af07dd7f8d 100644 --- a/client/src/components/Form/FormElement.vue +++ b/client/src/components/Form/FormElement.vue @@ -127,7 +127,7 @@ function onConnect() { const isHidden = computed(() => attrs.value["hidden"]); const elementId = computed(() => `form-element-${props.id}`); -const hasAlert = computed(() => Boolean(props.error || props.warning)); +const hasAlert = computed(() => alerts.value.length > 0); const showPreview = computed(() => (collapsed.value && attrs.value["collapsible_preview"]) || props.disabled); const showField = computed(() => !collapsed.value && !props.disabled); @@ -174,6 +174,16 @@ const isEmpty = computed(() => { const isRequired = computed(() => attrs.value["optional"] === false); const isRequiredType = computed(() => props.type !== "boolean"); const isOptional = computed(() => !isRequired.value && attrs.value["optional"] !== undefined); +const formAlert = ref(); +const alerts = computed(() => { + return [formAlert.value, props.error, props.warning] + .filter((v) => v !== undefined && v !== null) + .map((v) => linkify(sanitize(v!, { USE_PROFILES: { html: true } }))); +}); + +function onAlert(value: string | undefined) { + formAlert.value = value; +}