refactor: simplify url preset logic

This commit is contained in:
Carlos Valente
2025-01-31 21:19:25 +01:00
committed by Carlos Valente
parent 430bfb38f2
commit f0e6231338
3 changed files with 87 additions and 116 deletions
@@ -1,8 +1,8 @@
import { resolvePath } from 'react-router-dom';
import { generateUrlFromPreset, getRouteFromPreset, validateUrlPresetPath } from '../urlPresets';
import { generatePathFromPreset, getRouteFromPreset, validateUrlPresetPath } from '../urlPresets';
describe('A preset fails if incorrect', () => {
describe('validateUrlPresetPaths()', () => {
test.each([
// no empty
'',
@@ -17,87 +17,56 @@ describe('A preset fails if incorrect', () => {
// no editor
'editor',
'editor?test',
])('validateUrlPresetPath(%s) should return false', (t) => {
])('flags known edge cases: %s', (t) => {
expect(validateUrlPresetPath(t).isValid).toBeFalsy();
});
});
describe('generateUrlFromPreset and getRouteFromPreset function', () => {
test('generate the expected url from an alias', () => {
const testData = [
{
enabled: true,
alias: 'demopage',
pathAndParams: '/timer?user=guest',
},
];
describe('getRouteFromPreset()', () => {
const presets = [
{
enabled: true,
alias: 'demopage',
pathAndParams: '/timer?user=guest',
},
];
const expected = [
{
url: '/timer?user=guest&alias=demopage',
},
];
expect(generateUrlFromPreset(testData[0])).toStrictEqual(expected[0].url);
it('checks if the current location matches an enabled preset', () => {
// we make the current location be the alias
const location = resolvePath('demopage');
expect(getRouteFromPreset(location, presets)).toStrictEqual('timer?user=guest&alias=demopage');
});
test('generate the url to redirect to when the current URL is just the alias', () => {
const presets = [
{
enabled: true,
alias: 'demopage',
pathAndParams: '/timer?user=guest',
},
];
// let current location be the alias
const location = resolvePath(presets[0].alias);
const expected = [
{
url: '/timer?user=guest&alias=demopage',
},
];
// @ts-expect-error -- using a Path as a Location
expect(getRouteFromPreset(location, presets, null)).toStrictEqual(expected[0].url);
it('returns null if the current location is the exact match of an unwrapped alias', () => {
// we make the current location be the alias
const location = resolvePath('/timer?user=guest&alias=demopage');
expect(getRouteFromPreset(location, presets)).toEqual(null);
});
test('generate the url to redirect to when the current URL the same url but with a change of params', () => {
const presets = [
{
enabled: true,
alias: 'demopage',
pathAndParams: '/timer?user=guest',
},
];
// let current location be the actual url with alias attached to it
const location = resolvePath(presets[0].pathAndParams);
const urlSearchParams = new URLSearchParams(location.search);
urlSearchParams.append('alias', presets[0].alias); //
// update current alias with extra param
presets[0].pathAndParams += '&eventId=674';
const expected = [
{
url: '/timer?user=guest&eventId=674&alias=demopage',
},
];
// @ts-expect-error -- using a Path as a Location
expect(getRouteFromPreset(location, presets, urlSearchParams)).toStrictEqual(expected[0].url);
it('returns a new destination if the current location is an out-of-date unwrapped alias', () => {
// we make the current location be the alias
const location = resolvePath('/timer?user=admin&alias=demopage');
expect(getRouteFromPreset(location, presets)).toEqual('timer?user=guest&alias=demopage');
});
test('generate no url to redirect to when the current URL the same url', () => {
const presets = [
{
enabled: true,
alias: 'demopage',
pathAndParams: '/timer?user=guest',
},
];
// let current location be the actual url with alias attached to it
const location = resolvePath(presets[0].pathAndParams);
const urlSearchParams = new URLSearchParams(location.search);
urlSearchParams.append('alias', presets[0].alias);
// @ts-expect-error -- using a Path as a Location
expect(getRouteFromPreset(location, presets, urlSearchParams)).toBeNull();
it('checks if the current location contains an unwrapped preset', () => {
// we make the current location be the alias
const location = resolvePath('/timer?user=guest&alias=demopage');
expect(getRouteFromPreset(location, presets)).toEqual(null);
});
it('ignores a location that has no presets', () => {
// we make the current location be the alias
const location = resolvePath('/unknown');
expect(getRouteFromPreset(location, presets)).toEqual(null);
});
});
describe('generatePathFromPreset()', () => {
test.each([
['timer?user=guest', 'demopage', 'timer?user=guest&alias=demopage'],
['timer?user=admin', 'demopage', 'timer?user=admin&alias=demopage'],
])('generates a path from a preset: %s', (path, alias, expected) => {
expect(generatePathFromPreset(path, alias)).toEqual(expected);
});
});
+38 -36
View File
@@ -1,13 +1,11 @@
import isEqual from 'react-fast-compare';
import { Location, resolvePath } from 'react-router-dom';
import { Path, resolvePath } from 'react-router-dom';
import { URLPreset } from 'ontime-types';
/**
* Validates a preset against defined parameters
* @param {string} preset
* @returns {{message: string, isValid: boolean}}
* Used in the context of form validation
*/
export const validateUrlPresetPath = (preset: string): { message: string; isValid: boolean } => {
export function validateUrlPresetPath(preset: string): { message: string; isValid: boolean } {
if (preset === '' || preset == null) {
return { isValid: false, message: 'Path cannot be empty' };
}
@@ -26,7 +24,7 @@ export const validateUrlPresetPath = (preset: string): { message: string; isVali
}
return { isValid: true, message: 'ok' };
};
}
/**
* Utility removes trailing slash from a string
@@ -36,48 +34,52 @@ function removeTrailingSlash(text: string): string {
}
/**
* Gets the URL to send a preset to
* @param location
* @param data
* @param searchParams
* Checks whether the current location corresponds to a preset and returns the new path if necessary
*/
export const getRouteFromPreset = (location: Location, data: URLPreset[], searchParams: URLSearchParams) => {
export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]) {
// current url is the pathname without the leading slash
const currentURL = location.pathname.substring(1);
// we need to check if the whole url here is an alias, so we can redirect
const foundPreset = data.find((preset) => preset.alias === removeTrailingSlash(currentURL) && preset.enabled);
// we need to check if the whole url is an alias
const foundPreset = urlPresets.find((preset) => preset.alias === removeTrailingSlash(currentURL) && preset.enabled);
if (foundPreset) {
return generateUrlFromPreset(foundPreset);
// if so, we can redirect to the preset path
return generatePathFromPreset(foundPreset.pathAndParams, foundPreset.alias);
}
// if the current url is not an alias, we check if the alias is in the search parameters
const searchParams = new URLSearchParams(location.search);
const presetOnPage = searchParams.get('alias');
for (const d of data) {
if (presetOnPage) {
// if the alias fits the preset on this page, but the URL is different, 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 === presetOnPage) {
const newPath = resolvePath(d.pathAndParams);
const urlParams = new URLSearchParams(newPath.search);
urlParams.set('alias', d.alias);
// we confirm either the url parameters does not match or the url path doesnt
if (!isEqual(urlParams, searchParams) || newPath.pathname !== location.pathname) {
// we then redirect to the alias route, since the view listening to this alias has an outdated URL
return `${newPath.pathname}?${urlParams}`;
}
if (!presetOnPage) {
return null;
}
for (const preset of urlPresets) {
// if the page has a known enabled alias, we check if we need to redirect
if (preset.alias === presetOnPage && preset.enabled) {
const newPath = generatePathFromPreset(preset.pathAndParams, preset.alias);
const currentPath = `${location.pathname}${location.search}`.substring(1);
if (currentPath !== newPath) {
// if current path is out of date
// return new path so we can redirect
return newPath;
}
}
}
return null;
};
}
/**
* Generate URL from an preset
* @param presetData
* Handles generating a path and search parameters from a preset
*/
export const generateUrlFromPreset = (presetData: URLPreset) => {
const newPresetPath = resolvePath(presetData.pathAndParams);
const urlParams = new URLSearchParams(newPresetPath.search);
urlParams.set('alias', presetData.alias);
export function generatePathFromPreset(pathAndParams: string, alias: string): string {
const path = resolvePath(pathAndParams);
const searchParams = new URLSearchParams(path.search);
return `${newPresetPath.pathname}?${urlParams}`;
};
// save the alias so we have a reference to it being a preset and can update if necessary
searchParams.set('alias', alias);
// return path concatenated without the leading slash
return `${path.pathname}?${searchParams}`.substring(1);
}
+7 -7
View File
@@ -1,6 +1,6 @@
/* eslint-disable react/display-name */
import { ComponentType, useEffect } from 'react';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import useUrlPresets from '../common/hooks-query/useUrlPresets';
import { getRouteFromPreset } from '../common/utils/urlPresets';
@@ -8,19 +8,19 @@ import { getRouteFromPreset } from '../common/utils/urlPresets';
const withPreset = <P extends object>(Component: ComponentType<P>) => {
return (props: Partial<P>) => {
const { data } = useUrlPresets();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const location = useLocation();
// navigate if is alias route
useEffect(() => {
if (!data) return;
const url = getRouteFromPreset(location, data, searchParams);
// navigate to this route if its not empty
if (url) {
navigate(url);
const destination = getRouteFromPreset(location, data);
// navigate to this destination if its not null
if (destination) {
navigate(destination);
}
}, [data, searchParams, navigate, location]);
}, [data, navigate, location]);
return <Component {...(props as P)} />;
};