Merge pull request #23075 from itisAliRH/fix/my-tools-recent-tools-default-panel

[26.1] Fix recent tools for My Tools default panel
This commit is contained in:
Marius van den Beek
2026-07-07 13:03:13 +02:00
committed by GitHub
6 changed files with 140 additions and 9 deletions
+8 -2
View File
@@ -17,6 +17,7 @@ import {
buildToolEntries,
buildToolLabel,
buildToolSection,
countUniqueToolsInList,
countUniqueToolsInPanel,
FAVORITES_KEYS,
filterPanelByToolIds,
@@ -180,6 +181,8 @@ const defaultSectionsById = computed<Record<string, ToolPanelItem> | null>(() =>
return Object.keys(validSections).length > 0 ? validSections : null;
});
const myToolsDefaultSectionsById = computed(() => defaultSectionsById.value || localSectionsById.value);
// Use composable for favorites and recent tools — we only need the bits that
// drive the search-results split (favorites get their own section in mixed
// results). The full My-Tools landing state lives inside `MyToolsLanding.vue`.
@@ -189,7 +192,10 @@ const { favoritesCollapsed, favoriteToolIdSet, recentToolIdsToShowSet } = useToo
const { favoriteResults, nonFavoriteResults, hasMixedResults } = useFavoriteSearchResults(results, favoriteToolIdSet);
const toolsCount = computed(() =>
countUniqueToolsInPanel(defaultSectionsById.value || localSectionsById.value, toolsList.value.length),
countUniqueToolsInPanel(
defaultSectionsById.value || localSectionsById.value,
countUniqueToolsInList(toolsList.value),
),
);
const resultsSet = computed(() => new Set(results.value));
@@ -436,7 +442,7 @@ function onLabelToggle(labelId: string) {
<MyToolsLanding
v-if="showMyToolsLanding"
:local-tools-by-id="localToolsById"
:default-sections-by-id="defaultSectionsById"
:default-sections-by-id="myToolsDefaultSectionsById"
:local-sections-by-id="localSectionsById"
:tools-count="toolsCount"
@onClick="onToolClick"
+84 -2
View File
@@ -38,7 +38,7 @@ const DEFAULT_VIEW_ID = "default";
const PANEL_VIEW_ERR_MSG = "Error loading panel view";
const firstTool = toolsList[0];
const toolsListWithExtraVersion = [...toolsList, { ...firstTool, id: `${firstTool?.id}/older-version` }];
const toolsListWithExtraVersion = [...toolsList, { ...firstTool, id: `${firstTool?.id}/0.9`, version: "0.9" }];
vi.mock("@/composables/config");
@@ -91,6 +91,8 @@ describe("ToolPanel", () => {
failDefault: boolean = false,
captureToolsRequest?: (url: URL) => void,
panelResponses: Record<string, unknown> = {},
defaultPanelView: string = DEFAULT_VIEW_ID,
capturePanelRequest?: (panelView: string) => void,
) {
server.use(
http.untyped.get("/api/tools", ({ request }) => {
@@ -102,7 +104,7 @@ describe("ToolPanel", () => {
return HttpResponse.json([]);
}),
http.untyped.get(TEST_PANELS_URI, () => {
return HttpResponse.json({ default_panel_view: DEFAULT_VIEW_ID, views: viewsList });
return HttpResponse.json({ default_panel_view: defaultPanelView, views: viewsList });
}),
http.get("/api/users/{user_id}", ({ response }) => {
return response(200).json(getFakeRegisteredUser());
@@ -133,6 +135,7 @@ describe("ToolPanel", () => {
server.use(
http.untyped.get(/\/api\/tool_panels\/.*/, ({ request }) => {
const panelView = new URL(request.url).pathname.split("/").pop() || "";
capturePanelRequest?.(panelView);
return HttpResponse.json(panelResponses[panelView] ?? toolsListInPanel);
}),
);
@@ -274,6 +277,85 @@ describe("ToolPanel", () => {
expect(discoverButton.text()).toBe(`Discover ${formatted} Tools`);
});
it("uses the loaded default panel count when My Tools is the backend default view", async () => {
storePanelView("");
const wrapper = await createWrapper(
"",
false,
undefined,
{
my_panel: {
favorites: {
model_class: "ToolSection",
id: "favorites",
name: "Favorites",
tools: [],
},
},
},
"my_panel",
);
const count = getUniqueToolIdsInPanel(toolsListInPanel).size;
const formatted = count < 1000 ? `${count}` : `${Math.floor(count / 1000)}k+`;
const discoverButton = wrapper.find('[data-description="toolbox discover tools"]');
expect(discoverButton.text()).toBe(`Discover ${formatted} Tools`);
expect(discoverButton.text()).not.toBe(`Discover ${toolsListWithExtraVersion.length} Tools`);
});
it("ignores My Tools panel contents for the header count when My Tools is the backend default view", async () => {
storePanelView("");
const firstToolId = firstTool?.id;
if (!firstToolId) {
throw new Error("Expected at least one tool fixture");
}
const wrapper = await createWrapper(
"",
false,
undefined,
{
my_panel: {
favorites: {
model_class: "ToolSection",
id: "favorites",
name: "Favorites",
tools: [firstToolId],
},
},
},
"my_panel",
);
const count = getUniqueToolIdsInPanel(toolsListInPanel).size;
const formatted = count < 1000 ? `${count}` : `${Math.floor(count / 1000)}k+`;
const discoverButton = wrapper.find('[data-description="toolbox discover tools"]');
expect(discoverButton.text()).toBe(`Discover ${formatted} Tools`);
expect(discoverButton.text()).not.toBe("Discover 1 Tools");
});
it("loads default panel sections when My Tools is the backend default view", async () => {
storePanelView("");
const requestedPanels: string[] = [];
await createWrapper(
"",
false,
undefined,
{
my_panel: {
favorites: {
model_class: "ToolSection",
id: "favorites",
name: "Favorites",
tools: [],
},
},
},
"my_panel",
(panelView) => requestedPanels.push(panelView),
);
expect(requestedPanels).toContain(DEFAULT_VIEW_ID);
expect(requestedPanels).toContain("my_panel");
});
it("does not request tool tags during default tool panel startup", async () => {
let toolsRequestUrl: URL | undefined;
let toolTagsRequested = false;
+11 -3
View File
@@ -9,7 +9,7 @@ import localize from "@/utils/localization";
import { errorMessageAsString } from "@/utils/simple-error";
import { MY_PANEL_VIEW_ID } from "./panelViews";
import { countUniqueToolsInPanel } from "./utilities";
import { countUniqueToolsInList, countUniqueToolsInPanel } from "./utilities";
import LoadingSpan from "../LoadingSpan.vue";
import ActivityPanel from "./ActivityPanel.vue";
@@ -36,14 +36,22 @@ const isMyPanel = computed(() => currentPanelView.value === MY_PANEL_VIEW_ID);
const errorMessage = ref("");
const panelsFetched = ref(false);
const showFavorites = ref(false);
const defaultToolSections = computed(() => {
return (
toolSections.value.default ||
(defaultPanelView.value && defaultPanelView.value !== MY_PANEL_VIEW_ID
? toolSections.value[defaultPanelView.value]
: null)
);
});
const headerToolSections = computed(() => {
if (isMyPanel.value) {
return toolSections.value[defaultPanelView.value] || toolSections.value.default || currentToolSections.value;
return defaultToolSections.value || currentToolSections.value;
}
return currentToolSections.value;
});
const toolsCount = computed(() =>
countUniqueToolsInPanel(headerToolSections.value, Object.keys(toolsById.value).length),
countUniqueToolsInPanel(headerToolSections.value, countUniqueToolsInList(toolsById.value)),
);
function formatToolsCount(count: number) {
@@ -4,6 +4,7 @@ import toolsListInPanelUntyped from "@/components/ToolsView/testData/toolsListIn
import { describe, expect, it } from "vitest";
import {
countUniqueToolsInList,
createSortedResultPanel,
createWhooshQuery,
determineWidth,
@@ -98,6 +99,12 @@ describe("test helpers in tool searching utilities", () => {
expect(createWhooshQuery({ section: '"Get Data"' })).toEqual('(section:("Get Data"))');
});
it("counts versioned tools once when falling back to the full tools list", async () => {
const firstTool = toolsList[0]!;
const toolsWithOlderVersion = [...toolsList, { ...firstTool, id: `${firstTool.id}/0.9`, version: "0.9" }];
expect(countUniqueToolsInList(toolsWithOlderVersion)).toBe(toolsList.length);
});
it("lowercases tag clauses to match the Whoosh field analyzer", async () => {
// Whoosh indexes tool_tags lowercased; lowercase the search clause too.
expect(createWhooshQuery({ tag: ["Get Data"] })).toEqual('(tool_tags:("get data"))');
+18
View File
@@ -67,6 +67,24 @@ export function getUniqueToolIdsInPanel(panel: Record<string, ToolPanelItem> | n
return toolIds;
}
type CountableTool = Pick<Tool, "id"> & Partial<Pick<Tool, "version">>;
export function getVersionlessToolId(toolId: string, tool: CountableTool | undefined) {
if (tool?.version && toolId.endsWith(`/${tool.version}`)) {
return toolId.slice(0, -tool.version.length - 1);
}
return toolId;
}
export function countUniqueToolsInList<T extends CountableTool>(tools: Record<string, T> | T[]) {
const toolEntries = Array.isArray(tools) ? tools.map((tool) => [tool.id, tool] as const) : Object.entries(tools);
const toolIds = new Set<string>();
for (const [toolId, tool] of toolEntries) {
toolIds.add(getVersionlessToolId(toolId, tool));
}
return toolIds.size;
}
export function countUniqueToolsInPanel(panel: Record<string, ToolPanelItem> | null | undefined, fallbackCount = 0) {
return getUniqueToolIdsInPanel(panel).size || fallbackCount;
}
+12 -2
View File
@@ -109,6 +109,8 @@ const MY_PANEL_VIEW: Panel = {
searchable: true,
};
const DEFAULT_PANEL_VIEW_ID = "default";
export const useToolStore = defineStore("toolStore", () => {
const currentPanelView: Ref<string> = useUserLocalStorage("tool-store-view", "");
const defaultPanelView: Ref<string> = ref("");
@@ -369,8 +371,16 @@ export const useToolStore = defineStore("toolStore", () => {
async function setPanel(panelView: string) {
try {
if (panelView === MY_PANEL_VIEW_ID && defaultPanelView.value && panelView !== defaultPanelView.value) {
await fetchToolSections(defaultPanelView.value);
if (panelView === MY_PANEL_VIEW_ID) {
const sectionedPanelView =
defaultPanelView.value && defaultPanelView.value !== MY_PANEL_VIEW_ID
? defaultPanelView.value
: panels.value[DEFAULT_PANEL_VIEW_ID]
? DEFAULT_PANEL_VIEW_ID
: null;
if (sectionedPanelView) {
await fetchToolSections(sectionedPanelView);
}
}
await fetchToolSections(panelView);
currentPanelView.value = panelView;