Skip to content

Commit

Permalink
Merge pull request openedx#396 from openedx/knguyen2/ent-9159
Browse files Browse the repository at this point in the history
feat: add customer record card
  • Loading branch information
katrinan029 authored Aug 9, 2024
2 parents e541f84 + 75d3c8c commit 81ac21f
Show file tree
Hide file tree
Showing 10 changed files with 322 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useState } from 'react';
import {
Hyperlink,
Icon,
Expand All @@ -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 (
<div>
<div>
<Hyperlink
destination={`${HOME}/${row.original.slug}/view`}
destination={`${HOME}/${row.original.uuid}/view`}
key={row.original.uuid}
rel="noopener noreferrer"
variant="muted"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ jest.mock('@edx/frontend-platform', () => ({
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 = {
Expand Down Expand Up @@ -107,7 +109,7 @@ describe('CustomerDetails', () => {
<CustomerDetailLink row={row} />
</IntlProvider>,
);
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');
Expand Down
95 changes: 95 additions & 0 deletions src/Configuration/Customers/CustomerDetailView/CustomerCard.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<Card variant="dark" className="mb-0">
<Card.Section
actions={(
<ActionRow>
<Button>View Details</Button>
<Button
className="text-dark-500"
as="a"
href={`${LMS_BASE_URL}/admin/enterprise/enterprisecustomer/${enterpriseCustomer.uuid}/change`}
variant="inverse-primary"
target="_blank"
rel="noopener noreferrer"
iconAfter={Launch}
>
Open in Django
</Button>
</ActionRow>
)}
>
<p className="small font-weight-bold mb-0 mt-2">
CUSTOMER RECORD
</p>
<p className="lead font-weight-bold mb-0">
{enterpriseCustomer.name}
</p>
<Hyperlink
destination={`${ADMIN_PORTAL_BASE_URL}/${enterpriseCustomer.slug}/admin/learners`}
variant="muted"
target="_blank"
showLaunchIcon
className="small mb-1"
>
/{enterpriseCustomer.slug}/
</Hyperlink>
<div
role="presentation"
className="pgn-doc__icons-table__preview-footer"
>
<p className="small mb-1">
{enterpriseCustomer.uuid}
</p>
<Icon
key="ContentCopy"
src={ContentCopy}
data-testid="copy"
onClick={() => copyToClipboard(enterpriseCustomer.uuid)}
/>
</div>
<p className="small mb-1">
Created {formatDate(enterpriseCustomer.created)} • Last modified {formatDate(enterpriseCustomer.modified)}
</p>
</Card.Section>
</Card>
<Toast
onClose={() => setShowToast(false)}
show={showToast}
delay={2000}
>
Copied to clipboard
</Toast>
</div>

);
};

CustomerCard.propTypes = {
enterpriseCustomer: PropTypes.shape({
created: PropTypes.string,
modified: PropTypes.string,
slug: PropTypes.string,
name: PropTypes.string,
uuid: PropTypes.string,
}).isRequired,
};

export default CustomerCard;
Original file line number Diff line number Diff line change
@@ -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 (
<div>
{!isLoading ? (
<Container className="mt-5">
<Breadcrumb
arial-label="customer detail"
links={[
{
label: intl.formatMessage({
id: 'supportTool.customers.page.breadcrumb.customer',
defaultMessage: 'Customers',
description: 'Breadcrumb label for the customers page',
}),
href: '/enterprise-configuration/customers/',
},
{ label: enterpriseCustomer.name },
]}
/>
</Container>
) : <Skeleton />}
<Container className="mt-4">
<Stack gap={2}>
{!isLoading ? <CustomerCard enterpriseCustomer={enterpriseCustomer} /> : <Skeleton height={230} />}
</Stack>
</Container>
</div>
);
};

export default CustomerViewContainer;
Original file line number Diff line number Diff line change
@@ -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(
<IntlProvider locale="en">
<CustomerCard enterpriseCustomer={mockData} />
</IntlProvider>,
);
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'));
});
});
Original file line number Diff line number Diff line change
@@ -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(
<IntlProvider locale="en">
<CustomerViewContainer />
</IntlProvider>,
);
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();
});
});
});
});
24 changes: 24 additions & 0 deletions src/Configuration/Customers/data/utils.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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,
};
};
29 changes: 29 additions & 0 deletions src/Configuration/Customers/data/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
getCouponOrders,
getCustomerAgreements,
getSubsidyAccessPolicies,
getEnterpriseCustomer,
formatDate,
} from './utils';

jest.mock('@edx/frontend-platform/auth', () => ({
Expand Down Expand Up @@ -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);
});
});
Loading

0 comments on commit 81ac21f

Please sign in to comment.