From 720c327e84098c09415c371aae9185a26876d33b Mon Sep 17 00:00:00 2001 From: Bjoern Gruening Date: Mon, 5 Jan 2026 19:58:42 +0100 Subject: [PATCH 01/30] add new tool panel --- .../BaseComponents/Form/GFormInput.vue | 14 +- client/src/components/Panels/Common/Tool.vue | 41 +++- .../Panels/Common/ToolPanelLabel.vue | 44 +++- .../components/Panels/Common/ToolSection.vue | 4 + .../components/Panels/Menus/PanelViewMenu.vue | 12 +- client/src/components/Panels/ToolBox.vue | 212 ++++++++++++++++-- client/src/components/Panels/ToolPanel.vue | 7 +- .../components/Panels/testData/viewsList.json | 10 +- client/src/components/Panels/utilities.ts | 2 + client/src/stores/toolStore.ts | 51 ++++- 10 files changed, 360 insertions(+), 37 deletions(-) diff --git a/client/src/components/BaseComponents/Form/GFormInput.vue b/client/src/components/BaseComponents/Form/GFormInput.vue index e810350c1c1..2df280c0952 100644 --- a/client/src/components/BaseComponents/Form/GFormInput.vue +++ b/client/src/components/BaseComponents/Form/GFormInput.vue @@ -1,5 +1,5 @@ diff --git a/client/src/components/Panels/Common/ToolPanelLabel.vue b/client/src/components/Panels/Common/ToolPanelLabel.vue index 832713bdcaf..06fc0bf0386 100644 --- a/client/src/components/Panels/Common/ToolPanelLabel.vue +++ b/client/src/components/Panels/Common/ToolPanelLabel.vue @@ -1,4 +1,6 @@ @@ -33,4 +48,29 @@ const description = computed(() => props.definition.description || undefined); } } } + +.tool-panel-label.tool-panel-label-divider { + align-items: center; + background: transparent; + border-left: 0; + display: flex; + gap: 0.5rem; + padding: 0.375rem 0.75rem; + text-transform: none; + + &::before, + &::after { + border-bottom: 1px solid currentColor; + content: ""; + flex: 1; + opacity: 0.35; + } +} + +.tool-panel-label-divider-text { + align-items: center; + display: inline-flex; + gap: 0.25rem; + white-space: nowrap; +} diff --git a/client/src/components/Panels/Common/ToolSection.vue b/client/src/components/Panels/Common/ToolSection.vue index cb4528282ea..b7b8d88589d 100644 --- a/client/src/components/Panels/Common/ToolSection.vue +++ b/client/src/components/Panels/Common/ToolSection.vue @@ -32,6 +32,7 @@ interface Props { expanded?: boolean; sortItems?: boolean; hasFilterButton?: boolean; + searchActive?: boolean; } const props = withDefaults(defineProps(), { @@ -45,6 +46,7 @@ const props = withDefaults(defineProps(), { expanded: false, sortItems: true, hasFilterButton: false, + searchActive: false, }); const { config, isConfigLoaded } = useConfig(); @@ -194,6 +196,7 @@ function toggleMenu(nextState = !opened.value) { :hide-name="hideName" :operation-title="operationTitle" :operation-icon="operationIcon" + :show-favorite-button="searchActive" @onOperation="onOperation" @onClick="onClick" /> @@ -208,6 +211,7 @@ function toggleMenu(nextState = !opened.value) { :hide-name="hideName" :operation-title="operationTitle" :operation-icon="operationIcon" + :show-favorite-button="searchActive" @onOperation="onOperation" @onClick="onClick" /> diff --git a/client/src/components/Panels/Menus/PanelViewMenu.vue b/client/src/components/Panels/Menus/PanelViewMenu.vue index 42bc4ca2755..40334e01081 100644 --- a/client/src/components/Panels/Menus/PanelViewMenu.vue +++ b/client/src/components/Panels/Menus/PanelViewMenu.vue @@ -21,6 +21,7 @@ const groupsDefinitions = [ { type: "publication", title: "...from Publication" }, { type: "training", title: "...for Training" }, ]; +const groupedPanelViewTypes = new Set(groupsDefinitions.map((group) => group.type)); const props = defineProps<{ /** Whether the menu is compact: doesn't take up full width of the parent when `true`. @@ -81,7 +82,16 @@ const groupedPanelViews = computed(() => { return groups; }); -const ungroupedPanelViews = computed(() => panelViewsOfType("generic")); +const ungroupedPanelViews = computed(() => { + const defaultPanelId = defaultPanelView.value?.id; + return Object.values(panels.value).filter((panel) => { + if (!panel || panel.id === defaultPanelId) { + return false; + } + const viewType = panel.view_type; + return viewType ? !groupedPanelViewTypes.has(viewType) : false; + }); +}); function panelViewsOfType(panelViewType: string) { const panelViews = []; diff --git a/client/src/components/Panels/ToolBox.vue b/client/src/components/Panels/ToolBox.vue index c03f8f7ece7..40a4d75e656 100644 --- a/client/src/components/Panels/ToolBox.vue +++ b/client/src/components/Panels/ToolBox.vue @@ -1,14 +1,16 @@ @@ -70,7 +151,31 @@ const isFavoritesDivider = computed(() => props.definition.id === FAVORITES_LABE .tool-panel-label-divider-text { align-items: center; display: inline-flex; - gap: 0.25rem; + gap: 0.5rem; white-space: nowrap; } + +.tool-panel-label-actionable { + align-items: center; + display: flex; + gap: 0.5rem; + justify-content: space-between; +} + +.tool-panel-label-action { + margin-left: auto; +} + +.tool-panel-label-divider-action { + margin-left: 0; +} + +.tool-panel-label-toggle { + opacity: 0.8; +} + +.tool-panel-label-clickable { + cursor: pointer; + user-select: none; +} diff --git a/client/src/components/Panels/Common/ToolSection.vue b/client/src/components/Panels/Common/ToolSection.vue index b7b8d88589d..3222fd86b1b 100644 --- a/client/src/components/Panels/Common/ToolSection.vue +++ b/client/src/components/Panels/Common/ToolSection.vue @@ -16,6 +16,7 @@ const emit = defineEmits<{ (e: "onClick", tool: any, evt: Event): void; (e: "onFilter", filter: string): void; (e: "onOperation", tool: any, evt: Event): void; + (e: "onLabelToggle", labelId: string): void; }>(); const eventBus = useEventBus("open-tool-section"); @@ -33,6 +34,8 @@ interface Props { sortItems?: boolean; hasFilterButton?: boolean; searchActive?: boolean; + showFavoriteButton?: boolean; + collapsedLabels?: Record; } const props = withDefaults(defineProps(), { @@ -47,6 +50,8 @@ const props = withDefaults(defineProps(), { sortItems: true, hasFilterButton: false, searchActive: false, + showFavoriteButton: false, + collapsedLabels: () => ({}), }); const { config, isConfigLoaded } = useConfig(); @@ -149,6 +154,9 @@ function onClick(tool: any, evt: Event) { function onOperation(tool: any, evt: Event) { emit("onOperation", tool, evt); } +function onLabelToggle(labelId: string) { + emit("onLabelToggle", labelId); +} function toggleMenu(nextState = !opened.value) { opened.value = nextState; } @@ -186,7 +194,9 @@ function toggleMenu(nextState = !opened.value) { + :definition="el" + :collapsed="props.collapsedLabels[el.id]" + @toggle="onLabelToggle" /> @@ -204,14 +214,18 @@ function toggleMenu(nextState = !opened.value) {
- +
diff --git a/client/src/components/Panels/ToolBox.vue b/client/src/components/Panels/ToolBox.vue index 40a4d75e656..fe96503d50d 100644 --- a/client/src/components/Panels/ToolBox.vue +++ b/client/src/components/Panels/ToolBox.vue @@ -7,7 +7,6 @@ import { computed, type ComputedRef, type Ref, ref, watch } from "vue"; import { useGlobalUploadModal } from "@/composables/globalUploadModal"; import { useToolRouting } from "@/composables/route"; -import { getAppRoot } from "@/onload/loadConfig"; import type { Tool, ToolSection as ToolSectionType, ToolSectionLabel } from "@/stores/toolStore"; import { useToolStore } from "@/stores/toolStore"; import { useUserStore } from "@/stores/userStore"; @@ -57,7 +56,7 @@ const resultPanel: Ref | null> = ref(null const closestTerm: Ref = ref(null); const toolStore = useToolStore(); -const { currentFavorites } = storeToRefs(useUserStore()); +const { currentFavorites, recentTools } = storeToRefs(useUserStore()); const { currentPanelView, currentToolSections, defaultPanelView, toolSections } = storeToRefs(toolStore); const hasResults = computed(() => results.value.length > 0); @@ -130,11 +129,20 @@ type PanelItem = Tool | ToolSectionType | ToolSectionLabel; const favoriteToolIds = computed(() => currentFavorites.value.tools || []); const favoriteToolIdSet = computed(() => new Set(favoriteToolIds.value)); +const favoriteToolIdsInPanel = computed(() => + favoriteToolIds.value.filter((toolId) => !!localToolsById.value[toolId]), +); +const recentToolIds = computed(() => recentTools.value || []); +const recentToolIdsInPanel = computed(() => recentToolIds.value.filter((toolId) => !!localToolsById.value[toolId])); +const recentToolIdsToShow = computed(() => + recentToolIdsInPanel.value.filter((toolId) => !favoriteToolIdSet.value.has(toolId)), +); +const recentToolIdsToShowSet = computed(() => new Set(recentToolIdsToShow.value)); const favoriteResults = computed(() => results.value.filter((id) => favoriteToolIdSet.value.has(id))); const nonFavoriteResults = computed(() => results.value.filter((id) => !favoriteToolIdSet.value.has(id))); const hasMixedResults = computed(() => favoriteResults.value.length > 0 && nonFavoriteResults.value.length > 0); -const toolsListHref = computed(() => `${getAppRoot()}tools/list`); +const toolsListRoute = "/tools/list"; const toolsCount = computed(() => toolsList.value.length); const showEmptyFavorites = computed( () => props.favoritesDefault && !query.value && favoriteToolIds.value.length === 0, @@ -143,6 +151,12 @@ const showEmptyFavorites = computed( const FAVORITES_RESULTS_SECTION_ID = "favorites_results"; const FAVORITES_RESULTS_LABEL_ID = "favorites_results_label"; const SEARCH_RESULTS_LABEL_ID = "search_results_label"; +const FAVORITES_LABEL_ID = "favorites_label"; +const RECENT_TOOLS_LABEL_ID = "recent_tools_label"; +const FAVORITES_EMPTY_ALERT_ID = "favorites_empty_alert"; + +const favoritesCollapsed = ref(false); +const recentToolsCollapsed = ref(false); function buildToolSection(id: string, name: string, tools: string[]): ToolSectionType { return { @@ -224,10 +238,12 @@ const flatResultsPanel = computed | null>(() => { } const entries: Array<[string, PanelItem]> = []; entries.push([FAVORITES_RESULTS_LABEL_ID, buildToolLabel(FAVORITES_RESULTS_LABEL_ID, localize("Favorites"))]); - for (const toolId of favoriteResults.value) { - const tool = localToolsById.value[toolId]; - if (tool) { - entries.push([toolId, tool]); + if (!favoritesCollapsed.value) { + for (const toolId of favoriteResults.value) { + const tool = localToolsById.value[toolId]; + if (tool) { + entries.push([toolId, tool]); + } } } entries.push([SEARCH_RESULTS_LABEL_ID, buildToolLabel(SEARCH_RESULTS_LABEL_ID, localize("Search results"))]); @@ -240,6 +256,41 @@ const flatResultsPanel = computed | null>(() => { return Object.fromEntries(entries); }); +const favoritesDefaultPanel = computed | null>(() => { + if (!props.favoritesDefault || query.value) { + return null; + } + const entries: Array<[string, PanelItem]> = []; + const favorites = favoriteToolIdsInPanel.value; + const recents = recentToolIdsToShow.value; + + entries.push([FAVORITES_LABEL_ID, buildToolLabel(FAVORITES_LABEL_ID, localize("Favorites"))]); + if (!favoritesCollapsed.value) { + if (showEmptyFavorites.value) { + entries.push([FAVORITES_EMPTY_ALERT_ID, buildToolLabel(FAVORITES_EMPTY_ALERT_ID, "")]); + } else { + for (const toolId of favorites) { + const tool = localToolsById.value[toolId]; + if (tool) { + entries.push([toolId, tool]); + } + } + } + } + if (recents.length > 0) { + entries.push([RECENT_TOOLS_LABEL_ID, buildToolLabel(RECENT_TOOLS_LABEL_ID, localize("Recent tools"))]); + if (!recentToolsCollapsed.value) { + for (const toolId of recents) { + const tool = localToolsById.value[toolId]; + if (tool) { + entries.push([toolId, tool]); + } + } + } + } + return Object.fromEntries(entries); +}); + const sectionedResultsPanel = computed | null>(() => { if (!hasResults.value) { return null; @@ -274,7 +325,7 @@ const sectionedResultsPanel = computed | */ const localPanel: ComputedRef | null> = computed(() => { if (props.favoritesDefault && !query.value) { - return filterTools(localToolsById.value, favoriteToolIds.value) as Record; + return favoritesDefaultPanel.value || {}; } if (hasResults.value) { if (showSections.value) { @@ -345,6 +396,27 @@ function onSearchQuery(q: string) { function onToggle() { showSections.value = !showSections.value; } + +const collapsedLabels = computed(() => + props.favoritesDefault + ? { + [FAVORITES_LABEL_ID]: favoritesCollapsed.value, + [FAVORITES_RESULTS_LABEL_ID]: favoritesCollapsed.value, + [RECENT_TOOLS_LABEL_ID]: recentToolsCollapsed.value, + } + : {}, +); + +function onLabelToggle(labelId: string) { + if (!props.favoritesDefault) { + return; + } + if (labelId === FAVORITES_LABEL_ID || labelId === FAVORITES_RESULTS_LABEL_ID) { + favoritesCollapsed.value = !favoritesCollapsed.value; + } else if (labelId === RECENT_TOOLS_LABEL_ID) { + recentToolsCollapsed.value = !recentToolsCollapsed.value; + } +}