diff --git a/apps/client/src/AppRouter.tsx b/apps/client/src/AppRouter.tsx index b8e42c73a..71eb86541 100644 --- a/apps/client/src/AppRouter.tsx +++ b/apps/client/src/AppRouter.tsx @@ -1,8 +1,7 @@ -import { lazy, Suspense, useEffect } from 'react'; -import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; - -import useAliases from './common/hooks-query/useAliases'; +import { lazy, Suspense } from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; import withData from './features/viewers/ViewWrapper'; +import withAlias from './features/AliasWrapper'; const Editor = lazy(() => import('./features/editors/ProtectedEditor')); const Cuesheet = lazy(() => import('./features/cuesheet/ProtectedCuesheet')); @@ -17,14 +16,14 @@ const Public = lazy(() => import('./features/viewers/public/Public')); const Lower = lazy(() => import('./features/viewers/lower-thirds/LowerWrapper')); const StudioClock = lazy(() => import('./features/viewers/studio/StudioClock')); -const STimer = withData(TimerView); -const SMinimalTimer = withData(MinimalTimerView); -const SClock = withData(ClockView); -const SCountdown = withData(Countdown); -const SBackstage = withData(Backstage); -const SPublic = withData(Public); -const SLowerThird = withData(Lower); -const SStudio = withData(StudioClock); +const STimer = withAlias(withData(TimerView)); +const SMinimalTimer = withAlias(withData(MinimalTimerView)); +const SClock = withAlias(withData(ClockView)); +const SCountdown = withAlias(withData(Countdown)); +const SBackstage = withAlias(withData(Backstage)); +const SPublic = withAlias(withData(Public)); +const SLowerThird = withAlias(withData(Lower)); +const SStudio = withAlias(withData(StudioClock)); const EditorFeatureWrapper = lazy(() => import('./features/EditorFeatureWrapper')); const RundownPanel = lazy(() => import('./features/rundown/RundownExport')); @@ -33,22 +32,6 @@ const MessageControl = lazy(() => import('./features/control/message/MessageCont const Info = lazy(() => import('./features/info/InfoExport')); export default function AppRouter() { - const { data } = useAliases(); - const location = useLocation(); - const navigate = useNavigate(); - - // navigate if is alias route - useEffect(() => { - if (!data) return; - - for (const d of data) { - if (`/${d.alias}` === location.pathname && d.enabled) { - navigate(`/${d.pathAndParams}`); - break; - } - } - }, [data, location, navigate]); - return ( diff --git a/apps/client/src/common/utils/__tests__/aliases.test.js b/apps/client/src/common/utils/__tests__/aliases.test.js index 9dee998d5..5a50a004b 100644 --- a/apps/client/src/common/utils/__tests__/aliases.test.js +++ b/apps/client/src/common/utils/__tests__/aliases.test.js @@ -1,4 +1,5 @@ -import { validateAlias } from '../aliases'; +import { resolvePath } from 'react-router-dom'; +import { validateAlias, generateURLFromAlias, getAliasRoute } from '../aliases'; describe('An alias fails if incorrect', () => { const testsToFail = [ @@ -23,3 +24,79 @@ describe('An alias fails if incorrect', () => { }), ); }); +describe('generateURLFromAlias and getAliasRoute function', () => { + test('generate the expected url from an alias', () => { + const testData = [ + { + enabled: true, + alias: 'demopage', + pathAndParams: '/timer?user=guest', + }, + ]; + + const expected = [ + { + url: '/timer?user=guest&alias=demopage', + }, + ]; + + expect(generateURLFromAlias(testData[0])).toStrictEqual(expected[0].url); + }); + test('generate the url to redirect to when the current URL is just the alias', () => { + const aliases = [ + { + enabled: true, + alias: 'demopage', + pathAndParams: '/timer?user=guest', + }, + ]; + // let current location be the alias + const location = resolvePath(aliases[0].alias); + + const expected = [ + { + url: '/timer?user=guest&alias=demopage', + }, + ]; + + expect(getAliasRoute(location, aliases, null)).toStrictEqual(expected[0].url); + }); + test('generate the url to redirect to when the current URL the same url but with a change of params', () => { + const aliases = [ + { + enabled: true, + alias: 'demopage', + pathAndParams: '/timer?user=guest', + }, + ]; + // let current location be the actual url with alias attached to it + const location = resolvePath(aliases[0].pathAndParams); + const urlSearchParams = new URLSearchParams(location.search); + urlSearchParams.append('alias', aliases[0].alias); // + + // update current alias with extra param + aliases[0].pathAndParams += '&eventId=674'; + const expected = [ + { + url: '/timer?user=guest&eventId=674&alias=demopage', + }, + ]; + + expect(getAliasRoute(location, aliases, urlSearchParams)).toStrictEqual(expected[0].url); + }); + test('generate no url to redirect to when the current URL the same url', () => { + const aliases = [ + { + enabled: true, + alias: 'demopage', + pathAndParams: '/timer?user=guest', + }, + ]; + // let current location be the actual url with alias attached to it + const location = resolvePath(aliases[0].pathAndParams); + const urlSearchParams = new URLSearchParams(location.search); + urlSearchParams.append('alias', aliases[0].alias); // + + expect(getAliasRoute(location, aliases, urlSearchParams)).toBeNull(); + }); +}); diff --git a/apps/client/src/common/utils/aliases.ts b/apps/client/src/common/utils/aliases.ts index a6a56bb11..4478501bb 100644 --- a/apps/client/src/common/utils/aliases.ts +++ b/apps/client/src/common/utils/aliases.ts @@ -1,10 +1,13 @@ +import { Alias } from 'ontime-types'; +import isEqual from 'react-fast-compare'; +import { Location, resolvePath } from 'react-router-dom'; + /** * Validates an alias against defined parameters * @param {string} alias * @returns {{message: string, status: boolean}} */ export const validateAlias = (alias: string) => { - const valid = { status: true, message: 'ok' }; if (alias === '' || alias == null) { @@ -26,4 +29,49 @@ export const validateAlias = (alias: string) => { } return valid; -}; \ No newline at end of file +}; + +/** + * Gets the URL to send an alias to + * @param location + * @param data + * @param searchParams + */ +export const getAliasRoute = (location: Location, data: Alias[], searchParams: URLSearchParams) => { + const currentURL = location.pathname.substring(1); + // we need to check if the whole url here is an alias, so we can redirect + const foundAlias = data.filter((d) => d.alias === currentURL && d.enabled)[0]; + if (foundAlias) { + return generateURLFromAlias(foundAlias); + } + const aliasOnPage = searchParams.get('alias'); + for (const d of data) { + if (aliasOnPage) { + // if the alias fits the alias on this page, but the URL is diferent, we redirect user to the new URL + // if we have the same alias and its enabled and its not empty + if (d.alias !== '' && d.enabled && d.alias === aliasOnPage) { + const newAliasPath = resolvePath(d.pathAndParams); + const urlParams = new URLSearchParams(newAliasPath.search); + urlParams.set('alias', d.alias); + // we confirm either the url parameters does not match or the url path doesnt + if (!isEqual(urlParams, searchParams) || newAliasPath.pathname !== location.pathname) { + // we then redirect to the alias route, since the view listening to this alias has an outdated URL + return `${newAliasPath.pathname}?${urlParams}`; + } + } + } + } + return null; +}; + +/** + * Generate URL from an alias + * @param aliasData + */ +export const generateURLFromAlias = (aliasData: Alias) => { + const newAliasPath = resolvePath(aliasData.pathAndParams); + const urlParams = new URLSearchParams(newAliasPath.search); + urlParams.set('alias', aliasData.alias); + + return `${newAliasPath.pathname}?${urlParams}`; +}; diff --git a/apps/client/src/features/AliasWrapper.tsx b/apps/client/src/features/AliasWrapper.tsx new file mode 100644 index 000000000..b406e8b07 --- /dev/null +++ b/apps/client/src/features/AliasWrapper.tsx @@ -0,0 +1,28 @@ +/* eslint-disable react/display-name */ +import useAliases from '../common/hooks-query/useAliases'; +import { getAliasRoute } from '../common/utils/aliases'; +import { ComponentType, useEffect } from 'react'; +import { useSearchParams, useNavigate, useLocation } from 'react-router-dom'; + +const withAlias =

(Component: ComponentType

) => { + return (props: Partial

) => { + const { data } = useAliases(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const location = useLocation(); + + // navigate if is alias route + useEffect(() => { + if (!data) return; + const url = getAliasRoute(location, data, searchParams); + // navigate to this route if its not empty + if (url) { + navigate(url); + } + }, [data, searchParams, navigate, location]); + + return ; + }; +}; + +export default withAlias; diff --git a/apps/client/src/features/modals/settings-modal/AliasesForm.tsx b/apps/client/src/features/modals/settings-modal/AliasesForm.tsx index d25730737..e70837c00 100644 --- a/apps/client/src/features/modals/settings-modal/AliasesForm.tsx +++ b/apps/client/src/features/modals/settings-modal/AliasesForm.tsx @@ -131,7 +131,7 @@ export default function AliasesForm() { isDisabled={disableInputs} /> handleLinks(event, alias.pathAndParams)} + clickHandler={(event) => handleLinks(event, alias.alias)} tooltip='Test alias' aria-label='Test alias' size='xs'