diff --git a/package-lock.json b/package-lock.json index aefe50e1ee..79fd79fd84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "react-toastify": "^9.0.3", "redux": "^4.1.1", "redux-thunk": "^2.3.0", + "sanitize-html": "^2.12.1", "typedoc-plugin-markdown": "^3.17.1", "typescript": "^4.3.5", "web-vitals": "^1.0.1" @@ -73,6 +74,7 @@ "@types/react-dom": "^17.0.9", "@types/react-google-recaptcha": "^2.1.5", "@types/react-router-dom": "^5.1.8", + "@types/sanitize-html": "^2.11.0", "@typescript-eslint/eslint-plugin": "^5.9.0", "@typescript-eslint/parser": "^5.9.0", "cross-env": "^7.0.3", @@ -5686,6 +5688,15 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, + "node_modules/@types/sanitize-html": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==", + "dev": true, + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -8538,6 +8549,37 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-select/node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/css-select/node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/css-select/node_modules/domutils/node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "node_modules/css-select/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/css-select/node_modules/nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", @@ -9243,20 +9285,16 @@ } }, "node_modules/dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dependencies": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, "node_modules/domelementtype": { @@ -9289,19 +9327,32 @@ "node": ">=8" } }, - "node_modules/domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/domutils/node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } }, "node_modules/dot-case": { "version": "3.0.4", @@ -9450,7 +9501,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "engines": { "node": ">=0.12" }, @@ -12244,6 +12294,24 @@ "webpack": "^5.20.0" } }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -17455,6 +17523,11 @@ "node": ">=0.10.0" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -20783,6 +20856,27 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sanitize-html": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.12.1.tgz", + "integrity": "sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sanitize.css": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", diff --git a/package.json b/package.json index a46f26f1c4..1ddf091b20 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "react-toastify": "^9.0.3", "redux": "^4.1.1", "redux-thunk": "^2.3.0", + "sanitize-html": "^2.12.1", "typedoc-plugin-markdown": "^3.17.1", "typescript": "^4.3.5", "web-vitals": "^1.0.1" @@ -105,6 +106,7 @@ "@types/react-dom": "^17.0.9", "@types/react-google-recaptcha": "^2.1.5", "@types/react-router-dom": "^5.1.8", + "@types/sanitize-html": "^2.11.0", "@typescript-eslint/eslint-plugin": "^5.9.0", "@typescript-eslint/parser": "^5.9.0", "cross-env": "^7.0.3", diff --git a/public/locales/en.json b/public/locales/en.json index f3cc98f29f..742725278c 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -704,6 +704,24 @@ "firstName": "First name", "lastName": "Last name", "language": "Language", + "gender": "Gender", + "birthDate": "Birth Date", + "educationGrade": "Educational Grade", + "employmentStatus": "Employment Status", + "maritalStatus": "Marital Status", + "displayImage": "Display Image", + "phone": "Phone", + "address": "Address", + "countryCode": "Country Code", + "state": "State", + "city": "City", + "personalInfoHeading": "Personal Information", + "contactInfoHeading": "Contact Information", + "actionsHeading": "Actions", + "personalDetailsHeading": "Profile Details", + "appLanguageCode": "Choose Language", + "delete": "Delete User", + "saveChanges": "Save Changes", "adminApproved": "Admin approved", "pluginCreationAllowed": "Plugin creation allowed", "joined": "Joined", @@ -712,7 +730,12 @@ "membershipRequests": "Membership requests", "adminForEvents": "Admin for events", "addedAsAdmin": "User is added as admin.", - "talawaApiUnavailable": "Talawa-API service is unavailable. Kindly check your network connection and wait for a while." + "talawaApiUnavailable": "Talawa-API service is unavailable. Kindly check your network connection and wait for a while.", + "password": "Password", + "userType": "User Type", + "admin": "Admin", + "superAdmin": "Superadmin", + "cancel": "Cancel" }, "userLogin": { "login": "Login", diff --git a/public/locales/fr.json b/public/locales/fr.json index 47888f3fb2..21db17e77d 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -709,6 +709,24 @@ "firstName": "Prénom", "lastName": "Nom de famille", "language": "Langue", + "gender": "Genre", + "birthDate": "Date de naissance", + "educationGrade": "Niveau d'éducation", + "employmentStatus": "Statut d'emploi", + "maritalStatus": "État civil", + "displayImage": "Image de profil", + "phone": "Téléphone", + "address": "Adresse", + "countryCode": "Code pays", + "state": "État", + "city": "Ville", + "personalInfoHeading": "Informations personnelles", + "contactInfoHeading": "Coordonnées", + "actionsHeading": "Actions", + "personalDetailsHeading": "Détails du profil", + "appLanguageCode": "Choisir la langue", + "delete": "Supprimer l'utilisateur", + "saveChanges": "Enregistrer les modifications", "adminApproved": "Approuvé par l'administrateur", "pluginCreationAllowed": "Autorisation de création de plugin", "joined": "Rejoint", diff --git a/public/locales/hi.json b/public/locales/hi.json index 4399afa55e..c685435aec 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -713,6 +713,24 @@ "firstName": "पहला नाम", "lastName": "अंतिम नाम", "language": "भाषा", + "gender": "लिंग", + "birthDate": "जन्म तिथि", + "educationGrade": "शैक्षिक ग्रेड", + "employmentStatus": "रोजगार की स्थिति", + "maritalStatus": "वैवाहिक स्थिति", + "displayImage": "प्रदर्शन छवि", + "phone": "फोन", + "address": "पता", + "countryCode": "देश कोड", + "state": "राज्य", + "city": "शहर", + "personalInfoHeading": "व्यक्तिगत जानकारी", + "contactInfoHeading": "संपर्क जानकारी", + "actionsHeading": "कार्रवाई", + "personalDetailsHeading": "प्रोफ़ाइल विवरण", + "appLanguageCode": "भाषा चुनें", + "delete": "उपयोगकर्ता को हटाएं", + "saveChanges": "परिवर्तन सहेजें", "adminApproved": "व्यवस्थापक द्वारा स्वीकृत", "pluginCreationAllowed": "प्लगइन निर्माण अनुमति दी गई", "joined": "शामिल हुए", diff --git a/public/locales/sp.json b/public/locales/sp.json index 8bc5af519b..081ede2002 100644 --- a/public/locales/sp.json +++ b/public/locales/sp.json @@ -710,6 +710,24 @@ "firstName": "Nombre", "lastName": "Apellido", "language": "Idioma", + "gender": "Género", + "birthDate": "Fecha de Nacimiento", + "educationGrade": "Nivel Educativo", + "employmentStatus": "Estado Laboral", + "maritalStatus": "Estado Civil", + "displayImage": "Imagen de Perfil", + "phone": "Teléfono", + "address": "Dirección", + "countryCode": "Código de País", + "state": "Estado", + "city": "Ciudad", + "personalInfoHeading": "Información Personal", + "contactInfoHeading": "Información de Contacto", + "actionsHeading": "Acciones", + "personalDetailsHeading": "Detalles del perfil", + "appLanguageCode": "Elegir Idioma", + "delete": "Eliminar Usuario", + "saveChanges": "Guardar Cambios", "adminApproved": "Aprobado por el administrador", "pluginCreationAllowed": "Permitir creación de complementos", "joined": "Unido", diff --git a/public/locales/zh.json b/public/locales/zh.json index 4b8d992ff4..7c95514636 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -711,6 +711,24 @@ "firstName": "名字", "lastName": "姓氏", "language": "语言", + "gender": "性别", + "birthDate": "出生日期", + "educationGrade": "教育程度", + "employmentStatus": "就业状况", + "maritalStatus": "婚姻状况", + "displayImage": "显示图片", + "phone": "电话", + "address": "地址", + "countryCode": "国家代码", + "state": "州/省", + "city": "城市", + "personalInfoHeading": "个人信息", + "contactInfoHeading": "联系信息", + "actionsHeading": "操作", + "personalDetailsHeading": "个人详情", + "appLanguageCode": "选择语言", + "delete": "删除用户", + "saveChanges": "保存更改", "adminApproved": "管理员已批准", "pluginCreationAllowed": "允许创建插件", "joined": "加入", diff --git a/src/GraphQl/Mutations/mutations.ts b/src/GraphQl/Mutations/mutations.ts index d76274e286..7f7f445916 100644 --- a/src/GraphQl/Mutations/mutations.ts +++ b/src/GraphQl/Mutations/mutations.ts @@ -94,6 +94,7 @@ export const UPDATE_USER_MUTATION = gql` $empStatus: EmploymentStatus $maritalStatus: MaritalStatus $address: String + $city: String $state: String $country: String $image: String @@ -109,7 +110,12 @@ export const UPDATE_USER_MUTATION = gql` educationGrade: $grade employmentStatus: $empStatus maritalStatus: $maritalStatus - address: { line1: $address, state: $state, countryCode: $country } + address: { + line1: $address + state: $state + countryCode: $country + city: $city + } } file: $image ) { diff --git a/src/GraphQl/Queries/Queries.ts b/src/GraphQl/Queries/Queries.ts index c94aff663e..59af216bff 100644 --- a/src/GraphQl/Queries/Queries.ts +++ b/src/GraphQl/Queries/Queries.ts @@ -464,6 +464,20 @@ export const USER_DETAILS = gql` email image createdAt + birthDate + educationGrade + employmentStatus + gender + maritalStatus + phone { + mobile + } + address { + line1 + countryCode + city + state + } registeredEvents { _id } @@ -478,6 +492,8 @@ export const USER_DETAILS = gql` _id } isSuperAdmin + appLanguageCode + pluginCreationAllowed createdOrganizations { _id } diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index 2cf5f7dda2..e967781d64 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -8,6 +8,7 @@ interface InterfaceAvatarProps { size?: number; avatarStyle?: string; dataTestId?: string; + radius?: number; } const Avatar = ({ @@ -16,11 +17,13 @@ const Avatar = ({ size, avatarStyle, dataTestId, + radius, }: InterfaceAvatarProps): JSX.Element => { const avatar = useMemo(() => { return createAvatar(initials, { size: size || 128, seed: name, + radius: radius || 0, }).toDataUriSync(); }, [name, size]); diff --git a/src/components/DynamicDropDown/DynamicDropDown.module.css b/src/components/DynamicDropDown/DynamicDropDown.module.css new file mode 100644 index 0000000000..0edf85b621 --- /dev/null +++ b/src/components/DynamicDropDown/DynamicDropDown.module.css @@ -0,0 +1,12 @@ +.dropwdownToggle { + background-color: #f1f3f6; + color: black; + border: none; + padding: 0.5rem; + text-align: left; + display: flex; + align-items: center; + justify-content: space-between; + min-width: 8rem; + outline: 1px solid var(--bs-gray-400); +} diff --git a/src/components/DynamicDropDown/DynamicDropDown.test.tsx b/src/components/DynamicDropDown/DynamicDropDown.test.tsx new file mode 100644 index 0000000000..c77ac5aebf --- /dev/null +++ b/src/components/DynamicDropDown/DynamicDropDown.test.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { render, screen, act, waitFor } from '@testing-library/react'; +import DynamicDropDown from './DynamicDropDown'; +import { BrowserRouter } from 'react-router-dom'; +import { I18nextProvider } from 'react-i18next'; +import i18nForTest from 'utils/i18nForTest'; +import userEvent from '@testing-library/user-event'; + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +describe('DynamicDropDown component', () => { + test('renders with name and alt attribute', async () => { + const [formData, setFormData] = [ + { + fieldName: 'TEST', + }, + jest.fn(), + ]; + + render( + + + + + , + ); + const containterElement = screen.getByTestId( + 'fieldname-dropdown-container', + ); + await act(async () => { + userEvent.click(containterElement); + }); + + const optionButton = screen.getByTestId('fieldname-dropdown-btn'); + + await act(async () => { + userEvent.click(optionButton); + }); + + const optionElement = screen.getByTestId('change-fieldname-btn-TEST'); + await act(async () => { + userEvent.click(optionElement); + }); + await wait(); + expect(containterElement).toBeInTheDocument(); + await waitFor(() => { + expect(optionButton).toHaveTextContent('label1'); + }); + }); +}); diff --git a/src/components/DynamicDropDown/DynamicDropDown.tsx b/src/components/DynamicDropDown/DynamicDropDown.tsx new file mode 100644 index 0000000000..170b6be29b --- /dev/null +++ b/src/components/DynamicDropDown/DynamicDropDown.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Dropdown } from 'react-bootstrap'; +import styles from './DynamicDropDown.module.css'; + +interface InterfaceChangeDropDownProps { + parentContainerStyle?: string; + btnStyle?: string; + btnTextStyle?: string; + setFormState: React.Dispatch>; + formState: any; + fieldOptions: { value: string; label: string }[]; // Field options for dropdown + fieldName: string; // Field name for labeling +} + +const DynamicDropDown = (props: InterfaceChangeDropDownProps): JSX.Element => { + const handleFieldChange = (value: string): void => { + props.setFormState({ ...props.formState, [props.fieldName]: value }); + }; + + const getLabel = (value: string): string => { + const selectedOption = props.fieldOptions.find( + (option) => option.value === value, + ); + return selectedOption ? selectedOption.label : `None`; + }; + + return ( + + + {getLabel(props.formState[props.fieldName])} + + + {props.fieldOptions.map((option, index: number) => ( + handleFieldChange(option.value)} + data-testid={`change-${props.fieldName.toLowerCase()}-btn-${option.value}`} + > + {option.label} + + ))} + + + ); +}; + +export default DynamicDropDown; diff --git a/src/components/UserPortal/UserSidebar/UserSidebar.test.tsx b/src/components/UserPortal/UserSidebar/UserSidebar.test.tsx index 69081b0b8f..b62eae9ed0 100644 --- a/src/components/UserPortal/UserSidebar/UserSidebar.test.tsx +++ b/src/components/UserPortal/UserSidebar/UserSidebar.test.tsx @@ -39,6 +39,20 @@ const MOCKS = [ joinedOrganizations: [], membershipRequests: [], registeredEvents: [], + gender: '', + birthDate: '2024-03-14', + educationGrade: '', + employmentStatus: '', + maritalStatus: '', + address: { + line1: '', + countryCode: '', + city: '', + state: '', + }, + phone: { + mobile: '', + }, }, appUserProfile: { _id: 'properId', @@ -48,6 +62,8 @@ const MOCKS = [ createdEvents: [], eventAdmin: [], isSuperAdmin: true, + pluginCreationAllowed: true, + appLanguageCode: 'en', }, }, }, @@ -100,6 +116,20 @@ const MOCKS = [ joinedOrganizations: [], membershipRequests: [], registeredEvents: [], + gender: '', + birthDate: '2024-03-14', + educationGrade: '', + employmentStatus: '', + maritalStatus: '', + address: { + line1: '', + countryCode: '', + city: '', + state: '', + }, + phone: { + mobile: '', + }, }, appUserProfile: { _id: '2', @@ -109,6 +139,8 @@ const MOCKS = [ eventAdmin: [], isSuperAdmin: true, adminApproved: true, + pluginCreationAllowed: true, + appLanguageCode: 'en', }, }, }, @@ -161,6 +193,20 @@ const MOCKS = [ joinedOrganizations: [], membershipRequests: [], registeredEvents: [], + gender: '', + birthDate: '2024-03-14', + educationGrade: '', + employmentStatus: '', + maritalStatus: '', + address: { + line1: '', + countryCode: '', + city: '', + state: '', + }, + phone: { + mobile: '', + }, }, appUserProfile: { _id: 'orgEmpty', @@ -170,6 +216,8 @@ const MOCKS = [ createdEvents: [], eventAdmin: [], isSuperAdmin: true, + pluginCreationAllowed: true, + appLanguageCode: 'en', }, }, }, diff --git a/src/components/UserUpdate/UserUpdate.module.css b/src/components/UserUpdate/UserUpdate.module.css deleted file mode 100644 index fb9b781dcb..0000000000 --- a/src/components/UserUpdate/UserUpdate.module.css +++ /dev/null @@ -1,97 +0,0 @@ -/* .userupdatediv{ - border: 1px solid #e8e5e5; - box-shadow: 2px 1px #e8e5e5; - padding:25px 16px; - border-radius: 5px; - background:#fdfdfd; -} */ -.settingstitle { - color: #707070; - font-size: 20px; - margin-bottom: 30px; - text-align: center; - margin-top: -10px; -} -.dispflex { - display: flex; - flex-direction: column; - justify-content: flex-start; - margin: 0 8rem; -} - -.dispflex > div { - width: 100%; - margin-right: 50px; -} - -.radio_buttons > input { - margin-bottom: 20px; - border: none; - box-shadow: none; - padding: 0 0; - border-radius: 5px; - background: none; - width: 50%; -} - -.dispbtnflex { - margin-top: 20px; - display: flex; - justify-content: center; -} - -.whitebtn { - margin: 1rem 0 0; - margin-top: 10px; - border: 1px solid #e8e5e5; - box-shadow: 0 2px 2px #e8e5e5; - padding: 10px 20px; - border-radius: 5px; - background: none; - font-size: 16px; - color: #31bb6b; - outline: none; - font-weight: 600; - cursor: pointer; - float: left; - transition: - transform 0.2s, - box-shadow 0.2s; -} -.greenregbtn { - margin: 1rem 0 0; - margin-top: 10px; - margin-right: 30px; - border: 1px solid #e8e5e5; - box-shadow: 0 2px 2px #e8e5e5; - padding: 10px 10px; - border-radius: 5px; - background-color: #31bb6b; - font-size: 16px; - color: white; - outline: none; - font-weight: 600; - cursor: pointer; - transition: - transform 0.2s, - box-shadow 0.2s; -} -.radio_buttons { - width: 55%; - margin-top: 10px; - display: flex; - color: #707070; - font-weight: 600; - font-size: 14px; -} -.radio_buttons > input { - transform: scale(1.2); -} -.radio_buttons > label { - margin-top: -4px; - margin-left: 0px; - margin-right: 7px; -} -.idtitle { - width: 88%; -} diff --git a/src/components/UserUpdate/UserUpdate.test.tsx b/src/components/UserUpdate/UserUpdate.test.tsx deleted file mode 100644 index c7d7a511e6..0000000000 --- a/src/components/UserUpdate/UserUpdate.test.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import React from 'react'; -import { act, render, screen } from '@testing-library/react'; -import { MockedProvider } from '@apollo/react-testing'; -import userEvent from '@testing-library/user-event'; -import { I18nextProvider } from 'react-i18next'; -import { BrowserRouter as Router } from 'react-router-dom'; -import UserUpdate from './UserUpdate'; -import { UPDATE_USER_MUTATION } from 'GraphQl/Mutations/mutations'; -import i18nForTest from 'utils/i18nForTest'; -import { USER_DETAILS } from 'GraphQl/Queries/Queries'; -import { StaticMockLink } from 'utils/StaticMockLink'; -import { toast } from 'react-toastify'; - -const MOCKS = [ - { - request: { - query: USER_DETAILS, - variables: { - userId: '1', - }, - }, - result: { - data: { - user: { - __typename: 'UserData', - appUserProfile: { - __typename: 'AppUserProfile', - _id: '1', - adminFor: [ - { __typename: 'Organization', _id: '65e0df0906dd1228350cfd4a' }, - { __typename: 'Organization', _id: '65e0e2abb92c9f3e29503d4e' }, - ], - createdEvents: [ - { __typename: 'Event', _id: '65e32a5b2a1f4288ca1f086a' }, - ], - eventAdmin: [ - { __typename: 'Event', _id: '65e32a5b2a1f4288ca1f086a' }, - ], - createdOrganizations: [ - { __typename: 'Organization', _id: '65e0df0906dd1228350cfd4a' }, - { __typename: 'Organization', _id: '65e0e2abb92c9f3e29503d4e' }, - ], - pluginCreationAllowed: true, - appLanguageCode: 'fr', - isSuperAdmin: true, - adminApproved: true, - }, - user: { - __typename: 'User', - _id: '1', - firstName: 'Aditya', - lastName: 'Agarwal', - createdAt: '2024-02-26T10:36:33.098Z', - image: null, - email: 'adi79@gmail.com', - joinedOrganizations: [ - { __typename: 'Organization', _id: '65e0df0906dd1228350cfd4a' }, - { __typename: 'Organization', _id: '65e0e2abb92c9f3e29503d4e' }, - ], - membershipRequests: [], - registeredEvents: [ - { __typename: 'Event', _id: '65e32a5b2a1f4288ca1f086a' }, - ], - organizationsBlockedBy: [], - }, - }, - }, - }, - }, - { - request: { - query: UPDATE_USER_MUTATION, - variable: { - data: { - firstName: 'Adi', - lastName: 'Agarwal', - email: 'adi79@gmail.com', - appLanguageCode: 'en', - }, - file: null, - }, - }, - result: { - data: { - users: [ - { - _id: '1', - }, - ], - }, - }, - }, -]; - -const link = new StaticMockLink(MOCKS, true); - -async function wait(ms = 5): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} - -describe('Testing User Update', () => { - const props = { - key: '123', - id: '1', - toggleStateValue: jest.fn(), - }; - - const formData = { - firstName: 'Adi', - lastName: 'Agarwal', - email: 'adi79@gmail.com', - image: new File(['hello'], 'hello.png', { type: 'image/png' }), - }; - - global.alert = jest.fn(); - - test('should render props and text elements test for the page component', async () => { - render( - - - - - - - , - ); - - await wait(); - - userEvent.clear(screen.getByPlaceholderText(/First Name/i)); - - userEvent.type( - screen.getByPlaceholderText(/First Name/i), - formData.firstName, - ); - - userEvent.clear(screen.getByPlaceholderText(/Last Name/i)); - - userEvent.type( - screen.getByPlaceholderText(/Last Name/i), - formData.lastName, - ); - - userEvent.clear(screen.getByPlaceholderText(/Email/i)); - - userEvent.type(screen.getByPlaceholderText(/Email/i), formData.email); - userEvent.selectOptions(screen.getByTestId('applangcode'), 'Français'); - userEvent.upload(screen.getByLabelText(/Display Image:/i), formData.image); - await wait(); - - userEvent.click(screen.getByText(/Save Changes/i)); - - expect(screen.getByPlaceholderText(/First Name/i)).toHaveValue( - formData.firstName, - ); - expect(screen.getByPlaceholderText(/Last Name/i)).toHaveValue( - formData.lastName, - ); - expect(screen.getByPlaceholderText(/Email/i)).toHaveValue(formData.email); - - expect(screen.getByText(/Cancel/i)).toBeTruthy(); - expect(screen.getByPlaceholderText(/First Name/i)).toBeInTheDocument(); - expect(screen.getByPlaceholderText(/Last Name/i)).toBeInTheDocument(); - expect(screen.getByPlaceholderText(/Email/i)).toBeInTheDocument(); - expect(screen.getByText(/Display Image/i)).toBeInTheDocument(); - }); - test('should display warnings for blank form submission', async () => { - jest.spyOn(toast, 'warning'); - - render( - - - - - - - , - ); - - await wait(); - - userEvent.clear(screen.getByPlaceholderText(/First Name/i)); - userEvent.clear(screen.getByPlaceholderText(/Last Name/i)); - userEvent.clear(screen.getByPlaceholderText(/Email/i)); - - userEvent.click(screen.getByText(/Save Changes/i)); - - expect(toast.warning).toHaveBeenCalledWith('First Name cannot be blank!'); - expect(toast.warning).toHaveBeenCalledWith('Last Name cannot be blank!'); - expect(toast.warning).toHaveBeenCalledWith('Email cannot be blank!'); - }); -}); diff --git a/src/components/UserUpdate/UserUpdate.tsx b/src/components/UserUpdate/UserUpdate.tsx deleted file mode 100644 index d2914a99d9..0000000000 --- a/src/components/UserUpdate/UserUpdate.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import React from 'react'; -import { useMutation, useQuery } from '@apollo/client'; -import { UPDATE_USER_MUTATION } from 'GraphQl/Mutations/mutations'; -import { useTranslation } from 'react-i18next'; -import Button from 'react-bootstrap/Button'; -import styles from './UserUpdate.module.css'; -import convertToBase64 from 'utils/convertToBase64'; -import { USER_DETAILS } from 'GraphQl/Queries/Queries'; -import { useLocation, useNavigate } from 'react-router-dom'; - -import { languages } from 'utils/languages'; -import { toast } from 'react-toastify'; -import { errorHandler } from 'utils/errorHandler'; -import { Form } from 'react-bootstrap'; -import Loader from 'components/Loader/Loader'; -import useLocalStorage from 'utils/useLocalstorage'; - -interface InterfaceUserUpdateProps { - id: string; - toggleStateValue: () => void; -} - -const UserUpdate: React.FC = ({ - id, - toggleStateValue, -}): JSX.Element => { - const location = useLocation(); - const navigate = useNavigate(); - const { getItem, setItem } = useLocalStorage(); - const currentUrl = location.state?.id || getItem('id') || id; - const { t } = useTranslation('translation', { - keyPrefix: 'userUpdate', - }); - const [formState, setFormState] = React.useState({ - firstName: '', - lastName: '', - email: '', - password: '', - applangcode: '', - file: '', - }); - - const [updateUser] = useMutation(UPDATE_USER_MUTATION); - - const { - data: data, - loading: loading, - error: error, - } = useQuery(USER_DETAILS, { - variables: { userId: currentUrl }, // For testing we are sending the id as a prop - }); - React.useEffect(() => { - if (data) { - setFormState({ - ...formState, - firstName: data?.user?.user?.firstName, - lastName: data?.user?.user?.lastName, - email: data?.user?.user?.email, - applangcode: data?.user?.appUserProfile?.appLanguageCode, - }); - } - }, [data]); - - if (loading) { - return ; - } - - /* istanbul ignore next */ - if (error) { - navigate(`/orgsettings/${currentUrl}`); - } - - const loginLink = async (): Promise => { - try { - const firstName = formState.firstName; - const lastName = formState.lastName; - const email = formState.email; - const file = formState.file; - const appLangCode = formState.applangcode; - let toSubmit = true; - if (firstName.trim().length == 0 || !firstName) { - toast.warning('First Name cannot be blank!'); - toSubmit = false; - } - if (lastName.trim().length == 0 || !lastName) { - toast.warning('Last Name cannot be blank!'); - toSubmit = false; - } - if (email.trim().length == 0 || !email) { - toast.warning('Email cannot be blank!'); - toSubmit = false; - } - if (!toSubmit) return; - const { data } = await updateUser({ - variables: { - //Currently on these fields are supported by the api - data: { - firstName: firstName, - lastName: lastName, - email: email, - appLanguageCode: appLangCode, - }, - file: file, - }, - }); - /* istanbul ignore next */ - if (data) { - if (getItem('id') === currentUrl) { - setItem('FirstName', firstName); - setItem('LastName', lastName); - setItem('Email', email); - setItem('UserImage', file); - } - setFormState({ - firstName: '', - lastName: '', - email: '', - password: '', - applangcode: '', - file: '', - }); - - toast.success('Successful updated'); - - navigate(0); - } - } catch (error: any) { - /* istanbul ignore next */ - errorHandler(t, error); - } - }; - - /* istanbul ignore next */ - const cancelUpdate = (): void => { - toggleStateValue(); - }; - - return ( - <> -
-
- {/*

Update Your Details

*/} -
-
- - { - setFormState({ - ...formState, - firstName: e.target.value, - }); - }} - /> -
-
-
-
- - { - setFormState({ - ...formState, - lastName: e.target.value, - }); - }} - /> -
-
-
-
- - { - setFormState({ - ...formState, - email: e.target.value, - }); - }} - /> -
-
- -
-
- -
-
-
- -
- - -
-
-
-
- - ); -}; -export default UserUpdate; diff --git a/src/screens/MemberDetail/MemberDetail.module.css b/src/screens/MemberDetail/MemberDetail.module.css index 85246ed62b..603e55d1d9 100644 --- a/src/screens/MemberDetail/MemberDetail.module.css +++ b/src/screens/MemberDetail/MemberDetail.module.css @@ -448,3 +448,76 @@ .inactiveBtn:hover i { color: #31bb6b; } + +.topRadius { + border-top-left-radius: 16px; + border-top-right-radius: 16px; +} + +.inputColor { + background: #f1f3f6; +} + +.width60 { + width: 60%; +} + +.maxWidth40 { + max-width: 40%; +} + +.allRound { + border-radius: 16px; +} + +.WidthFit { + width: fit-content; +} + +.datebox { + border-radius: 7px; + border-color: #e8e5e5; + outline: none; + box-shadow: none; + padding-top: 2px; + padding-bottom: 2px; + padding-right: 5px; + padding-left: 5px; + margin-right: 5px; + margin-left: 5px; +} + +.datebox > div > input { + padding: 0.5rem 0 0.5rem 0.5rem !important; /* top, right, bottom, left */ + background-color: #f1f3f6; + border-radius: var(--bs-border-radius) !important; + border: none !important; +} + +.datebox > div > div { + margin-left: 0px !important; +} + +.datebox > div > fieldset { + border: none !important; + /* background-color: #f1f3f6; */ + border-radius: var(--bs-border-radius) !important; +} + +.datebox > div { + margin: 0.5rem !important; + background-color: #f1f3f6; +} + +input::file-selector-button { + background-color: black; + color: white; +} + +.noOutline { + outline: none; +} + +.Outline { + outline: 1px solid var(--bs-gray-400); +} diff --git a/src/screens/MemberDetail/MemberDetail.test.tsx b/src/screens/MemberDetail/MemberDetail.test.tsx index 1d4fe3a9b8..bdf15950e5 100644 --- a/src/screens/MemberDetail/MemberDetail.test.tsx +++ b/src/screens/MemberDetail/MemberDetail.test.tsx @@ -1,16 +1,28 @@ import React from 'react'; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; import { MockedProvider } from '@apollo/react-testing'; -import { act, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { ADD_ADMIN_MUTATION } from 'GraphQl/Mutations/mutations'; -import { USER_DETAILS } from 'GraphQl/Queries/Queries'; -import { I18nextProvider } from 'react-i18next'; -import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; import { store } from 'state/store'; -import { StaticMockLink } from 'utils/StaticMockLink'; +import { I18nextProvider } from 'react-i18next'; +import { + ADD_ADMIN_MUTATION, + UPDATE_USERTYPE_MUTATION, + UPDATE_USER_MUTATION, +} from 'GraphQl/Mutations/mutations'; +import { USER_DETAILS } from 'GraphQl/Queries/Queries'; import i18nForTest from 'utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; import MemberDetail, { getLanguageName, prettyDate } from './MemberDetail'; +import { toast } from 'react-toastify'; + const MOCKS1 = [ { request: { @@ -24,6 +36,7 @@ const MOCKS1 = [ user: { __typename: 'UserData', appUserProfile: { + _id: '1', __typename: 'AppUserProfile', adminFor: [ { @@ -63,12 +76,27 @@ const MOCKS1 = [ adminApproved: true, }, user: { + _id: '1', __typename: 'User', createdAt: '2024-02-26T10:36:33.098Z', email: 'adi790u@gmail.com', firstName: 'Aditya', image: null, lastName: 'Agarwal', + gender: '', + birthDate: '2024-03-14', + educationGrade: '', + employmentStatus: '', + maritalStatus: '', + address: { + line1: '', + countryCode: '', + city: '', + state: '', + }, + phone: { + mobile: '', + }, joinedOrganizations: [ { __typename: 'Organization', @@ -119,40 +147,87 @@ const MOCKS2 = [ result: { data: { user: { + __typename: 'UserData', + appUserProfile: { + _id: '1', + __typename: 'AppUserProfile', + adminFor: [ + { + __typename: 'Organization', + _id: '65e0df0906dd1228350cfd4a', + }, + { + __typename: 'Organization', + _id: '65e0e2abb92c9f3e29503d4e', + }, + ], + isSuperAdmin: true, + appLanguageCode: 'en', + createdEvents: [ + { + __typename: 'Event', + _id: '65e32a5b2a1f4288ca1f086a', + }, + ], + createdOrganizations: [ + { + __typename: 'Organization', + _id: '65e0df0906dd1228350cfd4a', + }, + { + __typename: 'Organization', + _id: '65e0e2abb92c9f3e29503d4e', + }, + ], + eventAdmin: [ + { + __typename: 'Event', + _id: '65e32a5b2a1f4288ca1f086a', + }, + ], + pluginCreationAllowed: true, + adminApproved: true, + }, user: { - __typename: 'UserData', - appUserProfile: { - __typename: 'AppUserProfile', - adminFor: [ - { - __typename: 'Organization', - _id: '65e0df0906dd1228350cfd4a', - }, - { - __typename: 'Organization', - _id: '65e0e2abb92c9f3e29503d4e', - }, - ], - isSuperAdmin: true, - appLanguageCode: 'en', - createdEvents: [], - createdOrganizations: [], - eventAdmin: [], - pluginCreationAllowed: true, - adminApproved: true, + _id: '1', + __typename: 'User', + createdAt: '2024-02-26T10:36:33.098Z', + email: 'adi790u@gmail.com', + firstName: 'Aditya', + image: 'https://placeholder.com/200x200', + lastName: 'Agarwal', + gender: '', + birthDate: '2024-03-14', + educationGrade: '', + employmentStatus: '', + maritalStatus: '', + address: { + line1: '', + countryCode: '', + city: '', + state: '', }, - user: { - __typename: 'User', - createdAt: '2024-02-26T10:36:33.098Z', - email: 'adi790u@gmail.com', - firstName: 'Aditya', - image: null, - lastName: 'Agarwal', - joinedOrganizations: [], - membershipRequests: [], - organizationsBlockedBy: [], - registeredEvents: [], + phone: { + mobile: '', }, + joinedOrganizations: [ + { + __typename: 'Organization', + _id: '65e0df0906dd1228350cfd4a', + }, + { + __typename: 'Organization', + _id: '65e0e2abb92c9f3e29503d4e', + }, + ], + membershipRequests: [], + organizationsBlockedBy: [], + registeredEvents: [ + { + __typename: 'Event', + _id: '65e32a5b2a1f4288ca1f086a', + }, + ], }, }, }, @@ -177,10 +252,18 @@ const MOCKS2 = [ const link1 = new StaticMockLink(MOCKS1, true); const link2 = new StaticMockLink(MOCKS2, true); -async function wait(ms = 2): Promise { +async function wait(ms = 20): Promise { await act(() => new Promise((resolve) => setTimeout(resolve, ms))); } +jest.mock('@mui/x-date-pickers/DateTimePicker', () => { + return { + DateTimePicker: jest.requireActual( + '@mui/x-date-pickers/DesktopDateTimePicker', + ).DesktopDateTimePicker, + }; +}); + jest.mock('react-toastify'); describe('MemberDetail', () => { @@ -205,35 +288,18 @@ describe('MemberDetail', () => { expect(screen.queryByText('Loading data...')).not.toBeInTheDocument(); await wait(); - - waitFor(() => { - expect(screen.getByTestId('addAdminBtn')).toBeInTheDocument(); - expect(screen.getByTestId('dashboardTitleBtn')).toBeInTheDocument(); - expect(screen.getByTestId('dashboardTitleBtn')).toHaveTextContent( - 'User Details', - ); - expect(screen.getAllByText(/Email/i)).toBeTruthy(); - expect(screen.getAllByText(/Main/i)).toBeTruthy(); - expect(screen.getAllByText(/First name/i)).toBeTruthy(); - expect(screen.getAllByText(/Last name/i)).toBeTruthy(); - expect(screen.getAllByText(/Language/i)).toBeTruthy(); - expect(screen.getByText(/Admin approved/i)).toBeInTheDocument(); - expect(screen.getByText(/Plugin creation allowed/i)).toBeInTheDocument(); - expect(screen.getAllByText(/Created on/i)).toBeTruthy(); - expect(screen.getAllByText(/Admin for organizations/i)).toBeTruthy(); - expect(screen.getAllByText(/Membership requests/i)).toBeTruthy(); - expect(screen.getAllByText(/Events/i)).toBeTruthy(); - expect(screen.getAllByText(/Admin for events/i)).toBeTruthy(); - - expect(screen.getAllByText(/Created On/i)).toHaveLength(2); - expect(screen.getAllByText(/User Details/i)).toHaveLength(2); - expect(screen.getAllByText(/Role/i)).toHaveLength(2); - expect(screen.getAllByText(/Created/i)).toHaveLength(4); - expect(screen.getAllByText(/Joined/i)).toHaveLength(2); - expect(screen.getByTestId('addAdminBtn')).toBeInTheDocument(); - expect(screen.getByTestId('stateBtn')).toBeInTheDocument(); - userEvent.click(screen.getByTestId('stateBtn')); - }); + expect(screen.getAllByText(/Email/i)).toBeTruthy(); + expect(screen.getAllByText(/First name/i)).toBeTruthy(); + expect(screen.getAllByText(/Last name/i)).toBeTruthy(); + expect(screen.getAllByText(/Language/i)).toBeTruthy(); + expect(screen.getByText(/Admin approved/i)).toBeInTheDocument(); + expect(screen.getByText(/Plugin creation allowed/i)).toBeInTheDocument(); + expect(screen.getAllByText(/Joined on/i)).toBeTruthy(); + expect(screen.getAllByText(/Joined On/i)).toHaveLength(1); + expect(screen.getAllByText(/Personal Information/i)).toHaveLength(1); + expect(screen.getAllByText(/Profile Details/i)).toHaveLength(1); + expect(screen.getAllByText(/Actions/i)).toHaveLength(1); + expect(screen.getAllByText(/Contact Information/i)).toHaveLength(1); }); test('prettyDate function should work properly', () => { @@ -254,12 +320,116 @@ describe('MemberDetail', () => { expect(getLangName('')).toBe('Unavailable'); }); + test('should render props and text elements test for the page component', async () => { + const props = { + id: '1', + }; + + const formData = { + firstName: 'Ansh', + lastName: 'Goyal', + email: 'ansh@gmail.com', + image: new File(['hello'], 'hello.png', { type: 'image/png' }), + address: 'abc', + countryCode: 'IN', + state: 'abc', + city: 'abc', + phoneNumber: '1234567890', + birthDate: '03/28/2022', + }; + render( + + + + + + + + + , + ); + expect(screen.queryByText('Loading data...')).not.toBeInTheDocument(); + await wait(); + expect(screen.getAllByText(/Email/i)).toBeTruthy(); + const birthDateDatePicker = screen.getByTestId('birthDate'); + fireEvent.change(birthDateDatePicker, { + target: { value: formData.birthDate }, + }); + + userEvent.type( + screen.getByPlaceholderText(/First Name/i), + formData.firstName, + ); + userEvent.type( + screen.getByPlaceholderText(/Last Name/i), + formData.lastName, + ); + userEvent.type(screen.getByPlaceholderText(/Address/i), formData.address); + userEvent.type( + screen.getByPlaceholderText(/Country Code/i), + formData.countryCode, + ); + userEvent.type(screen.getByPlaceholderText(/State/i), formData.state); + userEvent.type(screen.getByPlaceholderText(/City/i), formData.city); + userEvent.type(screen.getByPlaceholderText(/Email/i), formData.email); + userEvent.type(screen.getByPlaceholderText(/Phone/i), formData.phoneNumber); + userEvent.click(screen.getByPlaceholderText(/adminApproved/i)); + userEvent.click(screen.getByPlaceholderText(/pluginCreationAllowed/i)); + userEvent.selectOptions(screen.getByTestId('applangcode'), 'Français'); + userEvent.upload(screen.getByLabelText(/Display Image:/i), formData.image); + await wait(); + + userEvent.click(screen.getByText(/Save Changes/i)); + + expect(screen.getByPlaceholderText(/First Name/i)).toHaveValue( + formData.firstName, + ); + expect(screen.getByPlaceholderText(/Last Name/i)).toHaveValue( + formData.lastName, + ); + expect(birthDateDatePicker).toHaveValue(formData.birthDate); + expect(screen.getByPlaceholderText(/Email/i)).toHaveValue(formData.email); + expect(screen.getByPlaceholderText(/First Name/i)).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/Last Name/i)).toBeInTheDocument(); + expect(screen.getByPlaceholderText(/Email/i)).toBeInTheDocument(); + expect(screen.getByText(/Display Image/i)).toBeInTheDocument(); + }); + + test('should display warnings for blank form submission', async () => { + jest.spyOn(toast, 'warning'); + const props = { + key: '123', + id: '1', + toggleStateValue: jest.fn(), + }; + + render( + + + + + + + + + , + ); + + await wait(); + + userEvent.click(screen.getByText(/Save Changes/i)); + + expect(toast.warning).toHaveBeenCalledWith('First Name cannot be blank!'); + expect(toast.warning).toHaveBeenCalledWith('Last Name cannot be blank!'); + expect(toast.warning).toHaveBeenCalledWith('Email cannot be blank!'); + }); + test('Should display dicebear image if image is null', async () => { const props = { id: 'rishav-jha-mech', from: 'orglist', }; - const user = MOCKS1[0].result.data.user; + render( @@ -273,14 +443,11 @@ describe('MemberDetail', () => { ); expect(screen.queryByText('Loading data...')).not.toBeInTheDocument(); - waitFor(() => - expect(screen.getByTestId('userImageAbsent')).toBeInTheDocument(), - ); - waitFor(() => - expect(screen.getByTestId('userImageAbsent').getAttribute('src')).toBe( - `https://api.dicebear.com/5.x/initials/svg?seed=${user?.user?.firstName} ${user?.user?.lastName}`, - ), - ); + const dicebearUrl = `mocked-data-uri`; + + const userImage = await screen.findByTestId('userImageAbsent'); + expect(userImage).toBeInTheDocument(); + expect(userImage.getAttribute('src')).toBe(dicebearUrl); }); test('Should display image if image is present', async () => { @@ -303,16 +470,10 @@ describe('MemberDetail', () => { expect(screen.queryByText('Loading data...')).not.toBeInTheDocument(); - const user = MOCKS2[0].result.data.user; - - waitFor(() => - expect(screen.getByTestId('userImagePresent')).toBeInTheDocument(), - ); - waitFor(() => - expect(screen.getByTestId('userImagePresent').getAttribute('src')).toBe( - user?.user.user.image, - ), - ); + const user = MOCKS2[0].result?.data?.user?.user; + const userImage = await screen.findByTestId('userImagePresent'); + expect(userImage).toBeInTheDocument(); + expect(userImage.getAttribute('src')).toBe(user?.image); }); test('should call setState with 2 when button is clicked', async () => { @@ -320,7 +481,7 @@ describe('MemberDetail', () => { id: 'rishav-jha-mech', }; render( - + @@ -341,7 +502,7 @@ describe('MemberDetail', () => { id: 'rishav-jha-mech', }; render( - + @@ -376,4 +537,18 @@ describe('MemberDetail', () => { expect(screen.getByTestId('adminApproved')).toHaveTextContent('No'); }); }); + test('should be redirected to / if member id is undefined', async () => { + render( + + + + + + + + + , + ); + expect(window.location.pathname).toEqual('/'); + }); }); diff --git a/src/screens/MemberDetail/MemberDetail.tsx b/src/screens/MemberDetail/MemberDetail.tsx index 63580cedfe..520a965e65 100644 --- a/src/screens/MemberDetail/MemberDetail.tsx +++ b/src/screens/MemberDetail/MemberDetail.tsx @@ -1,19 +1,35 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useMutation, useQuery } from '@apollo/client'; -import Col from 'react-bootstrap/Col'; -import Row from 'react-bootstrap/Row'; import Button from 'react-bootstrap/Button'; import { useTranslation } from 'react-i18next'; -import { useParams, useLocation, Navigate } from 'react-router-dom'; -import UserUpdate from 'components/UserUpdate/UserUpdate'; +import { useLocation } from 'react-router-dom'; import { USER_DETAILS } from 'GraphQl/Queries/Queries'; import styles from './MemberDetail.module.css'; import { languages } from 'utils/languages'; -import { ADD_ADMIN_MUTATION } from 'GraphQl/Mutations/mutations'; +import { UPDATE_USER_MUTATION } from 'GraphQl/Mutations/mutations'; import { toast } from 'react-toastify'; import { errorHandler } from 'utils/errorHandler'; import Loader from 'components/Loader/Loader'; import useLocalStorage from 'utils/useLocalstorage'; +import Avatar from 'components/Avatar/Avatar'; +import { + CalendarIcon, + DatePicker, + LocalizationProvider, +} from '@mui/x-date-pickers'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { Form } from 'react-bootstrap'; +import convertToBase64 from 'utils/convertToBase64'; +import sanitizeHtml from 'sanitize-html'; +import type { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; +import { + educationGradeEnum, + maritalStatusEnum, + genderEnum, + employmentStatusEnum, +} from 'utils/memberFields'; +import DynamicDropDown from 'components/DynamicDropDown/DynamicDropDown'; type MemberDetailProps = { id?: string; // This is the userId @@ -24,314 +40,530 @@ const MemberDetail: React.FC = ({ id }): JSX.Element => { keyPrefix: 'memberDetail', }); const location = useLocation(); - - const [state, setState] = useState(1); - const [isAdmin, setIsAdmin] = useState(false); - - const { getItem } = useLocalStorage(); + const isMounted = useRef(true); + const { getItem, setItem } = useLocalStorage(); const currentUrl = location.state?.id || getItem('id') || id; - const { orgId } = useParams(); document.title = t('title'); - - const [adda] = useMutation(ADD_ADMIN_MUTATION); - - const { - data: userData, - loading: loading, - error: error, - refetch: refetch, - } = useQuery(USER_DETAILS, { - variables: { userId: currentUrl }, // For testing we are sending the id as a prop + const [formState, setFormState] = useState({ + firstName: '', + lastName: '', + email: '', + appLanguageCode: '', + image: '', + gender: '', + birthDate: '2024-03-14', + grade: '', + empStatus: '', + maritalStatus: '', + phoneNumber: '', + address: '', + state: '', + city: '', + country: '', + pluginCreationAllowed: false, + adminApproved: false, }); + // Handle date change + const handleDateChange = (date: Dayjs | null): void => { + if (date) { + setFormState((prevState) => ({ + ...prevState, + birthDate: dayjs(date).format('YYYY-MM-DD'), // Convert Dayjs object to JavaScript Date object + })); + } + }; + const [updateUser] = useMutation(UPDATE_USER_MUTATION); + const { data: user, loading: loading } = useQuery(USER_DETAILS, { + variables: { id: currentUrl }, // For testing we are sending the id as a prop + }); + const userData = user?.user; useEffect(() => { - if (userData) { - const isAdmin = - userData.user.appUserProfile.adminFor.length > 0 || - userData.user.appUserProfile.isSuperAdmin; - setIsAdmin(isAdmin); + if (userData && isMounted) { + // console.log(userData); + setFormState({ + ...formState, + firstName: userData?.user?.firstName, + lastName: userData?.user?.lastName, + email: userData?.user?.email, + appLanguageCode: userData?.appUserProfile?.appLanguageCode, + gender: userData?.user?.gender, + birthDate: userData?.user?.birthDate || '2020-03-14', + grade: userData?.user?.educationGrade, + empStatus: userData?.user?.employmentStatus, + maritalStatus: userData?.user?.maritalStatus, + phoneNumber: userData?.user?.phone?.mobile, + address: userData.user?.address?.line1, + state: userData?.user?.address?.state, + city: userData?.user?.address?.city, + country: userData?.user?.address?.countryCode, + pluginCreationAllowed: userData?.appUserProfile?.pluginCreationAllowed, + adminApproved: userData?.appUserProfile?.adminApproved, + image: userData?.user?.image || '', + }); } - }, [userData]); + }, [userData, user]); + + useEffect(() => { + // check component is mounted or not + return () => { + isMounted.current = false; + }; + }, []); - /* istanbul ignore next */ - const toggleStateValue = (): void => { - if (state === 1) setState(2); - else setState(1); - refetch(); + const handleChange = (e: React.ChangeEvent): void => { + const { name, value } = e.target; + // setFormState({ + // ...formState, + // [name]: value, + // }); + // console.log(name, value); + setFormState((prevState) => ({ + ...prevState, + [name]: value, + })); + // console.log(formState); }; - if (loading) { - return ; - } + // const handlePhoneChange = (e: React.ChangeEvent): void => { + // const { name, value } = e.target; + // setFormState({ + // ...formState, + // phoneNumber: { + // ...formState.phoneNumber, + // [name]: value, + // }, + // }); + // // console.log(formState); + // }; - /* istanbul ignore next */ - if (error) { - return ; - } + const handleToggleChange = (e: React.ChangeEvent): void => { + // console.log(e.target.checked); + const { name, checked } = e.target; + setFormState((prevState) => ({ + ...prevState, + [name]: checked, + })); + // console.log(formState); + }; - const addAdmin = async (): Promise => { + const loginLink = async (): Promise => { try { - const { data } = await adda({ - variables: { - userid: location.state?.id, - orgid: orgId, - }, - }); - - if (data) { - toast.success(t('addedAsAdmin')); - setTimeout(() => { - window.location.reload(); - }, 2000); + // console.log(formState); + const firstName = formState.firstName; + const lastName = formState.lastName; + const email = formState.email; + // const appLanguageCode = formState.appLanguageCode; + const image = formState.image; + // const gender = formState.gender; + let toSubmit = true; + if (firstName.trim().length == 0 || !firstName) { + toast.warning('First Name cannot be blank!'); + toSubmit = false; + } + if (lastName.trim().length == 0 || !lastName) { + toast.warning('Last Name cannot be blank!'); + toSubmit = false; + } + if (email.trim().length == 0 || !email) { + toast.warning('Email cannot be blank!'); + toSubmit = false; + } + if (!toSubmit) return; + try { + const { data } = await updateUser({ + variables: { + //! Currently only some fields are supported by the api + id: currentUrl, + ...formState, + }, + }); + /* istanbul ignore next */ + if (data) { + if (getItem('id') === currentUrl) { + setItem('FirstName', firstName); + setItem('LastName', lastName); + setItem('Email', email); + setItem('UserImage', image); + } + toast.success('Successful updated'); + } + } catch (error: unknown) { + if (error instanceof Error) { + errorHandler(t, error); + } } - /* istanbul ignore next */ } catch (error: unknown) { /* istanbul ignore next */ - errorHandler(t, error); + if (error instanceof Error) { + errorHandler(t, error); + } } }; - return ( - <> - - - {state == 1 ? ( -
- -

- {t('title')} -

-
- + if (loading) { + return ; + } - + const sanitizedSrc = sanitizeHtml(formState.image, { + allowedTags: ['img'], + allowedAttributes: { + img: ['src', 'alt'], + }, + }); + + return ( + +
+
+
+ {/* Personal */} +
+
+

{t('personalInfoHeading')}

+
+
+
+

{t('firstName')}

+
- - - -
- {userData?.user?.image ? ( - - ) : ( - - )} +
+

{t('lastName')}

+ +
+
+

{t('gender')}

+
+
- - - {/* User section */} +
+
+

{t('birthDate')}

-

- - {userData?.user?.user?.firstName}{' '} - {userData?.user?.user?.lastName} - -

-

- {t('role')} :{' '} - - {userData.user.appUserProfile.isSuperAdmin - ? 'SuperAdmin' - : userData.user.appUserProfile.adminFor.length > 0 - ? 'Admin' - : 'User'} - + +

+
+
+

{t('educationGrade')}

+ +
+
+

{t('employmentStatus')}

+ +
+
+

{t('maritalStatus')}

+ +
+

+ +

+
+
+ {/* Contact Info */} +
+
+

{t('contactInfoHeading')}

+
+
+
+

{t('phone')}

+ +
+
+

{t('email')}

+ +
+
+

{t('address')}

+ +
+
+

{t('countryCode')}

+ +
+
+

{t('city')}

+ +
+
+

{t('state')}

+ +
+
+
+
+
+ {/* Personal */} +
+
+

{t('personalDetailsHeading')}

+
+
+
+ {formState.image ? ( + + ) : ( + <> + + + )} +
+
+

{formState?.firstName}

+
+

+ {userData?.appUserProfile?.isSuperAdmin + ? 'Super Admin' + : userData?.appUserProfile?.adminFor.length > 0 + ? 'Admin' + : 'User'}

-

- {t('email')} :{' '} - {userData?.user?.user?.email} +

+

{formState.email}

+

+ + Joined on {prettyDate(userData?.user?.createdAt)} +

+
+
+
+ + {/* Actions */} +
+
+

{t('actionsHeading')}

+
+
+
+
+ +

+ {`${t('adminApproved')} (API not supported yet)`}

-

- {t('createdOn')} :{' '} - {prettyDate(userData?.user.user?.createdAt)} +

+
+ +

+ {`${t('pluginCreationAllowed')} (API not supported yet)`}

- - -
-
-
- {/* Main Section And Activity section */} -
- - {/* Main Section */} - -
-
-
- {t('main')} -
-
-
- - {t('firstName')} - {userData?.user?.user?.firstName} - - - {t('lastName')} - {userData?.user?.user?.lastName} - - - {t('role')} - - {userData.user.appUserProfile.isSuperAdmin - ? 'SuperAdmin' - : userData.user.appUserProfile.adminFor.length > 0 - ? 'Admin' - : 'User'} - - - - {t('language')} - - {getLanguageName( - userData?.user?.appUserProfile?.appLanguageCode, - )} - - - - {t('adminApproved')} - - {userData?.user?.appUserProfile?.adminApproved - ? 'Yes' - : 'No'} - - - - {t('pluginCreationAllowed')} - - {userData?.user?.appUserProfile - ?.pluginCreationAllowed - ? 'Yes' - : 'No'} - - - - {t('createdOn')} - - {prettyDate(userData?.user?.user?.createdAt)} - - -
-
- - {/* Activity Section */} - - {/* Organizations */} -
-
-
- {t('organizations')} -
-
-
- - {t('created')} - - { - userData?.user?.appUserProfile - ?.createdOrganizations?.length - } - - - - {t('joined')} - - {userData?.user?.user?.joinedOrganizations?.length} - - - - {t('adminForOrganizations')} - - {userData?.user?.appUserProfile?.adminFor?.length} - - - - {t('membershipRequests')} - - {userData?.user?.user?.membershipRequests?.length} - - -
-
- {/* Events */} -
-
-
- {t('events')} -
-
-
- - {t('created')} - - { - userData?.user?.appUserProfile?.createdEvents - ?.length - } - - - - {t('joined')} - - {userData?.user?.user?.registeredEvents?.length} - - - - {t('adminForEvents')} - - {userData?.user?.appUserProfile?.eventAdmin?.length} - - -
+
+
+
+
+
- - -
+
+
+ + +
+
+
- ) : ( - - )} - - - +
+ +
+
+
+
+
); }; - export const prettyDate = (param: string): string => { const date = new Date(param); if (date?.toDateString() === 'Invalid Date') { return 'Unavailable'; } - return `${date?.toDateString()} ${date.toLocaleTimeString()}`; + const day = date.getDate(); + const month = date.toLocaleString('default', { month: 'long' }); + const year = date.getFullYear(); + return `${day} ${month} ${year}`; }; - export const getLanguageName = (code: string): string => { let language = 'Unavailable'; languages.map((data) => { @@ -341,5 +573,4 @@ export const getLanguageName = (code: string): string => { }); return language; }; - export default MemberDetail; diff --git a/src/utils/memberFields.ts b/src/utils/memberFields.ts new file mode 100644 index 0000000000..440dbfcf4c --- /dev/null +++ b/src/utils/memberFields.ts @@ -0,0 +1,149 @@ +const educationGradeEnum = [ + { + value: 'NO_GRADE', + label: 'No Grade', + }, + { + value: 'PRE_KG', + label: 'Pre-KG', + }, + { + value: 'KG', + label: 'KG', + }, + { + value: 'GRADE_1', + label: 'Grade 1', + }, + { + value: 'GRADE_2', + label: 'Grade 2', + }, + { + value: 'GRADE_3', + label: 'Grade 3', + }, + { + value: 'GRADE_4', + label: 'Grade 4', + }, + { + value: 'GRADE_5', + label: 'Grade 5', + }, + { + value: 'GRADE_6', + label: 'Grade 6', + }, + { + value: 'GRADE_7', + label: 'Grade 7', + }, + { + value: 'GRADE_8', + label: 'Grade 8', + }, + { + value: 'GRADE_9', + label: 'Grade 9', + }, + { + value: 'GRADE_10', + label: 'Grade 10', + }, + { + value: 'GRADE_11', + label: 'Grade 11', + }, + { + value: 'GRADE_12', + label: 'Grade 12', + }, + { + value: 'GRADUATE', + label: 'Graduate', + }, +]; + +const maritalStatusEnum = [ + { + value: 'SINGLE', + label: 'Single', + }, + { + value: 'ENGAGED', + label: 'Engaged', + }, + { + value: 'MARRIED', + label: 'Married', + }, + { + value: 'DIVORCED', + label: 'Divorced', + }, + { + value: 'WIDOWED', + label: 'Widowed', + }, + { + value: 'SEPARATED', + label: 'Separated', + }, +]; + +const genderEnum = [ + { + value: 'MALE', + label: 'Male', + }, + { + value: 'FEMALE', + label: 'Female', + }, + { + value: 'OTHER', + label: 'Other', + }, +]; + +const employmentStatusEnum = [ + { + value: 'FULL_TIME', + label: 'Full Time', + }, + { + value: 'PART_TIME', + label: 'Part Time', + }, + { + value: 'UNEMPLOYED', + label: 'Unemployed', + }, +]; +const userTypeEnum = [ + { + value: 'USER', + label: 'User', + }, + { + value: 'ADMIN', + label: 'Admin', + }, + { + value: 'SUPERADMIN', + label: 'Super Admin', + }, + { + value: 'NON_USER', + label: 'Non-User', + }, +]; + +export { + educationGradeEnum, + maritalStatusEnum, + genderEnum, + employmentStatusEnum, + userTypeEnum, +};