Skip to content

Commit

Permalink
Merge pull request #215 from n8io/n8io/ch18/add-user-administration-n…
Browse files Browse the repository at this point in the history
…avigation-link-to
  • Loading branch information
n8io authored May 17, 2020
2 parents f8d88fd + df3740f commit e02a81f
Show file tree
Hide file tree
Showing 32 changed files with 535 additions and 27 deletions.
3 changes: 2 additions & 1 deletion packages/common/src/types/permission/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { hasPermission } from './selectors';
import { Enumeration } from './typedef';
import { Enumeration, values } from './typedef';

const Permission = {
...Enumeration,
hasPermission,
values,
};

export { Permission };
5 changes: 4 additions & 1 deletion packages/common/src/types/permission/typedef.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { values as ramdaValues } from 'ramda';
import { UserRole } from 'types/userRole';

const { ADMIN } = UserRole;
Expand All @@ -10,4 +11,6 @@ const Access = {
USERS_MANAGE: [ADMIN],
};

export { Access, Enumeration };
const values = ramdaValues(Enumeration);

export { Access, Enumeration, values };
3 changes: 2 additions & 1 deletion packages/common/src/types/userRole/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Enumeration } from './typedef';
import { Enumeration, values } from './typedef';

const UserRole = {
...Enumeration,
values,
};

export { UserRole };
6 changes: 5 additions & 1 deletion packages/common/src/types/userRole/typedef.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { values as ramdaValues } from 'ramda';

const Enumeration = {
ADMIN: 'ADMIN',
USER: 'USER',
};

export { Enumeration };
const values = ramdaValues(Enumeration);

export { Enumeration, values };
4 changes: 2 additions & 2 deletions packages/ui/craco.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module.exports = {
plugins: [
'add-react-displayname',
['babel-plugin-styled-components', { ssr: false }],
...whenDev(() => ['react-refresh/babel'], []),
...whenDev(() => [['react-refresh/babel', { skipEnvCheck: true }]], []),
...whenProd(() => ['babel-plugin-jsx-remove-data-test-id'], []),
],
},
Expand Down Expand Up @@ -48,7 +48,7 @@ module.exports = {
},
plugins: [
...(webpackConfig.plugins || []),
...whenDev(() => [new ReactRefreshPlugin()], []),
...whenDev(() => [new ReactRefreshPlugin({ overlay: false })], []),
],
}),
},
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"start:pre": "(cat ../../.ini/certs/cert.pem ../../.ini/certs/key.pem > ../../node_modules/webpack-dev-server/ssl/server.pem) || :",
"start:prod": "yarn run -s serve -l 3000 --ssl-cert ../../.ini/certs/cert.pem --ssl-key ../../.ini/certs/key.pem",
"start:prod:heroku": "yarn run -s serve",
"test": "craco test --passWithNoTests",
"test": "NODE_ENV=test craco test --passWithNoTests",
"test:ci": "JEST_JUNIT_OUTPUT_DIR=../../.tmp/test-results/jest/ui yarn -s test --ci --runInBand --reporters=default --reporters=jest-junit",
"test:watch": "craco test --passWithNoTests --color"
},
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/i18n/en/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const app = {
registration: 'Sign Up',
root: 'Home',
testPage: 'Test Page',
userManagement: 'Users',
},
openNavigation: 'Open navigation',
tapHereOrSwipeToClose: 'Tap here or swipe to close',
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/src/i18n/en/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const users = {
users: 'Users',
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Permission } from '@boilerplate-monorepo/common';
import { A11y } from '@boilerplate-monorepo/ui-common';
import { func } from 'prop-types';
import React, { useCallback, useState } from 'react';
Expand All @@ -16,7 +17,6 @@ import { styles as themeStyles } from './theme';

const { Role } = A11y;
const { LINK } = Context;
const routes = Route.filterToNavigation(Route.values);
const TOUCH_HANDLE_DISTANCE = 20;

const StyledNav = styled.nav`
Expand Down Expand Up @@ -47,26 +47,52 @@ const ButtonContainer = styled.div`
white-space: nowrap;
`;

const toNavLink = (authContext, onClick) => (route) => {
const { isAuthenticated, role } = authContext;
const { isAuthenticationRequired, requiredPermission } = route;

if (isAuthenticationRequired && !isAuthenticated) {
return null;
}

if (
requiredPermission &&
!Permission.hasPermission(role, requiredPermission)
) {
return null;
}

return (
<NavLink
data-testid={route.name}
key={route.name}
onClick={onClick}
route={route}
/>
);
};

const InnerSideBar = ({ onClose, t }) => {
const commonT = useTranslate({
component: 'common',
namespace: 'common',
});
const { isAuthenticated } = useAuth();
const authContext = useAuth();
const { isAuthenticated } = authContext;
const authRoute = isAuthenticated ? Route.LOGOUT : Route.LOGIN;
const routes = Route.filterToNavigation(Route.values);

const authDisplayText = isAuthenticated
? commonT('logout')
: commonT('login');
const authRoute = isAuthenticated ? Route.LOGOUT : Route.LOGIN;

return (
<StyledNav aria-label="sidebar" role={Role.NAVIGATION}>
<Container>
{routes.map((route) => (
<NavLink key={route.name} onClick={onClose} route={route} />
))}
{routes.map(toNavLink(authContext, onClose))}
{isAuthenticated && (
<NavLink
data-testid={Route.USER_ACCOUNT.name}
onClick={onClose}
route={Route.USER_ACCOUNT}
title={t('account')}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { UserRole, Permission } from '@boilerplate-monorepo/common';
import React from 'react';
import * as UseAuthHook from 'shared/useAuth';
import { render } from 'testHelpers';
import { Route } from 'types/route';
import { Navigation } from '.';

jest.mock('shared/Button');
Expand All @@ -20,4 +23,110 @@ describe('<Navigation/>', () => {

expect(container.firstChild).toMatchSnapshot();
});

const authRouteName = 'AUTH_ROUTE_NAME';
const isAuthenticationRequired = true;
const role = UserRole.USER;

const navRoutes = [
Route.example({ isAuthenticationRequired, name: authRouteName }),
];

describe('when user is authenticated', () => {
const isAuthenticated = true;

let useAuth = null;

beforeEach(() => {
useAuth = td.replace(UseAuthHook, 'useAuth');

td.when(useAuth()).thenReturn({ isAuthenticated, role });
td.replace(Route, 'values', navRoutes);
});

test('renders the user account link', () => {
const { getByTestId } = renderComponent();

expect(getByTestId(Route.USER_ACCOUNT.name)).not.toBeNull();
});

describe('and the nav link requires permission', () => {
const permissionRouteName = 'PERMISSION_ROUTE_NAME';

const permissionRoutes = [
Route.example({
isAuthenticationRequired,
name: permissionRouteName,
requiredPermission: Permission.USERS_MANAGE,
}),
];

beforeEach(() => {
td.replace(Route, 'values', permissionRoutes);
});

describe('and user has permission', () => {
const adminRole = UserRole.ADMIN;

beforeEach(() => {
td.when(useAuth()).thenReturn({ isAuthenticated, role: adminRole });
});

test('renders the permission nav link', () => {
const { getByTestId } = renderComponent();

expect(getByTestId(permissionRouteName)).not.toBeNull();
});
});

describe('and user DOES NOT have permission', () => {
beforeEach(() => {
td.when(useAuth()).thenReturn({ isAuthenticated, role });
});

test('does not renders the permission nav link', () => {
const { queryByTestId } = renderComponent();

expect(queryByTestId(permissionRouteName)).toBeNull();
});
});
});

describe('and the nav link DOES NOT require permission', () => {
test('renders the authenticated nav link', () => {
td.when(useAuth()).thenReturn({ isAuthenticated, role });

const { getByTestId } = renderComponent();

expect(getByTestId(authRouteName)).not.toBeNull();
});
});
});

describe('when user is NOT authenticated', () => {
const isAuthenticated = false;

let useAuth = null;

beforeEach(() => {
useAuth = td.replace(UseAuthHook, 'useAuth');

td.when(useAuth()).thenReturn({ isAuthenticated, role: null });
td.replace(Route, 'values', navRoutes);
});

test('DOES NOT render the user account link', () => {
const { queryByTestId } = renderComponent();

expect(queryByTestId(Route.USER_ACCOUNT.name)).toBeNull();
});

test('does not render the authenticated nav link', () => {
td.when(useAuth()).thenReturn({ isAuthenticated, role });

const { queryByTestId } = renderComponent();

expect(queryByTestId(authRouteName)).toBeNull();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ exports[`<Navigation/> renders properly 1`] = `
class="Navigation__NavLinks-hlefot-1 blIUvd"
>
<x-navlink
data-testid="dashboard"
id="navigation"
route="[object Object]"
/>
<x-navlink
data-testid="about"
route="[object Object]"
/>
<x-navlink
data-testid="userManagement"
route="[object Object]"
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Permission } from '@boilerplate-monorepo/common';
import { A11y, SkipToDestination } from '@boilerplate-monorepo/ui-common';
import React from 'react';
import { useAuth } from 'shared/useAuth';
import styled from 'styled-components/macro';
import { CustomProperty } from 'types/customProperties';
import { GridTemplateArea } from 'types/gridTemplateArea';
Expand All @@ -20,10 +22,26 @@ const Container = styled.nav`
width: max-content;
`;

const toNavLink = (route, index) => {
// eslint-disable-next-line complexity
const toNavLink = (authContext) => (route, index) => {
const id = index === 0 ? SkipToDestination.NAVIGATION : undefined;
const { isAuthenticated, role } = authContext;
const { isAuthenticationRequired, requiredPermission } = route;

return <NavLink id={id} key={route.name} route={route} />;
if (isAuthenticationRequired && !isAuthenticated) {
return null;
}

if (
requiredPermission &&
!Permission.hasPermission(role, requiredPermission)
) {
return null;
}

return (
<NavLink data-testid={route.name} id={id} key={route.name} route={route} />
);
};

const NavLinks = styled.div`
Expand All @@ -32,11 +50,15 @@ const NavLinks = styled.div`
grid-area: ${GridTemplateArea.NAV_LINK};
`;

const Navigation = () => (
<Container role={Role.NAVIGATION}>
<NavLinks>{navRoutes.map(toNavLink)}</NavLinks>
<AuthLink />
</Container>
);
const Navigation = () => {
const authContext = useAuth();

return (
<Container role={Role.NAVIGATION}>
<NavLinks>{navRoutes.map(toNavLink(authContext))}</NavLinks>
<AuthLink />
</Container>
);
};

export { Navigation };
Loading

0 comments on commit e02a81f

Please sign in to comment.