From 8fb835cf4010ea2af3999eea8b1cc1b9e49df67f Mon Sep 17 00:00:00 2001 From: ktx-vaidehi Date: Mon, 3 Aug 2026 14:30:35 +0530 Subject: [PATCH] feat(dashboards): reorder and inline-rename tabs on the dashboard strip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable reorderable OTabs on the live tab strip (edit-only) and wire @reorder to optimistically reorder the tab array, then persist via the same updateDashboard path the settings screen uses (TabsSettings.handleDragEnd). Snaps back + refreshes on failure (409 handled). Rename a tab in place via a hover pencil affordance or a double-click on the name. The editor reads as the tab label itself (transparent, inherits the tab text, auto-sizes) and carries no underline of its own — the tab is made active while editing so OTabs' own indicator is the single line beneath it. Enter/blur saves, Escape reverts; persists via the existing editTab helper. The edited tab opts out of drag while focused; the v-for key is fixed to tabId so tabs keep identity and animate on reorder. --- .../components/dashboards/tabs/TabList.vue | 198 ++++++++++++++++-- 1 file changed, 186 insertions(+), 12 deletions(-) diff --git a/web/src/components/dashboards/tabs/TabList.vue b/web/src/components/dashboards/tabs/TabList.vue index eaa5c44a41..ff1ada3cac 100644 --- a/web/src/components/dashboards/tabs/TabList.vue +++ b/web/src/components/dashboards/tabs/TabList.vue @@ -1,4 +1,4 @@ - + + @@ -76,11 +118,16 @@ along with this program. If not, see . import OTabs from "@/lib/navigation/Tabs/OTabs.vue"; import OTab from "@/lib/navigation/Tabs/OTab.vue"; import OButton from "@/lib/core/Button/OButton.vue"; +import OIcon from "@/lib/core/Icon/OIcon.vue"; import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue"; -import { computed, inject, ref } from "vue"; +import { computed, inject, nextTick, ref } from "vue"; import { defineComponent } from "vue"; +import { useStore } from "vuex"; +import { useI18n } from "vue-i18n"; import AddTab from "@/components/dashboards/tabs/AddTab.vue"; import { useRoute } from "vue-router"; +import { editTab, updateDashboard } from "@/utils/commons"; +import useNotifications from "@/composables/useNotifications"; export default defineComponent({ name: "TabList", @@ -89,6 +136,7 @@ export default defineComponent({ OTabs, OTab, OButton, + OIcon, OTooltip, }, props: { @@ -104,6 +152,14 @@ export default defineComponent({ emits: ["refresh"], setup(props, { emit }) { const route = useRoute(); + const store = useStore(); + const { t } = useI18n(); + const { + showPositiveNotification, + showErrorNotification, + showConfictErrorNotificationWithRefreshBtn, + } = useNotifications(); + const showAddTabDialog = ref(false); const isHovered = ref(false); @@ -114,18 +170,136 @@ export default defineComponent({ return props.dashboardData?.tabs ?? []; }); + // Reorder and rename affordances are edit-only — a view-only dashboard shows + // no grip and its names aren't editable. + const canManage = computed(() => !props.viewOnly); + + const folderId = computed(() => (route.query.folder as string) ?? "default"); + const refreshDashboard = () => { emit("refresh"); showAddTabDialog.value = false; }; + // Shared failure handling for both tab operations: surface a 409 with the + // refresh CTA, everything else as a plain error, then reload canonical data. + const notifyTabFailure = (error: any, failKey: string) => { + if (error?.response?.status === 409) { + showConfictErrorNotificationWithRefreshBtn( + error?.response?.data?.message ?? error?.message ?? t(failKey), + ); + } else { + showErrorNotification(error?.message ?? t(failKey)); + } + emit("refresh"); + }; + + // ── Reorder ──────────────────────────────────────────────────────────── + // OTabs reports the move by tab id (from/to/before); apply it optimistically + // to the live tab list so the strip re-renders (and OTabs' FLIP animates the + // slide), then persist via the same updateDashboard path the settings screen + // uses (TabsSettings.handleDragEnd) — reorder is just a new tab array order. + const onReorder = async ({ + from, + to, + before, + }: { + from: string | number; + to: string | number; + before: boolean; + }) => { + const list = [...tabs.value]; + const fromIdx = list.findIndex((tab: any) => tab.tabId === from); + const toIdx = list.findIndex((tab: any) => tab.tabId === to); + if (fromIdx === -1 || toIdx === -1) return; + + const [moved] = list.splice(fromIdx, 1); + // toIdx was computed on the pre-splice array; recompute against the target. + const insertAt = list.findIndex((tab: any) => tab.tabId === to) + (before ? 0 : 1); + list.splice(insertAt, 0, moved); + + // Optimistic: mutate the shared tab array in place so the keyed v-for moves + // the existing DOM nodes (what the FLIP animation slides). + props.dashboardData!.tabs = list; + + try { + await updateDashboard( + store, + store.state.selectedOrganization.identifier, + props.dashboardData?.dashboardId, + props.dashboardData, + folderId.value, + ); + showPositiveNotification(t("dashboard.tabsSettings.dashboardUpdated")); + } catch (error: any) { + notifyTabFailure(error, "dashboard.tabsSettings.tabReorderFailed"); + } + }; + + // ── Inline rename ────────────────────────────────────────────────────── + const editingTabId = ref(null); + const editingName = ref(""); + const renameInputRef = ref(null); + + const startRename = async (tab: any) => { + // Activate the tab being renamed so OTabs' active indicator sits under the + // input (double-click already selects it; the hover pencil path needs this). + selectedTabId.value = tab.tabId; + editingTabId.value = tab.tabId; + editingName.value = tab?.name ?? ""; + // Focus (and select) the freshly-mounted input so typing replaces the name. + await nextTick(); + const el = Array.isArray(renameInputRef.value) + ? renameInputRef.value[0] + : renameInputRef.value; + el?.focus(); + el?.select(); + }; + + const cancelRename = () => { + editingTabId.value = null; + editingName.value = ""; + }; + + const commitRename = async (tab: any) => { + // Enter closes the field, so the follow-up blur re-enters here with the tab + // no longer active — that early-returns, keeping the save single. + if (editingTabId.value !== tab.tabId) return; + const name = editingName.value.trim(); + editingTabId.value = null; + // Nothing to save: empty or unchanged → keep the old name. + if (!name || name === tab?.name) { + editingName.value = ""; + return; + } + + try { + await editTab(store, props.dashboardData?.dashboardId, folderId.value, tab.tabId, { name }); + emit("refresh"); + showPositiveNotification(t("dashboard.tabsSettings.tabUpdated")); + } catch (error: any) { + notifyTabFailure(error, "dashboard.tabsSettings.tabUpdationFailed"); + } finally { + editingName.value = ""; + } + }; + return { + t, showAddTabDialog, refreshDashboard, tabs, route, isHovered, selectedTabId, + canManage, + onReorder, + editingTabId, + editingName, + renameInputRef, + startRename, + commitRename, + cancelRename, }; }, });