Skip to content

feat(webapp): adds users data table #935

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 6, 2024
Merged
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
12 changes: 12 additions & 0 deletions packages/webapp/src/browser/core/i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export const translations = {
},
users: {
title: 'Users list',
name: 'Name',
email: 'Email',
phone: 'Phone',
},
user: {
title: 'User information',
},
},
fr: {
Expand All @@ -25,6 +31,12 @@ export const translations = {
},
users: {
title: 'Liste d\'utilisateurs',
name: 'Nom',
email: 'Courriel',
phone: 'Téléphone',
},
user: {
title: 'Information sur l\'utilisateur',
},
},
};
17 changes: 14 additions & 3 deletions packages/webapp/src/browser/modules/app/App.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import { RouterProvider } from 'react-router-dom';
import { Configuration, ConfigurationProvider, createRouter, initializeConfiguration } from '../../core';
import { Provider as UsersProvider } from '../users/components/Provider.component';
import { AppLayout, AppLoader, UnexpectedErrorBoundary } from './layout';
import { ROUTER_ROUTES } from './routes';

Expand All @@ -21,6 +22,14 @@ const router = createRouter([
basename: appConfig.publicPath,
});

export const DataProviders: FunctionComponent = (
{ children },
) => (
<UsersProvider>
{children}
</UsersProvider>
);

export const App: FunctionComponent<AppProps> = ({
configuration,
}) => {
Expand All @@ -30,9 +39,11 @@ export const App: FunctionComponent<AppProps> = ({
<ConfigurationProvider configuration={configuration}>
<UnexpectedErrorBoundary>
<DesignSystem language={i18n.language}>
<AppLoader>
<RouterProvider router={router} />
</AppLoader>
<DataProviders>
<AppLoader>
<RouterProvider router={router} />
</AppLoader>
</DataProviders>
</DesignSystem>
</UnexpectedErrorBoundary>
</ConfigurationProvider>
Expand Down
7 changes: 6 additions & 1 deletion packages/webapp/src/browser/modules/app/routes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RouteObject } from 'react-router';
import { RouteCollection, RouteDefinition } from '../../core';
import { HomePage } from '../home';
import { UsersPage } from '../users/Users.component';
import { UserPage, UsersPage } from '../users';

export const ROUTES: RouteCollection = {
home: {
Expand All @@ -14,6 +14,11 @@ export const ROUTES: RouteCollection = {
component: UsersPage,
end: true,
},
user: {
path: '/user/:id?',
component: UserPage,
end: true,
},
};

export const ROUTER_ROUTES: RouteObject[] = Object.keys(ROUTES).map((key: string): RouteObject => {
Expand Down
11 changes: 11 additions & 0 deletions packages/webapp/src/browser/modules/users/User.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Heading } from '@equisoft/design-elements-react';
import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';

export const UserPage: FunctionComponent = () => {
const { t } = useTranslation('user');

return (
<Heading bold noMargin type='xlarge' tag="h1">{t('title')}</Heading>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import {
} from '@equisoft/design-elements-react';
import { FunctionComponent } from 'react';
import { useTranslation } from 'react-i18next';
import { Table as UsersTable } from './components/table/Table.component';

export const UsersPage: FunctionComponent = () => {
const { t } = useTranslation();

return (
<Heading bold noMargin type='xlarge' tag="h1">{t('users:title')}</Heading>
<>
<Heading bold noMargin type='xlarge' tag="h1">{t('users:title')}</Heading>
<UsersTable />
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { FunctionComponent, useEffect, useMemo, useReducer } from 'react';
import { initialUsersContext } from '../constants';
import { usersReducer, UsersDataContext, UsersDispatchContext } from '../state';
import { UsersAction } from '../types';
import { loadUsers } from '../utils';

export const Provider: FunctionComponent = ({ children }) => {
const [state, dispatch] = useReducer(usersReducer, initialUsersContext);

useEffect(() => {
dispatch({
type: UsersAction.LOAD_USERS,
users: loadUsers(),
});
}, []);

const usersValue = useMemo(() => state, [state]);
const dispatchValue = useMemo(() => dispatch, [dispatch]);

return (
<UsersDataContext.Provider value={usersValue}>
<UsersDispatchContext.Provider value={dispatchValue}>
{children}
</UsersDispatchContext.Provider>
</UsersDataContext.Provider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Pagination } from '@equisoft/design-elements-react';
import { FunctionComponent, ReactElement, useCallback } from 'react';
import styled from 'styled-components';
import { useUsersActions, useUsersContext } from '../../state';
import { UsersAction } from '../../types';

const FooterPaginationWrapper = styled.nav`
align-items: center;
align-self: stretch;
border-radius: 8px;
display: flex;
gap: 8px;
justify-content: space-between;
`;

export const Footer: FunctionComponent = () => {
const { table } = useUsersContext();
const dispatch = useUsersActions();

const renderPagination = useCallback((): ReactElement => (
<Pagination
resultsPerPage={table.usersPerPage}
numberOfResults={table.totalCount}
activePage={table.currentPage}
onPageChange={(page) => {
dispatch({
type: UsersAction.UPDATE_TABLE,
key: 'currentPage',
value: page,
});
}}
pagesShown={3}
/>
), [table.usersPerPage, table.totalCount, table.currentPage, dispatch]);

return (
<FooterPaginationWrapper aria-label="table's pagination">
<div />
{renderPagination()}
</FooterPaginationWrapper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
TableColumn,
Table as DataTable,
} from '@equisoft/design-elements-react';
import { FunctionComponent, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { useUsersActions, useUsersContext } from '../../state';
import { User, UserKeys, UsersAction } from '../../types';
import { Footer as TableFooter } from './Footer.component';
import { Name as NameCell } from './cells/Name.component';

const TableContainer = styled.div`
align-items: flex-start;
align-self: stretch;
background: #ffffff;
border: 1px solid #f1f2f2;
border-radius: 8px;
box-shadow: 0 4px 20px -8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
flex-shrink: 0;
gap: 8px;
padding: 16px 32px;

.action-column {
box-sizing: border-box;
width: auto;
}

.data-column {
box-sizing: border-box;
width: 35%;
}
`;

export const Table: FunctionComponent = () => {
const { t } = useTranslation('users');
const { table } = useUsersContext();
const dispatch = useUsersActions();

const columns: TableColumn<User>[] = useMemo(() => [
{
id: 'name',
header: t('name'),
accessorKey: 'id',
className: 'data-column',
sortable: true,
sortDescFirst: false,
focusable: true,
// eslint-disable-next-line react/no-unstable-nested-components
cell: (props) => (
<NameCell id={props.cell.getValue() as User['id']} />
),
},
{
id: 'email',
header: t('email'),
accessorKey: 'email',
className: 'data-column',
sortable: true,
},
{
id: 'phone',
header: t('phone'),
accessorKey: 'phone',
className: 'data-column',
sortable: true,
},
{
id: 'actions',
headerAriaLabel: 'actions',
header: '',
className: 'action-column',
sortable: false,
},
], [t]);

return (
<TableContainer>
<DataTable
rowSize="small"
columns={columns}
data={table.currentPageUsers}
manualSort
onSort={(sort) => dispatch({
type: UsersAction.UPDATE_TABLE,
key: 'sortBy',
value: sort ? {
key: sort.id as UserKeys,
desc: sort.desc,
} : undefined,
})}
/>
<TableFooter />
</TableContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { RouteLink } from '@equisoft/design-elements-react';
import { FunctionComponent } from 'react';
import { NavLink } from 'react-router-dom';
import { useUserContext } from '../../../state';
import { User } from '../../../types';

interface NameCellProps {
id: User['id'];
}

export const Name: FunctionComponent<NameCellProps> = (
{ id },
) => {
const user = useUserContext(id);

return (
<RouteLink
label={user?.name}
href={`/user/${user?.id}`}
routerLink={NavLink}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { initialUsersContext } from './initial-context';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { UsersContextProps } from '../types';

const INITIAL_PAGE = 1;
const DEFAULT_USERS_PER_PAGE = 10;

export const initialUsersContext: UsersContextProps = {
users: [],
table: {
currentPage: INITIAL_PAGE,
usersPerPage: DEFAULT_USERS_PER_PAGE,
currentPageUsers: [],
totalCount: 0,
},
};

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/webapp/src/browser/modules/users/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { UsersPage } from './Users.component';
export { UserPage } from './User.component';
34 changes: 34 additions & 0 deletions packages/webapp/src/browser/modules/users/state/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { createContext, Dispatch, useContext } from 'react';
import { User, UsersActionProps, UsersContextProps } from '../types';

export const UsersDataContext = createContext<UsersContextProps | undefined>(undefined);

export const UsersDispatchContext = createContext<Dispatch<UsersActionProps> | undefined>(undefined);

export const useUsersContext = (): UsersContextProps => {
const context = useContext(UsersDataContext);

if (context === undefined) {
throw new Error('useUsersContext must be used within a UsersProvider');
}

return context;
};

export const useUserContext = (id?: User['id']): User | undefined => {
const context = useContext(UsersDataContext);

if (context === undefined) {
throw new Error('useUserContext must be used within a UsersProvider');
}

return context.users.find((u) => u.id === id);
};

export const useUsersActions = (): Dispatch<UsersActionProps> => {
const context = useContext(UsersDispatchContext);
if (context === undefined) {
throw new Error('useUsersActions must be used within a UsersProvider');
}
return context;
};
8 changes: 8 additions & 0 deletions packages/webapp/src/browser/modules/users/state/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export {
UsersDataContext,
UsersDispatchContext,
useUsersContext,
useUserContext,
useUsersActions,
} from './context';
export { usersReducer } from './reducer';
Loading
Loading