Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions packages/webapp-libs/webapp-core/src/utils/__tests__/path.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('Utils: path', () => {
getRelativeUrl: expect.any(Function),
getLocalePath: expect.any(Function),
getTenantPath: expect.any(Function),
getMountPath: expect.any(Function),
});
});

Expand Down Expand Up @@ -50,10 +51,12 @@ describe('Utils: path', () => {
getRelativeUrl: expect.any(Function),
getLocalePath: expect.any(Function),
getTenantPath: expect.any(Function),
getMountPath: expect.any(Function),
},
getRelativeUrl: expect.any(Function),
getLocalePath: expect.any(Function),
getTenantPath: expect.any(Function),
getMountPath: expect.any(Function),
});
});

Expand Down Expand Up @@ -81,6 +84,77 @@ describe('Utils: path', () => {

expect(getLocalePath(result.nestedStep.anotherStep)).toEqual('/:lang/root/nested/another-step/:id');
});
it('should propagate prefix through 3 levels of nesting', () => {
const result = nestedPath('admin', {
payments: nestedPath('payments', {
customers: nestedPath('customers', {
list: '',
add: 'add',
}),
}),
});
expect(result.payments.customers.list).toEqual('admin/payments/customers/');
expect(result.payments.customers.add).toEqual('admin/payments/customers/add');
expect(result.payments.customers.index).toEqual('admin/payments/customers/*');
});

it('should propagate prefix through 4 levels of nesting', () => {
const result = nestedPath('a', {
b: nestedPath('b', {
c: nestedPath('c', {
d: nestedPath('d', {
leaf: '',
}),
}),
}),
});
expect(result.b.c.d.leaf).toEqual('a/b/c/d/');
});

it('should preserve helpers on deeply nested objects', () => {
const result = nestedPath('admin', {
payments: nestedPath('payments', {
customers: nestedPath('customers', {
list: '',
}),
}),
});
expect(result.payments.customers.getRelativeUrl('list')).toEqual('');
expect(getLocalePath(result.payments.customers.list)).toEqual('/:lang/admin/payments/customers/');
});

it('should return mount path with wildcard for nested route groups', () => {
const result = nestedPath('root', {
step: 'step',
nestedStep: nestedPath('nested', {
anotherStep: 'another-step/:id',
}),
});
expect(result.nestedStep.getMountPath()).toEqual('nested/*');
});

it('should return mount path correctly at multiple nesting depths', () => {
const result = nestedPath('admin', {
payments: nestedPath('payments', {
customers: nestedPath('customers', {
list: '',
}),
}),
});
expect(result.payments.getMountPath()).toEqual('payments/*');
expect(result.payments.customers.getMountPath()).toEqual('customers/*');
});

it('should support mixed getMountPath and getRelativeUrl in route mounts', () => {
const result = nestedPath('admin', {
payments: nestedPath('payments', {
customers: nestedPath('customers', { list: '' }),
cart: 'cart/:transactionId',
}),
});
expect(result.payments.customers.getMountPath()).toEqual('customers/*');
expect(result.payments.getRelativeUrl('cart')).toEqual('cart/:transactionId');
});
});
});
});
27 changes: 24 additions & 3 deletions packages/webapp-libs/webapp-core/src/utils/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ export const getTenantPathHelper = (p: string) => getLocalePath(removeInitialSla
* )
* }
* ```
*
* @example
* When mounting a nested route group as a parent route
*
* Use `getMountPath()` to get the segment with a wildcard suffix (e.g. `'example/*'`),
* suitable for use as the `path` prop on a parent `<Route>` whose element renders
* its own nested `<Routes>` tree. Use `getRelativeUrl(key)` for leaf routes that
* map directly to a component without further child routes.
*
* ```tsx showLineNumbers
* import { Route, Routes } from 'react-router-dom';
*
* <Routes>
* <Route path={RoutesConfig.example.getMountPath()} element={<Example />} />
* <Route path={RoutesConfig.crudDemoItem.getRelativeUrl('details')} element={<Details />} />
* </Routes>
* ```
*/
export const nestedPath = <T extends object>(root: string, nestedRoutes: T) => {
const absoluteUrlsMapper = (value: any) => {
Expand All @@ -82,17 +99,21 @@ export const nestedPath = <T extends object>(root: string, nestedRoutes: T) => {

return {
...paths,
getRelativeUrl: (route: keyof T) => nestedRoutes[route],
getRelativeUrl: <K extends keyof T>(route: K): T[K] => nestedRoutes[route],
getLocalePath: assignLocalePathFn<T>(getLocalePath, paths),
getTenantPath: assignLocalePathFn<T>(getTenantPathHelper, paths),
getMountPath: () => `${root}/*`,
};
};

const mapRoot = <N>(root: string, obj: N): N => {
const override: Partial<N> = {};
for (const key in obj) {
if (typeof obj[key] === 'string') {
override[key] = (root + '/' + obj[key]) as N[Extract<keyof N, string>];
const value = obj[key];
if (typeof value === 'string') {
override[key] = (root + '/' + value) as N[Extract<keyof N, string>];
} else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
override[key] = mapRoot(root, value) as N[Extract<keyof N, string>];
}
}
return {
Expand Down