From fbb284dc4ac43d8c0131b6622eabed3bdc3401a0 Mon Sep 17 00:00:00 2001 From: cstns Date: Mon, 5 Oct 2026 12:42:19 +0300 Subject: [PATCH] Pick the editor landing tab from the team that owns the instance or device The immersive editor routes decide between the Expert and Overview tab in a route redirect, which runs before the instance or device is loaded. At that point the active team is whatever the session last had, or nothing at all, so a team with AI disabled could still land on the Expert tab. Re-check the landing tab once the entity and its owning team are loaded. Move off Expert when that team has it disabled, or onto it when the redirect only picked Overview because the owning team was not known yet. Closes #8749 --- frontend/src/composables/EditorLandingTab.js | 35 ++++++++++++++++++++ frontend/src/mixins/Instance.js | 4 ++- frontend/src/pages/device/Editor/index.vue | 15 ++++++++- frontend/src/pages/instance/Editor/index.vue | 12 ++++++- 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 frontend/src/composables/EditorLandingTab.js diff --git a/frontend/src/composables/EditorLandingTab.js b/frontend/src/composables/EditorLandingTab.js new file mode 100644 index 0000000000..1d15a3e134 --- /dev/null +++ b/frontend/src/composables/EditorLandingTab.js @@ -0,0 +1,35 @@ +import { useRoute, useRouter } from 'vue-router' + +import { useAccountSettingsStore } from '@/stores/account-settings.js' + +/** + * The immersive editor routes choose their landing tab (Expert or Overview) in a route + * redirect, which runs before the instance or device has been loaded. At that point the + * active team is whatever the session last had, or nothing at all, rather than the team + * that owns the entity being opened. + * + * Call `syncLandingTab` once the entity and its team are loaded. It moves away from the + * Expert tab when the owning team has Expert disabled, and onto it when the redirect + * landed on Overview only because the owning team was not yet known. + */ +export function useEditorLandingTab ({ editorRouteName, expertRouteName, overviewRouteName }) { + const route = useRoute() + const router = useRouter() + const accountSettings = useAccountSettingsStore() + + function syncLandingTab () { + const { isExpertAssistantFeatureEnabled, isExpertInsightsFeatureEnabled } = accountSettings.featuresCheck + const expertEnabled = isExpertAssistantFeatureEnabled || isExpertInsightsFeatureEnabled + const onExpert = route.name === expertRouteName + const landedViaRedirect = route.redirectedFrom?.name === editorRouteName + + if (onExpert && !expertEnabled) { + return router.replace({ name: overviewRouteName, params: route.params }) + } + if (!onExpert && expertEnabled && landedViaRedirect) { + return router.replace({ name: expertRouteName, params: route.params }) + } + } + + return { syncLandingTab } +} diff --git a/frontend/src/mixins/Instance.js b/frontend/src/mixins/Instance.js index 01e0bf0aee..9a217877c8 100644 --- a/frontend/src/mixins/Instance.js +++ b/frontend/src/mixins/Instance.js @@ -78,8 +78,10 @@ export default { } try { const data = await InstanceApi.getInstance(instanceId) + // Load the owning team before exposing the instance, so anything + // reacting to the instance sees the right team and feature checks. + await useAccountStore().setTeam(data.team.slug) this.instance = { ...{ deviceSettings: {} }, ...this.instance, ...data } - useAccountStore().setTeam(this.instance.team.slug) this.instance.deviceSettings = await InstanceApi.getInstanceDeviceSettings(instanceId) if (this.instance.deviceSettings?.targetSnapshot) { this.instance.targetSnapshot = await SnapshotApi.getSnapshot(instanceId, this.instance.deviceSettings.targetSnapshot) diff --git a/frontend/src/pages/device/Editor/index.vue b/frontend/src/pages/device/Editor/index.vue index 3b2052fb20..bdb3449cbb 100644 --- a/frontend/src/pages/device/Editor/index.vue +++ b/frontend/src/pages/device/Editor/index.vue @@ -71,6 +71,7 @@ import DrawerTrigger from '../../../components/immersive-editor/DrawerTrigger.vu import EditorDrawer from '../../../components/immersive-editor/EditorDrawer.vue' import EditorWrapper from '../../../components/immersive-editor/RemoteInstanceEditorWrapper.vue' import { useDeviceHelper } from '../../../composables/DeviceHelper.js' +import { useEditorLandingTab } from '../../../composables/EditorLandingTab.js' import usePermissions from '../../../composables/Permissions.js' import Alerts from '../../../services/alerts.js' import { isInstanceOnNR5Plus } from '../../../utils/instanceVersion' @@ -115,7 +116,14 @@ export default { getDeviceEditorProxy } = useDeviceHelper() + const { syncLandingTab } = useEditorLandingTab({ + editorRouteName: 'device-editor', + expertRouteName: 'device-editor-expert', + overviewRouteName: 'device-editor-overview' + }) + return { + syncLandingTab, device, hasPermission, restartDevice, @@ -303,8 +311,13 @@ export default { } } + // Load the owning team before exposing the device, so the device + // watcher and the landing tab check see the right feature checks. + await useAccountStore().setTeam(device.team.slug) this.device = device - await useAccountStore().setTeam(this.device.team.slug) + if (this.isEditorAvailable) { + this.syncLandingTab() + } }, showConfirmDeleteDialog () { this.showDeleteDeviceDialog() diff --git a/frontend/src/pages/instance/Editor/index.vue b/frontend/src/pages/instance/Editor/index.vue index eab5c62dcc..8f431dd645 100644 --- a/frontend/src/pages/instance/Editor/index.vue +++ b/frontend/src/pages/instance/Editor/index.vue @@ -59,6 +59,7 @@ import DrawerTrigger from '../../../components/immersive-editor/DrawerTrigger.vu import EditorDrawer from '../../../components/immersive-editor/EditorDrawer.vue' import EditorWrapper from '../../../components/immersive-editor/HostedInstanceEditorWrapper.vue' import InstanceActionsButton from '../../../components/instance/ActionButton.vue' +import { useEditorLandingTab } from '../../../composables/EditorLandingTab.js' import usePermissions from '../../../composables/Permissions.js' import instanceMixin from '../../../mixins/Instance.js' @@ -85,10 +86,16 @@ export default { mixins: [instanceMixin], setup () { const { hasAMinimumTeamRoleOf, isVisitingAdmin } = usePermissions() + const { syncLandingTab } = useEditorLandingTab({ + editorRouteName: 'instance-editor', + expertRouteName: 'instance-editor-expert', + overviewRouteName: 'instance-editor-overview' + }) return { isVisitingAdmin, - hasAMinimumTeamRoleOf + hasAMinimumTeamRoleOf, + syncLandingTab } }, data () { @@ -177,6 +184,9 @@ export default { watch: { instance (instance) { this.setInstance(instance) + }, + 'instance.id' () { + this.syncLandingTab() } }, unmounted () {