diff --git a/CHANGELOG b/CHANGELOG index 86cf71ddfc0504a04f47136fd4663d31821972f4..fa563f9d10a89561e1f4f27b0eb4bd028d6649be 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ minerva-front (18.0.0~beta.5) stable; urgency=medium + * Small improvements: when ToS is defined ask user to accept it (#298) * Small improvements: there is a waiting spinner after clicking on download button (#297) * Small improvements: when exporting map as image provide default selections diff --git a/public/config.js b/public/config.js index 21e90e5000e69039e2ab45bf06a29750a0884be1..486d01447b0a63fc61cf907a5f10c709b56f9832 100644 --- a/public/config.js +++ b/public/config.js @@ -1,7 +1,9 @@ +// const root = 'https://minerva-dev.lcsb.uni.lu'; +const root = 'https://lux1.atcomp.pl'; window.config = { - BASE_API_URL: 'https://minerva-dev.lcsb.uni.lu/minerva/api', - BASE_NEW_API_URL: 'https://minerva-dev.lcsb.uni.lu/minerva/new_api/', - BASE_MAP_IMAGES_URL: 'https://minerva-dev.lcsb.uni.lu/', + BASE_API_URL: `${root}/minerva/api`, + BASE_NEW_API_URL: `${root}/minerva/new_api/`, + BASE_MAP_IMAGES_URL: `${root}/`, DEFAULT_PROJECT_ID: 'sample', - ADMIN_PANEL_URL: 'https://minerva-dev.lcsb.uni.lu/minerva/admin.xhtml', + ADMIN_PANEL_URL: `${root}/minerva/admin.xhtml`, }; diff --git a/src/components/AppWrapper/AppWrapper.component.tsx b/src/components/AppWrapper/AppWrapper.component.tsx index 5014ee69cbf7e8d0baed57003a58d81dfe049335..2bae3997dd16e426802fd547b3235057ce22aa8c 100644 --- a/src/components/AppWrapper/AppWrapper.component.tsx +++ b/src/components/AppWrapper/AppWrapper.component.tsx @@ -8,19 +8,21 @@ interface AppWrapperProps { children: ReactNode; } -export const AppWrapper = ({ children }: AppWrapperProps): JSX.Element => ( - <MapInstanceProvider> - <Provider store={store}> - <> - <Toaster - position="top-center" - visibleToasts={1} - style={{ - width: '700px', - }} - /> - {children} - </> - </Provider> - </MapInstanceProvider> -); +export const AppWrapper = ({ children }: AppWrapperProps): JSX.Element => { + return ( + <MapInstanceProvider> + <Provider store={store}> + <> + <Toaster + position="top-center" + visibleToasts={1} + style={{ + width: '700px', + }} + /> + {children} + </> + </Provider> + </MapInstanceProvider> + ); +}; diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx index 8b97ece7a85b1931420cb95c9c781dd9a72d6067..2768dfa3c5a4a66d968a6a7c3ce5ec61c4ea2c2a 100644 --- a/src/components/FunctionalArea/Modal/Modal.component.tsx +++ b/src/components/FunctionalArea/Modal/Modal.component.tsx @@ -4,6 +4,7 @@ import dynamic from 'next/dynamic'; import { AccessDeniedModal } from '@/components/FunctionalArea/Modal/AccessDeniedModal/AccessDeniedModal.component'; import { AddCommentModal } from '@/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component'; import { LicenseModal } from '@/components/FunctionalArea/Modal/LicenseModal'; +import { ToSModal } from '@/components/FunctionalArea/Modal/ToSModal/ToSModal.component'; import { EditOverlayModal } from './EditOverlayModal'; import { LoginModal } from './LoginModal'; import { ErrorReportModal } from './ErrorReportModal'; @@ -63,6 +64,11 @@ export const Modal = (): React.ReactNode => { <AccessDeniedModal /> </ModalLayout> )} + {isOpen && modalName === 'terms-of-service' && ( + <ModalLayout> + <ToSModal /> + </ModalLayout> + )} {isOpen && modalName === 'select-project' && ( <ModalLayout> <AccessDeniedModal /> diff --git a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx index 56e1991ab095814ea1a03a1856b76e4ada43c07d..b202afcdc6c6088f8db048f00695235c4cbfe94d 100644 --- a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx +++ b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx @@ -30,6 +30,7 @@ export const ModalLayout = ({ children }: ModalLayoutProps): JSX.Element => { modalName === 'login' && 'h-auto w-[400px]', modalName === 'access-denied' && 'h-auto w-[400px]', modalName === 'select-project' && 'h-auto w-[400px]', + modalName === 'terms-of-service' && 'h-auto w-[400px]', modalName === 'add-comment' && 'h-auto w-[400px]', modalName === 'error-report' && 'h-auto w-[800px]', ['edit-overlay', 'logged-in-menu'].includes(modalName) && 'h-auto w-[432px]', @@ -46,7 +47,7 @@ export const ModalLayout = ({ children }: ModalLayoutProps): JSX.Element => { <div> {modalTitle} </div> )} - {modalName !== 'logged-in-menu' && ( + {modalName !== 'logged-in-menu' && modalName !== 'terms-of-service' && ( <button type="button" onClick={handleCloseModal} aria-label="close button"> <Icon name="close" className="fill-font-500" /> </button> diff --git a/src/components/FunctionalArea/Modal/ToSModal/ToSModal.component.tsx b/src/components/FunctionalArea/Modal/ToSModal/ToSModal.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7d3d4e766253a5b8f061f9dc0758f65f4e10df2e --- /dev/null +++ b/src/components/FunctionalArea/Modal/ToSModal/ToSModal.component.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { Button } from '@/shared/Button'; + +import { getSessionValid, logout, updateUser } from '@/redux/user/user.thunks'; +import { closeModal } from '@/redux/modal/modal.slice'; +import { userSelector } from '@/redux/user/user.selectors'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { termsOfServiceValSelector } from '@/redux/configuration/configuration.selectors'; + +export const ToSModal: React.FC = () => { + const dispatch = useAppDispatch(); + const { userData } = useAppSelector(userSelector); + + const termsOfService = useAppSelector(termsOfServiceValSelector); + + const updateUserTosHandler = async (): Promise<void> => { + // eslint-disable-next-line no-console + console.log('update'); + if (userData) { + const user = { ...userData, termsOfUseConsent: true }; + await dispatch(updateUser(user)); + await dispatch(getSessionValid()); + dispatch(closeModal()); + } + }; + + const logoutHandler = async (): Promise<void> => { + await dispatch(logout()); + dispatch(closeModal()); + }; + + return ( + <div className="w-[400px] border border-t-[#E1E0E6] bg-white p-[24px]"> + <div> + I agree to the minerva{' '} + <a href={termsOfService} target="_blank" className="underline"> + Terms of Service. + </a> + </div> + <div className="mt-4 grid grid-cols-2 gap-2"> + <div> + <Button + className="ring-transparent hover:ring-transparent" + variantStyles="secondary" + onClick={updateUserTosHandler} + > + OK + </Button> + </div> + <div className="text-center"> + <Button className="block w-full" onClick={logoutHandler}> + I disagree + </Button> + </div> + </div> + </div> + ); +}; diff --git a/src/components/FunctionalArea/TopBar/User/AuthenticatedUser/AuthenticatedUser.component.tsx b/src/components/FunctionalArea/TopBar/User/AuthenticatedUser/AuthenticatedUser.component.tsx index 57b836750126ed657facc8a79affd6edfa2b9d7e..3c5dc54d450e58321a497549684c9d1f3922f59f 100644 --- a/src/components/FunctionalArea/TopBar/User/AuthenticatedUser/AuthenticatedUser.component.tsx +++ b/src/components/FunctionalArea/TopBar/User/AuthenticatedUser/AuthenticatedUser.component.tsx @@ -1,6 +1,11 @@ import { useSelect } from 'downshift'; import { IconButton } from '@/shared/IconButton'; import { twMerge } from 'tailwind-merge'; +import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { userSelector } from '@/redux/user/user.selectors'; +import { openToSModal } from '@/redux/modal/modal.slice'; +import { termsOfServiceValSelector } from '@/redux/configuration/configuration.selectors'; import { useUserActions } from '../hooks/useUserActions'; export const AuthenticatedUser = (): React.ReactNode => { @@ -10,6 +15,14 @@ export const AuthenticatedUser = (): React.ReactNode => { items: actions, }); + const dispatch = useAppDispatch(); + const { userData } = useAppSelector(userSelector); + const termsOfService = useAppSelector(termsOfServiceValSelector); + + if (userData && !userData.termsOfUseConsent && termsOfService) { + dispatch(openToSModal()); + } + return ( <> <IconButton diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 944cfa00adaa176e99eb468559a8e841fac84832..0b30a948bfdbabe8f5ff8edabfee360541ed7b3f 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -102,6 +102,7 @@ export const apiPath = { getSubmapConnections: (): string => `projects/${PROJECT_ID}/submapConnections/`, logout: (): string => `doLogout`, user: (login: string): string => `users/${login}`, + updateUser: (login: string): string => `users/${login}`, getStacktrace: (code: string): string => `stacktrace/${code}`, submitError: (): string => `minervanet/submitError`, getOauthProviders: (): string => `oauth/providers/`, diff --git a/src/redux/configuration/configuration.constants.ts b/src/redux/configuration/configuration.constants.ts index 59ef10da2e7defa1753613d5b185ad0d6d454439..28578664321a61e1fc7aeda6ef2006b3edaef8ee 100644 --- a/src/redux/configuration/configuration.constants.ts +++ b/src/redux/configuration/configuration.constants.ts @@ -5,6 +5,7 @@ export const NEUTRAL_COLOR_VAL_NAME_ID = 'NEUTRAL_COLOR_VAL'; export const OVERLAY_OPACITY_NAME_ID = 'OVERLAY_OPACITY'; export const SEARCH_DISTANCE_NAME_ID = 'SEARCH_DISTANCE'; export const REQUEST_ACCOUNT_EMAIL = 'REQUEST_ACCOUNT_EMAIL'; +export const TERMS_OF_SERVICE_ID = 'TERMS_OF_USE'; export const LEGEND_FILE_NAMES_IDS = [ 'LEGEND_FILE_1', diff --git a/src/redux/configuration/configuration.selectors.ts b/src/redux/configuration/configuration.selectors.ts index ae5b97b7bc077a3f8316f26faf57e32ce2f8a5bf..176847d40d89b9e31dd50f1fe1e8fb3657c14b94 100644 --- a/src/redux/configuration/configuration.selectors.ts +++ b/src/redux/configuration/configuration.selectors.ts @@ -18,6 +18,7 @@ import { SVG_IMAGE_HANDLER_NAME_ID, SEARCH_DISTANCE_NAME_ID, REQUEST_ACCOUNT_EMAIL, + TERMS_OF_SERVICE_ID, } from './configuration.constants'; import { ConfigurationHandlersIds, ConfigurationImageHandlersIds } from './configuration.types'; @@ -63,6 +64,11 @@ export const adminEmailValSelector = createSelector( state => configurationAdapterSelectors.selectById(state, REQUEST_ACCOUNT_EMAIL)?.value, ); +export const termsOfServiceValSelector = createSelector( + configurationOptionsSelector, + state => configurationAdapterSelectors.selectById(state, TERMS_OF_SERVICE_ID)?.value, +); + export const defaultLegendImagesSelector = createSelector(configurationOptionsSelector, state => LEGEND_FILE_NAMES_IDS.map( legendNameId => configurationAdapterSelectors.selectById(state, legendNameId)?.value, diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts index 4556c17cc91238adaa87cb538d3d8548ea4376e4..236f7f809f2b3d01bca4d20b095ad25917dba6a8 100644 --- a/src/redux/modal/modal.reducers.ts +++ b/src/redux/modal/modal.reducers.ts @@ -109,3 +109,9 @@ export const openLicenseModalReducer = (state: ModalState, action: PayloadAction state.modalName = 'license'; state.modalTitle = `License: ${action.payload}`; }; + +export const openToSModalReducer = (state: ModalState): void => { + state.isOpen = true; + state.modalName = 'terms-of-service'; + state.modalTitle = 'Terms of service!'; +}; diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts index 3e945e4a1e01b8d6cba771860700e4e2e9177927..bb145852246d450ffde3acb6e935f77eaac280f6 100644 --- a/src/redux/modal/modal.slice.ts +++ b/src/redux/modal/modal.slice.ts @@ -15,6 +15,7 @@ import { openAccessDeniedModalReducer, openSelectProjectModalReducer, openLicenseModalReducer, + openToSModalReducer, } from './modal.reducers'; const modalSlice = createSlice({ @@ -35,6 +36,7 @@ const modalSlice = createSlice({ openAccessDeniedModal: openAccessDeniedModalReducer, openSelectProjectModal: openSelectProjectModalReducer, openLicenseModal: openLicenseModalReducer, + openToSModal: openToSModalReducer, }, }); @@ -53,6 +55,7 @@ export const { openAccessDeniedModal, openSelectProjectModal, openLicenseModal, + openToSModal, } = modalSlice.actions; export default modalSlice.reducer; diff --git a/src/redux/user/user.thunks.ts b/src/redux/user/user.thunks.ts index 3e1f7bc1079e8524940facf371cd28bf14511fb1..9b016dd81526e58f529f591b18c39c7d9f451bf0 100644 --- a/src/redux/user/user.thunks.ts +++ b/src/redux/user/user.thunks.ts @@ -9,9 +9,11 @@ import { getError } from '@/utils/error-report/getError'; import axios, { HttpStatusCode } from 'axios'; import { showToast } from '@/utils/showToast'; import { setLoginForOldMinerva } from '@/utils/setLoginForOldMinerva'; -import { apiPath } from '../apiPath'; -import { closeModal, openLoggedInMenuModal } from '../modal/modal.slice'; +import { ThunkConfig } from '@/types/store'; +import { userSchema } from '@/models/userSchema'; import { hasPrivilege } from './user.utils'; +import { closeModal, openLoggedInMenuModal } from '../modal/modal.slice'; +import { apiPath } from '../apiPath'; const getUserRole = (privileges: UserPrivilege[]): string => { if (hasPrivilege(privileges, 'IS_ADMIN')) { @@ -118,3 +120,29 @@ export const logout = createAsyncThunk('user/logout', async () => { return Promise.reject(getError({ error, prefix: 'Log out' })); } }); + +export const updateUser = createAsyncThunk<undefined, User, ThunkConfig>( + 'users/updateUser', + // eslint-disable-next-line consistent-return + async user => { + try { + const newUser = await axiosInstance.patch<User>( + apiPath.updateUser(user.login), + { + user: { + termsOfUseConsent: user.termsOfUseConsent, + }, + }, + { + withCredentials: true, + }, + ); + + validateDataUsingZodSchema(newUser, userSchema); + + showToast({ type: 'success', message: 'ToS agreement registered' }); + } catch (error) { + return Promise.reject(getError({ error })); + } + }, +); diff --git a/src/types/modal.ts b/src/types/modal.ts index 1030c46b8b181cbc81f54e7e74054064825b6e16..eaf3a498c59005f9a2b5c2f2470305970f2e07dc 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -10,4 +10,5 @@ export type ModalName = | 'error-report' | 'access-denied' | 'select-project' + | 'terms-of-service' | 'logged-in-menu';