diff --git a/client/src/components/History/CurrentHistory/HistoryPanel.vue b/client/src/components/History/CurrentHistory/HistoryPanel.vue
index 8b2062323ca..ab8bc7d6358 100644
--- a/client/src/components/History/CurrentHistory/HistoryPanel.vue
+++ b/client/src/components/History/CurrentHistory/HistoryPanel.vue
@@ -212,6 +212,36 @@ watch(
}
);
+function dragSameHistory() {
+ return getDragData().sameHistory;
+}
+
+function getDragData() {
+ const eventStore = useEventStore();
+ const multiple = eventStore.multipleDragData;
+ let data: HistoryItem[] | undefined;
+ let historyId: string | undefined;
+ try {
+ if (multiple) {
+ const dragData = eventStore.getDragData() as Record;
+ // set historyId to the first history_id in the multiple drag data
+ const firstItem = Object.values(dragData)[0];
+ if (firstItem) {
+ historyId = firstItem.history_id;
+ }
+ data = Object.values(dragData);
+ } else {
+ data = [eventStore.getDragData() as HistoryItem];
+ if (data[0]) {
+ historyId = data[0].history_id;
+ }
+ }
+ } catch (error) {
+ // this was not a valid object for this dropzone, ignore
+ }
+ return { data, sameHistory: historyId === props.history.id, multiple };
+}
+
function getHighlight(item: HistoryItem) {
if (unref(isLoading)) {
return undefined;
@@ -319,45 +349,33 @@ function onOperationError(error: any) {
}
function onDragEnter(e: DragEvent) {
+ if (dragSameHistory()) {
+ return;
+ }
dragTarget.value = e.target;
showDropZone.value = true;
}
+function onDragOver(e: DragEvent) {
+ if (dragSameHistory()) {
+ return;
+ }
+ e.preventDefault();
+}
+
function onDragLeave(e: DragEvent) {
+ if (dragSameHistory()) {
+ return;
+ }
if (dragTarget.value === e.target) {
showDropZone.value = false;
}
}
-async function onDrop(evt: any) {
- const eventStore = useEventStore();
+async function onDrop() {
showDropZone.value = false;
- let data: HistoryItem[] | undefined;
- let historyId: string | undefined;
- const multiple = eventStore.multipleDragData;
- try {
- if (multiple) {
- const dragData = eventStore.getDragData() as Record;
- // set historyId to the first history_id in the multiple drag data
- const firstItem = Object.values(dragData)[0];
- if (firstItem) {
- historyId = firstItem.history_id;
- }
- data = Object.values(dragData);
- } else {
- data = [eventStore.getDragData() as HistoryItem];
- if (data[0]) {
- historyId = data[0].history_id;
- }
- }
- } catch (error) {
- // this was not a valid object for this dropzone, ignore
- }
-
- if (!data) {
- return;
- } else if (historyId === props.history.id) {
- Toast.error("Cannot copy to the same history");
+ const { data, sameHistory, multiple } = getDragData();
+ if (!data || sameHistory) {
return;
}
@@ -486,7 +504,7 @@ function setItemDragstart(
class="history-layout d-flex flex-column w-100 h-100"
@drop.prevent="onDrop"
@dragenter.prevent="onDragEnter"
- @dragover.prevent
+ @dragover="onDragOver"
@dragleave.prevent="onDragLeave">
diff --git a/client/src/components/TagsMultiselect/HeadlessMultiselect.test.ts b/client/src/components/TagsMultiselect/HeadlessMultiselect.test.ts
index ca97b4615ba..4ef562b2efd 100644
--- a/client/src/components/TagsMultiselect/HeadlessMultiselect.test.ts
+++ b/client/src/components/TagsMultiselect/HeadlessMultiselect.test.ts
@@ -20,7 +20,7 @@ describe("HeadlessMultiselect", () => {
});
};
- const sampleOptions = ["#named", "#named_2", "#named_3", "abc", "def", "ghi"];
+ const sampleOptions = ["name:named", "name:named_2", "name:named_3", "abc", "def", "ghi"];
const selectors = {
openButton: ".toggle-button",
@@ -197,27 +197,27 @@ describe("HeadlessMultiselect", () => {
const input = await open(wrapper);
await keyPress(input, "Enter");
- expect(wrapper.emitted()["input"]?.[0]?.[0]).toEqual(["#named"]);
+ expect(wrapper.emitted()["input"]?.[0]?.[0]).toEqual(["name:named"]);
await keyPress(input, "ArrowDown");
await keyPress(input, "Enter");
- expect(wrapper.emitted()["input"]?.[1]?.[0]).toEqual(["#named_2"]);
+ expect(wrapper.emitted()["input"]?.[1]?.[0]).toEqual(["name:named_2"]);
});
it("deselects options via keyboard", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
- selected: ["#named", "#named_2", "#named_3"],
+ selected: ["name:named", "name:named_2", "name:named_3"],
});
const input = await open(wrapper);
await keyPress(input, "Enter");
- expect(wrapper.emitted()["input"]?.[0]?.[0]).toEqual(["#named_2", "#named_3"]);
+ expect(wrapper.emitted()["input"]?.[0]?.[0]).toEqual(["name:named_2", "name:named_3"]);
await keyPress(input, "ArrowDown");
await keyPress(input, "Enter");
- expect(wrapper.emitted()["input"]?.[1]?.[0]).toEqual(["#named", "#named_3"]);
+ expect(wrapper.emitted()["input"]?.[1]?.[0]).toEqual(["name:named", "name:named_3"]);
});
it("allows for adding new options", async () => {
@@ -243,26 +243,26 @@ describe("HeadlessMultiselect", () => {
const options = wrapper.findAll(selectors.option);
await options.at(0).trigger("click");
- expect(wrapper.emitted()["input"]?.[0]?.[0]).toEqual(["#named"]);
+ expect(wrapper.emitted()["input"]?.[0]?.[0]).toEqual(["name:named"]);
await options.at(1).trigger("click");
- expect(wrapper.emitted()["input"]?.[1]?.[0]).toEqual(["#named_2"]);
+ expect(wrapper.emitted()["input"]?.[1]?.[0]).toEqual(["name:named_2"]);
});
it("deselects options with mouse", async () => {
const wrapper = mountWithProps({
options: sampleOptions,
- selected: ["#named", "#named_2", "#named_3"],
+ selected: ["name:named", "name:named_2", "name:named_3"],
});
await open(wrapper);
const options = wrapper.findAll(selectors.option);
await options.at(0).trigger("click");
- expect(wrapper.emitted()["input"]?.[0]?.[0]).toEqual(["#named_2", "#named_3"]);
+ expect(wrapper.emitted()["input"]?.[0]?.[0]).toEqual(["name:named_2", "name:named_3"]);
await options.at(1).trigger("click");
- expect(wrapper.emitted()["input"]?.[1]?.[0]).toEqual(["#named", "#named_3"]);
+ expect(wrapper.emitted()["input"]?.[1]?.[0]).toEqual(["name:named", "name:named_3"]);
});
});
});
diff --git a/client/src/components/TagsMultiselect/HeadlessMultiselect.vue b/client/src/components/TagsMultiselect/HeadlessMultiselect.vue
index 3a07de33a79..95660d1ced8 100644
--- a/client/src/components/TagsMultiselect/HeadlessMultiselect.vue
+++ b/client/src/components/TagsMultiselect/HeadlessMultiselect.vue
@@ -15,6 +15,7 @@ import { computed, nextTick, ref, watch } from "vue";
import Vue2Teleport from "vue2-teleport";
import { useUid } from "@/composables/utils/uid";
+import { normalizeTag } from "@/stores/userTagsStore";
library.add(faCheck, faChevronUp, faPlus, faTags, faTimes);
@@ -147,6 +148,8 @@ function getOptionWithId(id: number) {
* was not part of the provided options
*/
function onOptionSelected(option: string) {
+ option = normalizeTag(option);
+
if (!optionsAsSet.value.has(option)) {
emit("addOption", option);
return;
@@ -318,7 +321,7 @@ whenever(isOpen, async () => {
:data-parent-id="props.id"
class="headless-multiselect__option"
role="option"
- :aria-selected="props.selected.includes(option)"
+ :aria-selected="props.selected.includes(normalizeTag(option))"
:class="{
invalid: i === 0 && !searchValueValid,
highlighted: highlightedOption === i,
@@ -332,7 +335,7 @@ whenever(isOpen, async () => {
{{ option }}
-
+
{
return { message, title };
@@ -88,7 +94,7 @@ describe("StatelessTags", () => {
expect(visibleOptions.length).toBe(autocompleteTags.length);
visibleOptions.wrappers.forEach((option, i) => {
- expect(option.text()).toContain(autocompleteTags[i]);
+ expect(normalize(option.text())).toContain(autocompleteTags[i]);
});
});
diff --git a/client/src/components/TagsMultiselect/StatelessTags.vue b/client/src/components/TagsMultiselect/StatelessTags.vue
index 413def84e2a..dfc30060dbb 100644
--- a/client/src/components/TagsMultiselect/StatelessTags.vue
+++ b/client/src/components/TagsMultiselect/StatelessTags.vue
@@ -111,7 +111,7 @@ function onTagClicked(tag: string) {
{
await until(dbLoaded).toBe(true);
tag = normalizeTag(tag);
- const storedTag = tags.value.find((o) => o.tag === tag);
- const id = storedTag?.id;
+ const dbTag = await db.tags.get({ tag });
- if (id !== undefined) {
- // put instead of update, because `removeOldestEntries` may have deleted this tag on init
- await db.tags.put({ ...storedTag, lastUsed: Date.now() } as StoredTag, id);
+ if (dbTag) {
+ await db.tags.update(dbTag, { lastUsed: Date.now() });
+ } else {
+ const storedTag = tags.value.find((o) => o.tag === tag);
+ const id = storedTag?.id;
+
+ if (id !== undefined) {
+ await db.tags.add({ ...storedTag, lastUsed: Date.now() } as StoredTag);
+ }
}
}
diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py
index bebd0344678..9f5140c39aa 100644
--- a/lib/galaxy/managers/histories.py
+++ b/lib/galaxy/managers/histories.py
@@ -215,6 +215,7 @@ class HistoryManager(sharable.SharableModelManager, deletable.PurgableManagerMix
total_matches = None
if payload.sort_by == "username":
sort_column = model.User.username
+ stmt = stmt.add_columns(sort_column)
else:
sort_column = getattr(model.History, payload.sort_by)
if payload.sort_desc:
diff --git a/lib/galaxy/webapps/galaxy/api/histories.py b/lib/galaxy/webapps/galaxy/api/histories.py
index c8b23b35c78..743d31a48cc 100644
--- a/lib/galaxy/webapps/galaxy/api/histories.py
+++ b/lib/galaxy/webapps/galaxy/api/histories.py
@@ -191,7 +191,7 @@ class FastAPIHistories:
trans, serialization_params, filter_query_params, deleted_only=deleted, all_histories=all
)
else:
- payload = HistoryIndexQueryPayload.construct(
+ payload = HistoryIndexQueryPayload.model_construct(
show_own=show_own,
show_published=show_published,
show_shared=show_shared,
@@ -201,7 +201,9 @@ class FastAPIHistories:
offset=offset,
search=search,
)
- entries, total_matches = self.service.index_query(trans, payload, include_total_count=True)
+ entries, total_matches = self.service.index_query(
+ trans, payload, serialization_params, include_total_count=True
+ )
response.headers["total_matches"] = str(total_matches)
return entries
diff --git a/lib/galaxy/webapps/galaxy/services/datasets.py b/lib/galaxy/webapps/galaxy/services/datasets.py
index d238d9a216b..fc67929c3c2 100644
--- a/lib/galaxy/webapps/galaxy/services/datasets.py
+++ b/lib/galaxy/webapps/galaxy/services/datasets.py
@@ -891,12 +891,11 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin):
extra_info = None
mode = kwargs.get("mode", "Auto")
- indexer = None
# Coverage mode uses index data.
if mode == "Coverage":
# Get summary using minimal cutoffs.
- indexer = self.data_provider_registry.get_data_provider(trans, original_dataset=dataset, source="index")
+ indexer = self._get_indexer(trans, dataset)
return indexer.get_data(chrom, low, high, **kwargs)
# TODO:
@@ -906,7 +905,7 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin):
# If mode is Auto, need to determine what type of data to return.
if mode == "Auto":
# Get stats from indexer.
- indexer = self.data_provider_registry.get_data_provider(trans, original_dataset=dataset, source="index")
+ indexer = self._get_indexer(trans, dataset)
stats = indexer.get_data(chrom, low, high, stats=True)
# If stats were requested, return them.
@@ -956,8 +955,7 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin):
)
# Get mean depth.
- if not indexer:
- indexer = self.data_provider_registry.get_data_provider(trans, original_dataset=dataset, source="index")
+ indexer = self._get_indexer(trans, dataset)
stats = indexer.get_data(chrom, low, high, stats=True)
mean_depth = stats["data"]["mean"]
@@ -1008,3 +1006,11 @@ class DatasetsService(ServiceBase, UsesVisualizationMixin):
data = data_provider.get_data(**kwargs)
return data
+
+ def _get_indexer(self, trans, dataset):
+ indexer = self.data_provider_registry.get_data_provider(trans, original_dataset=dataset, source="index")
+ if indexer is None:
+ msg = f"No indexer available for dataset {self.encode_id(dataset.id)}"
+ log.exception(msg)
+ raise galaxy_exceptions.ObjectNotFound(msg)
+ return indexer
diff --git a/lib/galaxy/webapps/galaxy/services/histories.py b/lib/galaxy/webapps/galaxy/services/histories.py
index ab3fe0e69dc..3d75284d07e 100644
--- a/lib/galaxy/webapps/galaxy/services/histories.py
+++ b/lib/galaxy/webapps/galaxy/services/histories.py
@@ -216,6 +216,7 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores):
self,
trans,
payload: HistoryIndexQueryPayload,
+ serialization_params: SerializationParams,
include_total_count: bool = False,
) -> Tuple[List[AnyHistoryView], int]:
"""Return a list of History accessible by the user
@@ -223,10 +224,9 @@ class HistoriesService(ServiceBase, ConsumesModelStores, ServesExportStores):
:rtype: list
:returns: dictionaries containing History details
"""
- serialization_params = SerializationParams(default_view="detailed")
entries, total_matches = self.manager.index_query(trans, payload, include_total_count)
return (
- [self._serialize_history(trans, entry, serialization_params) for entry in entries],
+ [self._serialize_history(trans, entry, serialization_params, default_view="summary") for entry in entries],
total_matches,
)
diff --git a/lib/galaxy/webapps/galaxy/services/users.py b/lib/galaxy/webapps/galaxy/services/users.py
index 91bcf390c0a..fa820748c11 100644
--- a/lib/galaxy/webapps/galaxy/services/users.py
+++ b/lib/galaxy/webapps/galaxy/services/users.py
@@ -122,10 +122,9 @@ class UsersService(ServiceBase):
def _anon_user_api_value(self, trans: ProvidesHistoryContext):
"""Return data for an anonymous user, truncated to only usage and quota_percent"""
if not trans.user and not trans.history:
- # Can't return info about this user, may not have a history yet.
- # return {}
- raise glx_exceptions.MessageException(err_msg="The user has no history, which should always be the case.")
- usage = self.quota_agent.get_usage(trans, history=trans.history)
+ usage = None
+ else:
+ usage = self.quota_agent.get_usage(trans, history=trans.history)
percent = self.quota_agent.get_percent(trans=trans, usage=usage)
usage = usage or 0
return {
diff --git a/lib/galaxy_test/api/test_histories.py b/lib/galaxy_test/api/test_histories.py
index e263423b2b5..a3c8e28a9f8 100644
--- a/lib/galaxy_test/api/test_histories.py
+++ b/lib/galaxy_test/api/test_histories.py
@@ -140,6 +140,56 @@ class TestHistoriesApi(ApiTestCase, BaseHistories):
assert len(index_response) == 1
assert index_response[0]["name"] == expected_history_name
+ def test_index_views(self):
+ # Make sure there is at least one history
+ self._create_history(f"TestHistoryForViews_{uuid4()}")["id"]
+ # By default the view is summary
+ index_response = self._get("histories").json()
+ for history in index_response:
+ assert "state" not in history
+
+ # Change the view to detailed
+ index_response = self._get("histories?view=detailed").json()
+ for history in index_response:
+ assert "state" in history
+
+ # Expect only specific keys
+ expected_keys = ["name"]
+ unexpected_keys = ["deleted", "state"]
+ index_response = self._get(f"histories?keys={','.join(expected_keys)}").json()
+ for history in index_response:
+ for key in expected_keys:
+ assert key in history
+ for key in unexpected_keys:
+ assert key not in history
+
+ def test_index_search_mode_views(self):
+ # Make sure there is at least one history
+ expected_name_contains = "SearchMode"
+ self._create_history(f"TestHistory{expected_name_contains}_{uuid4()}")["id"]
+ # By default the view is summary
+ data = dict(search=expected_name_contains, show_published=False)
+ index_response = self._get("histories", data=data).json()
+ for history in index_response:
+ assert "state" not in history
+
+ # Change the view to detailed
+ data = dict(search=expected_name_contains, show_published=False)
+ index_response = self._get("histories?view=detailed", data=data).json()
+ for history in index_response:
+ assert "state" in history
+
+ # Expect only specific keys
+ expected_keys = ["name"]
+ unexpected_keys = ["deleted", "state"]
+ data = dict(search=expected_name_contains, show_published=False, keys=",".join(expected_keys))
+ index_response = self._get("histories", data=data).json()
+ for history in index_response:
+ for key in expected_keys:
+ assert key in history
+ for key in unexpected_keys:
+ assert key not in history
+
def test_index_case_insensitive_contains_query(self):
# Create the histories with a different user to ensure the test
# is not conflicted with the current user's histories.
diff --git a/test/integration/oidc/test_auth_oidc.py b/test/integration/oidc/test_auth_oidc.py
index 1674e935307..29ff1c18b2b 100644
--- a/test/integration/oidc/test_auth_oidc.py
+++ b/test/integration/oidc/test_auth_oidc.py
@@ -296,7 +296,8 @@ class TestGalaxyOIDCLoginIntegration(AbstractTestCases.BaseKeycloakIntegrationTe
response = session.get(response.json()["redirect_uri"], verify=False)
# make sure we can no longer request the user
response = session.get(self._api_url("users/current"))
- self._assert_status_code_is(response, 400)
+ self._assert_status_code_is(response, 200)
+ assert "email" not in response.json()
def test_auth_by_access_token_logged_in_once(self):
# login at least once