diff --git a/package.json b/package.json
index 523fc4e..b47212f 100644
--- a/package.json
+++ b/package.json
@@ -79,8 +79,8 @@
"test:coverage": "npm run test -- --watchAll=false --coverage",
"lint": "eslint --quiet .",
"lint:fix": "eslint . --fix",
- "prettier": "./node_modules/.bin/prettier --config .prettierrc --check 'src/**/*.js'",
- "prettier:fix": "./node_modules/.bin/prettier --config .prettierrc --write 'src/**/*.js'",
+ "prettier": "./node_modules/.bin/prettier --config .prettierrc --check src/**/*.js",
+ "prettier:fix": "./node_modules/.bin/prettier --config .prettierrc --write src/**/*.js",
"eject": "react-scripts eject",
"push:pre": "npm-run-all --parallel test:ci lint prettier",
"publish:clean": "rm -rf ./template/src ./template/server ./template/.vscode ./template/public && rm -f ./template/.editorconfig ./template/.env ./template/.eslintignore ./template/.eslintrc.js ./template/.prettierrc ./template/commitlint.config.js ./template/jest.config.json ./template/PULL_REQUEST_TEMPLATE.md ./template/README.md ./template/webpack.config.js",
diff --git a/src/App.js b/src/App.js
index cd83f68..cf61721 100644
--- a/src/App.js
+++ b/src/App.js
@@ -6,9 +6,10 @@ import { Helmet } from 'react-helmet';
import ErrorBoundary from './pages/Fallback/ErrorBoundary';
import Shell from './components/Shell/Shell';
import Routes from './routes/Routes';
-
import './App.css';
import CenteredContent from './components/Layout/CenteredContent';
+import SessionTimeoutDialog from './components/SessionTimeout/SessionTimeoutDialog';
+import InformationZone from './components/InformationZone/InformationZone';
const style = {
emptySpace: {
@@ -29,6 +30,9 @@ function App() {
+
+
+
);
}
diff --git a/src/components/InformationDialog/InformationDialog.js b/src/components/InformationDialog/InformationDialog.js
new file mode 100644
index 0000000..d7753b6
--- /dev/null
+++ b/src/components/InformationDialog/InformationDialog.js
@@ -0,0 +1,132 @@
+import React, { useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import { spacing } from '@ui5/webcomponents-react-base';
+import { Icon } from '@ui5/webcomponents-react/lib/Icon';
+import { Button } from '@ui5/webcomponents-react/lib/Button';
+import { ButtonDesign } from '@ui5/webcomponents-react/lib/ButtonDesign';
+import { Dialog } from '@ui5/webcomponents-react/lib/Dialog';
+import { FlexBox } from '@ui5/webcomponents-react/lib/FlexBox';
+import { FlexBoxDirection } from '@ui5/webcomponents-react/lib/FlexBoxDirection';
+import { FlexBoxAlignItems } from '@ui5/webcomponents-react/lib/FlexBoxAlignItems';
+import { FlexBoxJustifyContent } from '@ui5/webcomponents-react/lib/FlexBoxJustifyContent';
+import { Text } from '@ui5/webcomponents-react/lib/Text';
+
+const KEYBOARD_KEYS = {
+ ESCAPE: 27,
+};
+
+const style = {
+ warning: {
+ width: '1.5rem',
+ height: '1.5rem',
+ color: '#feb60a',
+ },
+ error: {
+ width: '1.5rem',
+ height: '1.5rem',
+ color: '#ff5254',
+ },
+ information: {
+ width: '1.5rem',
+ height: '1.5rem',
+ color: 'black',
+ },
+ text: {
+ lineHeight: '20px',
+ },
+};
+
+const _getHeaderIcon = (type) => {
+ switch (type) {
+ case Type.Warning:
+ return _getHeaderWarningIcon();
+ case Type.Error:
+ return _getHeaderErrorIcon();
+ default:
+ return _getHeaderInfoIcon();
+ }
+};
+
+const _getHeaderWarningIcon = () => {
+ return ;
+};
+
+const _getHeaderErrorIcon = () => {
+ return ;
+};
+
+const _getHeaderInfoIcon = () => {
+ return ;
+};
+
+const _handleAvoidEscapeClosing = (avoidEscapeClose) => {
+ document.addEventListener(
+ 'keydown',
+ (e) => {
+ if (e.keyCode === KEYBOARD_KEYS.ESCAPE && avoidEscapeClose) {
+ e.stopPropagation();
+ }
+ },
+ true,
+ );
+};
+
+const InformationDialog = ({ dialogRef, avoidEscapeClose, headerText, innerText, closeButtonText, children, onClose, type }) => {
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ _handleAvoidEscapeClosing(avoidEscapeClose);
+ });
+
+ const _onClose = () => {
+ onClose && onClose();
+ if (dialogRef.current) {
+ dialogRef.current.close();
+ }
+ };
+
+ const _getFooter = () => {
+ return (
+
+
+
+ );
+ };
+
+ const _getHeader = () => {
+ return (
+
+ {_getHeaderIcon(type)}
+
+ {headerText}
+
+
+ );
+ };
+
+ return (
+
+ );
+};
+
+export default InformationDialog;
+
+export const Type = {
+ Warning: 'WARNING',
+ Error: 'ERROR',
+ Info: 'INFO',
+};
diff --git a/src/components/InformationDialog/InformationDialog.test.js b/src/components/InformationDialog/InformationDialog.test.js
new file mode 100644
index 0000000..6a9c548
--- /dev/null
+++ b/src/components/InformationDialog/InformationDialog.test.js
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import '@testing-library/jest-dom/extend-expect';
+import { render, screen } from '../../util/TestSetup';
+import InformationDialog, { Type } from './InformationDialog';
+
+describe('InformationDialog.js Test Suite', () => {
+ test('should render', () => {
+ const dialog = ;
+ render(dialog);
+ const infoDialog = screen.getByTestId('information-dialog');
+ expect(infoDialog).toBeInTheDocument();
+ });
+
+ test('should render child when not inner text is passed', () => {
+ const dialog = (
+
+
+
+ );
+ render(dialog);
+ const child = screen.getByTestId('information-dialog-child');
+ expect(child).toBeInTheDocument();
+ });
+});
diff --git a/src/components/InformationZone/InformationZone.js b/src/components/InformationZone/InformationZone.js
new file mode 100644
index 0000000..2b423ee
--- /dev/null
+++ b/src/components/InformationZone/InformationZone.js
@@ -0,0 +1,3 @@
+export default function InformationZone({ children }) {
+ return children;
+}
diff --git a/src/components/SessionTimeout/SessionTimeoutDialog.js b/src/components/SessionTimeout/SessionTimeoutDialog.js
new file mode 100644
index 0000000..f53e19c
--- /dev/null
+++ b/src/components/SessionTimeout/SessionTimeoutDialog.js
@@ -0,0 +1,70 @@
+import React, { useEffect, useState, useRef } from 'react';
+import InformationDialog, { Type } from '../InformationDialog/InformationDialog';
+import i18n from '../../util/i18n';
+
+const SESSION = {
+ TIMEOUT_INTERVAL: 60000,
+ REFRESH_LIMIT: 15,
+ REFRESH_WARNING: 13,
+};
+
+const TIMEOUT_MODE = {
+ type: Type.Error,
+ headerText: i18n.t('session.expired'),
+ closeButtonText: i18n.t('session.expired.button.reload'),
+ innerText: i18n.t('session.expired.text'),
+ onClose: () => window.location.reload(),
+};
+
+const WARNING_MODE = {
+ type: Type.Warning,
+ headerText: i18n.t('session.warning'),
+ closeButtonText: i18n.t('app.generics.close'),
+ innerText: i18n.t('session.warning.text'),
+ onClose: null,
+};
+
+const SessionTimeoutDialog = ({ timeoutScale = SESSION.TIMEOUT_INTERVAL, hasExpiredLimit = SESSION.REFRESH_LIMIT, isExpiringLimit = SESSION.REFRESH_WARNING }) => {
+ const dialogRef = useRef(null);
+ const ACTIVITY_EVENTS = ['click', 'focus', 'blur', 'keyup', 'keydown', 'mousemove', 'scroll'];
+ const [sessionIntervalCount, setSessionIntervalCount] = useState(1);
+ const [options, setOptions] = useState(WARNING_MODE);
+
+ useEffect(() => {
+ let sessionIntervalFinder = setInterval(() => {
+ if (sessionIntervalCount >= isExpiringLimit && sessionIntervalCount < hasExpiredLimit) {
+ setOptions(WARNING_MODE);
+ dialogRef.current && dialogRef.current.open();
+ } else if (sessionIntervalCount >= hasExpiredLimit) {
+ setOptions(TIMEOUT_MODE);
+ dialogRef.current && dialogRef.current.open();
+ }
+
+ setSessionIntervalCount(sessionIntervalCount + 1);
+ }, timeoutScale);
+ handleUserActivity(sessionIntervalFinder);
+ });
+
+ const handleUserActivity = (sessionIntervalFinder) => {
+ ACTIVITY_EVENTS.forEach((EVENT) => {
+ window.addEventListener(EVENT, () => {
+ setSessionIntervalCount(0);
+ clearInterval(sessionIntervalFinder);
+ });
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default SessionTimeoutDialog;
diff --git a/src/components/SessionTimeout/SessionTimeoutDialog.test.js b/src/components/SessionTimeout/SessionTimeoutDialog.test.js
new file mode 100644
index 0000000..fe4cccb
--- /dev/null
+++ b/src/components/SessionTimeout/SessionTimeoutDialog.test.js
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import '@testing-library/jest-dom/extend-expect';
+import { render, screen, waitFor } from '../../util/TestSetup';
+import SessionTimeoutDialog from './SessionTimeoutDialog';
+
+describe('SessionTimeoutDialog.js Test Suite', () => {
+ beforeEach(() => {
+ render();
+ });
+
+ test('should render, wait 13 cycles and see the text “Session Almost Expiring”', async () => {
+ const text = 'Session Almost Expiring';
+ const warning = await waitFor(() => screen.getByText(text));
+ expect(warning).toBeInTheDocument();
+ });
+});
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index ed0f792..c99666b 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -8,5 +8,11 @@
"page.error.alt": "Error",
"page.notfound.text": "Hmmm, we could find this URL",
"page.notfound.alt": "Not Found",
- "page.fallback.reload.text": "Reload this page"
+ "page.fallback.reload.text": "Reload this page",
+ "session.expired": "Session Expired",
+ "session.expired.button.reload": "Reload",
+ "session.expired.text": "Your session has expired. Please reload to continue.",
+ "session.warning": "Session Almost Expiring",
+ "session.warning.text": "If you not perform an action within 2 minutes, your session will expire.",
+ "app.generics.close": "Close"
}
diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json
index 3db8eca..e3ab8c5 100644
--- a/src/locales/pt/translation.json
+++ b/src/locales/pt/translation.json
@@ -8,5 +8,11 @@
"page.error.alt": "Erro",
"page.notfound.text": "Hmmm, não conseguimos encontrar esta página",
"page.notfound.alt": "Não Encontrado",
- "page.fallback.reload.text": "Recarregar"
+ "page.fallback.reload.text": "Recarregar",
+ "session.expired": "Sessão expirada",
+ "session.expired.button.reload": "Recarregar",
+ "session.expired.text": "Sua sessão expirou. Recarregue para continuar.",
+ "session.warning": "Sessão quase expirada",
+ "session.warning.text": "Se você não efetuar uma ação nos próximos 2 minutos, a sua sessão irá expirar.",
+ "app.generics.close": "Fechar"
}
diff --git a/src/pages/Todo/List/TodoList.js b/src/pages/Todo/List/TodoList.js
index 5b881e8..ea757a4 100644
--- a/src/pages/Todo/List/TodoList.js
+++ b/src/pages/Todo/List/TodoList.js
@@ -1,14 +1,19 @@
-import React from 'react';
-import { Helmet } from 'react-helmet';
+import React, { useRef } from 'react';
import { useHistory } from 'react-router-dom';
+import { Helmet } from 'react-helmet';
import { MobileView, BrowserView, IEView, isMobile, isTablet, isDesktop, isIE, isChrome, isOpera } from 'react-device-detect';
import HyperLink from '../../../components/HyperLink/HyperLink';
import BrowserURL from '../../../util/BrowserURL';
import ComponentValidator from '../../../auth/Components/Validator';
+import InformationDialog, { Type } from '../../../components/InformationDialog/InformationDialog';
export default function TodoList() {
+ const dialogRef = useRef(null);
const history = useHistory();
+ const openInformationDialog = () => {
+ dialogRef.current.open();
+ };
return (
<>
@@ -23,6 +28,9 @@ export default function TodoList() {
Component Validator
Drop Application (this is a restricted text and you should not see unless you have access)
+
+
+
Device Detect