diff --git a/packages/webapp/src/browser/bootstrap/bootstrap.tsx b/packages/webapp/src/browser/bootstrap/bootstrap.tsx index f1444829c8..3342e7521c 100644 --- a/packages/webapp/src/browser/bootstrap/bootstrap.tsx +++ b/packages/webapp/src/browser/bootstrap/bootstrap.tsx @@ -10,6 +10,6 @@ export async function bootstrap(document: NonElementParentNode): Promise { if (rootElement) { const root = createRoot(rootElement); - root.render(); + root.render(); } } diff --git a/packages/webapp/src/browser/core/i18n/translations.ts b/packages/webapp/src/browser/core/i18n/translations.ts index 05ed366969..6c0d757fbb 100644 --- a/packages/webapp/src/browser/core/i18n/translations.ts +++ b/packages/webapp/src/browser/core/i18n/translations.ts @@ -2,6 +2,8 @@ export const translations = { en: { core: { helloWorld: 'Hello World!', + notFoundTitle: 'Page not found', + notFoundMessage: 'The page you are looking for does not exist.', }, nav: { changeLanguageTo: 'Change language to {{nextLanguage}} ({{lng}})', @@ -19,14 +21,20 @@ export const translations = { deleteUser: 'Delete {{user}}', deleteUserMessage: 'Are you sure you want to delete this user?', deleteUserSuccess: 'User {{user}} has been deleted!', + create: 'Create user...', }, user: { - viewTitle: 'User information', + readTitle: 'User information', + editTitle: 'Edit user', + createTitle: 'Create user', + requiredFields: 'Required fields are marked with an asterisk', }, }, fr: { core: { helloWorld: 'Bonjour le monde!', + notFoundTitle: 'Page non trouvée', + notFoundMessage: 'La page que vous cherchez n\'existe pas.', }, nav: { changeLanguageTo: 'Changer la langue pour {{nextLanguage}} ({{lng}})', @@ -44,9 +52,13 @@ export const translations = { deleteUser: 'Supprimer {{user}}', deleteUserMessage: 'Êtes-vous sûr de vouloir supprimer cet utilisateur?', deleteUserSuccess: 'L\'utilisateur {{user}} a été supprimé!', + create: 'Créer un utilisateur...', }, user: { - viewTitle: 'Information sur l\'utilisateur', + readTitle: 'Information sur l\'utilisateur', + editTitle: 'Modifier l\'utilisateur', + createTitle: 'Créer un utilisateur', + requiredFields: 'Les champs obligatoires sont marqués d\'un astérisque', }, }, }; diff --git a/packages/webapp/src/browser/modules/app/routes.ts b/packages/webapp/src/browser/core/navigation/routes.ts similarity index 74% rename from packages/webapp/src/browser/modules/app/routes.ts rename to packages/webapp/src/browser/core/navigation/routes.ts index a1d6e03059..3448de7ac6 100644 --- a/packages/webapp/src/browser/modules/app/routes.ts +++ b/packages/webapp/src/browser/core/navigation/routes.ts @@ -1,7 +1,7 @@ import { RouteObject } from 'react-router'; -import { RouteCollection, RouteDefinition } from '../../core'; -import { HomePage } from '../home'; -import { UserPage, UsersPage } from '../users'; +import { HomePage } from '../../modules/home'; +import { UserPage, UsersPage } from '../../modules/users'; +import { RouteCollection, RouteDefinition } from './route.types'; export const ROUTES: RouteCollection = { home: { @@ -15,7 +15,7 @@ export const ROUTES: RouteCollection = { end: true, }, user: { - path: '/user/:id?', + path: '/users/:mode?/:id?', component: UserPage, end: true, }, diff --git a/packages/webapp/src/browser/modules/app/App.component.tsx b/packages/webapp/src/browser/modules/app/App.component.tsx index 5ef79fb130..a2d2d52ac5 100644 --- a/packages/webapp/src/browser/modules/app/App.component.tsx +++ b/packages/webapp/src/browser/modules/app/App.component.tsx @@ -1,51 +1,12 @@ -import { DesignSystem } from '@equisoft/design-elements-react'; 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'; - -export interface AppProps { - configuration: Configuration; -} - -const appConfig = initializeConfiguration(); -const router = createRouter([ - { - path: '/', - element: , - children: ROUTER_ROUTES, - }, -], { - basename: appConfig.publicPath, -}); - -export const DataProviders: FunctionComponent = ( - { children }, -) => ( - - {children} - +import { AppLoader } from './layout'; +import { AppProviders } from './Providers.component'; +import { AppRouter } from './Router.component'; + +export const App: FunctionComponent = () => ( + + + + + ); - -export const App: FunctionComponent = ({ - configuration, -}) => { - const { i18n } = useTranslation(); - - return ( - - - - - - - - - - - - ); -}; diff --git a/packages/webapp/src/browser/modules/app/Providers.component.tsx b/packages/webapp/src/browser/modules/app/Providers.component.tsx new file mode 100644 index 0000000000..f411feffab --- /dev/null +++ b/packages/webapp/src/browser/modules/app/Providers.component.tsx @@ -0,0 +1,25 @@ +import { DesignSystem } from '@equisoft/design-elements-react'; +import { FunctionComponent } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ConfigurationProvider, initializeConfiguration } from '../../core'; +import { Provider as UsersProvider } from '../users/components/Provider.component'; +import { UnexpectedErrorBoundary } from './layout'; + +export const AppProviders: FunctionComponent = ( + { children }, +) => { + const appConfig = initializeConfiguration(); + const { i18n } = useTranslation(); + + return ( + + + + + {children} + + + + + ); +}; diff --git a/packages/webapp/src/browser/modules/app/Router.component.tsx b/packages/webapp/src/browser/modules/app/Router.component.tsx new file mode 100644 index 0000000000..aeff292fc6 --- /dev/null +++ b/packages/webapp/src/browser/modules/app/Router.component.tsx @@ -0,0 +1,23 @@ +import { FunctionComponent } from 'react'; +import { RouterProvider } from 'react-router-dom'; +import { createRouter, useConfiguration } from '../../core'; +import { ROUTER_ROUTES } from '../../core/navigation/routes'; +import { AppLayout } from './layout'; + +export const AppRouter: FunctionComponent = () => { + const { configuration } = useConfiguration(); + + const router = createRouter([ + { + path: '/', + element: , + children: ROUTER_ROUTES, + }, + ], { + basename: configuration.publicPath, + }); + + return ( + + ); +}; diff --git a/packages/webapp/src/browser/modules/app/index.ts b/packages/webapp/src/browser/modules/app/index.ts index f8e3c97091..1b6c8de891 100644 --- a/packages/webapp/src/browser/modules/app/index.ts +++ b/packages/webapp/src/browser/modules/app/index.ts @@ -1 +1 @@ -export { App, AppProps } from './App.component'; +export { App } from './App.component'; diff --git a/packages/webapp/src/browser/modules/app/layout/AppLayout.component.tsx b/packages/webapp/src/browser/modules/app/layout/AppLayout.component.tsx index 13855a5e3e..1d90452d94 100644 --- a/packages/webapp/src/browser/modules/app/layout/AppLayout.component.tsx +++ b/packages/webapp/src/browser/modules/app/layout/AppLayout.component.tsx @@ -4,7 +4,7 @@ import { Outlet } from 'react-router'; import styled from 'styled-components'; import { Header } from './Header.component'; import { Menu } from './Menu.component'; -import { UnexpectedErrorBoundary } from './UnexpectedErrorBoundary.component'; +import { UnexpectedErrorBoundary } from './ErrorBoundary.component'; const Screen = styled.div` display: grid; diff --git a/packages/webapp/src/browser/modules/app/layout/ErrorBoundary.component.tsx b/packages/webapp/src/browser/modules/app/layout/ErrorBoundary.component.tsx new file mode 100644 index 0000000000..be55c95db4 --- /dev/null +++ b/packages/webapp/src/browser/modules/app/layout/ErrorBoundary.component.tsx @@ -0,0 +1,40 @@ +import { Link } from '@equisoft/design-elements-react'; +import { FC, FunctionComponent, PropsWithChildren } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { ErrorBoundary } from '../../../core'; +import { ROUTES } from '../../../core/navigation/routes'; +import { ScreenCentered } from './styled-elements'; + +const NotFoundContainer = styled.div` + align-items: center; + display: flex; + flex-direction: column; + font-weight: 600; /* font-semibold */ + margin-top: 13rem; /* mt-52 */ +`; + +export const NotFoundRoute: FC = () => { + const { t } = useTranslation('core'); + return ( + +

{t('notFoundTitle')}

+

{t('notFoundMessage')}

+ + {t('backToHome')} + +
+ ); +}; + +export const UnexpectedError: FunctionComponent = () => { + const { t } = useTranslation(); + + return ({t('unexpectedError')}); +}; + +export const UnexpectedErrorBoundary: FunctionComponent> = ({ children }) => ( + }> + {children} + +); diff --git a/packages/webapp/src/browser/modules/app/layout/Menu.component.tsx b/packages/webapp/src/browser/modules/app/layout/Menu.component.tsx index 5242316aa3..87d00b68d8 100644 --- a/packages/webapp/src/browser/modules/app/layout/Menu.component.tsx +++ b/packages/webapp/src/browser/modules/app/layout/Menu.component.tsx @@ -1,7 +1,7 @@ import { GlobalNavigation } from '@equisoft/design-elements-react'; import { FunctionComponent } from 'react'; import { useTranslation } from 'react-i18next'; -import { ROUTES } from '../routes'; +import { ROUTES } from '../../../core/navigation/routes'; export const Menu: FunctionComponent = () => { const { t } = useTranslation(); diff --git a/packages/webapp/src/browser/modules/app/layout/UnexpectedErrorBoundary.component.tsx b/packages/webapp/src/browser/modules/app/layout/UnexpectedErrorBoundary.component.tsx deleted file mode 100644 index 099f84732a..0000000000 --- a/packages/webapp/src/browser/modules/app/layout/UnexpectedErrorBoundary.component.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { FunctionComponent, PropsWithChildren } from 'react'; -import { useTranslation } from 'react-i18next'; -import { ErrorBoundary } from '../../../core'; -import { ScreenCentered } from './styled-elements'; - -const UnexpectedError: FunctionComponent = () => { - const { t } = useTranslation(); - - return ({t('unexpectedError')}); -}; - -export const UnexpectedErrorBoundary: FunctionComponent> = ({ children }) => ( - }> - {children} - -); diff --git a/packages/webapp/src/browser/modules/app/layout/index.ts b/packages/webapp/src/browser/modules/app/layout/index.ts index f079bd85ba..e6c931c74f 100644 --- a/packages/webapp/src/browser/modules/app/layout/index.ts +++ b/packages/webapp/src/browser/modules/app/layout/index.ts @@ -1,3 +1,3 @@ export { AppLayout } from './AppLayout.component'; export { AppLoader } from './AppLoader.component'; -export { UnexpectedErrorBoundary } from './UnexpectedErrorBoundary.component'; +export { UnexpectedErrorBoundary, UnexpectedError, NotFoundRoute } from './ErrorBoundary.component'; diff --git a/packages/webapp/src/browser/modules/users/User.component.tsx b/packages/webapp/src/browser/modules/users/User.component.tsx index df4a34a9dd..55476da18e 100644 --- a/packages/webapp/src/browser/modules/users/User.component.tsx +++ b/packages/webapp/src/browser/modules/users/User.component.tsx @@ -1,11 +1,57 @@ import { Heading } from '@equisoft/design-elements-react'; import { FunctionComponent } from 'react'; import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router'; +import styled from 'styled-components'; +import { Form as UserForm } from './components/form/Form.component'; +import { UserMode } from './types'; + +const PageWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; + +const StyledWarning = styled.span` + color: ${({ theme }) => theme.alias['color-content']}; + display: flex; + flex-direction: row; + font-size: 12px; + font-style: normal; + font-weight: 400; + gap: 4px; + letter-spacing: 0.2px; + line-height: 20px; +`; + +const StyledAsterisk = styled.span` + color: ${({ theme }) => theme.alias['color-feedback-border-alert']}; + font-size: 12px; + font-style: normal; + font-weight: 400; + letter-spacing: 0.2px; + line-height: 20px; +`; export const UserPage: FunctionComponent = () => { const { t } = useTranslation('user'); + const { mode, id } = useParams(); + const userMode = mode as UserMode || UserMode.CREATE; + const titleMap = { + [UserMode.CREATE]: t('createTitle'), + [UserMode.EDIT]: t('editTitle'), + [UserMode.READ]: t('readTitle'), + }; + const title = titleMap[userMode]; return ( - {t('viewTitle')} + + {title} + + {t('requiredFields')} + * + + + ); }; diff --git a/packages/webapp/src/browser/modules/users/components/form/Form.component.tsx b/packages/webapp/src/browser/modules/users/components/form/Form.component.tsx new file mode 100644 index 0000000000..bb49affdd9 --- /dev/null +++ b/packages/webapp/src/browser/modules/users/components/form/Form.component.tsx @@ -0,0 +1,15 @@ +import { FunctionComponent } from 'react'; + +interface UserFormProps { + id?: string; +} + +export const Form: FunctionComponent = ( + { id }, +) => { + // eslint-disable-next-line no-console + console.log(id); + return ( +
+ ); +}; diff --git a/packages/webapp/src/browser/modules/users/components/table/Table.component.tsx b/packages/webapp/src/browser/modules/users/components/table/Table.component.tsx index 29d829e0ef..889d23e92c 100644 --- a/packages/webapp/src/browser/modules/users/components/table/Table.component.tsx +++ b/packages/webapp/src/browser/modules/users/components/table/Table.component.tsx @@ -12,6 +12,7 @@ import { Delete as DeleteCell, } from './cells'; import { Footer as TableFooter } from './Footer.component'; +import { ToolBar } from './toolbar/Toolbar.component'; const TableContainer = styled.div` align-items: flex-start; @@ -25,7 +26,7 @@ const TableContainer = styled.div` flex-shrink: 0; gap: 8px; padding: 16px 32px; - + td:nth-child(4) { padding-left: 0; padding-right: 0; @@ -58,7 +59,7 @@ export const Table: FunctionComponent = () => { focusable: true, // eslint-disable-next-line react/no-unstable-nested-components cell: (props) => ( - + ), }, { @@ -84,13 +85,14 @@ export const Table: FunctionComponent = () => { sortable: false, // eslint-disable-next-line react/no-unstable-nested-components cell: (props) => ( - + ), }, ], [t]); return ( + = ( - { id }, -) => { - const user = useUserContext(id); - - return ( - - ); -}; + { id, name }, +) => ( + + {name} + +); diff --git a/packages/webapp/src/browser/modules/users/components/table/toolbar/CreateUser.component.tsx b/packages/webapp/src/browser/modules/users/components/table/toolbar/CreateUser.component.tsx new file mode 100644 index 0000000000..9db0825d75 --- /dev/null +++ b/packages/webapp/src/browser/modules/users/components/table/toolbar/CreateUser.component.tsx @@ -0,0 +1,19 @@ +import { Icon, Link } from '@equisoft/design-elements-react'; +import { FunctionComponent } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NavLink } from 'react-router-dom'; +import { UserMode } from '../../../types'; + +export const CreateUser: FunctionComponent = () => { + const { t } = useTranslation('users'); + return ( + + + {t('create')} + + ); +}; diff --git a/packages/webapp/src/browser/modules/users/components/table/toolbar/Toolbar.component.tsx b/packages/webapp/src/browser/modules/users/components/table/toolbar/Toolbar.component.tsx new file mode 100644 index 0000000000..9edd06ff7e --- /dev/null +++ b/packages/webapp/src/browser/modules/users/components/table/toolbar/Toolbar.component.tsx @@ -0,0 +1,18 @@ +import { FunctionComponent } from 'react'; +import styled from 'styled-components'; +import { CreateUser } from './CreateUser.component'; + +const ToolBarContainer = styled.div` + align-items: center; + align-self: stretch; + border-radius: 8px; + display: flex; + justify-content: flex-end; + padding: 8px 8px; +`; + +export const ToolBar: FunctionComponent = () => ( + + + +); diff --git a/packages/webapp/src/browser/modules/users/types/index.ts b/packages/webapp/src/browser/modules/users/types/index.ts index 3f95ebdfac..6596ee03df 100644 --- a/packages/webapp/src/browser/modules/users/types/index.ts +++ b/packages/webapp/src/browser/modules/users/types/index.ts @@ -3,3 +3,4 @@ export { UsersContextProps, TableContextProps, TableContextKeys, } from './users-context'; export { UsersActionProps, UsersAction } from './users-action'; +export * from './user-mode'; diff --git a/packages/webapp/src/browser/modules/users/types/user-mode.ts b/packages/webapp/src/browser/modules/users/types/user-mode.ts new file mode 100644 index 0000000000..a6b88b375d --- /dev/null +++ b/packages/webapp/src/browser/modules/users/types/user-mode.ts @@ -0,0 +1,5 @@ +export enum UserMode { + EDIT = 'edit', + READ = 'read', + CREATE = 'create', +}