diff --git a/src/Configuration/Customers/CustomerDataTable/CustomerDetails.jsx b/src/Configuration/Customers/CustomerDataTable/CustomerDetails.jsx index 688ca79c6..6224f86ce 100644 --- a/src/Configuration/Customers/CustomerDataTable/CustomerDetails.jsx +++ b/src/Configuration/Customers/CustomerDataTable/CustomerDetails.jsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { Hyperlink, Icon, @@ -8,22 +7,19 @@ import { getConfig } from '@edx/frontend-platform'; import { Check, ContentCopy } from '@openedx/paragon/icons'; import PropTypes from 'prop-types'; import ROUTES from '../../../data/constants/routes'; +import { useCopyToClipboard } from '../data/utils'; const { HOME } = ROUTES.CONFIGURATION.SUB_DIRECTORY.CUSTOMERS; export const CustomerDetailLink = ({ row }) => { - const [showToast, setShowToast] = useState(false); - const copyToClipboard = (id) => { - navigator.clipboard.writeText(id); - setShowToast(true); - }; + const { showToast, copyToClipboard, setShowToast } = useCopyToClipboard(); const { ADMIN_PORTAL_BASE_URL } = getConfig(); return (
({ getConfig: jest.fn(), })); -Object.assign(navigator, { - clipboard: { - writeText: () => {}, - }, -}); +jest.mock('../../data/utils', () => ({ + useCopyToClipboard: jest.fn(() => ({ + showToast: true, + copyToClipboard: jest.fn(), + setShowToast: jest.fn(), + })), +})); describe('CustomerDetails', () => { const row = { @@ -107,7 +109,7 @@ describe('CustomerDetails', () => { , ); - expect(screen.getByRole('link', { name: 'Ash Ketchum' })).toHaveAttribute('href', '/enterprise-configuration/customers/ash-ketchum/view'); + expect(screen.getByRole('link', { name: 'Ash Ketchum' })).toHaveAttribute('href', '/enterprise-configuration/customers/123456789/view'); expect(screen.getByRole('link', { name: '/ash-ketchum/ in a new tab' })).toHaveAttribute('href', 'http://www.testportal.com/ash-ketchum/admin/learners'); expect(screen.getByText('123456789')).toBeInTheDocument(); const copy = screen.getByTestId('copy'); diff --git a/src/Configuration/Customers/CustomerDetailView/CustomerCard.jsx b/src/Configuration/Customers/CustomerDetailView/CustomerCard.jsx new file mode 100644 index 000000000..cf3df68f3 --- /dev/null +++ b/src/Configuration/Customers/CustomerDetailView/CustomerCard.jsx @@ -0,0 +1,95 @@ +import PropTypes from 'prop-types'; +import { + ActionRow, + Button, + Card, + Icon, + Hyperlink, + Toast, +} from '@openedx/paragon'; +import { Launch, ContentCopy } from '@openedx/paragon/icons'; +import { getConfig } from '@edx/frontend-platform'; +import { formatDate, useCopyToClipboard } from '../data/utils'; + +const CustomerCard = ({ enterpriseCustomer }) => { + const { ADMIN_PORTAL_BASE_URL, LMS_BASE_URL } = getConfig(); + const { showToast, copyToClipboard, setShowToast } = useCopyToClipboard(); + + return ( +
+ + + + + + )} + > +

+ CUSTOMER RECORD +

+

+ {enterpriseCustomer.name} +

+ + /{enterpriseCustomer.slug}/ + +
+

+ {enterpriseCustomer.uuid} +

+ copyToClipboard(enterpriseCustomer.uuid)} + /> +
+

+ Created {formatDate(enterpriseCustomer.created)} • Last modified {formatDate(enterpriseCustomer.modified)} +

+
+
+ setShowToast(false)} + show={showToast} + delay={2000} + > + Copied to clipboard + +
+ + ); +}; + +CustomerCard.propTypes = { + enterpriseCustomer: PropTypes.shape({ + created: PropTypes.string, + modified: PropTypes.string, + slug: PropTypes.string, + name: PropTypes.string, + uuid: PropTypes.string, + }).isRequired, +}; + +export default CustomerCard; diff --git a/src/Configuration/Customers/CustomerDetailView/CustomerViewContainer.jsx b/src/Configuration/Customers/CustomerDetailView/CustomerViewContainer.jsx new file mode 100644 index 000000000..ee4fc809c --- /dev/null +++ b/src/Configuration/Customers/CustomerDetailView/CustomerViewContainer.jsx @@ -0,0 +1,67 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useParams } from 'react-router-dom'; +import { logError } from '@edx/frontend-platform/logging'; +import { + Breadcrumb, + Container, + Skeleton, + Stack, +} from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import CustomerCard from './CustomerCard'; +import { getEnterpriseCustomer } from '../data/utils'; + +const CustomerViewContainer = () => { + const { id } = useParams(); + const [enterpriseCustomer, setEnterpriseCustomer] = useState({}); + const [isLoading, setIsLoading] = useState(true); + const intl = useIntl(); + + const fetchData = useCallback( + async () => { + try { + const response = await getEnterpriseCustomer({ uuid: id }); + setEnterpriseCustomer(response[0]); + } catch (error) { + logError(error); + } finally { + setIsLoading(false); + } + }, + [], + ); + + useEffect(() => { + fetchData(); + }, []); + + return ( +
+ {!isLoading ? ( + + + + ) : } + + + {!isLoading ? : } + + +
+ ); +}; + +export default CustomerViewContainer; diff --git a/src/Configuration/Customers/CustomerDetailView/tests/CustomerCard.test.jsx b/src/Configuration/Customers/CustomerDetailView/tests/CustomerCard.test.jsx new file mode 100644 index 000000000..82dbeceea --- /dev/null +++ b/src/Configuration/Customers/CustomerDetailView/tests/CustomerCard.test.jsx @@ -0,0 +1,40 @@ +/* eslint-disable react/prop-types */ +import { screen, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { formatDate } from '../../data/utils'; +import CustomerCard from '../CustomerCard'; + +jest.mock('../../data/utils', () => ({ + getEnterpriseCustomer: jest.fn(), + formatDate: jest.fn(), + useCopyToClipboard: jest.fn(() => ({ + showToast: true, + copyToClipboard: jest.fn(), + setShowToast: jest.fn(), + })), +})); + +const mockData = { + uuid: 'test-id', + name: 'Test Customer Name', + slug: 'customer-6', + created: '2024-07-23T20:02:57.651943Z', + modified: '2024-07-23T20:02:57.651943Z', +}; + +describe('CustomerCard', () => { + it('renders customer card data', () => { + formatDate.mockReturnValue('July 23, 2024'); + render( + + + , + ); + expect(screen.getByText('test-id')).toBeInTheDocument(); + expect(screen.getByText('/customer-6/')).toBeInTheDocument(); + expect(screen.getByText('Created July 23, 2024 • Last modified July 23, 2024')).toBeInTheDocument(); + expect(screen.getByText('Test Customer Name')); + }); +}); diff --git a/src/Configuration/Customers/CustomerDetailView/tests/CustomerViewContainer.test.jsx b/src/Configuration/Customers/CustomerDetailView/tests/CustomerViewContainer.test.jsx new file mode 100644 index 000000000..23f7a360b --- /dev/null +++ b/src/Configuration/Customers/CustomerDetailView/tests/CustomerViewContainer.test.jsx @@ -0,0 +1,49 @@ +/* eslint-disable react/prop-types */ +import { screen, render, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { getEnterpriseCustomer, formatDate } from '../../data/utils'; +import CustomerViewContainer from '../CustomerViewContainer'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ id: 'test-id' }), +})); + +jest.mock('../../data/utils', () => ({ + getEnterpriseCustomer: jest.fn(), + formatDate: jest.fn(), + useCopyToClipboard: jest.fn(() => ({ + showToast: true, + copyToClipboard: jest.fn(), + setShowToast: jest.fn(), + })), +})); + +describe('CustomerViewContainer', () => { + it('renders data', async () => { + getEnterpriseCustomer.mockReturnValue([{ + uuid: 'test-id', + name: 'Test Customer Name', + slug: 'customer-6', + created: '2024-07-23T20:02:57.651943Z', + modified: '2024-07-23T20:02:57.651943Z', + }]); + formatDate.mockReturnValue('July 23, 2024'); + render( + + + , + ); + await waitFor(() => { + expect(screen.getByText('test-id')).toBeInTheDocument(); + expect(screen.getByText('/customer-6/')).toBeInTheDocument(); + expect(screen.getByText('Created July 23, 2024 • Last modified July 23, 2024')).toBeInTheDocument(); + const customerNameText = screen.getAllByText('Test Customer Name'); + customerNameText.forEach(customerName => { + expect(customerName).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/src/Configuration/Customers/data/utils.js b/src/Configuration/Customers/data/utils.js index 9252e9864..0ff8fe380 100644 --- a/src/Configuration/Customers/data/utils.js +++ b/src/Configuration/Customers/data/utils.js @@ -1,7 +1,10 @@ +import { useState } from 'react'; import { camelCaseObject } from '@edx/frontend-platform/utils'; import EcommerceApiService from '../../../data/services/EcommerceApiService'; import LicenseManagerApiService from '../../../data/services/LicenseManagerApiService'; import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService'; +import LmsApiService from '../../../data/services/EnterpriseApiService'; +import dayjs from '../../Provisioning/data/dayjs'; export const getEnterpriseOffers = async (enterpriseId) => { const response = await EcommerceApiService.fetchEnterpriseOffers(enterpriseId); @@ -26,3 +29,24 @@ export const getSubsidyAccessPolicies = async (enterpriseId) => { const subsidyAccessPolicies = camelCaseObject(response.data); return subsidyAccessPolicies; }; + +export const getEnterpriseCustomer = async (options) => { + const response = await LmsApiService.fetchEnterpriseCustomerSupportTool(options); + const enterpriseCustomer = camelCaseObject(response.data); + return enterpriseCustomer; +}; + +export const formatDate = (date) => dayjs(date).utc().format('MMMM DD, YYYY'); + +export const useCopyToClipboard = (id) => { + const [showToast, setShowToast] = useState(false); + const copyToClipboard = () => { + navigator.clipboard.writeText(id); + setShowToast(true); + }; + return { + showToast, + copyToClipboard, + setShowToast, + }; +}; diff --git a/src/Configuration/Customers/data/utils.test.js b/src/Configuration/Customers/data/utils.test.js index f5bcfe4b5..3ba7998f4 100644 --- a/src/Configuration/Customers/data/utils.test.js +++ b/src/Configuration/Customers/data/utils.test.js @@ -4,6 +4,8 @@ import { getCouponOrders, getCustomerAgreements, getSubsidyAccessPolicies, + getEnterpriseCustomer, + formatDate, } from './utils'; jest.mock('@edx/frontend-platform/auth', () => ({ @@ -100,3 +102,30 @@ describe('getCustomerAgreements', () => { expect(results).toEqual(agreementsResults.data); }); }); + +describe('getEnterpriseCustomer', () => { + it('returns the correct data', async () => { + const enterpriseCustomer = { + data: [{ + uuid: '0b466242-75ff-4c27-8237-680dac3737f7', + name: 'customer-6', + slug: 'customer-6', + active: true, + }], + }; + getAuthenticatedHttpClient.mockImplementation(() => ({ + get: jest.fn().mockResolvedValue(enterpriseCustomer), + })); + const results = await getEnterpriseCustomer(TEST_ENTERPRISE_UUID); + expect(results).toEqual(enterpriseCustomer.data); + }); +}); + +describe('formatDate', () => { + it('returns the formatted date', async () => { + const date = '2024-07-23T20:02:57.651943Z'; + const formattedDate = formatDate(date); + const expectedFormattedDate = 'July 23, 2024'; + expect(expectedFormattedDate).toEqual(formattedDate); + }); +}); diff --git a/src/Configuration/index.scss b/src/Configuration/index.scss index e7ede0f54..c3780b47c 100644 --- a/src/Configuration/index.scss +++ b/src/Configuration/index.scss @@ -7,7 +7,6 @@ .pgn__icon { align-self: center; margin-left: 8px; - opacity: .3; width: 1rem; height: 1rem; } @@ -25,4 +24,4 @@ font-size: 15px; color: black !important; font-weight: bold; -} \ No newline at end of file +} diff --git a/src/index.jsx b/src/index.jsx index 1bed2bec7..007efb559 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -29,6 +29,7 @@ import ProvisioningFormContainer from './Configuration/Provisioning/Provisioning import SubsidyDetailViewContainer from './Configuration/Provisioning/SubsidyDetailView/SubsidyDetailViewContainer'; import ErrorPageContainer from './Configuration/Provisioning/ErrorPage'; import SubsidyEditViewContainer from './Configuration/Provisioning/SubsidyEditView/SubsidyEditViewContainer'; +import CustomerViewContainer from './Configuration/Customers/CustomerDetailView/CustomerViewContainer'; const { CONFIGURATION, SUPPORT_TOOLS_TABS } = ROUTES; @@ -76,6 +77,11 @@ subscribe(APP_READY, () => { path={CONFIGURATION.SUB_DIRECTORY.CUSTOMERS.HOME} element={} />, + } + />, ]; ReactDOM.render(