diff --git a/src/users/UserSummary.jsx b/src/users/UserSummary.jsx index 1503778e4..6849d3e2d 100644 --- a/src/users/UserSummary.jsx +++ b/src/users/UserSummary.jsx @@ -1,41 +1,22 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { Modal, Button, Input } from '@edx/paragon'; -import { postTogglePasswordStatus, postResetPassword } from './data/api'; import Table from '../Table'; import { formatDate } from '../utils'; import { getAccountActivationUrl } from './data/urls'; import IdentityVerificationStatus from './IdentityVerificationStatus'; import OnboardingStatus from './OnboardingStatus'; import SingleSignOnRecords from './SingleSignOnRecords'; +import TogglePasswordStatus from './account-actions/TogglePasswordStatus'; +import ResetPassword from './account-actions/ResetPassword'; +import PasswordHistory from './account-actions/PasswordHistory'; export default function UserSummary({ userData, changeHandler, }) { - const [disableUserModalIsOpen, setDisableUserModalIsOpen] = useState(false); - const [disableHistoryModalIsOpen, setDisableHistoryModalIsOpen] = useState(false); - const [resetPasswordModalIsOpen, setResetPasswordModalIsOpen] = useState(false); - const [comment, setComment] = useState(''); - const [userPasswordHistoryData, setUserPasswordHistoryData] = useState([]); const userToggleVisible = true; // TO-DO: Only expose "Disable/Enable User" for specific roles - const PASSWORD_STATUS = { - USABLE: 'Usable', - UNUSABLE: 'Unusable', - }; - - const togglePasswordStatus = () => { - postTogglePasswordStatus(userData.username, comment); - changeHandler(); - }; - - const resetPassword = () => { - postResetPassword(userData.email); - changeHandler(); - }; - const userAccountData = [ { dataName: 'Full Name', @@ -86,36 +67,6 @@ export default function UserSummary({ }, ]; - const userPasswordHistoryColumns = [ - { - label: 'Date', - key: 'created', - }, - { - label: 'Comment', - key: 'comment', - }, - { - label: 'Action', - key: 'disabled', - }, - { - label: 'By', - key: 'createdBy', - }, - ]; - - const openHistoryModel = () => { - const tableData = userData.passwordStatus.passwordToggleHistory.map(result => ({ - created: formatDate(result.created), - comment: result.comment, - disabled: result.disabled ? 'Disabled' : 'Enabled', - createdBy: result.createdBy, - })); - setUserPasswordHistoryData(tableData); - setDisableHistoryModalIsOpen(true); - }; - if (!userData.isActive) { let dataValue; if (userData.activationKey !== null) { @@ -144,29 +95,19 @@ export default function UserSummary({ columns={columns} /> {userToggleVisible && ( -
- - - {userData.passwordStatus.passwordToggleHistory.length > 0 && ( - - )} +
+ + +
)}
@@ -178,66 +119,6 @@ export default function UserSummary({ - setDisableHistoryModalIsOpen(false)} - title="Enable/Disable History" - id="password-history" - body={( - - )} - /> - - Confirm - , - ]} - onClose={() => setDisableUserModalIsOpen(false)} - title={`${userData.passwordStatus.status === PASSWORD_STATUS.USABLE ? 'Disable user confirmation' : 'Enable user confirmation'}`} - body={( -
- - setComment(event.target.value)} - /> -
- )} - /> - - Confirm - , - ]} - onClose={() => setResetPasswordModalIsOpen(false)} - title="Reset Password" - body={( -
- { /* eslint-disable-next-line jsx-a11y/label-has-associated-control */ } - -
- )} - /> ); diff --git a/src/users/UserSummary.test.jsx b/src/users/UserSummary.test.jsx index b68ba492a..52ac6d023 100644 --- a/src/users/UserSummary.test.jsx +++ b/src/users/UserSummary.test.jsx @@ -96,142 +96,4 @@ describe('User Summary Component Tests', () => { expect(rowValue.text()).toEqual('N/A'); }); }); - - describe('Disable User Button', () => { - it('Disable User button for active user', () => { - const passwordActionButton = wrapper.find('#toggle-password').hostNodes(); - expect(passwordActionButton.text()).toEqual('Disable User'); - expect(passwordActionButton.disabled).toBeFalsy(); - }); - - it('Disable User Modal', () => { - const mockApiCall = jest.spyOn(api, 'postTogglePasswordStatus').mockImplementation(() => {}); - const passwordActionButton = wrapper.find('#toggle-password').hostNodes(); - let disableDialogModal = wrapper.find('Modal#user-account-status-toggle'); - - expect(disableDialogModal.prop('open')).toEqual(false); - expect(passwordActionButton.text()).toEqual('Disable User'); - expect(passwordActionButton.disabled).toBeFalsy(); - - passwordActionButton.simulate('click'); - disableDialogModal = wrapper.find('Modal#user-account-status-toggle'); - - expect(disableDialogModal.prop('open')).toEqual(true); - expect(disableDialogModal.prop('title')).toEqual('Disable user confirmation'); - disableDialogModal.find('input[name="comment"]').simulate('change', { target: { value: 'Disable Test User' } }); - disableDialogModal.find('button.btn-danger').hostNodes().simulate('click'); - - expect(UserSummaryData.changeHandler).toHaveBeenCalled(); - disableDialogModal.find('button.btn-link').simulate('click'); - disableDialogModal = wrapper.find('Modal#user-account-status-toggle'); - expect(disableDialogModal.prop('open')).toEqual(false); - mockApiCall.mockRestore(); - }); - }); - - describe('Enable User Button', () => { - beforeEach(() => { - const passwordStatusData = { ...UserSummaryData.userData.passwordStatus, status: 'Unusable' }; - const userData = { ...UserSummaryData.userData, passwordStatus: passwordStatusData }; - mountUserSummaryWrapper({ ...UserSummaryData, userData }); - }); - - it('Enable User button for disabled user', () => { - const passwordActionButton = wrapper.find('#toggle-password').hostNodes(); - expect(passwordActionButton.text()).toEqual('Enable User'); - expect(passwordActionButton.disabled).toBeFalsy(); - }); - - it('Enable User Modal', () => { - const mockApiCall = jest.spyOn(api, 'postTogglePasswordStatus').mockImplementation(() => {}); - const passwordActionButton = wrapper.find('#toggle-password').hostNodes(); - let enableUserModal = wrapper.find('Modal#user-account-status-toggle'); - - expect(enableUserModal.prop('open')).toEqual(false); - expect(passwordActionButton.text()).toEqual('Enable User'); - expect(passwordActionButton.disabled).toBeFalsy(); - - passwordActionButton.simulate('click'); - enableUserModal = wrapper.find('Modal#user-account-status-toggle'); - - expect(enableUserModal.prop('open')).toEqual(true); - expect(enableUserModal.prop('title')).toEqual('Enable user confirmation'); - enableUserModal.find('input[name="comment"]').simulate('change', { target: { value: 'Enable Test User' } }); - enableUserModal.find('button.btn-danger').hostNodes().simulate('click'); - - expect(UserSummaryData.changeHandler).toHaveBeenCalled(); - mockApiCall.mockRestore(); - }); - }); - - describe('Reset Password Button', () => { - it('Reset Password button for a User', () => { - const passwordResetButton = wrapper.find('#reset-password').hostNodes(); - expect(passwordResetButton.text()).toEqual('Reset Password'); - }); - - it('Reset Password Modal', () => { - const mockApiCall = jest.spyOn(api, 'postResetPassword').mockImplementation(() => {}); - const passwordResetButton = wrapper.find('#reset-password').hostNodes(); - let resetPasswordModal = wrapper.find('Modal#user-account-reset-password'); - - expect(resetPasswordModal.prop('open')).toEqual(false); - expect(passwordResetButton.text()).toEqual('Reset Password'); - - passwordResetButton.simulate('click'); - resetPasswordModal = wrapper.find('Modal#user-account-reset-password'); - - expect(resetPasswordModal.prop('open')).toEqual(true); - expect(resetPasswordModal.prop('title')).toEqual('Reset Password'); - const confirmLabel = resetPasswordModal.find('label'); - expect(confirmLabel.text()).toContain('Do you wish to proceed?'); - resetPasswordModal.find('button.btn-danger').hostNodes().simulate('click'); - - expect(UserSummaryData.changeHandler).toHaveBeenCalled(); - resetPasswordModal.find('button.btn-link').simulate('click'); - resetPasswordModal = wrapper.find('Modal#user-account-reset-password'); - expect(resetPasswordModal.prop('open')).toEqual(false); - mockApiCall.mockRestore(); - }); - }); - - describe('Password Toggle History', () => { - beforeEach(() => { - const passwordHistory = [ - { - created: Date().toLocaleString(), - comment: 'Test Disabled', - disabled: false, - createdBy: 'staff', - }, - { - created: Date().toLocaleString(), - comment: 'Test Enable', - disabled: true, - createdBy: 'staff', - }, - ]; - const passwordStatusData = { ...UserSummaryData.userData.passwordStatus, passwordToggleHistory: passwordHistory }; - const userData = { ...UserSummaryData.userData, passwordStatus: passwordStatusData }; - mountUserSummaryWrapper({ ...UserSummaryData, userData }); - }); - it('Password History Modal', () => { - const passwordHistoryButton = wrapper.find('button#toggle-password-history'); - let historyModal = wrapper.find('Modal#password-history'); - - expect(historyModal.prop('open')).toEqual(false); - expect(passwordHistoryButton.text()).toEqual('Show History'); - expect(passwordHistoryButton.disabled).toBeFalsy(); - - passwordHistoryButton.simulate('click'); - historyModal = wrapper.find('Modal#password-history'); - - expect(historyModal.prop('open')).toEqual(true); - expect(historyModal.find('table tbody tr')).toHaveLength(2); - - historyModal.find('button.btn-link').simulate('click'); - historyModal = wrapper.find('Modal#password-history'); - expect(historyModal.prop('open')).toEqual(false); - }); - }); }); diff --git a/src/users/account-actions/PasswordHistory.jsx b/src/users/account-actions/PasswordHistory.jsx new file mode 100644 index 000000000..804a27aa5 --- /dev/null +++ b/src/users/account-actions/PasswordHistory.jsx @@ -0,0 +1,73 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, Button, Table } from '@edx/paragon'; +import { formatDate } from '../../utils'; + +export default function PasswordHistory({ + passwordStatus, +}) { + const [passwordHistoryData, setPasswordHistoryData] = useState([]); + const [passwordHistoryModalIsOpen, setPasswordHistoryModalIsOpen] = useState(false); + + const openHistoryModal = () => { + const tableData = passwordStatus.passwordToggleHistory.map(result => ({ + created: formatDate(result.created), + comment: result.comment, + disabled: result.disabled ? 'Disabled' : 'Enabled', + createdBy: result.createdBy, + })); + setPasswordHistoryData(tableData); + setPasswordHistoryModalIsOpen(true); + }; + + const userPasswordHistoryColumns = [ + { + label: 'Date', + key: 'created', + }, + { + label: 'Comment', + key: 'comment', + }, + { + label: 'Action', + key: 'disabled', + }, + { + label: 'By', + key: 'createdBy', + }, + ]; + + return ( +
+ + {passwordStatus.passwordToggleHistory.length > 0 && ( + + )} + + setPasswordHistoryModalIsOpen(false)} + title="Enable/Disable History" + id="password-history" + body={( +
+ )} + /> + + ); +} + +PasswordHistory.propTypes = { + passwordStatus: PropTypes.string.isRequired, +}; diff --git a/src/users/account-actions/PasswordHistory.test.jsx b/src/users/account-actions/PasswordHistory.test.jsx new file mode 100644 index 000000000..f94072069 --- /dev/null +++ b/src/users/account-actions/PasswordHistory.test.jsx @@ -0,0 +1,39 @@ +import { mount } from 'enzyme'; +import React from 'react'; + +import PasswordHistory from './PasswordHistory'; +import UserSummaryData from '../data/test/userSummary'; + +describe('Password History Component Tests', () => { + let wrapper; + + beforeEach(() => { + const data = { + passwordStatus: UserSummaryData.userData.passwordStatus, + }; + wrapper = mount(); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + it('Password History Modal', () => { + const passwordHistoryButton = wrapper.find('button#toggle-password-history'); + let historyModal = wrapper.find('Modal#password-history'); + + expect(historyModal.prop('open')).toEqual(false); + expect(passwordHistoryButton.text()).toEqual('Show History'); + expect(passwordHistoryButton.disabled).toBeFalsy(); + + passwordHistoryButton.simulate('click'); + historyModal = wrapper.find('Modal#password-history'); + + expect(historyModal.prop('open')).toEqual(true); + expect(historyModal.find('table tbody tr')).toHaveLength(2); + + historyModal.find('button.btn-link').simulate('click'); + historyModal = wrapper.find('Modal#password-history'); + expect(historyModal.prop('open')).toEqual(false); + }); +}); diff --git a/src/users/account-actions/ResetPassword.jsx b/src/users/account-actions/ResetPassword.jsx new file mode 100644 index 000000000..00ca3bcf1 --- /dev/null +++ b/src/users/account-actions/ResetPassword.jsx @@ -0,0 +1,56 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, Button } from '@edx/paragon'; +import { postResetPassword } from '../data/api'; + +export default function ResetPassword({ + email, + changeHandler, +}) { + const [resetPasswordModalIsOpen, setResetPasswordModalIsOpen] = useState(false); + + const resetPassword = () => { + postResetPassword(email); + changeHandler(); + }; + + return ( +
+ + + + Confirm + , + ]} + onClose={() => setResetPasswordModalIsOpen(false)} + title="Reset Password" + body={( +
+ { /* eslint-disable-next-line jsx-a11y/label-has-associated-control */ } + +
+ )} + /> +
+ ); +} + +ResetPassword.propTypes = { + email: PropTypes.string.isRequired, + changeHandler: PropTypes.func.isRequired, +}; diff --git a/src/users/account-actions/ResetPassword.test.jsx b/src/users/account-actions/ResetPassword.test.jsx new file mode 100644 index 000000000..cc5c2ad63 --- /dev/null +++ b/src/users/account-actions/ResetPassword.test.jsx @@ -0,0 +1,51 @@ +import { mount } from 'enzyme'; +import React from 'react'; + +import * as api from '../data/api'; +import ResetPassword from './ResetPassword'; +import UserSummaryData from '../data/test/userSummary'; + +describe('Reset Password Component Tests', () => { + let wrapper; + + beforeEach(() => { + const data = { + email: UserSummaryData.userData.email, + changeHandler: UserSummaryData.changeHandler, + }; + wrapper = mount(); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + it('Reset Password button for a User', () => { + const passwordResetButton = wrapper.find('#reset-password').hostNodes(); + expect(passwordResetButton.text()).toEqual('Reset Password'); + }); + + it('Reset Password Modal', () => { + const mockApiCall = jest.spyOn(api, 'postResetPassword').mockImplementation(() => {}); + const passwordResetButton = wrapper.find('#reset-password').hostNodes(); + let resetPasswordModal = wrapper.find('Modal#user-account-reset-password'); + + expect(resetPasswordModal.prop('open')).toEqual(false); + expect(passwordResetButton.text()).toEqual('Reset Password'); + + passwordResetButton.simulate('click'); + resetPasswordModal = wrapper.find('Modal#user-account-reset-password'); + + expect(resetPasswordModal.prop('open')).toEqual(true); + expect(resetPasswordModal.prop('title')).toEqual('Reset Password'); + const confirmLabel = resetPasswordModal.find('label'); + expect(confirmLabel.text()).toContain('Do you wish to proceed?'); + resetPasswordModal.find('button.btn-danger').hostNodes().simulate('click'); + + expect(UserSummaryData.changeHandler).toHaveBeenCalled(); + resetPasswordModal.find('button.btn-link').simulate('click'); + resetPasswordModal = wrapper.find('Modal#user-account-reset-password'); + expect(resetPasswordModal.prop('open')).toEqual(false); + mockApiCall.mockRestore(); + }); +}); diff --git a/src/users/account-actions/TogglePasswordStatus.jsx b/src/users/account-actions/TogglePasswordStatus.jsx new file mode 100644 index 000000000..8f0571020 --- /dev/null +++ b/src/users/account-actions/TogglePasswordStatus.jsx @@ -0,0 +1,65 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, Button, Input } from '@edx/paragon'; +import { postTogglePasswordStatus } from '../data/api'; + +import { + DISABLE_USER, ENABLE_USER, DISABLE_USER_CONFIRMATION, ENABLE_USER_CONFIRMATION, PASSWORD_STATUS, +} from './constants'; + +export default function TogglePasswordStatus({ + username, + passwordStatus, + changeHandler, +}) { + const [disableUserModalIsOpen, setDisableUserModalIsOpen] = useState(false); + const [comment, setComment] = useState(''); + + const togglePasswordStatus = () => { + postTogglePasswordStatus(username, comment); + changeHandler(); + }; + + return ( +
+ + + Confirm + , + ]} + onClose={() => setDisableUserModalIsOpen(false)} + title={`${passwordStatus.status === PASSWORD_STATUS.USABLE ? DISABLE_USER_CONFIRMATION : ENABLE_USER_CONFIRMATION}`} + body={( +
+ + setComment(event.target.value)} + /> +
+ )} + /> +
+ ); +} + +TogglePasswordStatus.propTypes = { + username: PropTypes.string.isRequired, + passwordStatus: PropTypes.string.isRequired, + changeHandler: PropTypes.func.isRequired, +}; diff --git a/src/users/account-actions/TogglePasswordStatus.test.jsx b/src/users/account-actions/TogglePasswordStatus.test.jsx new file mode 100644 index 000000000..1a2700c26 --- /dev/null +++ b/src/users/account-actions/TogglePasswordStatus.test.jsx @@ -0,0 +1,94 @@ +import { mount } from 'enzyme'; +import React from 'react'; + +import * as api from '../data/api'; +import TogglePasswordStatus from './TogglePasswordStatus'; +import UserSummaryData from '../data/test/userSummary'; + +describe('Toggle Password Status Component Tests', () => { + let wrapper; + + beforeEach(() => { + const data = { + username: UserSummaryData.userData.username, + passwordStatus: UserSummaryData.userData.passwordStatus, + changeHandler: UserSummaryData.changeHandler, + }; + wrapper = mount(); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + describe('Disable User Button', () => { + it('Disable User button for active user', () => { + const passwordActionButton = wrapper.find('#toggle-password').hostNodes(); + expect(passwordActionButton.text()).toEqual('Disable User'); + expect(passwordActionButton.disabled).toBeFalsy(); + }); + + it('Disable User Modal', () => { + const mockApiCall = jest.spyOn(api, 'postTogglePasswordStatus').mockImplementation(() => {}); + const passwordActionButton = wrapper.find('#toggle-password').hostNodes(); + let disableDialogModal = wrapper.find('Modal#user-account-status-toggle'); + + expect(disableDialogModal.prop('open')).toEqual(false); + expect(passwordActionButton.text()).toEqual('Disable User'); + expect(passwordActionButton.disabled).toBeFalsy(); + + passwordActionButton.simulate('click'); + disableDialogModal = wrapper.find('Modal#user-account-status-toggle'); + + expect(disableDialogModal.prop('open')).toEqual(true); + expect(disableDialogModal.prop('title')).toEqual('Disable user confirmation'); + disableDialogModal.find('input[name="comment"]').simulate('change', { target: { value: 'Disable Test User' } }); + disableDialogModal.find('button.btn-danger').hostNodes().simulate('click'); + + expect(UserSummaryData.changeHandler).toHaveBeenCalled(); + disableDialogModal.find('button.btn-link').simulate('click'); + disableDialogModal = wrapper.find('Modal#user-account-status-toggle'); + expect(disableDialogModal.prop('open')).toEqual(false); + mockApiCall.mockRestore(); + }); + }); + + describe('Enable User Button', () => { + beforeEach(() => { + const passwordStatusData = { ...UserSummaryData.userData.passwordStatus, status: 'Unusable' }; + const data = { + username: UserSummaryData.userData.username, + passwordStatus: passwordStatusData, + changeHandler: UserSummaryData.changeHandler, + }; + wrapper = mount(); + }); + + it('Enable User button for disabled user', () => { + const passwordActionButton = wrapper.find('#toggle-password').hostNodes(); + expect(passwordActionButton.text()).toEqual('Enable User'); + expect(passwordActionButton.disabled).toBeFalsy(); + }); + + it('Enable User Modal', () => { + const mockApiCall = jest.spyOn(api, 'postTogglePasswordStatus').mockImplementation(() => {}); + const passwordActionButton = wrapper.find('#toggle-password').hostNodes(); + let enableUserModal = wrapper.find('Modal#user-account-status-toggle'); + + expect(enableUserModal.prop('open')).toEqual(false); + expect(passwordActionButton.text()).toEqual('Enable User'); + expect(passwordActionButton.disabled).toBeFalsy(); + + passwordActionButton.simulate('click'); + enableUserModal = wrapper.find('Modal#user-account-status-toggle'); + + expect(enableUserModal.prop('open')).toEqual(true); + expect(enableUserModal.prop('title')).toEqual('Enable user confirmation'); + enableUserModal.find('input[name="comment"]').simulate('change', { target: { value: 'Enable Test User' } }); + enableUserModal.find('button.btn-danger').hostNodes().simulate('click'); + + expect(UserSummaryData.changeHandler).toHaveBeenCalled(); + mockApiCall.mockRestore(); + }); + }); +}); diff --git a/src/users/account-actions/constants.js b/src/users/account-actions/constants.js new file mode 100644 index 000000000..bf702a2df --- /dev/null +++ b/src/users/account-actions/constants.js @@ -0,0 +1,9 @@ +export const DISABLE_USER = 'Disable User'; +export const ENABLE_USER = 'Enable User'; +export const DISABLE_USER_CONFIRMATION = 'Disable user confirmation'; +export const ENABLE_USER_CONFIRMATION = 'Enable user confirmation'; + +export const PASSWORD_STATUS = { + USABLE: 'Usable', + UNUSABLE: 'Unusable', +}; diff --git a/src/users/data/test/userSummary.js b/src/users/data/test/userSummary.js index 157e517cd..71c49476b 100644 --- a/src/users/data/test/userSummary.js +++ b/src/users/data/test/userSummary.js @@ -10,7 +10,19 @@ const UserSummaryData = { dateJoined: null, lastLogin: null, passwordStatus: { - passwordToggleHistory: [], + passwordToggleHistory: [ + { + created: Date().toLocaleString(), + comment: 'Test Disabled', + disabled: false, + createdBy: 'staff', + }, + { + created: Date().toLocaleString(), + comment: 'Test Enable', + disabled: true, + createdBy: 'staff', + }], status: 'Usable', }, },