@@ -45,7 +43,6 @@ const useEndpoint = (endpoint, details, params) => {
};
export const ResourcePage = ({
- resourceName,
columns,
createConfig,
endpoint,
@@ -70,6 +67,7 @@ export const ResourcePage = ({
hasBESAdminAccess,
needsBESAdminAccess,
actionLabel,
+ resourceName,
}) => {
const { '*': unusedParam, locale, ...params } = useParams();
const { data: details } = useItemDetails(params, parent);
@@ -107,7 +105,6 @@ export const ResourcePage = ({
/>
)}
str[0].toUpperCase() + str.slice(1);
+export const capitalizeFirst = str => str?.[0].toUpperCase() + str?.slice(1);
-export const getPluralForm = ({ singular, plural }) => plural ?? `${singular}s`;
+export const getPluralForm = resourceName => {
+ if (!resourceName) return null;
+ const { singular, plural } = resourceName;
+ return plural ?? `${singular}s`;
+};
export const generateTitle = resourceName => capitalizeFirst(getPluralForm(resourceName));
diff --git a/packages/admin-panel/src/profileRoutes.jsx b/packages/admin-panel/src/profileRoutes.jsx
index dd90e7a53c..499d453a9d 100644
--- a/packages/admin-panel/src/profileRoutes.jsx
+++ b/packages/admin-panel/src/profileRoutes.jsx
@@ -14,13 +14,13 @@ export const PROFILE_ROUTES = [
path: '/profile',
childViews: [
{
- title: 'Profile',
+ label: 'Profile',
path: '',
icon: ,
Component: ProfilePage,
},
{
- title: 'Change Password',
+ label: 'Change Password',
path: '/change-password',
icon: ,
Component: ChangePasswordPage,
diff --git a/packages/admin-panel/src/rootReducer.js b/packages/admin-panel/src/rootReducer.js
index da189b85f4..e1fb76169e 100644
--- a/packages/admin-panel/src/rootReducer.js
+++ b/packages/admin-panel/src/rootReducer.js
@@ -3,7 +3,7 @@
* Copyright (c) 2017 Beyond Essential Systems Pty Ltd
*/
import { combineReducers } from 'redux';
-import { reducer as authentication, LOGOUT } from './authentication';
+import { LOGOUT } from './authentication';
import { reducer as tables } from './table';
import { reducer as autocomplete } from './autocomplete/reducer'; // Needs to be imported from reducer file or console shows autocomplete not found error
import { reducer as editor } from './editor';
@@ -14,7 +14,6 @@ import { reducer as qrCode } from './qrCode';
import { reducer as resubmitSurveyResponse } from './surveyResponse';
const appReducer = combineReducers({
- authentication,
tables,
autocomplete,
editor,
@@ -26,9 +25,9 @@ const appReducer = combineReducers({
});
export const rootReducer = (state, action) => {
- // on logout, wipe all redux state except auth
+ // on logout, wipe all redux state
if (action.type === LOGOUT) {
- return appReducer({ authentication: state.authentication }, action);
+ return appReducer({}, action);
}
return appReducer(state, action);
diff --git a/packages/admin-panel/src/routes/entities/entityHierarchies.js b/packages/admin-panel/src/routes/entities/entityHierarchies.js
index 29a4bfa870..91668fcd84 100644
--- a/packages/admin-panel/src/routes/entities/entityHierarchies.js
+++ b/packages/admin-panel/src/routes/entities/entityHierarchies.js
@@ -6,7 +6,7 @@
import { EntityHierarchiesPage } from '../../pages/resources';
export const entityHierarchies = {
- title: 'Entity hierarchies',
+ label: 'Entity hierarchies',
path: '/permission-groups-viewer',
Component: EntityHierarchiesPage,
};
diff --git a/packages/admin-panel/src/routes/index.js b/packages/admin-panel/src/routes/index.js
index 5515cdf5e8..369b9fb15d 100644
--- a/packages/admin-panel/src/routes/index.js
+++ b/packages/admin-panel/src/routes/index.js
@@ -3,7 +3,7 @@
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
-export { ROUTES } from './routes';
+export { ROUTES, AUTH_ROUTES } from './routes';
export {
surveys,
questions,
diff --git a/packages/admin-panel/src/routes/projects/strive.js b/packages/admin-panel/src/routes/projects/strive.js
index d022530607..e7f5c709a8 100644
--- a/packages/admin-panel/src/routes/projects/strive.js
+++ b/packages/admin-panel/src/routes/projects/strive.js
@@ -6,7 +6,7 @@
import { StrivePage } from '../../pages/StrivePage';
export const strive = {
- title: 'Strive',
+ label: 'Strive',
path: '/strive',
Component: StrivePage,
isBESAdminOnly: true,
diff --git a/packages/admin-panel/src/routes/routes.jsx b/packages/admin-panel/src/routes/routes.jsx
index 2b5b6869d9..bacd59cccf 100644
--- a/packages/admin-panel/src/routes/routes.jsx
+++ b/packages/admin-panel/src/routes/routes.jsx
@@ -18,3 +18,9 @@ export const ROUTES = [
projectsTabRoutes,
externalDataTabRoutes,
];
+
+export const AUTH_ROUTES = {
+ LOGIN: '/login',
+ FORGOT_PASSWORD: '/forgot-password',
+ RESET_PASSWORD: '/reset-password',
+};
diff --git a/packages/admin-panel/src/routes/users/permissionGroupsViewer.js b/packages/admin-panel/src/routes/users/permissionGroupsViewer.js
index 4d1b5cdab6..075afeb24e 100644
--- a/packages/admin-panel/src/routes/users/permissionGroupsViewer.js
+++ b/packages/admin-panel/src/routes/users/permissionGroupsViewer.js
@@ -5,7 +5,7 @@
import { PermissionGroupsViewerPage } from '../../pages/resources';
export const permissionGroupsViewer = {
- title: 'Permission groups viewer',
+ label: 'Permission groups viewer',
path: '/permission-groups-viewer',
Component: PermissionGroupsViewerPage,
};
diff --git a/packages/admin-panel/src/table/DataFetchingTable/DataFetchingTable.jsx b/packages/admin-panel/src/table/DataFetchingTable/DataFetchingTable.jsx
index 1ec4879f0c..53558619c2 100644
--- a/packages/admin-panel/src/table/DataFetchingTable/DataFetchingTable.jsx
+++ b/packages/admin-panel/src/table/DataFetchingTable/DataFetchingTable.jsx
@@ -300,7 +300,7 @@ const DataFetchingTableComponent = memo(
}
isButtonColumn
>
- Action
+ {actionLabel}
)}
diff --git a/packages/admin-panel/src/theme/colors.js b/packages/admin-panel/src/theme/colors.js
index 95d795bd01..2051402cf3 100644
--- a/packages/admin-panel/src/theme/colors.js
+++ b/packages/admin-panel/src/theme/colors.js
@@ -8,14 +8,14 @@ export const WHITE = '#FFFFFF';
export const BLACK = '#000000';
export const BLUE = '#328DE5';
export const YELLOW = '#FFCC24';
-export const RED = '#D13333';
+export const RED = '#F76853';
export const ORANGE = '#EF5A06';
export const GREEN = '#02B851';
export const DARKGREY = '#283238'; // dark background
export const LIGHTGREY = '#F9F9F9'; // page background
export const TEXT_DARKGREY = '#414D55';
-export const TEXT_MIDGREY = '#666666';
-export const TEXT_LIGHTGREY = '#DEDEDE';
+export const TEXT_MIDGREY = '#898989';
+export const TEXT_LIGHTGREY = '#B8B8B8';
export const DARK_BLUE = '#328DE5';
export const DARK_GREEN = '#00972E';
@@ -27,11 +27,10 @@ export const LIGHT_ORANGE = '#FFECE1';
// Greys (based on first 2 letters of hex code)
export const GREY_72 = '#727D84';
export const GREY_9F = '#9FA6AA';
-export const GREY_DE = '#DEDEE0'; // use for border colors of cards
export const GREY_E2 = '#E2E2E2';
export const GREY_F1 = '#F1F1F1';
export const GREY_FB = '#FBF9F9';
-export const GREY_B8 = '#B8B8B8';
+export const GREY_DE = '#DEDEDE';
// Blues
export const BLUE_BF = '#BFD5E4';
diff --git a/packages/admin-panel/src/theme/theme.js b/packages/admin-panel/src/theme/theme.js
index f2c7e5055c..9cd4c7627b 100644
--- a/packages/admin-panel/src/theme/theme.js
+++ b/packages/admin-panel/src/theme/theme.js
@@ -35,6 +35,7 @@ const palette = {
secondary: COLORS.TEXT_MIDGREY,
tertiary: COLORS.TEXT_LIGHTGREY,
},
+ divider: COLORS.GREY_DE,
blue: {
100: COLORS.BLUE_F6,
200: COLORS.BLUE_E8,
@@ -129,11 +130,7 @@ const overrides = {
borderColor: COLORS.GREY_DE,
},
},
- MuiDivider: {
- root: {
- backgroundColor: COLORS.GREY_DE,
- },
- },
+
MuiFormLabel: {
root: {
fontSize: '0.875rem',
@@ -148,7 +145,7 @@ const overrides = {
input: {
fontSize: '0.875rem',
'&::placeholder': {
- color: COLORS.GREY_B8,
+ color: COLORS.TEXT_LIGHTGREY,
},
},
},
diff --git a/packages/admin-panel/src/usedBy/UsedBy.jsx b/packages/admin-panel/src/usedBy/UsedBy.jsx
index c4d7102fba..829c129179 100644
--- a/packages/admin-panel/src/usedBy/UsedBy.jsx
+++ b/packages/admin-panel/src/usedBy/UsedBy.jsx
@@ -39,7 +39,7 @@ const StyledLink = styled(Link)`
}
.MuiSvgIcon-root {
width: 16px;
- color: ${props => props.theme.palette.text.tertiary};
+ color: ${props => props.theme.palette.text.secondary};
margin-left: 15px;
}
`;
diff --git a/packages/admin-panel/src/utilities/StoreProvider.jsx b/packages/admin-panel/src/utilities/StoreProvider.jsx
index 0fc879b724..2344ca544d 100644
--- a/packages/admin-panel/src/utilities/StoreProvider.jsx
+++ b/packages/admin-panel/src/utilities/StoreProvider.jsx
@@ -6,25 +6,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { createStore, applyMiddleware, compose } from 'redux';
-import { PersistGate } from 'redux-persist/lib/integration/react';
import { Provider } from 'react-redux';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import thunk from 'redux-thunk';
-import localforage from 'localforage';
-import { persistReducer, persistStore } from 'redux-persist';
import { rootReducer } from '../rootReducer';
-import { RememberMeTransform } from '../authentication/reducer';
-
-const persistedRootReducer = persistReducer(
- {
- key: 'root',
- storage: localforage,
- transforms: [RememberMeTransform],
- whitelist: ['authentication'], // only persist logged in state
- },
- rootReducer,
-);
const initialState = {};
const enhancers = [];
@@ -36,27 +22,21 @@ if (import.meta.env.DEV) {
}
}
-export const StoreProvider = React.memo(({ children, api, persist }) => {
+export const StoreProvider = React.memo(({ children, api }) => {
const middleware = [thunk.withExtraArgument({ api })];
const composedEnhancers = compose(applyMiddleware(...middleware), ...enhancers);
- const store = createStore(persistedRootReducer, initialState, composedEnhancers);
- const persistor = persistStore(store);
+ const store = createStore(rootReducer, initialState, composedEnhancers);
+
const queryClient = new QueryClient();
api.injectReduxStore(store);
- if (!persist) {
- return {children};
- }
-
return (
-
-
-
- {children}
-
-
+
+
+ {children}
+
);
});
@@ -64,9 +44,4 @@ export const StoreProvider = React.memo(({ children, api, persist }) => {
StoreProvider.propTypes = {
api: PropTypes.object.isRequired,
children: PropTypes.node.isRequired,
- persist: PropTypes.bool,
-};
-
-StoreProvider.defaultProps = {
- persist: false,
};
diff --git a/packages/admin-panel/src/utilities/getHasBESAdminAccess.js b/packages/admin-panel/src/utilities/getHasBESAdminAccess.js
new file mode 100644
index 0000000000..2dd3663cd8
--- /dev/null
+++ b/packages/admin-panel/src/utilities/getHasBESAdminAccess.js
@@ -0,0 +1,7 @@
+// Assume that if a user has any BES Admin access, they are an internal user, to avoid having to check permissions for every country
+export const getHasBESAdminAccess = user => {
+ if (!user || !user.accessPolicy) return false;
+ return Object.keys(user.accessPolicy).some(countryCode =>
+ user.accessPolicy[countryCode].some(permissionGroupName => permissionGroupName === 'BES Admin'),
+ );
+};
diff --git a/packages/admin-panel/src/widgets/PageHeader.jsx b/packages/admin-panel/src/widgets/PageHeader.jsx
index 5bb026c00b..b925625260 100644
--- a/packages/admin-panel/src/widgets/PageHeader.jsx
+++ b/packages/admin-panel/src/widgets/PageHeader.jsx
@@ -56,7 +56,6 @@ export const PageHeader = ({
};
PageHeader.propTypes = {
- title: PropTypes.string.isRequired,
importConfig: PropTypes.object,
createConfig: PropTypes.object,
exportConfig: PropTypes.object,
diff --git a/packages/datatrak-web/src/api/mutations/useRequestResetPassword.ts b/packages/datatrak-web/src/api/mutations/useRequestResetPassword.ts
index be5d4228fe..89c3fde15d 100644
--- a/packages/datatrak-web/src/api/mutations/useRequestResetPassword.ts
+++ b/packages/datatrak-web/src/api/mutations/useRequestResetPassword.ts
@@ -12,7 +12,7 @@ type ResetPasswordParams = {
export const useRequestResetPassword = () => {
return useMutation(
({ emailAddress }: ResetPasswordParams) => {
- return post('auth/resetPassword', {
+ return post('requestResetPassword', {
data: {
emailAddress,
resetPasswordUrl: window.location.origin,
diff --git a/packages/lesmis/src/routes/AdminPanelApp.jsx b/packages/lesmis/src/routes/AdminPanelApp.jsx
index 9dfb9f6c75..64743c110c 100644
--- a/packages/lesmis/src/routes/AdminPanelApp.jsx
+++ b/packages/lesmis/src/routes/AdminPanelApp.jsx
@@ -3,27 +3,27 @@
* Copyright (c) 2017 - 2021 Beyond Essential Systems Pty Ltd
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
-import { connect } from 'react-redux';
+import { Route, Routes, Navigate } from 'react-router-dom';
import styled from 'styled-components';
import {
- LogoutPage,
PrivateRoute,
ResourcePage,
TabPageLayout,
getFlattenedChildViews,
+ PageContentWrapper,
+ AUTH_ROUTES,
+ AuthLayout,
+ useUser,
+ getHasBESAdminAccess,
AppPageLayout,
VizBuilderApp,
- PageContentWrapper,
} from '@tupaia/admin-panel';
import { LesmisAdminRedirect } from './LesmisAdminRedirect';
import { AdminPanelLoginPage } from '../views/AdminPanel/AdminPanelLoginPage';
-import { useAdminPanelUrl, useI18n, hasAdminPanelAccess } from '../utils';
+import { useAdminPanelUrl, useI18n } from '../utils';
import { Footer } from '../components';
import { getRoutes } from '../views/AdminPanel/routes';
-import { getIsBESAdmin } from '../views/AdminPanel/authentication';
import { NotAuthorisedView } from '../views/NotAuthorisedView';
const PageContentContainerComponent = styled(PageContentWrapper)`
@@ -37,6 +37,10 @@ const PageContentContainerComponent = styled(PageContentWrapper)`
`;
const AdminPanelWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ min-height: 30rem;
nav a {
font-size: 0.875rem; // make the font size smaller to fit more text in the nav and match the default font size
}
@@ -45,25 +49,32 @@ const AdminPanelWrapper = styled.div`
}
`;
-const AdminPanelApp = ({ user, isBESAdmin }) => {
+const AdminPanelApp = () => {
const { translate } = useI18n();
- const location = useLocation();
+ const { data: user } = useUser();
const adminUrl = useAdminPanelUrl();
- const userHasAdminPanelAccess = hasAdminPanelAccess(user);
+ const hasBESAdminAccess = getHasBESAdminAccess(user);
- const routes = getRoutes(adminUrl, translate, isBESAdmin);
+ const routes = getRoutes(adminUrl, translate, hasBESAdminAccess);
return (
- } />
- } />
+
+ }
+ >
+ } />
+
- }>
- }
- >
+ }>
+ }>
{
)
}
@@ -114,7 +125,6 @@ const AdminPanelApp = ({ user, isBESAdmin }) => {
{
}
/>
-
- }
- />
} />
@@ -160,25 +156,4 @@ const AdminPanelApp = ({ user, isBESAdmin }) => {
);
};
-AdminPanelApp.propTypes = {
- user: PropTypes.shape({
- name: PropTypes.string,
- email: PropTypes.string,
- firstName: PropTypes.string,
- profileImage: PropTypes.string,
- }),
- isBESAdmin: PropTypes.bool,
-};
-
-AdminPanelApp.defaultProps = {
- isBESAdmin: false,
- user: {},
-};
-
-export default connect(
- state => ({
- user: state?.authentication?.user || {},
- isBESAdmin: getIsBESAdmin(state),
- }),
- null,
-)(AdminPanelApp);
+export default AdminPanelApp;
diff --git a/packages/lesmis/src/routes/LesmisAdminRedirect.jsx b/packages/lesmis/src/routes/LesmisAdminRedirect.jsx
index 4438e241c6..8a031d53f4 100644
--- a/packages/lesmis/src/routes/LesmisAdminRedirect.jsx
+++ b/packages/lesmis/src/routes/LesmisAdminRedirect.jsx
@@ -5,9 +5,13 @@
*/
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
+import { useUser } from '@tupaia/admin-panel';
+import { hasAdminPanelAccess } from '../utils';
-export const LesmisAdminRedirect = ({ hasAdminPanelAccess = false }) => {
- if (!hasAdminPanelAccess) {
+export const LesmisAdminRedirect = () => {
+ const { data: user } = useUser();
+ const userHasAdminPanelAccess = hasAdminPanelAccess(user);
+ if (!userHasAdminPanelAccess) {
return ;
}
diff --git a/packages/lesmis/src/views/AdminPanel/AdminPanelLoginPage.jsx b/packages/lesmis/src/views/AdminPanel/AdminPanelLoginPage.jsx
index 9cf046c81f..a4dee93b81 100644
--- a/packages/lesmis/src/views/AdminPanel/AdminPanelLoginPage.jsx
+++ b/packages/lesmis/src/views/AdminPanel/AdminPanelLoginPage.jsx
@@ -4,22 +4,10 @@
*/
import React from 'react';
-import { useLocation } from 'react-router';
-import styled from 'styled-components';
import { useAdminPanelUrl } from '../../utils';
import { LoginPage } from './pages/LoginPage';
-const StyledImg = styled.img`
- height: 6rem;
- width: auto;
- margin-bottom: 2.5rem;
-`;
-
-const LoginPageLogo = () => ;
-
export const AdminPanelLoginPage = () => {
const adminUrl = useAdminPanelUrl();
- const location = useLocation();
- const redirectTo = location.state?.referrer || `${adminUrl}/survey-responses`;
- return ;
+ return ;
};
diff --git a/packages/lesmis/src/views/AdminPanel/AdminPanelProfileButton.jsx b/packages/lesmis/src/views/AdminPanel/AdminPanelProfileButton.jsx
deleted file mode 100644
index 4bd3be636e..0000000000
--- a/packages/lesmis/src/views/AdminPanel/AdminPanelProfileButton.jsx
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Tupaia
- * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
- */
-import React from 'react';
-import PropTypes from 'prop-types';
-import { ProfileButton, ProfileButtonItem } from '@tupaia/ui-components';
-import { useAdminPanelUrl } from '../../utils';
-
-export const AdminPanelProfileButton = ({ user }) => {
- const adminUrl = useAdminPanelUrl();
- const ProfileLinks = () => (
- <>
- Logout
- >
- );
- return ;
-};
-
-AdminPanelProfileButton.propTypes = {
- user: PropTypes.shape({
- name: PropTypes.string.isRequired,
- email: PropTypes.string.isRequired,
- firstName: PropTypes.string,
- profileImage: PropTypes.string,
- }).isRequired,
-};
diff --git a/packages/lesmis/src/views/AdminPanel/authentication/actions.js b/packages/lesmis/src/views/AdminPanel/authentication/actions.js
index bd1a81d2d9..54125e05d8 100644
--- a/packages/lesmis/src/views/AdminPanel/authentication/actions.js
+++ b/packages/lesmis/src/views/AdminPanel/authentication/actions.js
@@ -23,56 +23,66 @@ export const changeRememberMe = rememberMe => ({
rememberMe,
});
-export const login = (emailAddress, password) => async (dispatch, getState, { api }) => {
- const deviceName = window.navigator.userAgent;
- dispatch({
- // Set state to logging in
- type: LOGIN_REQUEST,
- });
- try {
- const userDetails = await api.login({
- emailAddress,
- password,
- deviceName,
+export const login =
+ (emailAddress, password) =>
+ async (dispatch, getState, { api }) => {
+ const deviceName = window.navigator.userAgent;
+ dispatch({
+ // Set state to logging in
+ type: LOGIN_REQUEST,
});
- dispatch(loginSuccess(userDetails));
- } catch (error) {
- dispatch(loginError(error.message));
- }
-};
+ try {
+ const userDetails = await api.post({
+ emailAddress,
+ password,
+ deviceName,
+ });
+ dispatch(loginSuccess(userDetails));
+ } catch (error) {
+ dispatch(loginError(error.message));
+ }
+ };
-export const loginSuccess = ({ user }) => dispatch => {
- dispatch({
- type: LOGIN_SUCCESS,
- user,
- });
-};
+export const loginSuccess =
+ ({ user }) =>
+ dispatch => {
+ dispatch({
+ type: LOGIN_SUCCESS,
+ user,
+ });
+ };
export const loginError = errorMessage => ({
type: LOGIN_ERROR,
errorMessage,
});
-export const logout = (errorMessage = null) => async (dispatch, getState, { api }) => {
- dispatch({
- // Set state to logging out
- type: LOGOUT,
- errorMessage,
- });
+export const logout =
+ (errorMessage = null) =>
+ async (dispatch, getState, { api }) => {
+ dispatch({
+ // Set state to logging out
+ type: LOGOUT,
+ errorMessage,
+ });
- await api.logout();
-};
+ await api.logout();
+ };
// Profile
-export const updateProfile = payload => async (dispatch, getState, { api }) => {
- await api.put(`me`, null, payload);
- const { body: user } = await api.get(`me`);
- dispatch({
- type: PROFILE_SUCCESS,
- ...user,
- });
-};
+export const updateProfile =
+ payload =>
+ async (dispatch, getState, { api }) => {
+ await api.put(`me`, null, payload);
+ const { body: user } = await api.get(`me`);
+ dispatch({
+ type: PROFILE_SUCCESS,
+ ...user,
+ });
+ };
// Password
-export const updatePassword = payload => async (dispatch, getState, { api }) =>
- api.post(`me/changePassword`, null, payload);
+export const updatePassword =
+ payload =>
+ async (dispatch, getState, { api }) =>
+ api.post(`me/changePassword`, null, payload);
diff --git a/packages/lesmis/src/views/AdminPanel/pages/LoginPage.jsx b/packages/lesmis/src/views/AdminPanel/pages/LoginPage.jsx
index 3a0bdec364..b829332c31 100644
--- a/packages/lesmis/src/views/AdminPanel/pages/LoginPage.jsx
+++ b/packages/lesmis/src/views/AdminPanel/pages/LoginPage.jsx
@@ -4,99 +4,24 @@
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { Navigate } from 'react-router-dom';
-import styled from 'styled-components';
-import MuiCard from '@material-ui/core/Card';
-import Typography from '@material-ui/core/Typography';
-import { FlexCenter } from '@tupaia/ui-components';
-import MuiLink from '@material-ui/core/Link';
-import { getIsUserAuthenticated } from '../authentication';
-import { LoginForm } from '../authentication/LoginForm';
-import { useI18n } from '../../../utils';
+import { LoginPage as AdminLoginPage } from '@tupaia/admin-panel';
+import { useAdminPanelUrl, useI18n } from '../../../utils';
-const Main = styled.main`
- height: 100vh;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- padding-top: 5%;
-`;
-
-const Container = styled.section`
- display: flex;
- flex-direction: column;
- justify-content: center;
-`;
-
-const StyledCard = styled(MuiCard)`
- width: 28rem;
- padding: 2.5rem 3.5rem 3rem 3rem;
- margin: 0 auto 2rem;
- box-shadow: 0 0 12px rgba(0, 0, 0, 0.15);
-`;
-
-const StyledImg = styled.img`
- height: 6rem;
- width: auto;
- margin-bottom: 2.5rem;
-`;
-
-const StyledHelperText = styled(Typography)`
- font-weight: 500;
- font-size: 0.875rem;
- line-height: 1rem;
- color: ${props => props.theme.palette.text.secondary};
-`;
-
-const StyledLink = styled(MuiLink)`
- font-weight: 500;
- font-size: 0.875rem;
- line-height: 1rem;
- margin-left: 0.3rem;
- color: ${props => props.theme.palette.primary.main};
-`;
-
-const requestAnAccountUrl = 'https://info.tupaia.org/contact';
-
-const Logo = () => ;
-
-const LoginPageComponent = ({ isLoggedIn, redirectTo, LogoComponent }) => {
- if (isLoggedIn) {
- return ;
- }
+export const LoginPage = () => {
+ const adminUrl = useAdminPanelUrl();
const { translate } = useI18n();
return (
-
-
-
-
-
-
-
- {translate('login.dontHaveAccess')}
- {translate('login.requestAnAccount')}
-
-
-
+
);
};
-
-LoginPageComponent.propTypes = {
- isLoggedIn: PropTypes.bool.isRequired,
- redirectTo: PropTypes.string,
- LogoComponent: PropTypes.elementType,
-};
-
-LoginPageComponent.defaultProps = {
- redirectTo: '/',
- LogoComponent: Logo,
-};
-
-const mapStateToProps = state => ({
- isLoggedIn: getIsUserAuthenticated(state),
-});
-
-export const LoginPage = connect(mapStateToProps)(LoginPageComponent);
diff --git a/packages/server-boilerplate/src/orchestrator/api/ApiBuilder.ts b/packages/server-boilerplate/src/orchestrator/api/ApiBuilder.ts
index 8443d48a61..76a79d7b1b 100644
--- a/packages/server-boilerplate/src/orchestrator/api/ApiBuilder.ts
+++ b/packages/server-boilerplate/src/orchestrator/api/ApiBuilder.ts
@@ -21,7 +21,7 @@ import { UnauthenticatedError } from '@tupaia/utils';
import morgan from 'morgan';
import { handleWith, handleError, emptyMiddleware, initialiseApiClient } from '../../utils';
import { TestRoute } from '../../routes';
-import { LoginRoute, LogoutRoute, OneTimeLoginRoute } from '../routes';
+import { LoginRoute, LogoutRoute, OneTimeLoginRoute, RequestResetPasswordRoute } from '../routes';
import { attachSession as defaultAttachSession } from '../session';
import { ExpressRequest, Params, ReqBody, ResBody, Query } from '../../routes/Route';
import { SessionModel } from '../models';
@@ -281,6 +281,12 @@ export class ApiBuilder {
handleWith(OneTimeLoginRoute),
);
+ this.app.post(
+ this.formatPath('requestResetPassword'),
+ this.logApiRequestMiddleware,
+ handleWith(RequestResetPasswordRoute),
+ );
+
this.handlers.forEach(handler => handler.add());
this.app.use(handleError);
diff --git a/packages/server-boilerplate/src/orchestrator/auth/AuthConnection.ts b/packages/server-boilerplate/src/orchestrator/auth/AuthConnection.ts
index 51742c2a63..c0446b5401 100644
--- a/packages/server-boilerplate/src/orchestrator/auth/AuthConnection.ts
+++ b/packages/server-boilerplate/src/orchestrator/auth/AuthConnection.ts
@@ -6,7 +6,7 @@
import { createBasicHeader, requireEnv } from '@tupaia/utils';
import { AccessPolicyObject } from '../../types';
-import { Credentials, OneTimeCredentials } from '../types';
+import { Credentials, OneTimeCredentials, RequestResetPasswordCredentials } from '../types';
import { ApiConnection } from '../../connections';
const DEFAULT_NAME = 'TUPAIA-SERVER';
@@ -59,6 +59,13 @@ export class AuthConnection extends ApiConnection {
return this.parseAuthResponse(response);
}
+ public async requestResetPassword({
+ emailAddress,
+ resetPasswordUrl,
+ }: RequestResetPasswordCredentials) {
+ return this.post('auth/resetPassword', {}, { emailAddress, resetPasswordUrl });
+ }
+
public async refreshAccessToken(refreshToken: string) {
const response = await this.post(
'auth',
diff --git a/packages/server-boilerplate/src/orchestrator/routes/RequestResetPasswordRoute.ts b/packages/server-boilerplate/src/orchestrator/routes/RequestResetPasswordRoute.ts
new file mode 100644
index 0000000000..301ea2d0d1
--- /dev/null
+++ b/packages/server-boilerplate/src/orchestrator/routes/RequestResetPasswordRoute.ts
@@ -0,0 +1,30 @@
+/*
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ *
+ */
+
+import { Request, Response, NextFunction } from 'express';
+import { AuthConnection, AuthResponse } from '../auth';
+import { Route } from '../../routes';
+import { EmptyObject } from '../../types';
+import { RequestResetPasswordCredentials } from '../types';
+
+export interface RequestResetPasswordRequest
+ extends Request {}
+
+export class RequestResetPasswordRoute extends Route {
+ private authConnection: AuthConnection;
+
+ public constructor(req: RequestResetPasswordRequest, res: Response, next: NextFunction) {
+ super(req, res, next);
+
+ this.authConnection = new AuthConnection();
+ }
+
+ public async buildResponse() {
+ const credentials = this.req.body;
+
+ return this.authConnection.requestResetPassword(credentials);
+ }
+}
diff --git a/packages/server-boilerplate/src/orchestrator/routes/index.ts b/packages/server-boilerplate/src/orchestrator/routes/index.ts
index 0cb7b56c81..89d2f00ae8 100644
--- a/packages/server-boilerplate/src/orchestrator/routes/index.ts
+++ b/packages/server-boilerplate/src/orchestrator/routes/index.ts
@@ -6,3 +6,7 @@
export { LoginRoute, LoginRequest } from './LoginRoute';
export { LogoutRoute } from './LogoutRoute';
export { OneTimeLoginRoute, OneTimeLoginRequest } from './OneTimeLoginRoute';
+export {
+ RequestResetPasswordRoute,
+ RequestResetPasswordRequest,
+} from './RequestResetPasswordRoute';
diff --git a/packages/server-boilerplate/src/orchestrator/types.ts b/packages/server-boilerplate/src/orchestrator/types.ts
index 1cb72d5aab..32053154c1 100644
--- a/packages/server-boilerplate/src/orchestrator/types.ts
+++ b/packages/server-boilerplate/src/orchestrator/types.ts
@@ -19,3 +19,8 @@ export interface OneTimeCredentials {
token: string;
deviceName: string;
}
+
+export interface RequestResetPasswordCredentials {
+ emailAddress: string;
+ resetPasswordUrl: string;
+}
diff --git a/packages/tupaia-web-server/src/app/createApp.ts b/packages/tupaia-web-server/src/app/createApp.ts
index 2fd0aecd61..f6872d48c4 100644
--- a/packages/tupaia-web-server/src/app/createApp.ts
+++ b/packages/tupaia-web-server/src/app/createApp.ts
@@ -100,7 +100,6 @@ export async function createApp(db: TupaiaDatabase = new TupaiaDatabase()) {
.use('logout', forwardRequest(WEB_CONFIG_API_URL, { authHandlerProvider }))
.use('projects', forwardRequest(WEB_CONFIG_API_URL, { authHandlerProvider }))
.use('resendEmail', forwardRequest(WEB_CONFIG_API_URL, { authHandlerProvider }))
- .use('resetPassword', forwardRequest(WEB_CONFIG_API_URL, { authHandlerProvider }))
.use('signup', forwardRequest(WEB_CONFIG_API_URL, { authHandlerProvider }))
.use('verifyEmail', forwardRequest(WEB_CONFIG_API_URL, { authHandlerProvider }));
const app = builder.build();
diff --git a/packages/tupaia-web/src/api/mutations/useRequestResetPassword.ts b/packages/tupaia-web/src/api/mutations/useRequestResetPassword.ts
index a488f315a2..bfe5b64f9b 100644
--- a/packages/tupaia-web/src/api/mutations/useRequestResetPassword.ts
+++ b/packages/tupaia-web/src/api/mutations/useRequestResetPassword.ts
@@ -12,7 +12,7 @@ type ResetPasswordParams = {
export const useRequestResetPassword = () => {
return useMutation(
({ emailAddress }: ResetPasswordParams) => {
- return post('resetPassword', {
+ return post('requestResetPassword', {
data: {
emailAddress,
},
diff --git a/packages/ui-components/src/constants.js b/packages/ui-components/src/constants.js
index 460fcf11d7..7d42af30b6 100644
--- a/packages/ui-components/src/constants.js
+++ b/packages/ui-components/src/constants.js
@@ -12,8 +12,16 @@ export const FORM_FIELD_VALIDATION = {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email',
},
+ required: {
+ value: true,
+ message: '*Required',
+ },
},
PASSWORD: {
minLength: { value: 8, message: 'Must be at least 8 characters long' },
+ required: {
+ value: true,
+ message: '*Required',
+ },
},
};
diff --git a/packages/ui-components/src/features/Auth/AuthLink.tsx b/packages/ui-components/src/features/Auth/AuthLink.tsx
index d6656f6b67..4a34755e38 100644
--- a/packages/ui-components/src/features/Auth/AuthLink.tsx
+++ b/packages/ui-components/src/features/Auth/AuthLink.tsx
@@ -12,5 +12,6 @@ export const AuthLink = styled(Typography).attrs({
margin-top: 1.25rem;
a {
color: ${props => props.theme.palette.text.primary};
+ margin-inline-start: 0.25rem;
}
`;
diff --git a/packages/ui-components/src/features/Auth/ForgotPasswordForm.tsx b/packages/ui-components/src/features/Auth/ForgotPasswordForm.tsx
index 20610aaeb5..411bd734a3 100644
--- a/packages/ui-components/src/features/Auth/ForgotPasswordForm.tsx
+++ b/packages/ui-components/src/features/Auth/ForgotPasswordForm.tsx
@@ -42,6 +42,7 @@ interface ForgotPasswordFormProps {
onSubmit: SubmitHandler;
loginLink: LinkProps['to'];
registerLink: LinkProps['to'];
+ RegisterLinkComponent?: React.ReactNode;
}
export const ForgotPasswordForm = ({
@@ -52,6 +53,7 @@ export const ForgotPasswordForm = ({
formContext,
loginLink,
registerLink,
+ RegisterLinkComponent,
}: ForgotPasswordFormProps) => {
const HEADING_TEXT = {
title: 'Forgot password',
@@ -84,7 +86,12 @@ export const ForgotPasswordForm = ({
Back to log in
- Don’t have an account? Register here
+ Don’t have an account?{' '}
+ {RegisterLinkComponent ? (
+ RegisterLinkComponent
+ ) : (
+ Sign up
+ )}
)}
diff --git a/packages/ui-components/src/features/Auth/LoginForm.tsx b/packages/ui-components/src/features/Auth/LoginForm.tsx
index bc0e8fc786..4e657c5a2f 100644
--- a/packages/ui-components/src/features/Auth/LoginForm.tsx
+++ b/packages/ui-components/src/features/Auth/LoginForm.tsx
@@ -47,6 +47,17 @@ interface LoginFormProps {
message?: Message | null;
formContext: ReturnType;
className?: string;
+ RegisterLinkComponent?: React.ReactNode;
+ labels?: {
+ title?: string;
+ subtitle?: string;
+ email?: string;
+ password?: string;
+ forgotPassword?: string;
+ login?: string;
+ dontHaveAnAccount?: string;
+ register?: string;
+ };
}
export const LoginForm = ({
@@ -58,9 +69,22 @@ export const LoginForm = ({
message,
formContext,
className,
+ RegisterLinkComponent,
+ labels,
}: LoginFormProps) => {
+ const showRegisterLink = registerLink || RegisterLinkComponent;
+ const {
+ title = 'Log in',
+ subtitle = 'Enter your details below to log in',
+ email = 'Email',
+ password = 'Password',
+ forgotPassword = 'Forgot password?',
+ login = 'Log in',
+ dontHaveAnAccount = "Don't have an account?",
+ register = 'Register here',
+ } = labels || {};
return (
-
+
{error && {error.message}}
{message && }
@@ -72,7 +96,7 @@ export const LoginForm = ({
options={FORM_FIELD_VALIDATION.EMAIL}
required
Input={AuthFormTextField}
- label="Email"
+ label={email}
disabled={isLoading}
/>
-
- Forgot password?
-
+ {forgotPasswordLink && (
+
+ {forgotPassword}
+
+ )}
- Log in
+ {login}
-
- Don’t have an account? Register here
-
+ {showRegisterLink && (
+
+ {dontHaveAnAccount}
+ {RegisterLinkComponent || {register}}
+
+ )}
);
diff --git a/packages/ui-components/src/features/Auth/ResetPasswordForm/PasswordForm.tsx b/packages/ui-components/src/features/Auth/ResetPasswordForm/PasswordForm.tsx
index f11efd8c07..d77fb1eb15 100644
--- a/packages/ui-components/src/features/Auth/ResetPasswordForm/PasswordForm.tsx
+++ b/packages/ui-components/src/features/Auth/ResetPasswordForm/PasswordForm.tsx
@@ -41,7 +41,7 @@ export const PasswordForm = ({
options: FORM_FIELD_VALIDATION.PASSWORD,
},
{
- name: 'passwordConfirm',
+ name: 'newPasswordConfirm',
label: 'Confirm password',
options: {
validate: (value: string) =>
diff --git a/packages/ui-components/src/features/Auth/index.ts b/packages/ui-components/src/features/Auth/index.ts
index 535ddb16a2..df191674c7 100644
--- a/packages/ui-components/src/features/Auth/index.ts
+++ b/packages/ui-components/src/features/Auth/index.ts
@@ -9,3 +9,4 @@ export { ResendVerificationEmailForm } from './ResendVerificationEmailForm';
export { EMAIL_VERIFICATION_STATUS } from './EmailVerificationDisplay';
export { ResetPasswordForm } from './ResetPasswordForm';
export { ForgotPasswordForm } from './ForgotPasswordForm';
+export { AuthSubmitButton } from './AuthSubmitButton';
diff --git a/packages/web-config-server/src/apiV1/index.js b/packages/web-config-server/src/apiV1/index.js
index 478680861c..f689e3ef5d 100755
--- a/packages/web-config-server/src/apiV1/index.js
+++ b/packages/web-config-server/src/apiV1/index.js
@@ -5,13 +5,7 @@
import { Router } from 'express';
-import {
- appRequestCountryAccess,
- appRequestResetPassword,
- appResendEmail,
- appSignup,
- appVerifyEmail,
-} from '/appServer';
+import { appRequestCountryAccess, appResendEmail, appSignup, appVerifyEmail } from '/appServer';
import { oneTimeLogin } from '/authSession';
import { exportChartHandler, ExportSurveyDataHandler, ExportSurveyResponsesHandler } from '/export';
import { getUser } from './getUser';
@@ -31,7 +25,6 @@ export const getRoutesForApiV1 = () => {
api.get('/getUser', catchAsyncErrors(getUser()));
api.post('/login/oneTimeLogin', catchAsyncErrors(oneTimeLogin));
api.post('/signup', catchAsyncErrors(appSignup()));
- api.post('/resetPassword', catchAsyncErrors(appRequestResetPassword()));
api.post('/requestCountryAccess', catchAsyncErrors(appRequestCountryAccess()));
api.get('/verifyEmail', catchAsyncErrors(appVerifyEmail()));
api.post('/resendEmail', catchAsyncErrors(appResendEmail()));
diff --git a/packages/web-config-server/src/appServer/handlers/requestResetPassword.js b/packages/web-config-server/src/appServer/handlers/requestResetPassword.js
deleted file mode 100644
index d8b5b97665..0000000000
--- a/packages/web-config-server/src/appServer/handlers/requestResetPassword.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { fetchFromCentralServer } from '/appServer/requestHelpers';
-
-/*
- * Issues a password reset request, notifying a user with a one-time-login link.
- */
-export const requestResetPassword = async req => {
- const endpoint = 'auth/resetPassword';
- return fetchFromCentralServer(endpoint, req.body);
-};
diff --git a/packages/web-config-server/src/appServer/index.js b/packages/web-config-server/src/appServer/index.js
index c44c0bf4d5..677531363e 100644
--- a/packages/web-config-server/src/appServer/index.js
+++ b/packages/web-config-server/src/appServer/index.js
@@ -1,7 +1 @@
-export {
- appSignup,
- appRequestResetPassword,
- appResendEmail,
- appRequestCountryAccess,
- appVerifyEmail,
-} from './routes';
+export { appSignup, appResendEmail, appRequestCountryAccess, appVerifyEmail } from './routes';
diff --git a/packages/web-config-server/src/appServer/routes.js b/packages/web-config-server/src/appServer/routes.js
index 9d5508c816..8f91a8f14c 100644
--- a/packages/web-config-server/src/appServer/routes.js
+++ b/packages/web-config-server/src/appServer/routes.js
@@ -1,6 +1,5 @@
import { createUser } from './handlers/createUser';
import { requestResendEmail, verifyEmail } from './handlers/verifyEmail';
-import { requestResetPassword } from './handlers/requestResetPassword';
import { requestCountryAccess } from './handlers/requestCountryAccess';
/**
@@ -13,11 +12,6 @@ export const appSignup = () => async (req, res) => {
res.send(result);
};
-export const appRequestResetPassword = () => async (req, res) => {
- const result = await requestResetPassword(req);
- res.send(result);
-};
-
export const appResendEmail = () => async (req, res) => {
const result = await requestResendEmail(req);
res.send(result);
From a56e91e4ed177cc5c2e2edd5ceeb4b6d5b84d48b Mon Sep 17 00:00:00 2001
From: Jasper Lai <33956381+jaskfla@users.noreply.github.com>
Date: Mon, 10 Jun 2024 13:00:20 +1200
Subject: [PATCH 09/26] tweak: Login form autocomplete (#5716)
* autocomplete in login form
* curly apostrophe
---
packages/ui-components/src/features/Auth/LoginForm.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/packages/ui-components/src/features/Auth/LoginForm.tsx b/packages/ui-components/src/features/Auth/LoginForm.tsx
index 4e657c5a2f..d4b8a68fb6 100644
--- a/packages/ui-components/src/features/Auth/LoginForm.tsx
+++ b/packages/ui-components/src/features/Auth/LoginForm.tsx
@@ -80,7 +80,7 @@ export const LoginForm = ({
password = 'Password',
forgotPassword = 'Forgot password?',
login = 'Log in',
- dontHaveAnAccount = "Don't have an account?",
+ dontHaveAnAccount = 'Don’t have an account?',
register = 'Register here',
} = labels || {};
return (
@@ -89,6 +89,7 @@ export const LoginForm = ({
{message && }
Date: Tue, 11 Jun 2024 12:34:51 +1200
Subject: [PATCH 10/26] fix(datatrak): RN-1321: Prevent bug where a user cannot
access datatrak if their refresh token has become invalid (#5685)
---
packages/database/src/DatabaseRecord.js | 5 ++
.../src/routes/UserRoute.ts | 2 +-
packages/datatrak-web/src/App.tsx | 5 +-
packages/datatrak-web/src/AppProviders.tsx | 54 ++++++++++---------
.../src/api/CurrentUserContext.tsx | 5 --
.../src/api/RedirectErrorHandler.tsx | 25 +++++++++
packages/datatrak-web/src/api/api.ts | 6 +--
packages/datatrak-web/src/api/fetchError.ts | 14 +++--
packages/datatrak-web/src/api/index.ts | 1 +
.../api/mutations/useExportSurveyResponses.ts | 2 +-
.../datatrak-web/src/constants/constants.ts | 2 +
.../auth/RequiresSessionAuthHandler.ts | 2 +-
.../auth/SessionSwitchingAuthHandler.ts | 2 +-
.../src/orchestrator/models/Session.ts | 22 ++++++--
packages/utils/src/errors.js | 9 +++-
15 files changed, 110 insertions(+), 46 deletions(-)
create mode 100644 packages/datatrak-web/src/api/RedirectErrorHandler.tsx
diff --git a/packages/database/src/DatabaseRecord.js b/packages/database/src/DatabaseRecord.js
index 1517bc12e1..a4a23d4bd9 100644
--- a/packages/database/src/DatabaseRecord.js
+++ b/packages/database/src/DatabaseRecord.js
@@ -169,4 +169,9 @@ export class DatabaseRecord {
this.id = record.id;
}
}
+
+ // Delete the record from the database
+ async delete() {
+ await this.model.deleteById(this.id);
+ }
}
diff --git a/packages/datatrak-web-server/src/routes/UserRoute.ts b/packages/datatrak-web-server/src/routes/UserRoute.ts
index e94320b6ee..42803ca0ce 100644
--- a/packages/datatrak-web-server/src/routes/UserRoute.ts
+++ b/packages/datatrak-web-server/src/routes/UserRoute.ts
@@ -50,7 +50,7 @@ export class UserRoute extends Route {
project = projects.find((p: WebServerProjectRequest.ResBody) => p.id === projectId);
}
if (countryId) {
- const countryResponse = await ctx.services.central.fetchResources(`/entities/${countryId}`, {
+ const countryResponse = await ctx.services.central.fetchResources(`entities/${countryId}`, {
columns: ['id', 'name', 'code'],
});
country = countryResponse || null;
diff --git a/packages/datatrak-web/src/App.tsx b/packages/datatrak-web/src/App.tsx
index c6f1e7937a..0e51b6cb4e 100755
--- a/packages/datatrak-web/src/App.tsx
+++ b/packages/datatrak-web/src/App.tsx
@@ -6,12 +6,15 @@ import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { AppProviders } from './AppProviders';
import { Routes } from './routes';
+import { RedirectErrorHandler } from './api';
export const App = () => {
return (
-
+
+
+
);
diff --git a/packages/datatrak-web/src/AppProviders.tsx b/packages/datatrak-web/src/AppProviders.tsx
index 936611290d..2823f853be 100755
--- a/packages/datatrak-web/src/AppProviders.tsx
+++ b/packages/datatrak-web/src/AppProviders.tsx
@@ -12,23 +12,29 @@ import { theme } from './theme';
import { Toast } from './components';
import { errorToast } from './utils';
import { CurrentUserContextProvider } from './api';
+import { REDIRECT_ERROR_PARAM } from './constants';
+
+const handleError = (error: any, query: any) => {
+ if (error.responseData.redirectClient) {
+ // Redirect the browser to the specified URL and display the error
+ window.location.href = `${error.responseData.redirectClient}?${REDIRECT_ERROR_PARAM}=${error.message}`;
+ }
+
+ if (!query?.meta || !query?.meta?.applyCustomErrorHandling) {
+ errorToast(error.message);
+ }
+};
const defaultQueryClient = new QueryClient({
mutationCache: new MutationCache({
// use the errorToast function to display errors by default. If you want to override this, apply an meta.applyCustomErrorHandling to the mutation
onError: (error: any, _variables: any, _context: any, mutation: any) => {
- if (!mutation?.meta || !mutation?.meta?.applyCustomErrorHandling) {
- errorToast(error.message);
- }
+ handleError(error, mutation);
},
}),
queryCache: new QueryCache({
// use the errorToast function to display errors by default. If you want to override this, apply an meta.applyCustomErrorHandling to the mutation or an onError to the query
- onError: (error: any, query) => {
- if (!query?.meta || !query?.meta?.applyCustomErrorHandling) {
- errorToast(error.message);
- }
- },
+ onError: handleError,
}),
defaultOptions: {
queries: {
@@ -50,23 +56,21 @@ export const AppProviders = ({ children, queryClient = defaultQueryClient }: App
-
-
-
- {children}
-
-
+
+
+ {children}
+
diff --git a/packages/datatrak-web/src/api/CurrentUserContext.tsx b/packages/datatrak-web/src/api/CurrentUserContext.tsx
index eab9335b5f..205c769cb6 100644
--- a/packages/datatrak-web/src/api/CurrentUserContext.tsx
+++ b/packages/datatrak-web/src/api/CurrentUserContext.tsx
@@ -5,7 +5,6 @@
import React, { createContext, useContext } from 'react';
import { DatatrakWebUserRequest } from '@tupaia/types';
import { FullPageLoader } from '@tupaia/ui-components';
-import { ErrorDisplay } from '../components';
import { useUser } from './queries';
export type CurrentUserContextType = DatatrakWebUserRequest.ResBody & { isLoggedIn: boolean };
@@ -27,10 +26,6 @@ export const CurrentUserContextProvider = ({ children }: { children: React.React
return ;
}
- if (currentUserQuery.isError) {
- return ;
- }
-
const data = currentUserQuery.data;
const userData = { ...data, isLoggedIn: !!data?.email };
diff --git a/packages/datatrak-web/src/api/RedirectErrorHandler.tsx b/packages/datatrak-web/src/api/RedirectErrorHandler.tsx
new file mode 100644
index 0000000000..e09de067c8
--- /dev/null
+++ b/packages/datatrak-web/src/api/RedirectErrorHandler.tsx
@@ -0,0 +1,25 @@
+/**
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ */
+
+import React, { ReactNode, useEffect } from 'react';
+import { REDIRECT_ERROR_PARAM } from '../constants';
+import { useSearchParams } from 'react-router-dom';
+import { errorToast } from '../utils';
+
+/**
+ * Wrapper component to ensure the the `redirectError` url parameter gets pushed to the toast notifications
+ */
+export const RedirectErrorHandler = ({ children }: { children?: ReactNode }) => {
+ const [searchParams] = useSearchParams();
+ const redirectError = searchParams.get(REDIRECT_ERROR_PARAM);
+
+ useEffect(() => {
+ if (redirectError) {
+ errorToast(redirectError);
+ }
+ }, [redirectError]);
+
+ return <>{children}>;
+};
diff --git a/packages/datatrak-web/src/api/api.ts b/packages/datatrak-web/src/api/api.ts
index 03ed620d4b..6fe3a51a80 100644
--- a/packages/datatrak-web/src/api/api.ts
+++ b/packages/datatrak-web/src/api/api.ts
@@ -42,15 +42,15 @@ const request = async (endpoint: string, options?: RequestParametersWithMethod)
// Some of the endpoints return 'details' with the message instead of 'message' or 'error'
if (data.details) {
- throw new FetchError(data.details, error.response.status);
+ throw new FetchError(data.details, error.response.status, data);
}
if (data.error) {
- throw new FetchError(data.error, error.response.status);
+ throw new FetchError(data.error, error.response.status, data);
}
if (data.message) {
- throw new FetchError(data.message, data.code);
+ throw new FetchError(data.message, data.code, data);
}
}
throw new Error(error);
diff --git a/packages/datatrak-web/src/api/fetchError.ts b/packages/datatrak-web/src/api/fetchError.ts
index 49aab402c0..cf82266431 100644
--- a/packages/datatrak-web/src/api/fetchError.ts
+++ b/packages/datatrak-web/src/api/fetchError.ts
@@ -4,11 +4,16 @@
*
*/
export default class FetchError extends Error {
- code: number;
+ public code: number;
+ public name: string;
+ public responseData: Record | undefined;
- name: string;
-
- constructor(message: Error['message'], code: number, ...params: ErrorOptions[]) {
+ constructor(
+ message: Error['message'],
+ code: number,
+ responseData?: Record,
+ ...params: ErrorOptions[]
+ ) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(message, ...params);
@@ -20,5 +25,6 @@ export default class FetchError extends Error {
this.name = 'FetchError';
// Custom debugging information
this.code = code;
+ this.responseData = responseData;
}
}
diff --git a/packages/datatrak-web/src/api/index.ts b/packages/datatrak-web/src/api/index.ts
index 90ddfc7191..f5720e672c 100644
--- a/packages/datatrak-web/src/api/index.ts
+++ b/packages/datatrak-web/src/api/index.ts
@@ -7,3 +7,4 @@ export * from './api';
export * from './queries';
export * from './mutations';
export * from './CurrentUserContext';
+export * from './RedirectErrorHandler';
diff --git a/packages/datatrak-web/src/api/mutations/useExportSurveyResponses.ts b/packages/datatrak-web/src/api/mutations/useExportSurveyResponses.ts
index e09ea31635..b77612b103 100644
--- a/packages/datatrak-web/src/api/mutations/useExportSurveyResponses.ts
+++ b/packages/datatrak-web/src/api/mutations/useExportSurveyResponses.ts
@@ -83,7 +83,7 @@ export const useExportSurveyResponses = () => {
const { error: message } = await getParsedBlob(data);
// Parse content and retrieve 'message'
- throw new FetchError(message, e.response.status);
+ throw new FetchError(message, e.response.status, data);
}
}
},
diff --git a/packages/datatrak-web/src/constants/constants.ts b/packages/datatrak-web/src/constants/constants.ts
index 35ba650c3d..ab8211dc38 100644
--- a/packages/datatrak-web/src/constants/constants.ts
+++ b/packages/datatrak-web/src/constants/constants.ts
@@ -6,3 +6,5 @@ export const HEADER_HEIGHT = '4.375rem';
export const MOBILE_BREAKPOINT = '960px'; // to match MUI md size
export const TITLE_BAR_HEIGHT = '3.875rem';
export const DESKTOP_MEDIA_QUERY = `@media screen and (min-width: 1440px) and (min-height: 900px)`;
+
+export const REDIRECT_ERROR_PARAM = 'redirectError';
diff --git a/packages/server-boilerplate/src/orchestrator/auth/RequiresSessionAuthHandler.ts b/packages/server-boilerplate/src/orchestrator/auth/RequiresSessionAuthHandler.ts
index aae1745805..642d989e96 100644
--- a/packages/server-boilerplate/src/orchestrator/auth/RequiresSessionAuthHandler.ts
+++ b/packages/server-boilerplate/src/orchestrator/auth/RequiresSessionAuthHandler.ts
@@ -36,6 +36,6 @@ export class RequiresSessionAuthHandler implements AuthHandler {
throw new UnauthenticatedError('Session is not attached');
}
- return this.session.getAuthHeader();
+ return this.session.getAuthHeader(this.req);
}
}
diff --git a/packages/server-boilerplate/src/orchestrator/auth/SessionSwitchingAuthHandler.ts b/packages/server-boilerplate/src/orchestrator/auth/SessionSwitchingAuthHandler.ts
index 40999e6ed8..bd390c7b9b 100644
--- a/packages/server-boilerplate/src/orchestrator/auth/SessionSwitchingAuthHandler.ts
+++ b/packages/server-boilerplate/src/orchestrator/auth/SessionSwitchingAuthHandler.ts
@@ -36,7 +36,7 @@ export class SessionSwitchingAuthHandler implements AuthHandler {
await this.getSession();
if (this.session) {
- return this.session.getAuthHeader();
+ return this.session.getAuthHeader(this.req);
}
return createBasicHeader(
diff --git a/packages/server-boilerplate/src/orchestrator/models/Session.ts b/packages/server-boilerplate/src/orchestrator/models/Session.ts
index 796653e873..971bee6abe 100644
--- a/packages/server-boilerplate/src/orchestrator/models/Session.ts
+++ b/packages/server-boilerplate/src/orchestrator/models/Session.ts
@@ -6,9 +6,10 @@
import { DatabaseModel, DatabaseRecord } from '@tupaia/database';
import { AccessPolicy } from '@tupaia/access-policy';
-import { createBearerHeader, getTokenExpiry } from '@tupaia/utils';
+import { RespondingError, createBearerHeader, getTokenExpiry } from '@tupaia/utils';
import { AccessPolicyObject } from '../../types';
import { AuthConnection } from '../auth';
+import { Request } from 'express';
interface SessionDetails {
email: string;
@@ -64,9 +65,24 @@ export class SessionRecord extends DatabaseRecord {
return this.access_token_expiry <= Date.now();
}
- public async getAuthHeader() {
+ public async getAuthHeader(req: Request) {
if (this.isAccessTokenExpired()) {
- await this.refreshAccessToken();
+ try {
+ await this.refreshAccessToken();
+ } catch (error) {
+ if (error instanceof RespondingError && error.statusCode === 401) {
+ // Refresh token is no longer valid
+ await this.delete(); // Delete this session from the database
+ const { res } = req;
+ if (res) {
+ res.clearCookie('sessionCookie'); // Delete the cookie from the user's browser
+ }
+
+ error.extraFields.redirectClient = '/login'; // Redirect client browser to the login page
+ }
+
+ throw error;
+ }
}
return createBearerHeader(this.access_token);
}
diff --git a/packages/utils/src/errors.js b/packages/utils/src/errors.js
index b907548463..b54fdeed98 100644
--- a/packages/utils/src/errors.js
+++ b/packages/utils/src/errors.js
@@ -10,11 +10,18 @@ import { respond } from './respond';
* the appropriate http status code
*/
export class RespondingError extends Error {
+ /**
+ * @param {string} message
+ * @param {number} statusCode
+ * @param {Record} extraFields
+ * @param {*} originalError
+ */
constructor(message, statusCode, extraFields = {}, originalError = null) {
super(message, { cause: originalError });
this.statusCode = statusCode;
this.extraFields = extraFields;
- this.respond = res => respond(res, { error: this.message, ...extraFields }, statusCode);
+ this.respond = res =>
+ respond(res, { error: this.message, ...this.extraFields }, this.statusCode);
}
}
From 0267e14fcdfd396e1ed41247d16919f2f96549af Mon Sep 17 00:00:00 2001
From: Jasper Lai <33956381+jaskfla@users.noreply.github.com>
Date: Tue, 11 Jun 2024 17:42:48 +1200
Subject: [PATCH 11/26] db(entityTypes): MAUI-4247: `province` entity type
(#5719)
* `province` entity type
* regenerate types
---
...9-AddProvinceEntityType-modifies-schema.js | 27 ++++
packages/types/src/schemas/schemas.ts | 117 ++++++++++++++++++
packages/types/src/types/models.ts | 1 +
3 files changed, 145 insertions(+)
create mode 100644 packages/database/src/migrations/20240611041639-AddProvinceEntityType-modifies-schema.js
diff --git a/packages/database/src/migrations/20240611041639-AddProvinceEntityType-modifies-schema.js b/packages/database/src/migrations/20240611041639-AddProvinceEntityType-modifies-schema.js
new file mode 100644
index 0000000000..741689607c
--- /dev/null
+++ b/packages/database/src/migrations/20240611041639-AddProvinceEntityType-modifies-schema.js
@@ -0,0 +1,27 @@
+'use strict';
+
+var dbm;
+var type;
+var seed;
+
+/**
+ * We receive the dbmigrate dependency from dbmigrate initially.
+ * This enables us to not have to rely on NODE_PATH.
+ */
+exports.setup = function (options, seedLink) {
+ dbm = options.dbmigrate;
+ type = dbm.dataType;
+ seed = seedLink;
+};
+
+exports.up = function (db) {
+ return db.runSql(`ALTER TYPE public.entity_type ADD VALUE IF NOT EXISTS 'province';`);
+};
+
+exports.down = function (db) {
+ return null;
+};
+
+exports._meta = {
+ version: 1,
+};
diff --git a/packages/types/src/schemas/schemas.ts b/packages/types/src/schemas/schemas.ts
index f550bf4687..3d758aa643 100644
--- a/packages/types/src/schemas/schemas.ts
+++ b/packages/types/src/schemas/schemas.ts
@@ -31494,6 +31494,7 @@ export const MeasureConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -31689,6 +31690,7 @@ export const EntityLevelSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -32035,6 +32037,7 @@ export const BaseMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -32183,6 +32186,7 @@ export const BaseMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -32321,6 +32325,7 @@ export const BaseMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -32750,6 +32755,7 @@ export const SpectrumMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -32898,6 +32904,7 @@ export const SpectrumMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -33036,6 +33043,7 @@ export const SpectrumMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -33570,6 +33578,7 @@ export const IconMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -33718,6 +33727,7 @@ export const IconMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -33856,6 +33866,7 @@ export const IconMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -34315,6 +34326,7 @@ export const RadiusMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -34463,6 +34475,7 @@ export const RadiusMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -34601,6 +34614,7 @@ export const RadiusMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -35034,6 +35048,7 @@ export const ColorMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -35182,6 +35197,7 @@ export const ColorMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -35320,6 +35336,7 @@ export const ColorMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -35767,6 +35784,7 @@ export const ShadingMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -35915,6 +35933,7 @@ export const ShadingMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -36053,6 +36072,7 @@ export const ShadingMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -36492,6 +36512,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -36640,6 +36661,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -36778,6 +36800,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -37311,6 +37334,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -37459,6 +37483,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -37597,6 +37622,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -38055,6 +38081,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -38203,6 +38230,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -38341,6 +38369,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -38773,6 +38802,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -38921,6 +38951,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -39059,6 +39090,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -39505,6 +39537,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -39653,6 +39686,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -39791,6 +39825,7 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -40430,6 +40465,7 @@ export const EntityQuestionConfigSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -40484,6 +40520,7 @@ export const EntityQuestionConfigSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -40960,6 +40997,7 @@ export const SurveyScreenComponentConfigSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -41014,6 +41052,7 @@ export const SurveyScreenComponentConfigSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -41538,6 +41577,12 @@ export const RecentEntitiesForCountrySchema = {
"items": {
"type": "string"
}
+ },
+ "province": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
},
"additionalProperties": false
@@ -68948,6 +68993,7 @@ export const DashboardRelationSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -69048,6 +69094,7 @@ export const DashboardRelationCreateSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -69143,6 +69190,7 @@ export const DashboardRelationUpdateSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -70347,6 +70395,7 @@ export const EntitySchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -70452,6 +70501,7 @@ export const EntityCreateSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -70557,6 +70607,7 @@ export const EntityUpdateSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -71796,6 +71847,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -71944,6 +71996,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -72082,6 +72135,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -72615,6 +72669,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -72763,6 +72818,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -72901,6 +72957,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -73359,6 +73416,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -73507,6 +73565,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -73645,6 +73704,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -74077,6 +74137,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -74225,6 +74286,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -74363,6 +74425,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -74809,6 +74872,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -74957,6 +75021,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -75095,6 +75160,7 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -75595,6 +75661,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -75743,6 +75810,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -75881,6 +75949,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -76414,6 +76483,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -76562,6 +76632,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -76700,6 +76771,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -77158,6 +77230,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -77306,6 +77379,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -77444,6 +77518,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -77876,6 +77951,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -78024,6 +78100,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -78162,6 +78239,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -78608,6 +78686,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -78756,6 +78835,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -78894,6 +78974,7 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -79387,6 +79468,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -79535,6 +79617,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -79673,6 +79756,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -80206,6 +80290,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -80354,6 +80439,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -80492,6 +80578,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -80950,6 +81037,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -81098,6 +81186,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -81236,6 +81325,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -81668,6 +81758,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -81816,6 +81907,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -81954,6 +82046,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -82400,6 +82493,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -82548,6 +82642,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -82686,6 +82781,7 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -83666,6 +83762,7 @@ export const PermissionsBasedMeditrakSyncQueueSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -83753,6 +83850,7 @@ export const PermissionsBasedMeditrakSyncQueueCreateSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -83837,6 +83935,7 @@ export const PermissionsBasedMeditrakSyncQueueUpdateSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -86221,6 +86320,7 @@ export const EntityTypeSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -86553,6 +86653,7 @@ export const MeditrakSurveyResponseRequestSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -87359,6 +87460,7 @@ export const EntityResponseSchema = {
"nursing_zone",
"postcode",
"project",
+ "province",
"repair_request",
"school",
"sub_catchment",
@@ -96462,6 +96564,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -96610,6 +96713,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -96748,6 +96852,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -97304,6 +97409,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -97452,6 +97558,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -97590,6 +97697,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -98071,6 +98179,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -98219,6 +98328,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -98357,6 +98467,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -98812,6 +98923,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -98960,6 +99072,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -99098,6 +99211,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -99567,6 +99681,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -99715,6 +99830,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -99853,6 +99969,7 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
+ "Province",
"RepairRequest",
"School",
"SubCatchment",
diff --git a/packages/types/src/types/models.ts b/packages/types/src/types/models.ts
index 4f89612965..800ad272a1 100644
--- a/packages/types/src/types/models.ts
+++ b/packages/types/src/types/models.ts
@@ -1761,6 +1761,7 @@ export enum EntityType {
'maintenance' = 'maintenance',
'larval_sample' = 'larval_sample',
'transfer' = 'transfer',
+ 'province' = 'province',
}
export enum DataTableType {
'analytics' = 'analytics',
From e5f7440b02a6eb51e1fa73f861ab546b1b96b625 Mon Sep 17 00:00:00 2001
From: Rohan Port <59544282+rohan-bes@users.noreply.github.com>
Date: Thu, 13 Jun 2024 09:37:24 +1000
Subject: [PATCH 12/26] fix(adminPanel): RN-1206: Fix broken 'Enities to
include' field in Survey Response export modal (#5661)
---
.../src/autocomplete/Autocomplete.jsx | 2 +-
.../SurveyResponsesExportModal.jsx | 37 ++++++++++++++-----
2 files changed, 29 insertions(+), 10 deletions(-)
diff --git a/packages/admin-panel/src/autocomplete/Autocomplete.jsx b/packages/admin-panel/src/autocomplete/Autocomplete.jsx
index 441c871865..0afd6f7db4 100644
--- a/packages/admin-panel/src/autocomplete/Autocomplete.jsx
+++ b/packages/admin-panel/src/autocomplete/Autocomplete.jsx
@@ -47,7 +47,7 @@ export const Autocomplete = props => {
debounce(newValue => {
onChangeSearchTerm(newValue);
}, 200),
- [],
+ [onChangeSearchTerm],
);
const muiPropsForCreateNewOptions = canCreateNewOptions
diff --git a/packages/admin-panel/src/importExport/SurveyResponsesExportModal.jsx b/packages/admin-panel/src/importExport/SurveyResponsesExportModal.jsx
index 148d2ca645..6454df7372 100644
--- a/packages/admin-panel/src/importExport/SurveyResponsesExportModal.jsx
+++ b/packages/admin-panel/src/importExport/SurveyResponsesExportModal.jsx
@@ -11,13 +11,26 @@ import { ReduxAutocomplete } from '../autocomplete';
import { ExportModal } from './ExportModal';
const MODES = {
- COUNTRY: 'country',
- ENTITY: 'entity',
+ COUNTRY: { value: 'country', formInput: 'countryCode' },
+ ENTITY: { value: 'entity', formInput: 'entityIds' },
};
export const SurveyResponsesExportModal = () => {
const [values, setValues] = useState({});
- const [mode, setMode] = useState(MODES.COUNTRY);
+ const [mode, setMode] = useState(MODES.COUNTRY.value);
+ const [countryCode, setCountryCode] = useState(); // Keep local copy to ensure form stays in sync with input
+ const [entityIds, setEntityIds] = useState(); // Keep local copy to ensure form stays in sync with input
+
+ const onChangeMode = newMode => {
+ setMode(newMode);
+ if (newMode === MODES.COUNTRY.value) {
+ handleValueChange(MODES.COUNTRY.formInput, countryCode);
+ handleValueChange(MODES.ENTITY.formInput, undefined);
+ } else {
+ handleValueChange(MODES.COUNTRY.formInput, undefined);
+ handleValueChange(MODES.ENTITY.formInput, entityIds);
+ }
+ };
const handleValueChange = (key, value) => {
setValues(prevState => ({
@@ -41,25 +54,28 @@ export const SurveyResponsesExportModal = () => {
setMode(event.currentTarget.value)}
+ onChange={event => onChangeMode(event.currentTarget.value)}
options={[
{
label: 'Country',
- value: MODES.COUNTRY,
+ value: MODES.COUNTRY.value,
},
{
label: 'Entity',
- value: MODES.ENTITY,
+ value: MODES.ENTITY.value,
},
]}
value={mode}
/>
- {mode === MODES.COUNTRY ? (
+ {mode === MODES.COUNTRY.value ? (
handleValueChange('countryCode', inputValue)}
+ onChange={inputValue => {
+ setCountryCode(inputValue);
+ handleValueChange('countryCode', inputValue);
+ }}
endpoint="countries"
optionLabelKey="name"
optionValueKey="code"
@@ -69,7 +85,10 @@ export const SurveyResponsesExportModal = () => {
label="Entities to include"
helperText="Please enter the names of the entities to be exported."
reduxId="entityIds"
- onChange={inputValue => handleValueChange('entityIds', inputValue)}
+ onChange={inputValue => {
+ setEntityIds(inputValue);
+ handleValueChange('entityIds', inputValue);
+ }}
endpoint="entities"
optionLabelKey="name"
optionValueKey="id"
From 0252942f2f541c7e1db3d00ca99372687911a7ef Mon Sep 17 00:00:00 2001
From: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Date: Thu, 13 Jun 2024 14:03:51 +1200
Subject: [PATCH 13/26] fix(adminPanel): Fix broken logo link (#5723)
---
packages/admin-panel/src/layout/navigation/HomeLink.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/admin-panel/src/layout/navigation/HomeLink.jsx b/packages/admin-panel/src/layout/navigation/HomeLink.jsx
index 3fd89a008f..14daf3a91d 100644
--- a/packages/admin-panel/src/layout/navigation/HomeLink.jsx
+++ b/packages/admin-panel/src/layout/navigation/HomeLink.jsx
@@ -28,7 +28,7 @@ const Link = styled(BaseLink)`
export const HomeLink = ({ logo, homeLink, disableHomeLink }) => {
return (
-
+
);
From b09f5338efed1b64d4b504b799fc0b98d3f137fb Mon Sep 17 00:00:00 2001
From: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Date: Thu, 13 Jun 2024 14:27:44 +1200
Subject: [PATCH 14/26] tweak(datatrakWeb): RN-1229: Remove question numbers
from surveys (#5711)
* remove question numbers from datatrak
* Remove unused vars
* Fix prettier
* Fix prettier styles
---
.../Survey/Components/SurveyQuestionGroup.tsx | 16 -----
.../Survey/Components/SurveyReviewSection.tsx | 6 +-
.../SurveySideMenu/SurveySideMenu.tsx | 15 ++---
.../Survey/SurveyContext/SurveyContext.tsx | 6 +-
.../features/Survey/SurveyContext/utils.ts | 19 +++---
.../features/Survey/useValidationResolver.ts | 57 +++++++----------
.../datatrak-web/src/features/Survey/utils.ts | 62 +++++++------------
packages/datatrak-web/src/types/surveys.ts | 1 -
8 files changed, 62 insertions(+), 120 deletions(-)
diff --git a/packages/datatrak-web/src/features/Survey/Components/SurveyQuestionGroup.tsx b/packages/datatrak-web/src/features/Survey/Components/SurveyQuestionGroup.tsx
index fa46c8657f..8e4a60b544 100644
--- a/packages/datatrak-web/src/features/Survey/Components/SurveyQuestionGroup.tsx
+++ b/packages/datatrak-web/src/features/Survey/Components/SurveyQuestionGroup.tsx
@@ -6,7 +6,6 @@ import React, { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
import { useLocation } from 'react-router';
import styled from 'styled-components';
-import { Typography } from '@material-ui/core';
import { SurveyScreenComponent } from '../../../types';
import { SurveyQuestion } from './SurveyQuestion';
@@ -22,15 +21,6 @@ const QuestionWrapper = styled.div<{
}
`;
-const QuestionNumber = styled(Typography)`
- width: 3.5rem;
- text-transform: lowercase;
- font-weight: ${({ theme }) => theme.typography.fontWeightMedium};
- ${({ theme }) => theme.breakpoints.up('md')} {
- font-weight: ${({ theme }) => theme.typography.fontWeightRegular};
- }
-`;
-
/**
* This is the component that renders questions in a survey.
*/
@@ -76,16 +66,10 @@ export const SurveyQuestionGroup = ({ questions }: { questions: SurveyScreenComp
detailLabel,
detail,
optionSetId,
- questionNumber,
updateFormDataOnChange,
}) => {
return (
- {type !== 'Instruction' && (
-
- {questionNumber}
-
- )}
{
// split the questions into sections by screen so it's easier to read the long form
const questionSections = visibleScreens.map(screen => {
const { surveyScreenComponents } = screen;
- const screenNumber = getSurveyScreenNumber(visibleScreens, screen);
const heading = surveyScreenComponents[0].text;
const firstQuestionIsInstruction = surveyScreenComponents[0].type === QuestionType.Instruction;
@@ -60,7 +58,7 @@ export const SurveyReviewSection = () => {
: surveyScreenComponents;
return {
heading,
- questions: formatSurveyScreenQuestions(questionsToDisplay, screenNumber),
+ questions: questionsToDisplay,
};
});
return (
diff --git a/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx b/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx
index 65ac122421..75254ad3db 100644
--- a/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx
+++ b/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx
@@ -7,10 +7,10 @@ import styled from 'styled-components';
import { To, Link as RouterLink } from 'react-router-dom';
import { useFormContext } from 'react-hook-form';
import { Drawer as BaseDrawer, ListItem, List, ButtonProps } from '@material-ui/core';
-import { useSurveyForm } from '../../SurveyContext';
-import { SideMenuButton } from './SideMenuButton';
import { useIsMobile } from '../../../../utils';
import { getSurveyScreenNumber } from '../../utils';
+import { useSurveyForm } from '../../SurveyContext';
+import { SideMenuButton } from './SideMenuButton';
export const SIDE_MENU_WIDTH = '20rem';
@@ -75,10 +75,6 @@ const SurveyMenuItem = styled(ListItem).attrs({
}
`;
-const SurveyScreenNumber = styled.span`
- width: 2rem;
-`;
-
const SurveyScreenTitle = styled.span`
width: 100%;
font-weight: ${({ theme }) => theme.typography.fontWeightRegular};
@@ -118,8 +114,8 @@ export const SurveySideMenu = () => {
const screens = visibleScreens?.map(screen => {
const { surveyScreenComponents, id } = screen;
const { text } = surveyScreenComponents[0];
- const visibleScreenNumber = getSurveyScreenNumber(visibleScreens, screen);
- return { id, text, screenNumber: visibleScreenNumber };
+ const surveyScreenNum = getSurveyScreenNumber(visibleScreens, screen);
+ return { id, text, screenNumber: surveyScreenNum };
});
return screens;
};
@@ -147,9 +143,6 @@ export const SurveySideMenu = () => {
onClick={onChangeScreen}
$isInstructionOnly={!screen.screenNumber}
>
- {screen.screenNumber && (
- {screen.screenNumber}:
- )}
{screen.text}
diff --git a/packages/datatrak-web/src/features/Survey/SurveyContext/SurveyContext.tsx b/packages/datatrak-web/src/features/Survey/SurveyContext/SurveyContext.tsx
index 5ccf28a545..998ac02a57 100644
--- a/packages/datatrak-web/src/features/Survey/SurveyContext/SurveyContext.tsx
+++ b/packages/datatrak-web/src/features/Survey/SurveyContext/SurveyContext.tsx
@@ -84,11 +84,7 @@ export const SurveyContext = ({ children }) => {
initialiseFormData();
}, [surveyCode]);
- const displayQuestions = getDisplayQuestions(
- activeScreen,
- flattenedScreenComponents,
- screenNumber,
- );
+ const displayQuestions = getDisplayQuestions(activeScreen, flattenedScreenComponents);
const screenHeader = activeScreen?.[0]?.text;
const screenDetail = activeScreen?.[0]?.detail;
diff --git a/packages/datatrak-web/src/features/Survey/SurveyContext/utils.ts b/packages/datatrak-web/src/features/Survey/SurveyContext/utils.ts
index 09e075dd6f..14f72cadcd 100644
--- a/packages/datatrak-web/src/features/Survey/SurveyContext/utils.ts
+++ b/packages/datatrak-web/src/features/Survey/SurveyContext/utils.ts
@@ -11,7 +11,6 @@ import {
QuestionType,
} from '@tupaia/types';
import { SurveyScreenComponent } from '../../../types';
-import { formatSurveyScreenQuestions } from '../utils';
import { generateMongoId, generateShortId } from './generateId';
export const getIsQuestionVisible = (
@@ -65,7 +64,6 @@ export const getIsDependentQuestion = (
export const getDisplayQuestions = (
activeScreen: SurveyScreenComponent[] = [],
screenComponents: SurveyScreenComponent[],
- screenNumber?: number | null,
) => {
// If the first question is an instruction, don't render it since we always just
// show the text of first questions as the heading. Format the questions with a question number to display
@@ -84,7 +82,7 @@ export const getDisplayQuestions = (
}
return question;
});
- return formatSurveyScreenQuestions(displayQuestions, screenNumber!);
+ return displayQuestions;
};
const getConditionIsMet = (expressionParser, formData, { formula, defaultValues = {} }) => {
@@ -173,18 +171,21 @@ const resetInvisibleQuestions = (
const hasConditionConfig = (
ssc: SurveyScreenComponent,
-): ssc is SurveyScreenComponent & { config: { condition: ConditionQuestionConfig } } =>
- ssc.type === QuestionType.Condition && ssc.config?.condition !== undefined;
+): ssc is SurveyScreenComponent & {
+ config: { condition: ConditionQuestionConfig };
+} => ssc.type === QuestionType.Condition && ssc.config?.condition !== undefined;
const hasArithmeticConfig = (
ssc: SurveyScreenComponent,
-): ssc is SurveyScreenComponent & { config: { arithmetic: ArithmeticQuestionConfig } } =>
- ssc.type === QuestionType.Arithmetic && ssc.config?.arithmetic !== undefined;
+): ssc is SurveyScreenComponent & {
+ config: { arithmetic: ArithmeticQuestionConfig };
+} => ssc.type === QuestionType.Arithmetic && ssc.config?.arithmetic !== undefined;
const hasCodeGeneratorConfig = (
ssc: SurveyScreenComponent,
-): ssc is SurveyScreenComponent & { config: { codeGenerator: CodeGeneratorQuestionConfig } } =>
- ssc.type === QuestionType.CodeGenerator && ssc.config?.codeGenerator !== undefined;
+): ssc is SurveyScreenComponent & {
+ config: { codeGenerator: CodeGeneratorQuestionConfig };
+} => ssc.type === QuestionType.CodeGenerator && ssc.config?.codeGenerator !== undefined;
const updateDependentQuestions = (
formData: Record,
diff --git a/packages/datatrak-web/src/features/Survey/useValidationResolver.ts b/packages/datatrak-web/src/features/Survey/useValidationResolver.ts
index 64bd148b87..0990e768c6 100644
--- a/packages/datatrak-web/src/features/Survey/useValidationResolver.ts
+++ b/packages/datatrak-web/src/features/Survey/useValidationResolver.ts
@@ -2,18 +2,15 @@
* Tupaia
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/
-import { useCallback } from "react";
-import * as yup from "yup";
-import { QuestionType } from "@tupaia/types";
-import { SurveyScreenComponent } from "../../types";
-import { getAllSurveyComponents, useSurveyForm } from ".";
+import { useCallback } from 'react';
+import * as yup from 'yup';
+import { QuestionType } from '@tupaia/types';
+import { SurveyScreenComponent } from '../../types';
+import { getAllSurveyComponents, useSurveyForm } from '.';
-const transformNumberValue = (
- value: string | number,
- originalValue: string | number
-) => {
+const transformNumberValue = (value: string | number, originalValue: string | number) => {
// This is a workaround for yup not handling empty number fields (https://github.com/jquense/yup/issues/298)
- return originalValue === "" || isNaN(originalValue as number) ? null : value;
+ return originalValue === '' || isNaN(originalValue as number) ? null : value;
};
const getBaseSchema = (type: QuestionType) => {
@@ -43,13 +40,13 @@ const getBaseSchema = (type: QuestionType) => {
.object({
latitude: yup
.number()
- .max(90, "Latitude must be between -90 and 90")
- .min(-90, "Latitude must be between -90 and 90")
+ .max(90, 'Latitude must be between -90 and 90')
+ .min(-90, 'Latitude must be between -90 and 90')
.transform(transformNumberValue),
longitude: yup
.number()
- .max(180, "Longitude must be between -180 and 180")
- .min(-180, "Longitude must be between -180 and 180")
+ .max(180, 'Longitude must be between -180 and 180')
+ .min(-180, 'Longitude must be between -180 and 180')
.transform(transformNumberValue),
})
.nullable()
@@ -69,42 +66,36 @@ const getValidationSchema = (screenComponents?: SurveyScreenComponent[]) => {
let fieldSchema = getBaseSchema(type);
if (mandatory) {
- fieldSchema = fieldSchema.required("Required");
+ fieldSchema = fieldSchema.required('Required');
// add custom validation for geolocate only when the question is required, so that the validation doesn't happen on each subfield when the question is not required
if (type === QuestionType.Geolocate) {
// @ts-ignore - handle issue with union type on schema from yup
fieldSchema = fieldSchema.test(
- "hasLatLong",
+ 'hasLatLong',
({ value }) => {
// Show the required message when the user has not entered a location at all
if (
(!value?.latitude && !value?.longitude) ||
(isNaN(value.latitude) && isNaN(value.longitude))
)
- return "Required";
+ return 'Required';
// Otherwise show the invalid location message
- return "Please enter a valid location";
+ return 'Please enter a valid location';
},
- (value) =>
+ value =>
value?.latitude &&
value?.longitude &&
!isNaN(value.latitude) &&
- !isNaN(value.longitude)
+ !isNaN(value.longitude),
);
}
}
if (min !== undefined) {
- fieldSchema = (fieldSchema as yup.NumberSchema).min(
- min,
- `Minimum value is ${min}`
- );
+ fieldSchema = (fieldSchema as yup.NumberSchema).min(min, `Minimum value is ${min}`);
}
if (max !== undefined) {
- fieldSchema = (fieldSchema as yup.NumberSchema).max(
- max,
- `Maximum value is ${max}`
- );
+ fieldSchema = (fieldSchema as yup.NumberSchema).max(max, `Maximum value is ${max}`);
}
return {
@@ -126,7 +117,7 @@ export const useValidationResolver = () => {
const yupSchema = yup.object().shape(validationSchema);
return useCallback(
- async (data) => {
+ async data => {
try {
const values = await yupSchema.validate(
{
@@ -135,7 +126,7 @@ export const useValidationResolver = () => {
},
{
abortEarly: false,
- }
+ },
);
return {
@@ -146,11 +137,11 @@ export const useValidationResolver = () => {
return {
values: {},
errors: errors?.inner?.reduce((allErrors, currentError) => {
- const questionName = currentError.path?.split(".")[0];
+ const questionName = currentError.path?.split('.')[0];
return {
...allErrors,
[questionName]: {
- type: currentError.type ?? "validation",
+ type: currentError.type ?? 'validation',
message: currentError.message,
},
};
@@ -158,6 +149,6 @@ export const useValidationResolver = () => {
};
}
},
- [yupSchema]
+ [yupSchema],
);
};
diff --git a/packages/datatrak-web/src/features/Survey/utils.ts b/packages/datatrak-web/src/features/Survey/utils.ts
index 21a78779de..08b9036119 100644
--- a/packages/datatrak-web/src/features/Survey/utils.ts
+++ b/packages/datatrak-web/src/features/Survey/utils.ts
@@ -2,19 +2,8 @@
* Tupaia
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/
-import { QuestionType } from '@tupaia/types';
-import { SurveyScreenComponent, SurveyScreen } from '../../types';
-
-export const convertNumberToLetter = (number: number) => {
- const alphabet = 'abcdefghijklmnopqrstuvwxyz';
- if (number > 25) {
- // occasionally there are more than 26 questions on a screen, so we then start at aa, ab....
- const firstLetter = alphabet[Math.floor(number / 26) - 1];
- const secondLetter = alphabet[number % 26];
- return `${firstLetter}${secondLetter}`;
- }
- return alphabet[number];
-};
+import { QuestionType } from "@tupaia/types";
+import { SurveyScreen } from "../../types";
export const READ_ONLY_QUESTION_TYPES = [
QuestionType.Condition,
@@ -26,51 +15,42 @@ export const getSurveyScreenNumber = (screens, screen) => {
if (!screen) return null;
const { surveyScreenComponents, id } = screen;
const nonInstructionScreens =
- screens?.filter(screen =>
- screen.surveyScreenComponents.some(component => component.type !== QuestionType.Instruction),
+ screens?.filter((screen) =>
+ screen.surveyScreenComponents.some(
+ (component) => component.type !== QuestionType.Instruction
+ )
) ?? [];
const screenNumber = surveyScreenComponents.some(
- component => component.type !== QuestionType.Instruction,
+ (component) => component.type !== QuestionType.Instruction
)
- ? nonInstructionScreens.findIndex(nonInstructionScreen => nonInstructionScreen.id === id) + 1
+ ? nonInstructionScreens.findIndex(
+ (nonInstructionScreen) => nonInstructionScreen.id === id
+ ) + 1
: null;
return screenNumber;
};
-export const formatSurveyScreenQuestions = (
- questions: SurveyScreenComponent[],
- screenNumber: number | string,
-) => {
- const nonReadOnlyQuestions = questions.filter(
- question => !READ_ONLY_QUESTION_TYPES.includes(question?.type!),
- );
-
- return questions.map(question => {
- const questionNumber = nonReadOnlyQuestions.findIndex(
- nonInstructionQuestion => question.questionId === nonInstructionQuestion.questionId,
- );
- if (questionNumber === -1) return question;
- return {
- ...question,
- questionNumber: `${screenNumber}${convertNumberToLetter(questionNumber)}.`,
- };
- });
-};
-
export const getAllSurveyComponents = (surveyScreens?: SurveyScreen[]) => {
- return surveyScreens?.map(({ surveyScreenComponents }) => surveyScreenComponents)?.flat() ?? [];
+ return (
+ surveyScreens
+ ?.map(({ surveyScreenComponents }) => surveyScreenComponents)
+ ?.flat() ?? []
+ );
};
export const getErrorsByScreen = (
errors: Record>,
- visibleScreens?: SurveyScreen[],
+ visibleScreens?: SurveyScreen[]
) => {
return (
Object.entries(errors).reduce((acc, [questionName, error]) => {
- const screenIndex = visibleScreens?.findIndex(({ surveyScreenComponents }) =>
- surveyScreenComponents.find(question => question.questionId === questionName),
+ const screenIndex = visibleScreens?.findIndex(
+ ({ surveyScreenComponents }) =>
+ surveyScreenComponents.find(
+ (question) => question.questionId === questionName
+ )
);
if (screenIndex === undefined || screenIndex === -1) return acc;
diff --git a/packages/datatrak-web/src/types/surveys.ts b/packages/datatrak-web/src/types/surveys.ts
index ae2b0b635a..cd4a6e80c3 100644
--- a/packages/datatrak-web/src/types/surveys.ts
+++ b/packages/datatrak-web/src/types/surveys.ts
@@ -10,7 +10,6 @@ export type SurveyScreen = Survey['screens'][number];
export type SurveyScreenComponent = SurveyScreen['surveyScreenComponents'][0] & {
updateFormDataOnChange?: boolean;
- questionNumber?: string;
};
export type SurveyParams = {
From 25e25a2626e5823f46fddab00938d17897c9b751 Mon Sep 17 00:00:00 2001
From: Jasper Lai <33956381+jaskfla@users.noreply.github.com>
Date: Thu, 13 Jun 2024 14:49:33 +1200
Subject: [PATCH 15/26] revert(entityTypes): MAUI-4247: abort addition of
`province` entity type (#5724)
Revert "db(entityTypes): MAUI-4247: `province` entity type (#5719)"
This reverts commit 0267e14fcdfd396e1ed41247d16919f2f96549af.
---
...9-AddProvinceEntityType-modifies-schema.js | 27 ----
packages/types/src/schemas/schemas.ts | 117 ------------------
packages/types/src/types/models.ts | 1 -
3 files changed, 145 deletions(-)
delete mode 100644 packages/database/src/migrations/20240611041639-AddProvinceEntityType-modifies-schema.js
diff --git a/packages/database/src/migrations/20240611041639-AddProvinceEntityType-modifies-schema.js b/packages/database/src/migrations/20240611041639-AddProvinceEntityType-modifies-schema.js
deleted file mode 100644
index 741689607c..0000000000
--- a/packages/database/src/migrations/20240611041639-AddProvinceEntityType-modifies-schema.js
+++ /dev/null
@@ -1,27 +0,0 @@
-'use strict';
-
-var dbm;
-var type;
-var seed;
-
-/**
- * We receive the dbmigrate dependency from dbmigrate initially.
- * This enables us to not have to rely on NODE_PATH.
- */
-exports.setup = function (options, seedLink) {
- dbm = options.dbmigrate;
- type = dbm.dataType;
- seed = seedLink;
-};
-
-exports.up = function (db) {
- return db.runSql(`ALTER TYPE public.entity_type ADD VALUE IF NOT EXISTS 'province';`);
-};
-
-exports.down = function (db) {
- return null;
-};
-
-exports._meta = {
- version: 1,
-};
diff --git a/packages/types/src/schemas/schemas.ts b/packages/types/src/schemas/schemas.ts
index 3d758aa643..f550bf4687 100644
--- a/packages/types/src/schemas/schemas.ts
+++ b/packages/types/src/schemas/schemas.ts
@@ -31494,7 +31494,6 @@ export const MeasureConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -31690,7 +31689,6 @@ export const EntityLevelSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -32037,7 +32035,6 @@ export const BaseMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -32186,7 +32183,6 @@ export const BaseMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -32325,7 +32321,6 @@ export const BaseMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -32755,7 +32750,6 @@ export const SpectrumMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -32904,7 +32898,6 @@ export const SpectrumMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -33043,7 +33036,6 @@ export const SpectrumMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -33578,7 +33570,6 @@ export const IconMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -33727,7 +33718,6 @@ export const IconMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -33866,7 +33856,6 @@ export const IconMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -34326,7 +34315,6 @@ export const RadiusMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -34475,7 +34463,6 @@ export const RadiusMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -34614,7 +34601,6 @@ export const RadiusMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -35048,7 +35034,6 @@ export const ColorMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -35197,7 +35182,6 @@ export const ColorMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -35336,7 +35320,6 @@ export const ColorMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -35784,7 +35767,6 @@ export const ShadingMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -35933,7 +35915,6 @@ export const ShadingMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -36072,7 +36053,6 @@ export const ShadingMapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -36512,7 +36492,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -36661,7 +36640,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -36800,7 +36778,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -37334,7 +37311,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -37483,7 +37459,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -37622,7 +37597,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -38081,7 +38055,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -38230,7 +38203,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -38369,7 +38341,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -38802,7 +38773,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -38951,7 +38921,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -39090,7 +39059,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -39537,7 +39505,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -39686,7 +39653,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -39825,7 +39791,6 @@ export const MapOverlayConfigSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -40465,7 +40430,6 @@ export const EntityQuestionConfigSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -40520,7 +40484,6 @@ export const EntityQuestionConfigSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -40997,7 +40960,6 @@ export const SurveyScreenComponentConfigSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -41052,7 +41014,6 @@ export const SurveyScreenComponentConfigSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -41577,12 +41538,6 @@ export const RecentEntitiesForCountrySchema = {
"items": {
"type": "string"
}
- },
- "province": {
- "type": "array",
- "items": {
- "type": "string"
- }
}
},
"additionalProperties": false
@@ -68993,7 +68948,6 @@ export const DashboardRelationSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -69094,7 +69048,6 @@ export const DashboardRelationCreateSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -69190,7 +69143,6 @@ export const DashboardRelationUpdateSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -70395,7 +70347,6 @@ export const EntitySchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -70501,7 +70452,6 @@ export const EntityCreateSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -70607,7 +70557,6 @@ export const EntityUpdateSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -71847,7 +71796,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -71996,7 +71944,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -72135,7 +72082,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -72669,7 +72615,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -72818,7 +72763,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -72957,7 +72901,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -73416,7 +73359,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -73565,7 +73507,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -73704,7 +73645,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -74137,7 +74077,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -74286,7 +74225,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -74425,7 +74363,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -74872,7 +74809,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -75021,7 +74957,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -75160,7 +75095,6 @@ export const MapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -75661,7 +75595,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -75810,7 +75743,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -75949,7 +75881,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -76483,7 +76414,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -76632,7 +76562,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -76771,7 +76700,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -77230,7 +77158,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -77379,7 +77306,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -77518,7 +77444,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -77951,7 +77876,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -78100,7 +78024,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -78239,7 +78162,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -78686,7 +78608,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -78835,7 +78756,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -78974,7 +78894,6 @@ export const MapOverlayCreateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -79468,7 +79387,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -79617,7 +79535,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -79756,7 +79673,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -80290,7 +80206,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -80439,7 +80354,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -80578,7 +80492,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -81037,7 +80950,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -81186,7 +81098,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -81325,7 +81236,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -81758,7 +81668,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -81907,7 +81816,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -82046,7 +81954,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -82493,7 +82400,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -82642,7 +82548,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -82781,7 +82686,6 @@ export const MapOverlayUpdateSchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -83762,7 +83666,6 @@ export const PermissionsBasedMeditrakSyncQueueSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -83850,7 +83753,6 @@ export const PermissionsBasedMeditrakSyncQueueCreateSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -83935,7 +83837,6 @@ export const PermissionsBasedMeditrakSyncQueueUpdateSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -86320,7 +86221,6 @@ export const EntityTypeSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -86653,7 +86553,6 @@ export const MeditrakSurveyResponseRequestSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -87460,7 +87359,6 @@ export const EntityResponseSchema = {
"nursing_zone",
"postcode",
"project",
- "province",
"repair_request",
"school",
"sub_catchment",
@@ -96564,7 +96462,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -96713,7 +96610,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -96852,7 +96748,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -97409,7 +97304,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -97558,7 +97452,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -97697,7 +97590,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -98179,7 +98071,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -98328,7 +98219,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -98467,7 +98357,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -98923,7 +98812,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -99072,7 +98960,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -99211,7 +99098,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -99681,7 +99567,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -99830,7 +99715,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
@@ -99969,7 +99853,6 @@ export const TranslatedMapOverlaySchema = {
"NursingZone",
"Postcode",
"Project",
- "Province",
"RepairRequest",
"School",
"SubCatchment",
diff --git a/packages/types/src/types/models.ts b/packages/types/src/types/models.ts
index 800ad272a1..4f89612965 100644
--- a/packages/types/src/types/models.ts
+++ b/packages/types/src/types/models.ts
@@ -1761,7 +1761,6 @@ export enum EntityType {
'maintenance' = 'maintenance',
'larval_sample' = 'larval_sample',
'transfer' = 'transfer',
- 'province' = 'province',
}
export enum DataTableType {
'analytics' = 'analytics',
From 56c28e24fe447a726d5f4b4270eb9a00228e07f3 Mon Sep 17 00:00:00 2001
From: Rohan Port <59544282+rohan-bes@users.noreply.github.com>
Date: Fri, 14 Jun 2024 13:18:17 +1000
Subject: [PATCH 16/26] fix(nginx): RN-1365: Fix occasional 504 Gateway Timeout
errors (#5727)
---
.../devops/configs/nginx-template/servers.template.conf | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/packages/devops/configs/nginx-template/servers.template.conf b/packages/devops/configs/nginx-template/servers.template.conf
index 84a47d68fd..3068f3482a 100644
--- a/packages/devops/configs/nginx-template/servers.template.conf
+++ b/packages/devops/configs/nginx-template/servers.template.conf
@@ -570,7 +570,12 @@ server {
listen [::]:__PORT__;
server_name __DOMAIN_PREFIX____DOMAIN__;
+ # Nginx caches the ip address of the default frontend, which resolves to the ip address of the Elastic Load Balancer when hosting on AWS
+ # Since this ip address can change, we need to make sure we refetch the cached ip address regularly to avoid pointing to the wrong ip address
+ resolver 127.0.0.53 valid=30s; # DNS address of 127.0.0.53 is used by systemd-resolved
+ set $default_frontend "__SUBDOMAIN_PREFIX____DEFAULT_FRONTEND__.__DOMAIN__";
+
location / {
- proxy_pass https://__SUBDOMAIN_PREFIX____DEFAULT_FRONTEND__.__DOMAIN__;
+ proxy_pass https://$default_frontend;
}
}
From 0398d39a32a89bfe9553a748db1301852e390926 Mon Sep 17 00:00:00 2001
From: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Date: Fri, 14 Jun 2024 15:46:04 +1200
Subject: [PATCH 17/26] feat(tupaiaWeb): RN-1327: Matrix pagination and
performance improvements (#5684)
* Don't fetch report for matrix when not expanded
* Display preview always for matrix
* memoise data
* Don't spread accumulators
* DOn't use spreads
* Update useReport.ts
* Matrix pagination
* Don't show pagination for only 1 page of results
* Set default page size to be 50
* Scroll to top of table when page changes
* remove setPageSize from matrix
* Fix build
---
.../table/DataFetchingTable/Pagination.jsx | 155 +------------
.../src/table/customPagination.jsx | 39 ----
.../tupaia-web/src/api/queries/useReport.ts | 10 +-
.../features/DashboardItem/DashboardItem.tsx | 35 +--
.../DashboardItem/DashboardItemContent.tsx | 8 +-
.../DashboardItem/DashboardItemContext.ts | 2 +
.../src/features/Visuals/Matrix/Matrix.tsx | 118 +++++-----
.../src/components/Inputs/Select.tsx | 7 +-
.../src/components/Matrix/Matrix.tsx | 27 ++-
.../components/Matrix/MatrixPagination.tsx | 50 +++++
.../src/components/Matrix/utils.ts | 7 +-
.../src/components/Pagination.tsx | 207 ++++++++++++++++++
.../ui-components/src/components/index.ts | 1 +
13 files changed, 406 insertions(+), 260 deletions(-)
delete mode 100644 packages/admin-panel/src/table/customPagination.jsx
create mode 100644 packages/ui-components/src/components/Matrix/MatrixPagination.tsx
create mode 100644 packages/ui-components/src/components/Pagination.tsx
diff --git a/packages/admin-panel/src/table/DataFetchingTable/Pagination.jsx b/packages/admin-panel/src/table/DataFetchingTable/Pagination.jsx
index 815a392f28..869d73930a 100644
--- a/packages/admin-panel/src/table/DataFetchingTable/Pagination.jsx
+++ b/packages/admin-panel/src/table/DataFetchingTable/Pagination.jsx
@@ -4,167 +4,36 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
-import { IconButton, Input, Typography } from '@material-ui/core';
import styled from 'styled-components';
-import { KeyboardArrowLeft, KeyboardArrowRight } from '@material-ui/icons';
-import { Select } from '@tupaia/ui-components';
+import { Pagination as UIPagination } from '@tupaia/ui-components';
const Wrapper = styled.div`
- font-size: 0.75rem;
- display: flex;
- justify-content: space-between;
- width: 100%;
- border-top: 1px solid ${({ theme }) => theme.palette.grey['400']};
- background-color: ${({ theme }) => theme.palette.background.paper};
- padding-block: 0.5rem;
- padding-inline: 1rem;
- label,
- p,
- .MuiInputBase-input {
- font-size: 0.75rem;
+ .pagination-wrapper {
+ border-top: 1px solid ${({ theme }) => theme.palette.grey['400']};
+ background-color: ${({ theme }) => theme.palette.background.paper};
}
-`;
-
-const ActionsWrapper = styled.div`
- display: flex;
- align-items: center;
- justify-content: space-between;
- :first-child {
- padding-inline-start: 1rem;
- }
-`;
-const Button = styled(IconButton)`
- border: 1px solid ${({ theme }) => theme.palette.grey['400']};
- padding: 0.4rem;
- .MuiSvgIcon-root {
- font-size: 1.2rem;
- }
- & + & {
- margin-left: 0.7rem;
- }
-`;
-
-const ManualPageInputContainer = styled.div`
- display: flex;
- align-items: center;
- margin-inline-start: 0.5rem;
- margin-inline-end: 0.8rem;
-`;
-
-const ManualPageInput = styled(Input)`
- border: 1px solid ${({ theme }) => theme.palette.grey['400']};
- border-radius: 4px;
- padding-block: 0.5rem;
- padding-inline: 0.8rem 0.2rem;
- margin-inline: 0.5rem;
- font-size: 0.75rem;
- .MuiInputBase-input {
- text-align: center;
- padding-block: 0;
- height: auto;
- }
-`;
-
-const Text = styled(Typography)`
- font-size: 0.75rem;
-`;
-
-const RowsSelect = styled(Select)`
- display: flex;
- flex-direction: row;
- margin-block-end: 0;
- margin-inline-end: 1.2rem;
- width: 12rem;
.MuiSelect-root {
padding-block: 0.5rem;
padding-inline: 0.8rem 0.2rem;
}
`;
-const PageSelectComponent = ({ onChangePage, page, pageCount }) => {
- const pageDisplay = page + 1;
- return (
-
-
-
- Page
-
- {
- const newPage = e.target.value ? Number(e.target.value) - 1 : '';
- onChangePage(newPage);
- }}
- inputProps={{ min: 1, max: pageCount }}
- aria-describedby="page-count"
- id="page"
- disableUnderline
- />
- of {pageCount}
-
-
-
-
- );
-};
-
-PageSelectComponent.propTypes = {
- onChangePage: PropTypes.func.isRequired,
- page: PropTypes.number.isRequired,
- pageCount: PropTypes.number.isRequired,
-};
-
-const RowsSelectComponent = ({ pageSize, setPageSize }) => {
- const pageSizes = [5, 10, 20, 25, 50, 100];
- return (
-
- setPageSize(Number(event.target.value))}
- options={pageSizes.map(size => ({ label: `Rows per page: ${size}`, value: size }))}
- variant="standard"
- />
-
- );
-};
-
-RowsSelectComponent.propTypes = {
- pageSize: PropTypes.number.isRequired,
- setPageSize: PropTypes.func.isRequired,
-};
-
export const Pagination = ({ page, pageCount, gotoPage, pageSize, setPageSize, totalRecords }) => {
if (!totalRecords) return null;
- const currentDisplayStart = page * pageSize + 1;
- const currentDisplayEnd = Math.min((page + 1) * pageSize, totalRecords);
const handleChangePage = newPage => {
gotoPage(newPage);
};
return (
-
-
- {currentDisplayStart} - {currentDisplayEnd} of {totalRecords} entries
-
-
-
-
-
-
+
);
};
diff --git a/packages/admin-panel/src/table/customPagination.jsx b/packages/admin-panel/src/table/customPagination.jsx
deleted file mode 100644
index 2726d5b9a8..0000000000
--- a/packages/admin-panel/src/table/customPagination.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Tupaia MediTrak
- * Copyright (c) 2018 Beyond Essential Systems Pty Ltd
- */
-
-/* eslint-disable react/prop-types */
-
-import React from 'react';
-import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore';
-import NavigateNextIcon from '@material-ui/icons/NavigateNext';
-import { Select } from '@tupaia/ui-components';
-import { IconButton } from '../widgets';
-
-export const customPagination = () => ({
- PreviousComponent: props => (
-
-
-
- ),
- NextComponent: props => (
-
-
-
- ),
- // @see https://github.com/tannerlinsley/react-table/tree/v6 for documentation of props
- renderPageSizeOptions: ({ pageSize, pageSizeOptions, rowsSelectorText, onPageSizeChange }) => (
-
-
- ),
-});
diff --git a/packages/tupaia-web/src/api/queries/useReport.ts b/packages/tupaia-web/src/api/queries/useReport.ts
index 5cb727b3af..03a1227efa 100644
--- a/packages/tupaia-web/src/api/queries/useReport.ts
+++ b/packages/tupaia-web/src/api/queries/useReport.ts
@@ -20,7 +20,11 @@ type QueryParams = Record & {
endDate?: Moment | string | null;
};
-export const useReport = (reportCode: DashboardItem['reportCode'], params: QueryParams) => {
+export const useReport = (
+ reportCode: DashboardItem['reportCode'],
+ params: QueryParams,
+ enabled = true,
+) => {
const { dashboardCode, projectCode, entityCode, itemCode, startDate, endDate, legacy, ...rest } =
params;
const timeZone = getBrowserTimeZone();
@@ -38,6 +42,7 @@ export const useReport = (reportCode: DashboardItem['reportCode'], params: Query
formattedStartDate,
formattedEndDate,
...Object.values(rest),
+ enabled,
],
(): Promise =>
get(`${endPoint}/${reportCode}`, {
@@ -53,7 +58,8 @@ export const useReport = (reportCode: DashboardItem['reportCode'], params: Query
},
}),
{
- enabled: !!reportCode && !!dashboardCode && !!projectCode && !!entityCode && !!itemCode,
+ enabled:
+ enabled && !!reportCode && !!dashboardCode && !!projectCode && !!entityCode && !!itemCode,
},
);
};
diff --git a/packages/tupaia-web/src/features/DashboardItem/DashboardItem.tsx b/packages/tupaia-web/src/features/DashboardItem/DashboardItem.tsx
index b4dd87f063..7fb166635a 100644
--- a/packages/tupaia-web/src/features/DashboardItem/DashboardItem.tsx
+++ b/packages/tupaia-web/src/features/DashboardItem/DashboardItem.tsx
@@ -9,8 +9,8 @@ import { Moment } from 'moment';
import { useParams } from 'react-router';
import { Typography } from '@material-ui/core';
import { getDefaultDates } from '@tupaia/utils';
-import { DashboardItemConfig } from '@tupaia/types';
-import { DashboardItem as DashboardItemType } from '../../types';
+import { DashboardItemConfig, DashboardItemType } from '@tupaia/types';
+import { DashboardItem as DashboardItemT } from '../../types';
import { useReport } from '../../api/queries';
import { useDashboard } from '../Dashboard';
import { DashboardItemContent } from './DashboardItemContent';
@@ -69,7 +69,7 @@ const getShowDashboardItemTitle = (config?: DashboardItemConfig, legacy?: boolea
/**
* This is the dashboard item, and renders the item in the dashboard itself, as well as a modal if the item is expandable
*/
-export const DashboardItem = ({ dashboardItem }: { dashboardItem: DashboardItemType }) => {
+export const DashboardItem = ({ dashboardItem }: { dashboardItem: DashboardItemT }) => {
const { projectCode, entityCode } = useParams();
const { activeDashboard } = useDashboard();
const { startDate: defaultStartDate, endDate: defaultEndDate } = getDefaultDates(
@@ -79,20 +79,28 @@ export const DashboardItem = ({ dashboardItem }: { dashboardItem: DashboardItemT
endDate?: Moment;
};
+ const type = dashboardItem?.config?.type;
+
+ const isEnabled = type !== DashboardItemType.Matrix; // don't fetch the report if the item is a matrix, because we only view the matrix in the modal
+
const {
data: report,
isLoading,
error,
refetch,
- } = useReport(dashboardItem?.reportCode, {
- projectCode,
- entityCode,
- dashboardCode: activeDashboard?.code,
- itemCode: dashboardItem?.code,
- startDate: defaultStartDate,
- endDate: defaultEndDate,
- legacy: dashboardItem?.legacy,
- });
+ } = useReport(
+ dashboardItem?.reportCode,
+ {
+ projectCode,
+ entityCode,
+ dashboardCode: activeDashboard?.code,
+ itemCode: dashboardItem?.code,
+ startDate: defaultStartDate,
+ endDate: defaultEndDate,
+ legacy: dashboardItem?.legacy,
+ },
+ isEnabled, // don't fetch the report if the item is a matrix, because we only view the matrix in the modal
+ );
const { config, legacy } = dashboardItem;
@@ -105,7 +113,8 @@ export const DashboardItem = ({ dashboardItem }: { dashboardItem: DashboardItemT
value={{
config: dashboardItem?.config,
report,
- isLoading,
+ isLoading: isEnabled && isLoading,
+ isEnabled,
error,
refetch,
reportCode: dashboardItem?.reportCode,
diff --git a/packages/tupaia-web/src/features/DashboardItem/DashboardItemContent.tsx b/packages/tupaia-web/src/features/DashboardItem/DashboardItemContent.tsx
index 930f58c5f6..72280f77c3 100644
--- a/packages/tupaia-web/src/features/DashboardItem/DashboardItemContent.tsx
+++ b/packages/tupaia-web/src/features/DashboardItem/DashboardItemContent.tsx
@@ -42,7 +42,8 @@ const getHasNoData = (report?: DashboardItemReport | null) => {
* DashboardItemContent handles displaying of the content within a dashboard item, e.g. charts. It also handles error messages and loading states
*/
export const DashboardItemContent = () => {
- const { config, report, isExport, isLoading, error, refetch } = useContext(DashboardItemContext);
+ const { config, report, isExport, isLoading, error, refetch, isEnabled } =
+ useContext(DashboardItemContext);
const getComponentKey = () => {
if (config?.type === 'component' && config) {
@@ -61,12 +62,13 @@ export const DashboardItemContent = () => {
if (!DisplayComponent) return null;
if (error) return ;
+
// there will be no report returned if type is component, so don't show the loader for that type
- if (isLoading || (!report && config?.type !== 'component'))
+ if (isLoading || (!report && config?.type !== 'component' && isEnabled))
return ;
// if there is no data for the selected dates, then we want to show a message to the user
- const showNoDataMessage = isLoading ? false : getHasNoData(report);
+ const showNoDataMessage = !isEnabled ? false : getHasNoData(report);
return (
<>
diff --git a/packages/tupaia-web/src/features/DashboardItem/DashboardItemContext.ts b/packages/tupaia-web/src/features/DashboardItem/DashboardItemContext.ts
index 9a38072a9a..dcdfad58fa 100644
--- a/packages/tupaia-web/src/features/DashboardItem/DashboardItemContext.ts
+++ b/packages/tupaia-web/src/features/DashboardItem/DashboardItemContext.ts
@@ -17,6 +17,7 @@ type DashboardItemState = {
isEnlarged?: boolean;
isExport?: boolean;
reportCode?: DashboardItem['reportCode'];
+ isEnabled?: boolean;
};
const defaultContext = {
config: null,
@@ -24,6 +25,7 @@ const defaultContext = {
isLoading: false,
error: null,
refetch: () => {},
+ isEnabled: true,
} as DashboardItemState;
export const DashboardItemContext = createContext(defaultContext);
diff --git a/packages/tupaia-web/src/features/Visuals/Matrix/Matrix.tsx b/packages/tupaia-web/src/features/Visuals/Matrix/Matrix.tsx
index 08737cf1ba..808d88f9c6 100644
--- a/packages/tupaia-web/src/features/Visuals/Matrix/Matrix.tsx
+++ b/packages/tupaia-web/src/features/Visuals/Matrix/Matrix.tsx
@@ -3,7 +3,7 @@
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/
-import React, { useContext, useEffect, useState } from 'react';
+import React, { useContext, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { useSearchParams } from 'react-router-dom';
import { Clear, Search } from '@material-ui/icons';
@@ -16,7 +16,13 @@ import {
NoData,
} from '@tupaia/ui-components';
import { formatDataValueByType } from '@tupaia/utils';
-import { MatrixConfig, MatrixReportColumn, MatrixReportRow, isMatrixReport } from '@tupaia/types';
+import {
+ DashboardItemType,
+ MatrixConfig,
+ MatrixReportColumn,
+ MatrixReportRow,
+ isMatrixReport,
+} from '@tupaia/types';
import { DashboardItemContext } from '../../DashboardItem';
import { MOBILE_BREAKPOINT, URL_SEARCH_PARAMS } from '../../../constants';
import { MatrixPreview } from './MatrixPreview';
@@ -105,46 +111,42 @@ const parseRows = (
);
// if there are no child rows, e.g. because the search filter is hiding them, then we don't need to render this row
if (!children.length) return result;
- return [
- ...result,
- {
- title: category,
- ...rest,
- children,
- },
- ];
+ result.push({
+ title: category,
+ ...rest,
+ children,
+ });
+ return result;
}
// if the row is a regular row, and there is a search filter, then we need to check if the row matches the search filter, and ignore this row if it doesn't. This filter only applies to standard rows, not category rows.
if (searchFilter && !dataElement.toLowerCase().includes(searchFilter.toLowerCase()))
return result;
// otherwise, handle as a regular row
- return [
- ...result,
- {
- title: dataElement,
- onClick: drillDown ? () => onDrillDown(row) : undefined,
- ...Object.entries(rest).reduce((acc, [key, item]) => {
- // some items are objects, and we need to parse them to get the value
- if (typeof item === 'object' && item !== null) {
- const { value, metadata } = item as { value: any; metadata?: any };
- return {
- ...acc,
- [key]: formatDataValueByType(
- {
- value,
- metadata,
- },
- valueTypeToUse,
- ),
- };
- }
- return {
- ...acc,
- [key]: formatDataValueByType({ value: item }, valueTypeToUse),
- };
- }, {}),
- },
- ];
+
+ const formattedProperties = Object.entries(rest).reduce((acc, [key, item]) => {
+ // some items are objects, and we need to parse them to get the value
+ if (typeof item === 'object' && item !== null) {
+ const { value, metadata } = item as { value: any; metadata?: any };
+ acc[key] = formatDataValueByType(
+ {
+ value,
+ metadata,
+ },
+ valueTypeToUse,
+ );
+ return acc;
+ }
+
+ acc[key] = formatDataValueByType({ value: item }, valueTypeToUse);
+ return acc;
+ }, {});
+
+ result.push({
+ title: dataElement,
+ onClick: drillDown ? () => onDrillDown(row) : undefined,
+ ...formattedProperties,
+ });
+ return result;
}, [] as MatrixRowType[]);
};
@@ -176,7 +178,7 @@ const MatrixVisual = () => {
const [urlSearchParams, setUrlSearchParams] = useSearchParams();
const activeDrillDownId = urlSearchParams.get(URL_SEARCH_PARAMS.REPORT_DRILLDOWN_ID);
- const { report, isEnlarged } = context;
+ const { report } = context;
// While we know that this component only ever gets a MatrixConfig, the Matrix component doesn't know that as it all comes from the same context, so we cast it here so it trickles down to child components
const config = context.config as MatrixConfig;
@@ -187,21 +189,28 @@ const MatrixVisual = () => {
const { periodGranularity, drillDown, valueType } = config;
- // in the dashboard, show a placeholder image
- if (!isEnlarged) {
- return ;
- }
-
- const parsedRows = parseRows(
- rows,
- undefined,
- searchFilter,
- drillDown,
- valueType,
- urlSearchParams,
- setUrlSearchParams,
+ // memoise the parsed rows and columns so that they don't get recalculated on every render, for performance reasons
+ const parsedRows = useMemo(
+ () =>
+ parseRows(
+ rows,
+ undefined,
+ searchFilter,
+ drillDown,
+ valueType,
+ urlSearchParams,
+ setUrlSearchParams,
+ ),
+ [
+ JSON.stringify(rows),
+ searchFilter,
+ JSON.stringify(drillDown),
+ valueType,
+ JSON.stringify(urlSearchParams),
+ setUrlSearchParams,
+ ],
);
- const parsedColumns = parseColumns(columns);
+ const parsedColumns = useMemo(() => parseColumns(columns), [JSON.stringify(columns)]);
const updateSearchFilter = (e: React.ChangeEvent) => {
setSearchFilter(e.target.value);
@@ -283,6 +292,11 @@ const MobileWarning = () => {
};
export const Matrix = () => {
+ const { isEnlarged, config } = useContext(DashboardItemContext);
+ // add a typeguard here to keep TS happy
+ // if the item is not enlarged and is a matrix, then we show the preview, because there won't be any loaded data at this point
+ if (!isEnlarged && config?.type === DashboardItemType.Matrix)
+ return ;
return (
diff --git a/packages/ui-components/src/components/Inputs/Select.tsx b/packages/ui-components/src/components/Inputs/Select.tsx
index eb55a1686e..7ec172c7ff 100644
--- a/packages/ui-components/src/components/Inputs/Select.tsx
+++ b/packages/ui-components/src/components/Inputs/Select.tsx
@@ -17,13 +17,12 @@ const KeyboardArrowDown = styled(MuiKeyboardArrowDown)`
`;
const StyledTextField = styled(TextField)`
+ .MuiInputBase-root {
+ background: transparent;
+ }
.MuiSelect-root {
padding-right: 1.8rem;
color: ${props => props.theme.palette.text.primary};
-
- &:focus {
- background: white;
- }
}
`;
diff --git a/packages/ui-components/src/components/Matrix/Matrix.tsx b/packages/ui-components/src/components/Matrix/Matrix.tsx
index bc39355feb..86b0dab515 100644
--- a/packages/ui-components/src/components/Matrix/Matrix.tsx
+++ b/packages/ui-components/src/components/Matrix/Matrix.tsx
@@ -3,7 +3,7 @@
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/
-import React, { ReactNode, useReducer, useRef } from 'react';
+import React, { ReactNode, useReducer, useRef, useState } from 'react';
import styled from 'styled-components';
import { Table, TableBody, TableContainer } from '@material-ui/core';
import { MatrixConfig } from '@tupaia/types';
@@ -12,6 +12,7 @@ import { MatrixHeader } from './MatrixHeader';
import { MatrixContext, MatrixDispatchContext, matrixReducer } from './MatrixContext';
import { MatrixRow } from './MatrixRow';
import { MatrixLegend } from './MatrixLegend';
+import { MatrixPagination } from './MatrixPagination';
const MatrixTable = styled.table`
color: ${({ theme }) => theme.palette.text.primary};
@@ -35,12 +36,27 @@ interface MatrixProps extends Omit {
rowHeaderColumnTitle?: ReactNode;
}
+const DEFAULT_PAGE_SIZE = 50;
+
export const Matrix = ({ columns = [], rows = [], disableExpand, ...config }: MatrixProps) => {
+ const [pageIndex, setPageIndex] = useState(0);
const [{ expandedRows }, dispatch] = useReducer(matrixReducer, {
expandedRows: [],
});
const tableEl = useRef(null);
+ const pageStart = pageIndex * DEFAULT_PAGE_SIZE;
+ const pageEnd = pageStart + DEFAULT_PAGE_SIZE;
+ const visibleRows = rows.slice(pageStart, pageEnd);
+
+ const onPageChange = (newPageIndex: number) => {
+ setPageIndex(newPageIndex);
+ if (tableEl.current) {
+ // scroll to the top of the table when changing pages
+ tableEl.current.scrollIntoView({ behavior: 'auto' });
+ }
+ };
+
return (
- {rows.map((row, i) => (
+ {visibleRows.map((row, i) => (
))}
+
);
diff --git a/packages/ui-components/src/components/Matrix/MatrixPagination.tsx b/packages/ui-components/src/components/Matrix/MatrixPagination.tsx
new file mode 100644
index 0000000000..35f971c762
--- /dev/null
+++ b/packages/ui-components/src/components/Matrix/MatrixPagination.tsx
@@ -0,0 +1,50 @@
+/*
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ */
+
+import React from 'react';
+import styled from 'styled-components';
+import { Pagination } from '../Pagination';
+
+const Wrapper = styled.div`
+ .MuiSelect-root {
+ text-align: left;
+ }
+ .pagination-wrapper {
+ border-width: 0 1px 1px 1px;
+ border-color: ${({ theme }) => theme.palette.divider};
+ border-style: solid;
+ }
+`;
+
+interface MatrixPaginationProps {
+ totalRows: number;
+ pageSize: number;
+ pageIndex: number;
+ columnsCount: number;
+ handleChangePage: (newPage: number) => void;
+}
+
+export const MatrixPagination = ({
+ totalRows,
+ pageSize,
+ pageIndex,
+ handleChangePage,
+}: MatrixPaginationProps) => {
+ const pageCount = Math.ceil(totalRows / pageSize);
+ if (pageCount === 1) return null;
+ return (
+
+
+
+ );
+};
diff --git a/packages/ui-components/src/components/Matrix/utils.ts b/packages/ui-components/src/components/Matrix/utils.ts
index 023aa1b778..decd02a406 100644
--- a/packages/ui-components/src/components/Matrix/utils.ts
+++ b/packages/ui-components/src/components/Matrix/utils.ts
@@ -131,8 +131,11 @@ export function checkIfApplyPillCellStyle(
export function getFlattenedColumns(columns: MatrixColumnType[]): MatrixColumnType[] {
return columns.reduce((cols, column) => {
if (column.children) {
- return [...cols, ...getFlattenedColumns(column.children)];
+ const childCols = getFlattenedColumns(column.children);
+ cols.push(...childCols);
+ return cols;
}
- return [...cols, column];
+ cols.push(column);
+ return cols;
}, [] as MatrixColumnType[]);
}
diff --git a/packages/ui-components/src/components/Pagination.tsx b/packages/ui-components/src/components/Pagination.tsx
new file mode 100644
index 0000000000..4fe3dad23e
--- /dev/null
+++ b/packages/ui-components/src/components/Pagination.tsx
@@ -0,0 +1,207 @@
+/*
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ */
+import React from 'react';
+import { IconButton, Input, Typography } from '@material-ui/core';
+import styled from 'styled-components';
+import { KeyboardArrowLeft, KeyboardArrowRight } from '@material-ui/icons';
+import { Select } from './Inputs';
+
+const Wrapper = styled.div`
+ font-size: 0.75rem;
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+
+ padding-block: 0.5rem;
+ padding-inline: 1rem;
+ label,
+ p,
+ .MuiInputBase-input {
+ font-size: 0.75rem;
+ }
+`;
+
+const ActionsWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ :first-child {
+ padding-inline-start: 1rem;
+ }
+`;
+const Button = styled(IconButton)`
+ border: 1px solid ${({ theme }) => theme.palette.grey['400']};
+ padding: 0.4rem;
+ .MuiSvgIcon-root {
+ font-size: 1.2rem;
+ }
+ & + & {
+ margin-left: 0.7rem;
+ }
+`;
+
+const ManualPageInputContainer = styled.div`
+ display: flex;
+ align-items: center;
+ margin-inline-start: 0.5rem;
+ margin-inline-end: 0.8rem;
+`;
+
+const ManualPageInput = styled(Input)`
+ border: 1px solid ${({ theme }) => theme.palette.grey['400']};
+ border-radius: 4px;
+ padding-block: 0.5rem;
+ padding-inline: 0.8rem 0.2rem;
+ margin-inline: 0.5rem;
+ font-size: 0.75rem;
+ .MuiInputBase-input {
+ text-align: center;
+ padding-block: 0;
+ height: auto;
+ }
+`;
+
+const Text = styled(Typography)`
+ font-size: 0.75rem;
+`;
+
+const RowsSelect = styled(Select)`
+ display: flex;
+ flex-direction: row;
+ margin-block-end: 0;
+ margin-inline-end: 1.2rem;
+ width: 12rem;
+ .MuiSelect-root {
+ padding-block: 0.5rem;
+ }
+`;
+
+interface PageSelectComponentProps {
+ onChangePage: (newPage: number) => void;
+ page: number;
+ pageCount: number;
+}
+
+const PageSelectComponent = ({ onChangePage, page, pageCount }: PageSelectComponentProps) => {
+ const pageDisplay = page + 1;
+
+ const onChange = (e: React.ChangeEvent) => {
+ const newPage = e.target.value ? Number(e.target.value) - 1 : 0;
+ onChangePage(newPage);
+ };
+ return (
+
+
+
+ Page
+
+
+ of {pageCount}
+
+
+
+
+ );
+};
+
+interface RowsSelectComponentProps {
+ pageSize: number;
+ setPageSize?: (pageSize: number) => void;
+ pageSizeOptions: number[];
+}
+
+const RowsSelectComponent = ({
+ pageSize,
+ setPageSize,
+ pageSizeOptions,
+}: RowsSelectComponentProps) => {
+ const displayOptions = pageSizeOptions.map(size => {
+ return { label: `Rows per page: ${size}`, value: size };
+ });
+
+ const handlePageSizeChange = (event: React.ChangeEvent<{ value: unknown }>) => {
+ if (!setPageSize) return;
+ setPageSize(Number(event.target.value));
+ };
+ return (
+
+
+
+ );
+};
+
+interface PaginationProps {
+ page: number;
+ pageCount: number;
+ onChangePage: PageSelectComponentProps['onChangePage'];
+ pageSize: number;
+ setPageSize?: RowsSelectComponentProps['setPageSize'];
+ totalRecords: number;
+ pageSizeOptions?: RowsSelectComponentProps['pageSizeOptions'];
+ applyRowsPerPage?: boolean;
+ showEntriesCount?: boolean;
+}
+export const Pagination = ({
+ page,
+ pageCount,
+ onChangePage,
+ pageSize,
+ setPageSize,
+ totalRecords,
+ pageSizeOptions = [5, 10, 20, 25, 50, 100],
+ applyRowsPerPage = true,
+ showEntriesCount = true,
+}: PaginationProps) => {
+ if (!totalRecords) return null;
+ const currentDisplayStart = page * pageSize + 1;
+ const currentDisplayEnd = Math.min((page + 1) * pageSize, totalRecords);
+
+ return (
+
+
+ {showEntriesCount && (
+
+ {currentDisplayStart} - {currentDisplayEnd} of {totalRecords} entries
+
+ )}
+
+
+ {applyRowsPerPage && (
+
+ )}
+
+
+
+ );
+};
diff --git a/packages/ui-components/src/components/index.ts b/packages/ui-components/src/components/index.ts
index b02b433a6a..5173f316ec 100644
--- a/packages/ui-components/src/components/index.ts
+++ b/packages/ui-components/src/components/index.ts
@@ -45,3 +45,4 @@ export * from './Toast';
export * from './Toolbar';
export * from './Tooltip';
export * from './UserMessage';
+export * from './Pagination';
From 9a711f3544f4583b7c5ab8393ec6718df527058b Mon Sep 17 00:00:00 2001
From: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Date: Fri, 14 Jun 2024 15:56:46 +1200
Subject: [PATCH 18/26] feat(tupaiaWeb): RN-1309: Search filters on matrix
visuals (#5686)
* Don't fetch report for matrix when not expanded
* Display preview always for matrix
* memoise data
* Don't spread accumulators
* DOn't use spreads
* Update useReport.ts
* Matrix pagination
* Don't show pagination for only 1 page of results
* Set default page size to be 50
* Scroll to top of table when page changes
* remove setPageSize from matrix
* Fix build
* WIP
* Styling
* Tidy ups
* enableSearch config type
* PR fixes
* Fix merge issues
* Fix no data message anda small screen message
* Fix padding
* Styling fixes
* Fix styling
* Format values before search
* Fix types
* Fix small screen warning
* Fix sizing
* Fix data element search
* Remove enable search
* Filter parent rows
* remove enable search
* Fix search persisting
* Remove enableSearch
* Revert "Fix search persisting"
This reverts commit 786150c70f8ce7218417cb7311c672ec4606d79d.
* Fix persisting search issue
---------
Co-authored-by: Andrew
---
.../src/features/Visuals/Matrix/Matrix.tsx | 223 ++++--------------
.../src/features/Visuals/Matrix/parseData.ts | 151 ++++++++++++
packages/tupaia-web/src/theme/theme.ts | 5 +-
packages/types/src/schemas/schemas.ts | 28 +++
.../models-extra/dashboard-item/matricies.ts | 4 +
.../src/components/Inputs/TextField.tsx | 2 +-
.../src/components/Matrix/Cell.tsx | 14 ++
.../src/components/Matrix/Matrix.tsx | 15 +-
.../src/components/Matrix/MatrixContext.ts | 31 ++-
.../src/components/Matrix/MatrixHeader.tsx | 38 +--
.../src/components/Matrix/MatrixRow.tsx | 9 +-
.../src/components/Matrix/MatrixSearchRow.tsx | 140 +++++++++++
.../src/components/Matrix/index.ts | 1 +
13 files changed, 435 insertions(+), 226 deletions(-)
create mode 100644 packages/tupaia-web/src/features/Visuals/Matrix/parseData.ts
create mode 100644 packages/ui-components/src/components/Matrix/MatrixSearchRow.tsx
diff --git a/packages/tupaia-web/src/features/Visuals/Matrix/Matrix.tsx b/packages/tupaia-web/src/features/Visuals/Matrix/Matrix.tsx
index 808d88f9c6..32a89672bd 100644
--- a/packages/tupaia-web/src/features/Visuals/Matrix/Matrix.tsx
+++ b/packages/tupaia-web/src/features/Visuals/Matrix/Matrix.tsx
@@ -6,26 +6,13 @@
import React, { useContext, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { useSearchParams } from 'react-router-dom';
-import { Clear, Search } from '@material-ui/icons';
-import { IconButton, Typography } from '@material-ui/core';
-import {
- MatrixColumnType,
- MatrixRowType,
- Matrix as MatrixComponent,
- TextField,
- NoData,
-} from '@tupaia/ui-components';
-import { formatDataValueByType } from '@tupaia/utils';
-import {
- DashboardItemType,
- MatrixConfig,
- MatrixReportColumn,
- MatrixReportRow,
- isMatrixReport,
-} from '@tupaia/types';
+import { Typography } from '@material-ui/core';
+import { Matrix as MatrixComponent, NoData, SearchFilter } from '@tupaia/ui-components';
+import { DashboardItemType, isMatrixReport } from '@tupaia/types';
import { DashboardItemContext } from '../../DashboardItem';
import { MOBILE_BREAKPOINT, URL_SEARCH_PARAMS } from '../../../constants';
import { MatrixPreview } from './MatrixPreview';
+import { parseColumns, parseRows } from './parseData';
const Wrapper = styled.div`
// override the base table styles to handle expanded rows, which need to be done with classes and JS because nth-child will not handle skipped rows
@@ -42,133 +29,10 @@ const Wrapper = styled.div`
}
`;
-const SearchInput = styled(TextField)`
- margin: 0;
- min-width: 10rem;
-
- .MuiInputBase-root {
- background-color: transparent;
- font-size: inherit; // override this to inherit the font size from the cell
- }
- .MuiInputBase-input {
- font-size: inherit; // override this to inherit the font size from the cell
- padding: 0.875rem;
- }
- .MuiTableCell-root:has(&) {
- padding-right: 0.7rem;
- padding-left: 0.7rem;
- }
-`;
-
const NoResultsMessage = styled(Typography)`
padding: 1rem;
`;
-// This is a recursive function that parses the rows of the matrix into a format that the Matrix component can use.
-const parseRows = (
- rows: MatrixReportRow[],
- categoryId: MatrixReportRow['categoryId'] | undefined,
- searchFilter: string | undefined,
- drillDown: MatrixConfig['drillDown'] | undefined,
- valueType: MatrixConfig['valueType'] | undefined,
- urlSearchParams: URLSearchParams,
- setUrlSearchParams: (searchParams: URLSearchParams) => void,
-): MatrixRowType[] => {
- const onDrillDown = row => {
- if (!drillDown) return;
- const { itemCode, parameterLink } = drillDown;
- if (!parameterLink || !itemCode) return;
- urlSearchParams?.set(URL_SEARCH_PARAMS.REPORT, itemCode);
- urlSearchParams?.set(URL_SEARCH_PARAMS.REPORT_DRILLDOWN_ID, row[parameterLink]);
- setUrlSearchParams(urlSearchParams);
- };
-
- let topLevelRows = [] as MatrixReportRow[];
- // if a categoryId is not passed in, then we need to find the top level rows
- if (!categoryId) {
- // get the highest level rows, which are the ones that have a category but no categoryId
- const highestLevel = rows.filter(row => !row.categoryId) as MatrixReportRow[];
- // if there are no highest level rows, then the top level rows are just all of the rows
- topLevelRows = highestLevel.length ? highestLevel : rows;
- } else {
- // otherwise, the top level rows are the ones that have the categoryId that was passed in
- topLevelRows = rows.filter(row => row.categoryId === categoryId);
- }
- // loop through the topLevelRows, and parse them into the format that the Matrix component can use
- return topLevelRows.reduce((result, row) => {
- const { dataElement = '', category, valueType: rowValueType, ...rest } = row;
- const valueTypeToUse = rowValueType || valueType;
- // if the row has a category, then it has children, so we need to parse them using this same function
- if (category) {
- const children = parseRows(
- rows,
- category,
- searchFilter,
- drillDown,
- valueTypeToUse,
- urlSearchParams,
- setUrlSearchParams,
- );
- // if there are no child rows, e.g. because the search filter is hiding them, then we don't need to render this row
- if (!children.length) return result;
- result.push({
- title: category,
- ...rest,
- children,
- });
- return result;
- }
- // if the row is a regular row, and there is a search filter, then we need to check if the row matches the search filter, and ignore this row if it doesn't. This filter only applies to standard rows, not category rows.
- if (searchFilter && !dataElement.toLowerCase().includes(searchFilter.toLowerCase()))
- return result;
- // otherwise, handle as a regular row
-
- const formattedProperties = Object.entries(rest).reduce((acc, [key, item]) => {
- // some items are objects, and we need to parse them to get the value
- if (typeof item === 'object' && item !== null) {
- const { value, metadata } = item as { value: any; metadata?: any };
- acc[key] = formatDataValueByType(
- {
- value,
- metadata,
- },
- valueTypeToUse,
- );
- return acc;
- }
-
- acc[key] = formatDataValueByType({ value: item }, valueTypeToUse);
- return acc;
- }, {});
-
- result.push({
- title: dataElement,
- onClick: drillDown ? () => onDrillDown(row) : undefined,
- ...formattedProperties,
- });
- return result;
- }, [] as MatrixRowType[]);
-};
-
-// This is a recursive function that parses the columns of the matrix into a format that the Matrix component can use.
-const parseColumns = (columns: MatrixReportColumn[]): MatrixColumnType[] => {
- return columns.map(column => {
- const { category, key, title, columns: children } = column;
- // if a column has a category, then it has children, so we need to parse them using this same function
- if (category)
- return {
- title: category,
- key: category,
- children: parseColumns(children!),
- };
- // otherwise, handle as a regular column
- return {
- title,
- key,
- };
- });
-};
-
/**
* This is the component that is used to display a matrix. It handles the parsing of the data into the format that the Matrix component can use, as well as placeholder images. It shows a message when there are no rows available to display.
*/
@@ -177,17 +41,16 @@ const MatrixVisual = () => {
const context = useContext(DashboardItemContext);
const [urlSearchParams, setUrlSearchParams] = useSearchParams();
const activeDrillDownId = urlSearchParams.get(URL_SEARCH_PARAMS.REPORT_DRILLDOWN_ID);
-
+ const reportPeriod = urlSearchParams.get(URL_SEARCH_PARAMS.REPORT_PERIOD);
const { report } = context;
- // While we know that this component only ever gets a MatrixConfig, the Matrix component doesn't know that as it all comes from the same context, so we cast it here so it trickles down to child components
- const config = context.config as MatrixConfig;
- // type guard to ensure that the report is a matrix report, even though we know it is
- if (!isMatrixReport(report)) return null;
+ // type guard to ensure that the report is a matrix report and config, even though we know it is
+ if (!isMatrixReport(report) || context.config?.type !== DashboardItemType.Matrix) return null;
+ const { config } = context;
const { columns = [], rows = [] } = report;
- const [searchFilter, setSearchFilter] = useState('');
+ const [searchFilters, setSearchFilters] = useState([]);
- const { periodGranularity, drillDown, valueType } = config;
+ const { drillDown, valueType } = config;
// memoise the parsed rows and columns so that they don't get recalculated on every render, for performance reasons
const parsedRows = useMemo(
@@ -195,7 +58,7 @@ const MatrixVisual = () => {
parseRows(
rows,
undefined,
- searchFilter,
+ searchFilters,
drillDown,
valueType,
urlSearchParams,
@@ -203,7 +66,7 @@ const MatrixVisual = () => {
),
[
JSON.stringify(rows),
- searchFilter,
+ JSON.stringify(searchFilters),
JSON.stringify(drillDown),
valueType,
JSON.stringify(urlSearchParams),
@@ -212,52 +75,47 @@ const MatrixVisual = () => {
);
const parsedColumns = useMemo(() => parseColumns(columns), [JSON.stringify(columns)]);
- const updateSearchFilter = (e: React.ChangeEvent) => {
- setSearchFilter(e.target.value);
+ const updateSearchFilter = ({ key, value }: SearchFilter) => {
+ const filtersWithoutKey = searchFilters.filter(filter => filter.key !== key);
+ const updatedSearchFilters = value
+ ? [
+ ...filtersWithoutKey,
+ {
+ key,
+ value,
+ },
+ ]
+ : filtersWithoutKey;
+
+ setSearchFilters(updatedSearchFilters);
};
- const clearSearchFilter = () => {
- setSearchFilter('');
+ const clearSearchFilter = key => {
+ setSearchFilters(searchFilters.filter(filter => filter.key !== key));
};
useEffect(() => {
// if the drillDownId changes, then we need to clear the search filter so that it doesn't persist across different drillDowns
- clearSearchFilter();
- }, [activeDrillDownId]);
+ setSearchFilters([]);
+ }, [activeDrillDownId, reportPeriod]);
- if (!parsedRows.length && !searchFilter) {
+ if (!parsedRows.length && !searchFilters.length) {
return ;
}
return (
-
-
- ) : (
-
- ),
- }}
- />
- )
- }
+ disableExpand={!!searchFilters.length}
+ searchFilters={searchFilters}
+ updateSearchFilter={updateSearchFilter}
+ clearSearchFilter={clearSearchFilter}
/>
- {searchFilter && !parsedRows.length && (
- No results found for the term: {searchFilter}
+ {searchFilters?.length > 0 && !parsedRows.length && (
+ No results found
)}
);
@@ -295,12 +153,15 @@ export const Matrix = () => {
const { isEnlarged, config } = useContext(DashboardItemContext);
// add a typeguard here to keep TS happy
// if the item is not enlarged and is a matrix, then we show the preview, because there won't be any loaded data at this point
- if (!isEnlarged && config?.type === DashboardItemType.Matrix)
- return ;
+
+ if (config?.type !== DashboardItemType.Matrix) return null;
+
+ const component = isEnlarged ? : ;
+
return (
-
+ {component}
);
};
diff --git a/packages/tupaia-web/src/features/Visuals/Matrix/parseData.ts b/packages/tupaia-web/src/features/Visuals/Matrix/parseData.ts
new file mode 100644
index 0000000000..f36fa9b3c1
--- /dev/null
+++ b/packages/tupaia-web/src/features/Visuals/Matrix/parseData.ts
@@ -0,0 +1,151 @@
+/*
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ */
+import { MatrixColumnType, MatrixRowType, SearchFilter } from '@tupaia/ui-components';
+import { formatDataValueByType } from '@tupaia/utils';
+import { MatrixConfig, MatrixReportColumn, MatrixReportRow } from '@tupaia/types';
+import { URL_SEARCH_PARAMS } from '../../../constants';
+
+const getValueMatchesSearchFilter = (value: any, searchTerm: SearchFilter['value']) => {
+ if (typeof value !== 'string' && typeof value !== 'number') return false;
+ const stringifiedValue = value.toString().toLowerCase();
+ const lowercaseSearchTerm = searchTerm.toLowerCase();
+ // handle % search - if the search term starts with %, then we want to check if the value includes the search term
+ if (lowercaseSearchTerm.startsWith('%')) {
+ const searchTermWithoutPercent = lowercaseSearchTerm.slice(1);
+ return stringifiedValue.includes(searchTermWithoutPercent);
+ }
+
+ // otherwise, check if the value starts with the search term
+ return stringifiedValue.startsWith(lowercaseSearchTerm);
+};
+
+const getRowMatchesSearchFilter = (row: MatrixReportRow, searchFilters: SearchFilter[]) => {
+ return searchFilters.every(filter => {
+ const rowValue = row[filter.key];
+ return getValueMatchesSearchFilter(rowValue, filter.value);
+ });
+};
+
+// This is a recursive function that parses the rows of the matrix into a format that the Matrix component can use.
+export const parseRows = (
+ rows: MatrixReportRow[],
+ categoryId: MatrixReportRow['categoryId'] | undefined,
+ searchFilters: SearchFilter[],
+ drillDown: MatrixConfig['drillDown'] | undefined,
+ valueType: MatrixConfig['valueType'] | undefined,
+ urlSearchParams: URLSearchParams,
+ setUrlSearchParams: (searchParams: URLSearchParams) => void,
+): MatrixRowType[] => {
+ const onDrillDown = row => {
+ if (!drillDown) return;
+ const { itemCode, parameterLink } = drillDown;
+ if (!parameterLink || !itemCode) return;
+ urlSearchParams?.set(URL_SEARCH_PARAMS.REPORT, itemCode);
+ urlSearchParams?.set(URL_SEARCH_PARAMS.REPORT_DRILLDOWN_ID, row[parameterLink]);
+ setUrlSearchParams(urlSearchParams);
+ };
+
+ let topLevelRows: MatrixReportRow[] = [];
+ // if a categoryId is not passed in, then we need to find the top level rows
+ if (!categoryId) {
+ // get the highest level rows, which are the ones that have a category but no categoryId
+ const highestLevel = rows.filter(row => !row.categoryId);
+ // if there are no highest level rows, then the top level rows are just all of the rows
+ topLevelRows = highestLevel.length ? highestLevel : rows;
+ } else {
+ // otherwise, the top level rows are the ones that have the categoryId that was passed in
+ topLevelRows = rows.filter(row => row.categoryId === categoryId);
+ }
+ // loop through the topLevelRows, and parse them into the format that the Matrix component can use
+ return topLevelRows.reduce((result: MatrixRowType[], row: MatrixReportRow) => {
+ const { dataElement = '', category, valueType: rowValueType, ...rest } = row;
+ const valueTypeToUse = rowValueType || valueType;
+ // if the row has a category, then it has children, so we need to parse them using this same function
+ if (category) {
+ const children = parseRows(
+ rows,
+ category,
+ searchFilters,
+ drillDown,
+ valueTypeToUse,
+ urlSearchParams,
+ setUrlSearchParams,
+ );
+
+ if (searchFilters.length > 0) {
+ const topLevelRowMatchesSearchFilter = getRowMatchesSearchFilter(
+ {
+ dataElement: category,
+ ...rest,
+ },
+ searchFilters,
+ );
+ if (!topLevelRowMatchesSearchFilter && !children.length) return result;
+ }
+
+ result.push({
+ title: category,
+ ...rest,
+ children,
+ });
+ return result;
+ }
+
+ const formattedRowValues = Object.entries(rest).reduce((acc, [key, item]) => {
+ // some items are objects, and we need to parse them to get the value
+ if (typeof item === 'object' && item !== null) {
+ const { value, metadata } = item as { value: any; metadata?: any };
+ acc[key] = formatDataValueByType(
+ {
+ value,
+ metadata,
+ },
+ valueTypeToUse,
+ );
+ return acc;
+ }
+ acc[key] = formatDataValueByType({ value: item }, valueTypeToUse);
+ return acc;
+ }, {});
+ // if the row is a regular row, and there is a search filter, then we need to check if the row matches the search filter, and ignore this row if it doesn't. This filter only applies to standard rows, not category rows.
+ if (searchFilters?.length > 0) {
+ const matchesSearchFilter = getRowMatchesSearchFilter(
+ {
+ dataElement,
+ ...formattedRowValues,
+ },
+ searchFilters,
+ );
+
+ if (!matchesSearchFilter) return result;
+ }
+ // otherwise, handle as a regular row
+ result.push({
+ title: dataElement,
+ onClick: drillDown ? () => onDrillDown(row) : undefined,
+ ...formattedRowValues,
+ });
+ return result;
+ }, []);
+};
+
+// This is a recursive function that parses the columns of the matrix into a format that the Matrix component can use.
+export const parseColumns = (columns: MatrixReportColumn[]): MatrixColumnType[] => {
+ return columns.map(column => {
+ const { category, key, title, columns: children } = column;
+ // if a column has a category, then it has children, so we need to parse them using this same function
+ if (category)
+ return {
+ title: category,
+ key: category,
+ children: parseColumns(children!),
+ };
+ // otherwise, handle as a regular column
+ return {
+ title,
+ key,
+ };
+ });
+};
diff --git a/packages/tupaia-web/src/theme/theme.ts b/packages/tupaia-web/src/theme/theme.ts
index e7d00566bc..8c1b428265 100644
--- a/packages/tupaia-web/src/theme/theme.ts
+++ b/packages/tupaia-web/src/theme/theme.ts
@@ -107,7 +107,7 @@ theme.overrides = {
root: {
backgroundColor: theme.palette.table?.odd,
// non expanded rows alternate background color - only apply this when in a modal, ie not on a multi row viz
- ['&:nth-child(even)']: {
+ ['&:not(.MuiTableRow-head)&nth-child(even)']: {
backgroundColor: theme.palette.table?.even,
},
},
@@ -119,8 +119,7 @@ theme.overrides = {
root: {
backgroundColor: 'inherit',
fontSize: '0.875rem',
- paddingBlock: '0.7rem',
- paddingInline: '1.56rem',
+ padding: '0.7rem',
lineHeight: '1.4',
borderBottom: 'none', // remove the bottom border from all cells, and it will be applied to the header cells below
['&.MuiTableCell-row-head']: {
diff --git a/packages/types/src/schemas/schemas.ts b/packages/types/src/schemas/schemas.ts
index f550bf4687..322bb3367d 100644
--- a/packages/types/src/schemas/schemas.ts
+++ b/packages/types/src/schemas/schemas.ts
@@ -1464,6 +1464,10 @@ export const MatrixConfigSchema = {
"placeholder": {
"description": "A url to an image to be used when a matrix is collapsed.",
"type": "string"
+ },
+ "enableSearch": {
+ "description": "Specify whether to show search filters on the matrix",
+ "type": "boolean"
}
},
"required": [
@@ -2162,6 +2166,10 @@ export const MatrixVizBuilderConfigSchema = {
"description": "A url to an image to be used when a matrix is collapsed.",
"type": "string"
},
+ "enableSearch": {
+ "description": "Specify whether to show search filters on the matrix",
+ "type": "boolean"
+ },
"output": {
"description": "Configuration for rows, columns, and categories of the matrix",
"type": "object",
@@ -22315,6 +22323,10 @@ export const DashboardItemConfigSchema = {
"placeholder": {
"description": "A url to an image to be used when a matrix is collapsed.",
"type": "string"
+ },
+ "enableSearch": {
+ "description": "Specify whether to show search filters on the matrix",
+ "type": "boolean"
}
},
"required": [
@@ -43420,6 +43432,10 @@ export const DashboardItemSchema = {
"placeholder": {
"description": "A url to an image to be used when a matrix is collapsed.",
"type": "string"
+ },
+ "enableSearch": {
+ "description": "Specify whether to show search filters on the matrix",
+ "type": "boolean"
}
},
"required": [
@@ -52095,6 +52111,10 @@ export const DashboardItemCreateSchema = {
"placeholder": {
"description": "A url to an image to be used when a matrix is collapsed.",
"type": "string"
+ },
+ "enableSearch": {
+ "description": "Specify whether to show search filters on the matrix",
+ "type": "boolean"
}
},
"required": [
@@ -60764,6 +60784,10 @@ export const DashboardItemUpdateSchema = {
"placeholder": {
"description": "A url to an image to be used when a matrix is collapsed.",
"type": "string"
+ },
+ "enableSearch": {
+ "description": "Specify whether to show search filters on the matrix",
+ "type": "boolean"
}
},
"required": [
@@ -88112,6 +88136,10 @@ export const DashboardWithMetadataSchema = {
"placeholder": {
"description": "A url to an image to be used when a matrix is collapsed.",
"type": "string"
+ },
+ "enableSearch": {
+ "description": "Specify whether to show search filters on the matrix",
+ "type": "boolean"
}
},
"required": [
diff --git a/packages/types/src/types/models-extra/dashboard-item/matricies.ts b/packages/types/src/types/models-extra/dashboard-item/matricies.ts
index 1dfc7db701..745fdb920f 100644
--- a/packages/types/src/types/models-extra/dashboard-item/matricies.ts
+++ b/packages/types/src/types/models-extra/dashboard-item/matricies.ts
@@ -39,6 +39,10 @@ export type MatrixConfig = BaseConfig & {
* @description A url to an image to be used when a matrix is collapsed.
*/
placeholder?: string;
+ /**
+ * @description Specify whether to show search filters on the matrix
+ */
+ enableSearch?: boolean;
};
export type MatrixVizBuilderConfig = MatrixConfig & {
diff --git a/packages/ui-components/src/components/Inputs/TextField.tsx b/packages/ui-components/src/components/Inputs/TextField.tsx
index a724ebe169..50ead9320b 100644
--- a/packages/ui-components/src/components/Inputs/TextField.tsx
+++ b/packages/ui-components/src/components/Inputs/TextField.tsx
@@ -124,7 +124,7 @@ export const TextField = ({
label = '',
tooltip,
...props
-}: TextFieldProps & {
+}: Partial & {
tooltip?: string;
}) => (
`
+ padding-inline-start: 1.5rem;
min-width: ${({ $characterLength = 0 }) =>
$characterLength > 30
? '23ch'
@@ -18,5 +19,18 @@ export const Cell = styled(TableCell)<{
white-space: pre-line;
&.MuiTableCell-row-head {
box-shadow: inset -1px 0 0 0 ${({ theme }) => theme.palette.divider}; // add a border to the right of the first cell, but use a box shadow so that it doesn't get hidden on scroll
+ padding-inline-end: 1.5rem;
+ }
+`;
+
+export const HeaderCell = styled(Cell)`
+ line-height: 1.4;
+ z-index: 3; // set the z-index of the first cell to be above the rest of the column header cells so that it doesn't get covered on horizontal scroll
+ box-shadow: inset 0 -1px 0 0 ${({ theme }) => theme.palette.divider}; // add a border to the bottom of cell
+ &.MuiTableCell-row-head {
+ box-shadow: inset -1px -1px 0 0 ${({ theme }) => theme.palette.divider}; // add a border to the right and bottom of the first cell, but use a box shadow so that it doesn't get hidden on scroll
+
+ z-index: 4; // set the z-index of the first cell to be above the rest of the column header cells so that it doesn't get covered on horizontal scroll
+ max-width: 14rem; // set the max-width of the first cell so that on larger screens the row header column doesn't take up too much space
}
`;
diff --git a/packages/ui-components/src/components/Matrix/Matrix.tsx b/packages/ui-components/src/components/Matrix/Matrix.tsx
index 86b0dab515..b8c41dc385 100644
--- a/packages/ui-components/src/components/Matrix/Matrix.tsx
+++ b/packages/ui-components/src/components/Matrix/Matrix.tsx
@@ -3,13 +3,11 @@
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/
-import React, { ReactNode, useReducer, useRef, useState } from 'react';
+import React, { useReducer, useRef, useState } from 'react';
import styled from 'styled-components';
import { Table, TableBody, TableContainer } from '@material-ui/core';
-import { MatrixConfig } from '@tupaia/types';
-import { MatrixColumnType, MatrixRowType } from '../../types';
import { MatrixHeader } from './MatrixHeader';
-import { MatrixContext, MatrixDispatchContext, matrixReducer } from './MatrixContext';
+import { MatrixContext, MatrixDispatchContext, MatrixProps, matrixReducer } from './MatrixContext';
import { MatrixRow } from './MatrixRow';
import { MatrixLegend } from './MatrixLegend';
import { MatrixPagination } from './MatrixPagination';
@@ -29,13 +27,6 @@ const ScrollContainer = styled(TableContainer)`
); // We already tell users the matrix can't be viewed properly on small screens, but we set some sensible limits just in case
`;
-interface MatrixProps extends Omit {
- columns: MatrixColumnType[];
- rows: MatrixRowType[];
- disableExpand?: boolean;
- rowHeaderColumnTitle?: ReactNode;
-}
-
const DEFAULT_PAGE_SIZE = 50;
export const Matrix = ({ columns = [], rows = [], disableExpand, ...config }: MatrixProps) => {
@@ -71,7 +62,7 @@ export const Matrix = ({ columns = [], rows = [], disableExpand, ...config }: Ma
-
+
{visibleRows.map((row, i) => (
diff --git a/packages/ui-components/src/components/Matrix/MatrixContext.ts b/packages/ui-components/src/components/Matrix/MatrixContext.ts
index 53c0837d6b..a048d3415b 100644
--- a/packages/ui-components/src/components/Matrix/MatrixContext.ts
+++ b/packages/ui-components/src/components/Matrix/MatrixContext.ts
@@ -4,24 +4,39 @@
*/
import { Dispatch, ReactNode, createContext } from 'react';
-import { MatrixConfig } from '@tupaia/types';
+import { MatrixConfig, MatrixReportRow } from '@tupaia/types';
import { MatrixColumnType, MatrixRowType } from '../../types';
type RowTitle = MatrixRowType['title'];
-const defaultContextValue = {
- rows: [],
- columns: [],
- expandedRows: [],
- disableExpand: false,
-} as Omit & {
+export type SearchFilter = {
+ key: keyof MatrixReportRow;
+ value: string;
+};
+
+export type MatrixProps = Omit & {
rows: MatrixRowType[];
columns: MatrixColumnType[];
- expandedRows: RowTitle[];
disableExpand?: boolean;
rowHeaderColumnTitle?: ReactNode;
+ enableSearch?: boolean;
+ searchFilters?: SearchFilter[];
+ updateSearchFilter?: (searchFilter: SearchFilter) => void;
+ clearSearchFilter?: (key: SearchFilter['key']) => void;
};
+export type MatrixContextT = MatrixProps & {
+ expandedRows: RowTitle[];
+};
+
+const defaultContextValue = {
+ rows: [],
+ columns: [],
+ expandedRows: [],
+ disableExpand: false,
+ enableSearch: true,
+} as MatrixContextT;
+
// This is the context for the rows, columns and presentation options of the matrix
export const MatrixContext = createContext(defaultContextValue);
diff --git a/packages/ui-components/src/components/Matrix/MatrixHeader.tsx b/packages/ui-components/src/components/Matrix/MatrixHeader.tsx
index 2c978cf0d0..7490b25237 100644
--- a/packages/ui-components/src/components/Matrix/MatrixHeader.tsx
+++ b/packages/ui-components/src/components/Matrix/MatrixHeader.tsx
@@ -7,31 +7,31 @@ import { darken, TableHead, TableRow } from '@material-ui/core';
import styled from 'styled-components';
import { MatrixColumnType } from '../../types';
import { MatrixContext } from './MatrixContext';
-import { Cell } from './Cell';
+import { HeaderCell } from './Cell';
+import { MatrixSearchRow } from './MatrixSearchRow';
import { getFlattenedColumns } from './utils';
-const HeaderCell = styled(Cell)`
- line-height: 1.4;
- z-index: 3; // set the z-index of the first cell to be above the rest of the column header cells so that it doesn't get covered on horizontal scroll
- box-shadow: inset 0 -1px 0 0 ${({ theme }) => theme.palette.divider}; // add a border to the bottom but ise a box shadow so that it doesn't get hidden on scroll
- &.MuiTableCell-row-head {
- z-index: 4; // set the z-index of the first cell to be above the rest of the column header cells so that it doesn't get covered on horizontal scroll
- max-width: 12rem; // set the max-width of the first cell so that on larger screens the row header column doesn't take up too much space
- box-shadow: inset -1px -1px 0 0 ${({ theme }) => theme.palette.divider}; // add a border to the right of the first cell, but use a box shadow so that it doesn't get hidden on scroll
- }
-`;
-
const ColGroup = styled.colgroup`
&:not(:first-of-type) {
border-right: 1px solid ${({ theme }) => darken(theme.palette.text.primary, 0.4)};
}
`;
+const THead = styled(TableHead)`
+ // Apply sticky positioning to the header element, as we now have 2 header rows
+ position: sticky;
+ top: 0;
+ z-index: 3;
+`;
/**
* This is a component that renders the header rows in the matrix. It renders the column groups and columns.
*/
-export const MatrixHeader = () => {
- const { columns, hideColumnTitles = false, rowHeaderColumnTitle } = useContext(MatrixContext);
+export const MatrixHeader = ({
+ onPageChange,
+}: {
+ onPageChange: (newPageIndex: number) => void;
+}) => {
+ const { columns, hideColumnTitles = false } = useContext(MatrixContext);
// Get the grouped columns
const columnGroups = columns.reduce((result: MatrixColumnType[], column: MatrixColumnType) => {
if (!column.children?.length) return result;
@@ -42,9 +42,7 @@ export const MatrixHeader = () => {
const hasParents = columnGroups.length > 0;
const RowHeaderColumn = (
-
- {rowHeaderColumnTitle}
-
+
);
const flattenedColumns = getFlattenedColumns(columns);
@@ -64,7 +62,7 @@ export const MatrixHeader = () => {
) : (