refactor(navigation): replace window.location with navigate helper for external URL handling across multiple components

This commit is contained in:
Ajit Mehrotra
2025-12-15 15:51:29 -05:00
parent fd1a04641a
commit 7969e44211
13 changed files with 60 additions and 21 deletions

View File

@@ -3,6 +3,7 @@ import { defineStore, storeToRefs } from 'pinia';
import { useSessionStorage } from '@vueuse/core';
import { ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY } from '~/consts';
import { navigate } from '~/helpers/external-navigation';
import { useActivationCodeDataStore } from '~/components/Activation/store/activationCodeData';
import { useCallbackActionsStore } from '~/store/callbackActions';
@@ -66,7 +67,7 @@ export const useActivationCodeModalStore = defineStore('activationCodeModal', ()
if (sequenceIndex === keySequence.length) {
setIsHidden(true);
// Redirect only if explicitly hidden via konami code, not just closed normally
window.location.href = '/Tools/Registration';
navigate('/Tools/Registration');
}
};

View File

@@ -35,6 +35,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@unraid/ui';
import { navigate } from '~/helpers/external-navigation';
import { extractGraphQLErrorMessage } from '~/helpers/functions';
import type { ApiKeyFragment, AuthAction, Role } from '~/composables/gql/graphql';
@@ -165,7 +166,7 @@ function applyTemplate() {
params.forEach((value, key) => {
authUrl.searchParams.append(key, value);
});
window.location.href = authUrl.toString();
navigate(authUrl.toString());
cancelTemplateInput();
} catch (_err) {

View File

@@ -4,6 +4,7 @@ import { storeToRefs } from 'pinia';
import { ClipboardDocumentIcon, EyeIcon, EyeSlashIcon } from '@heroicons/vue/24/outline';
import { Button, Input } from '@unraid/ui';
import { navigate } from '~/helpers/external-navigation';
import ApiKeyCreate from '~/components/ApiKey/ApiKeyCreate.vue';
import { useAuthorizationLink } from '~/composables/useAuthorizationLink.js';
@@ -93,12 +94,12 @@ const deny = () => {
if (hasValidRedirectUri.value) {
try {
const url = buildCallbackUrl(undefined, 'access_denied');
window.location.href = url;
navigate(url);
} catch {
window.location.href = '/';
navigate('/');
}
} else {
window.location.href = '/';
navigate('/');
}
};
@@ -108,7 +109,7 @@ const returnToApp = () => {
try {
const url = buildCallbackUrl(createdApiKey.value, undefined);
window.location.href = url;
navigate(url);
} catch (_err) {
error.value = 'Failed to redirect back to application';
}

View File

@@ -22,6 +22,7 @@ import { useDockerConsoleSessions } from '@/composables/useDockerConsoleSessions
import { useDockerEditNavigation } from '@/composables/useDockerEditNavigation';
import { stripLeadingSlash } from '@/utils/docker';
import { useAutoAnimate } from '@formkit/auto-animate/vue';
import { navigate } from '@/helpers/external-navigation';
import type {
DockerPortConflictsResult,
@@ -258,7 +259,7 @@ function handleAddContainerClick() {
const sanitizedPath = rawPath.replace(/\?.*$/, '').replace(/\/+$/, '');
const withoutAdd = sanitizedPath.replace(/\/AddContainer$/i, '');
const targetPath = withoutAdd ? `${withoutAdd}/AddContainer` : '/AddContainer';
window.location.assign(targetPath);
navigate(targetPath);
}
async function refreshContainers() {

View File

@@ -21,6 +21,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@unraid/ui';
import { navigate } from '~/helpers/external-navigation';
import { getReleaseNotesUrl, WEBGUI_TOOLS_DOWNGRADE, WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
import { useActivationCodeDataStore } from '~/components/Activation/store/activationCodeData';
@@ -126,7 +127,7 @@ const handleUpdateStatusClick = () => {
if (updateOsStatus.value.click) {
updateOsStatus.value.click();
} else if (updateOsStatus.value.href) {
window.location.href = updateOsStatus.value.href;
navigate(updateOsStatus.value.href);
}
};

View File

@@ -2,6 +2,8 @@
import { computed, reactive, ref, watch } from 'vue';
import { useMutation, useQuery, useSubscription } from '@vue/apollo-composable';
import { navigate } from '~/helpers/external-navigation';
import type { FragmentType } from '~/composables/gql';
import type {
NotificationFragmentFragment,
@@ -171,7 +173,7 @@ onNotificationAdded(({ data }) => {
label: 'Open',
onClick: () => {
if (notification.link) {
window.location.assign(notification.link);
navigate(notification.link);
}
},
});

View File

@@ -5,6 +5,7 @@ import { useMutation } from '@vue/apollo-composable';
import { computedAsync } from '@vueuse/core';
import { Markdown } from '@/helpers/markdown';
import { navigate } from '~/helpers/external-navigation';
import type { NotificationFragmentFragment } from '~/composables/gql/graphql';
@@ -66,7 +67,7 @@ const mutationError = computed(() => {
const openLink = () => {
if (props.link) {
window.location.assign(props.link);
navigate(props.link);
}
};

View File

@@ -3,6 +3,8 @@ import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useMutation, useQuery, useSubscription } from '@vue/apollo-composable';
import { navigate } from '~/helpers/external-navigation';
import ConfirmDialog from '~/components/ConfirmDialog.vue';
import {
archiveAllNotifications,
@@ -104,7 +106,7 @@ onNotificationAdded(({ data }) => {
const color = funcMapping[notif.importance];
const createOpener = () => ({
label: t('notifications.sidebar.toastOpen'),
onClick: () => window.location.assign(notif.link as string),
onClick: () => navigate(notif.link as string),
});
requestAnimationFrame(() =>
@@ -117,10 +119,6 @@ onNotificationAdded(({ data }) => {
);
});
const openSettings = () => {
window.location.assign('/Settings/Notifications');
};
const overview = computed(() => {
if (!result.value) {
return;
@@ -234,7 +232,7 @@ const tabs = computed(() => [
variant="ghost"
color="neutral"
icon="i-heroicons-cog-6-tooth-20-solid"
@click="openSettings"
@click="navigate('/Settings/Notifications')"
/>
</UTooltip>
</div>

View File

@@ -13,6 +13,7 @@ import {
XCircleIcon,
} from '@heroicons/vue/24/solid';
import { Badge, BrandLoading, Button } from '@unraid/ui';
import { navigate } from '~/helpers/external-navigation';
import { WEBGUI_TOOLS_REGISTRATION } from '~/helpers/urls';
import useDateTimeHelper from '~/composables/dateTime';
@@ -113,7 +114,7 @@ const checkButton = computed(() => {
const navigateToRegistration = () => {
if (typeof window !== 'undefined') {
window.location.href = WEBGUI_TOOLS_REGISTRATION;
navigate(WEBGUI_TOOLS_REGISTRATION);
}
};
</script>

View File

@@ -1,6 +1,8 @@
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { navigate } from '~/helpers/external-navigation';
export type AuthState = 'loading' | 'idle' | 'error';
export function useSsoAuth() {
@@ -75,7 +77,7 @@ export function useSsoAuth() {
// Redirect to OIDC authorization endpoint with state token and redirect URI
const authUrl = `/graphql/api/auth/oidc/authorize/${encodeURIComponent(providerId)}?state=${encodeURIComponent(state)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
window.location.href = authUrl;
navigate(authUrl);
};
const handleOAuthCallback = async () => {
@@ -120,7 +122,7 @@ export function useSsoAuth() {
// Redirect to our OIDC callback endpoint to exchange the code
const callbackUrl = `/graphql/api/auth/oidc/callback?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
window.location.href = callbackUrl;
navigate(callbackUrl);
return;
}

View File

@@ -1,4 +1,5 @@
import { featureFlags } from '@/helpers/env';
import { navigate } from '@/helpers/external-navigation';
import type { DockerContainer } from '@/composables/gql/graphql';
@@ -39,7 +40,7 @@ export function useDockerEditNavigation() {
if (!url) {
return false;
}
window.location.href = url;
navigate(url);
return true;
}

View File

@@ -0,0 +1,28 @@
/**
* Helper to ensure external URLs have a protocol
*/
function normalizeUrl(url: string): string {
// If it starts with www. and doesn't have a protocol, add https://
if (url.startsWith('www.') && !url.match(/^https?:\/\//)) {
return `https://${url}`;
}
return url;
}
/**
* Navigates to a new URL.
* Equivalent to window.location.assign()
*/
export const navigate = (url: string) => {
const target = normalizeUrl(url);
window.location.assign(target);
};
/**
* Navigates to a new URL without keeping the current page in history.
* Equivalent to window.location.replace()
*/
export const replace = (url: string) => {
const target = normalizeUrl(url);
window.location.replace(target);
};

View File

@@ -2,6 +2,7 @@ import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
import { WEBGUI_REDIRECT } from '~/helpers/urls';
import { navigate } from '~/helpers/external-navigation';
import dayjs, { extend } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
@@ -109,7 +110,7 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
// if current path is /Tools/Update, then we should redirect to /Tools
// otherwise it will redirect to the account update os page.
if (window.location.pathname === '/Tools/Update') {
window.location.href = '/Tools';
navigate('/Tools');
return;
}
// otherwise refresh the page